feat: 添加 Paradex 适配器支持,更新环境变量配置和适配器构建逻辑

This commit is contained in:
discountry
2025-10-06 18:06:23 +08:00
parent ccc3a2bf89
commit cfd5f4309d
18 changed files with 1681 additions and 177 deletions
+146 -17
View File
@@ -9,7 +9,7 @@ import type {
} from "../exchanges/types";
import { formatPriceToString } from "../utils/math";
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
import { isUnknownOrderError, isRateLimitError } from "../utils/errors";
import { extractMessage, isInsufficientBalanceError, isUnknownOrderError, isRateLimitError } from "../utils/errors";
import { getPosition } from "../utils/strategy";
import type { PositionSnapshot } from "../utils/strategy";
import { computePositionPnl } from "../utils/pnl";
@@ -49,12 +49,19 @@ export interface MakerEngineSnapshot {
desiredOrders: DesiredOrder[];
tradeLog: TradeLogEntry[];
lastUpdated: number | null;
feedStatus: {
account: boolean;
orders: boolean;
depth: boolean;
ticker: boolean;
};
}
type MakerEvent = "update";
type MakerListener = (snapshot: MakerEngineSnapshot) => void;
const EPS = 1e-5;
const INSUFFICIENT_BALANCE_COOLDOWN_MS = 15_000;
export class MakerEngine {
private accountSnapshot: AsterAccountSnapshot | null = null;
@@ -78,6 +85,28 @@ export class MakerEngine {
private initialOrderSnapshotReady = false;
private initialOrderResetDone = false;
private entryPricePendingLogged = false;
private readinessLogged = {
account: false,
depth: false,
ticker: false,
orders: false,
};
private feedArrived = {
account: false,
depth: false,
ticker: false,
orders: false,
};
private feedStatus = {
account: false,
depth: false,
ticker: false,
orders: false,
};
private insufficientBalanceCooldownUntil = 0;
private insufficientBalanceNotified = false;
private lastInsufficientMessage: string | null = null;
private lastDesiredSummary: string | null = null;
private readonly rateLimit: RateLimitController;
constructor(private readonly config: MakerConfig, private readonly exchange: ExchangeAdapter) {
@@ -127,6 +156,11 @@ export class MakerEngine {
}
const position = getPosition(snapshot, this.config.symbol);
this.sessionVolume.update(position, this.getReferencePrice());
if (!this.feedArrived.account) {
this.tradeLog.push("info", "账户快照已同步");
this.feedArrived.account = true;
}
this.feedStatus.account = true;
this.emitUpdate();
},
log,
@@ -150,6 +184,11 @@ export class MakerEngine {
}
}
this.initialOrderSnapshotReady = true;
if (!this.feedArrived.orders) {
this.tradeLog.push("info", "订单快照已返回");
this.feedArrived.orders = true;
}
this.feedStatus.orders = true;
this.emitUpdate();
},
log,
@@ -163,6 +202,11 @@ export class MakerEngine {
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
(depth) => {
this.depthSnapshot = depth;
if (!this.feedArrived.depth) {
this.tradeLog.push("info", "获得最新深度行情");
this.feedArrived.depth = true;
}
this.feedStatus.depth = true;
this.emitUpdate();
},
log,
@@ -176,6 +220,11 @@ export class MakerEngine {
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
(ticker) => {
this.tickerSnapshot = ticker;
if (!this.feedArrived.ticker) {
this.tradeLog.push("info", "Ticker 已就绪");
this.feedArrived.ticker = true;
}
this.feedStatus.ticker = true;
this.emitUpdate();
},
log,
@@ -185,18 +234,7 @@ export class MakerEngine {
}
);
// Maker strategy does not consume klines, but subscribe to keep parity with other modules
safeSubscribe<AsterKline[]>(
this.exchange.watchKlines.bind(this.exchange, this.config.symbol, "1m"),
(_klines) => {
/* no-op */
},
log,
{
subscribeFail: (error) => `订阅K线失败: ${String(error)}`,
processFail: (error) => `K线推送处理异常: ${String(error)}`,
}
);
// Maker strategy does not require realtime klines.
}
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void {
@@ -212,7 +250,12 @@ export class MakerEngine {
}
private isReady(): boolean {
return Boolean(this.accountSnapshot && this.depthSnapshot);
return Boolean(
this.feedStatus.account &&
this.feedStatus.depth &&
this.feedStatus.ticker &&
this.feedStatus.orders
);
}
private async tick(): Promise<void> {
@@ -229,9 +272,11 @@ export class MakerEngine {
return;
}
if (!this.isReady()) {
this.logReadinessBlockers();
this.emitUpdate();
return;
}
this.resetReadinessFlags();
if (!(await this.ensureStartupOrderReset())) {
this.emitUpdate();
return;
@@ -253,7 +298,8 @@ export class MakerEngine {
const position = getPosition(this.accountSnapshot, this.config.symbol);
const absPosition = Math.abs(position.positionAmt);
const desired: DesiredOrder[] = [];
const canEnter = !this.rateLimit.shouldBlockEntries();
const insufficientActive = this.applyInsufficientBalanceState(Date.now());
const canEnter = !this.rateLimit.shouldBlockEntries() && !insufficientActive;
if (absPosition < EPS) {
this.entryPricePendingLogged = false;
@@ -268,6 +314,7 @@ export class MakerEngine {
}
this.desiredOrders = desired;
this.logDesiredOrders(desired);
this.sessionVolume.update(position, this.getReferencePrice());
await this.syncOrders(desired);
await this.checkRisk(position, Number(closeBidPrice), Number(closeAskPrice));
@@ -331,7 +378,11 @@ export class MakerEngine {
private async syncOrders(targets: DesiredOrder[]): Promise<void> {
const availableOrders = this.openOrders.filter((o) => !this.pendingCancelOrders.has(String(o.orderId)));
const { toCancel, toPlace } = makeOrderPlan(availableOrders, targets);
const openOrders = availableOrders.filter((order) => {
const status = (order.status ?? "").toUpperCase();
return !status.includes("CLOSED") && !status.includes("FILLED") && !status.includes("CANCELED");
});
const { toCancel, toPlace } = makeOrderPlan(openOrders, targets);
for (const order of toCancel) {
if (this.pendingCancelOrders.has(String(order.orderId))) continue;
@@ -385,7 +436,14 @@ export class MakerEngine {
}
);
} catch (error) {
this.tradeLog.push("error", `挂单失败(${target.side} ${target.price}): ${String(error)}`);
if (isInsufficientBalanceError(error)) {
this.registerInsufficientBalance(error);
break;
}
this.tradeLog.push(
"error",
`挂单失败(${target.side} ${target.price}): ${extractMessage(error)}`
);
}
}
}
@@ -500,10 +558,81 @@ export class MakerEngine {
desiredOrders: this.desiredOrders,
tradeLog: this.tradeLog.all(),
lastUpdated: Date.now(),
feedStatus: { ...this.feedStatus },
};
}
private getReferencePrice(): number | null {
return getMidOrLast(this.depthSnapshot, this.tickerSnapshot);
}
private logReadinessBlockers(): void {
if (!this.feedStatus.account && !this.readinessLogged.account) {
this.tradeLog.push("info", "等待账户快照同步,尚未开始做市");
this.readinessLogged.account = true;
}
if (!this.feedStatus.depth && !this.readinessLogged.depth) {
this.tradeLog.push("info", "等待深度行情推送,尚未开始做市");
this.readinessLogged.depth = true;
}
if (!this.feedStatus.ticker && !this.readinessLogged.ticker) {
this.tradeLog.push("info", "等待Ticker推送,尚未开始做市");
this.readinessLogged.ticker = true;
}
if (!this.feedStatus.orders && !this.readinessLogged.orders) {
this.tradeLog.push("info", "等待订单快照返回,尚未执行初始化撤单");
this.readinessLogged.orders = true;
}
}
private resetReadinessFlags(): void {
this.readinessLogged = {
account: false,
depth: false,
ticker: false,
orders: false,
};
}
private logDesiredOrders(desired: DesiredOrder[]): void {
if (!desired.length) {
if (this.lastDesiredSummary !== "none") {
this.tradeLog.push("info", "当前无目标挂单,等待下一次刷新");
this.lastDesiredSummary = "none";
}
return;
}
const summary = desired
.map((order) => `${order.side}@${order.price}${order.reduceOnly ? "(RO)" : ""}`)
.join(" | ");
if (summary !== this.lastDesiredSummary) {
this.tradeLog.push("info", `目标挂单: ${summary}`);
this.lastDesiredSummary = summary;
}
}
private registerInsufficientBalance(error: unknown): void {
const now = Date.now();
const detail = extractMessage(error);
const alreadyActive = now < this.insufficientBalanceCooldownUntil;
if (alreadyActive && detail === this.lastInsufficientMessage) {
this.insufficientBalanceCooldownUntil = now + INSUFFICIENT_BALANCE_COOLDOWN_MS;
return;
}
this.insufficientBalanceCooldownUntil = now + INSUFFICIENT_BALANCE_COOLDOWN_MS;
this.lastInsufficientMessage = detail;
const seconds = Math.ceil(INSUFFICIENT_BALANCE_COOLDOWN_MS / 1000);
this.tradeLog.push("warn", `余额不足,暂停新挂单 ${seconds}s: ${detail}`);
this.insufficientBalanceNotified = true;
}
private applyInsufficientBalanceState(now: number): boolean {
const active = now < this.insufficientBalanceCooldownUntil;
if (!active && this.insufficientBalanceNotified) {
this.tradeLog.push("info", "余额检测恢复,重新尝试挂单");
this.insufficientBalanceNotified = false;
this.lastInsufficientMessage = null;
}
return active;
}
}