mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 09:18:08 +00:00
feat: enhance precision synchronization in trading strategies and improve order quantity normalization logic
This commit is contained in:
@@ -384,7 +384,13 @@ export async function marketClose(
|
|||||||
const qtyStep = opts?.qtyStep;
|
const qtyStep = opts?.qtyStep;
|
||||||
const rawQuantity = Math.abs(quantity);
|
const rawQuantity = Math.abs(quantity);
|
||||||
const normalizedQtyRaw = qtyStep != null ? roundQtyDownToStep(rawQuantity, qtyStep) : rawQuantity;
|
const normalizedQtyRaw = qtyStep != null ? roundQtyDownToStep(rawQuantity, qtyStep) : rawQuantity;
|
||||||
const normalizedQty = normalizedQtyRaw > 0 ? normalizedQtyRaw : rawQuantity;
|
let normalizedQty = normalizedQtyRaw > 0 ? normalizedQtyRaw : rawQuantity;
|
||||||
|
if (qtyStep != null) {
|
||||||
|
const epsilon = Math.max(qtyStep * 1e-4, 1e-10);
|
||||||
|
if (Math.abs(rawQuantity - normalizedQty) <= epsilon) {
|
||||||
|
normalizedQty = rawQuantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (normalizedQty <= 0) {
|
if (normalizedQty <= 0) {
|
||||||
log("error", "市价平仓数量无效,跳过下单");
|
log("error", "市价平仓数量无效,跳过下单");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ export interface KlineListener {
|
|||||||
(klines: AsterKline[]): void;
|
(klines: AsterKline[]): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExchangePrecision {
|
||||||
|
priceTick: number;
|
||||||
|
qtyStep: number;
|
||||||
|
priceDecimals?: number;
|
||||||
|
sizeDecimals?: number;
|
||||||
|
marketId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExchangeAdapter {
|
export interface ExchangeAdapter {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
supportsTrailingStops(): boolean;
|
supportsTrailingStops(): boolean;
|
||||||
@@ -39,4 +47,5 @@ export interface ExchangeAdapter {
|
|||||||
cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void>;
|
cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void>;
|
||||||
cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void>;
|
cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void>;
|
||||||
cancelAllOrders(params: { symbol: string }): Promise<void>;
|
cancelAllOrders(params: { symbol: string }): Promise<void>;
|
||||||
|
getPrecision?(): Promise<ExchangePrecision | null>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type {
|
|||||||
AccountListener,
|
AccountListener,
|
||||||
DepthListener,
|
DepthListener,
|
||||||
ExchangeAdapter,
|
ExchangeAdapter,
|
||||||
|
ExchangePrecision,
|
||||||
KlineListener,
|
KlineListener,
|
||||||
OrderListener,
|
OrderListener,
|
||||||
TickerListener,
|
TickerListener,
|
||||||
@@ -124,6 +125,22 @@ export class LighterExchangeAdapter implements ExchangeAdapter {
|
|||||||
await this.gateway.cancelAllOrders();
|
await this.gateway.cancelAllOrders();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPrecision(): Promise<ExchangePrecision | null> {
|
||||||
|
try {
|
||||||
|
const precision = await this.gateway.getPrecision();
|
||||||
|
return {
|
||||||
|
priceTick: precision.priceTick,
|
||||||
|
qtyStep: precision.qtyStep,
|
||||||
|
priceDecimals: precision.priceDecimals,
|
||||||
|
sizeDecimals: precision.sizeDecimals,
|
||||||
|
marketId: precision.marketId ?? undefined,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logError("precision", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ensureInitialized(context?: string): Promise<void> {
|
private ensureInitialized(context?: string): Promise<void> {
|
||||||
if (!this.initPromise) {
|
if (!this.initPromise) {
|
||||||
this.initContexts.clear();
|
this.initContexts.clear();
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ interface Pollers {
|
|||||||
const KLINE_DEFAULT_COUNT = 120;
|
const KLINE_DEFAULT_COUNT = 120;
|
||||||
const DEFAULT_TICKER_POLL_MS = 3000;
|
const DEFAULT_TICKER_POLL_MS = 3000;
|
||||||
const DEFAULT_KLINE_POLL_MS = 15000;
|
const DEFAULT_KLINE_POLL_MS = 15000;
|
||||||
|
const WS_HEARTBEAT_INTERVAL_MS = 5_000;
|
||||||
|
const WS_STALE_TIMEOUT_MS = 20_000;
|
||||||
|
|
||||||
const RESOLUTION_MS: Record<string, number> = {
|
const RESOLUTION_MS: Record<string, number> = {
|
||||||
"1m": 60_000,
|
"1m": 60_000,
|
||||||
@@ -172,6 +174,8 @@ export class LighterGateway {
|
|||||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
private readonly wsUrl: string;
|
private readonly wsUrl: string;
|
||||||
private connectPromise: Promise<void> | null = null;
|
private connectPromise: Promise<void> | null = null;
|
||||||
|
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
private lastMessageAt = 0;
|
||||||
|
|
||||||
private accountDetails: LighterAccountDetails | null = null;
|
private accountDetails: LighterAccountDetails | null = null;
|
||||||
private positions: LighterPosition[] = [];
|
private positions: LighterPosition[] = [];
|
||||||
@@ -438,25 +442,56 @@ export class LighterGateway {
|
|||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const ws = new WebSocket(this.wsUrl);
|
const ws = new WebSocket(this.wsUrl);
|
||||||
this.ws = ws;
|
this.ws = ws;
|
||||||
|
let settled = false;
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
ws.removeAllListeners();
|
ws.removeAllListeners();
|
||||||
|
this.stopHeartbeat();
|
||||||
|
if (this.ws === ws) {
|
||||||
|
this.ws = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const fail = (error: unknown) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
reject(error instanceof Error ? error : new Error(String(error)));
|
||||||
};
|
};
|
||||||
ws.on("open", async () => {
|
ws.on("open", async () => {
|
||||||
try {
|
try {
|
||||||
|
this.lastMessageAt = Date.now();
|
||||||
|
this.startHeartbeat();
|
||||||
await this.subscribeChannels();
|
await this.subscribeChannels();
|
||||||
|
settled = true;
|
||||||
resolve();
|
resolve();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error);
|
cleanup();
|
||||||
|
fail(error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ws.on("message", (data) => this.handleMessage(data));
|
ws.on("message", (data) => {
|
||||||
|
this.lastMessageAt = Date.now();
|
||||||
|
this.handleMessage(data);
|
||||||
|
});
|
||||||
|
ws.on("pong", () => {
|
||||||
|
this.lastMessageAt = Date.now();
|
||||||
|
});
|
||||||
ws.on("close", (code, reason) => {
|
ws.on("close", (code, reason) => {
|
||||||
cleanup();
|
cleanup();
|
||||||
|
const normalizedReason = typeof reason === "string" && reason.length ? reason : undefined;
|
||||||
|
if (!settled) {
|
||||||
|
fail(new Error(`WebSocket closed before ready (code=${code}${normalizedReason ? `, reason=${normalizedReason}` : ""})`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.scheduleReconnect();
|
this.scheduleReconnect();
|
||||||
});
|
});
|
||||||
ws.on("error", (error) => {
|
ws.on("error", (error) => {
|
||||||
cleanup();
|
|
||||||
this.logger("ws:error", error);
|
this.logger("ws:error", error);
|
||||||
|
cleanup();
|
||||||
|
if (!settled) {
|
||||||
|
fail(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.scheduleReconnect();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -506,6 +541,38 @@ export class LighterGateway {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private startHeartbeat(): void {
|
||||||
|
if (this.heartbeatTimer) return;
|
||||||
|
this.heartbeatTimer = setInterval(() => {
|
||||||
|
const ws = this.ws;
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - this.lastMessageAt > WS_STALE_TIMEOUT_MS) {
|
||||||
|
try {
|
||||||
|
ws.terminate();
|
||||||
|
} catch (error) {
|
||||||
|
this.logger("ws:terminate", error);
|
||||||
|
} finally {
|
||||||
|
this.stopHeartbeat();
|
||||||
|
this.scheduleReconnect();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ws.ping();
|
||||||
|
} catch (error) {
|
||||||
|
this.logger("ws:ping", error);
|
||||||
|
}
|
||||||
|
}, WS_HEARTBEAT_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopHeartbeat(): void {
|
||||||
|
if (this.heartbeatTimer) {
|
||||||
|
clearInterval(this.heartbeatTimer);
|
||||||
|
this.heartbeatTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private handleMessage(data: WebSocket.RawData): void {
|
private handleMessage(data: WebSocket.RawData): void {
|
||||||
try {
|
try {
|
||||||
const text = typeof data === "string" ? data : data.toString("utf8");
|
const text = typeof data === "string" ? data : data.toString("utf8");
|
||||||
@@ -785,6 +852,28 @@ export class LighterGateway {
|
|||||||
this.tickerEvent.emit(ticker);
|
this.tickerEvent.emit(ticker);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPrecision(): Promise<{
|
||||||
|
priceTick: number;
|
||||||
|
qtyStep: number;
|
||||||
|
priceDecimals: number;
|
||||||
|
sizeDecimals: number;
|
||||||
|
marketId: number | null;
|
||||||
|
}> {
|
||||||
|
await this.loadMetadata();
|
||||||
|
if (this.priceDecimals == null || this.sizeDecimals == null) {
|
||||||
|
throw new Error("Lighter market metadata not initialized");
|
||||||
|
}
|
||||||
|
const priceTick = decimalsToStep(this.priceDecimals);
|
||||||
|
const qtyStep = decimalsToStep(this.sizeDecimals);
|
||||||
|
return {
|
||||||
|
priceTick,
|
||||||
|
qtyStep,
|
||||||
|
priceDecimals: this.priceDecimals,
|
||||||
|
sizeDecimals: this.sizeDecimals,
|
||||||
|
marketId: this.marketId ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private mapCreateOrderParams(params: CreateOrderParams): Omit<CreateOrderSignParams, "nonce"> & {
|
private mapCreateOrderParams(params: CreateOrderParams): Omit<CreateOrderSignParams, "nonce"> & {
|
||||||
baseAmountScaledString: string;
|
baseAmountScaledString: string;
|
||||||
priceScaledString: string;
|
priceScaledString: string;
|
||||||
@@ -953,3 +1042,11 @@ function mapTimeInForce(timeInForce: string | undefined, type: OrderType): numbe
|
|||||||
return LIGHTER_TIME_IN_FORCE.GOOD_TILL_TIME;
|
return LIGHTER_TIME_IN_FORCE.GOOD_TILL_TIME;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function decimalsToStep(decimals: number): number {
|
||||||
|
if (!Number.isFinite(decimals) || decimals <= 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const step = Number(`1e-${decimals}`);
|
||||||
|
return Number.isFinite(step) ? step : Math.pow(10, -decimals);
|
||||||
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export class GridEngine {
|
|||||||
private readonly locks: OrderLockMap = {};
|
private readonly locks: OrderLockMap = {};
|
||||||
private readonly timers: OrderTimerMap = {};
|
private readonly timers: OrderTimerMap = {};
|
||||||
private readonly pendings: OrderPendingMap = {};
|
private readonly pendings: OrderPendingMap = {};
|
||||||
private readonly priceDecimals: number;
|
private priceDecimals: number;
|
||||||
private readonly now: () => number;
|
private readonly now: () => number;
|
||||||
private readonly configValid: boolean;
|
private readonly configValid: boolean;
|
||||||
private readonly gridLevels: number[];
|
private readonly gridLevels: number[];
|
||||||
@@ -134,6 +134,7 @@ export class GridEngine {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly log: LogHandler;
|
private readonly log: LogHandler;
|
||||||
|
private precisionSync: Promise<void> | null = null;
|
||||||
|
|
||||||
private timer: ReturnType<typeof setInterval> | null = null;
|
private timer: ReturnType<typeof setInterval> | null = null;
|
||||||
private processing = false;
|
private processing = false;
|
||||||
@@ -155,6 +156,7 @@ export class GridEngine {
|
|||||||
this.configValid = this.validateConfig();
|
this.configValid = this.validateConfig();
|
||||||
this.gridLevels = this.computeGridLevels();
|
this.gridLevels = this.computeGridLevels();
|
||||||
this.buildLevelMeta();
|
this.buildLevelMeta();
|
||||||
|
this.syncPrecision();
|
||||||
this.running = this.configValid;
|
this.running = this.configValid;
|
||||||
if (!this.configValid) {
|
if (!this.configValid) {
|
||||||
this.stopReason = "配置无效,已暂停网格";
|
this.stopReason = "配置无效,已暂停网格";
|
||||||
@@ -200,6 +202,52 @@ export class GridEngine {
|
|||||||
return this.buildSnapshot();
|
return this.buildSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private syncPrecision(): void {
|
||||||
|
if (this.precisionSync) return;
|
||||||
|
const getPrecision = this.exchange.getPrecision?.bind(this.exchange);
|
||||||
|
if (!getPrecision) return;
|
||||||
|
this.precisionSync = getPrecision()
|
||||||
|
.then((precision) => {
|
||||||
|
if (!precision) return;
|
||||||
|
let updated = false;
|
||||||
|
if (Number.isFinite(precision.priceTick) && precision.priceTick > 0) {
|
||||||
|
if (Math.abs(precision.priceTick - this.config.priceTick) > 1e-12) {
|
||||||
|
this.config.priceTick = precision.priceTick;
|
||||||
|
this.priceDecimals = decimalsOf(precision.priceTick);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Number.isFinite(precision.qtyStep) && precision.qtyStep > 0) {
|
||||||
|
if (Math.abs(precision.qtyStep - this.config.qtyStep) > 1e-12) {
|
||||||
|
this.config.qtyStep = precision.qtyStep;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updated) {
|
||||||
|
this.log(
|
||||||
|
"info",
|
||||||
|
`已同步交易精度: priceTick=${precision.priceTick} qtyStep=${precision.qtyStep}`
|
||||||
|
);
|
||||||
|
this.rebuildGridAfterPrecisionUpdate();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.log("error", `同步精度失败: ${extractMessage(error)}`);
|
||||||
|
this.precisionSync = null;
|
||||||
|
setTimeout(() => this.syncPrecision(), 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private rebuildGridAfterPrecisionUpdate(): void {
|
||||||
|
if (!this.configValid) return;
|
||||||
|
const reference = this.getReferencePrice();
|
||||||
|
const newLevels = this.computeGridLevels();
|
||||||
|
this.gridLevels.length = 0;
|
||||||
|
this.gridLevels.push(...newLevels);
|
||||||
|
this.buildLevelMeta(reference);
|
||||||
|
this.emitUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
private validateConfig(): boolean {
|
private validateConfig(): boolean {
|
||||||
if (this.config.lowerPrice <= 0 || this.config.upperPrice <= 0) {
|
if (this.config.lowerPrice <= 0 || this.config.upperPrice <= 0) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ export class MakerEngine {
|
|||||||
private readonly tradeLog: ReturnType<typeof createTradeLog>;
|
private readonly tradeLog: ReturnType<typeof createTradeLog>;
|
||||||
private readonly events = new StrategyEventEmitter<MakerEvent, MakerEngineSnapshot>();
|
private readonly events = new StrategyEventEmitter<MakerEvent, MakerEngineSnapshot>();
|
||||||
private readonly sessionVolume = new SessionVolumeTracker();
|
private readonly sessionVolume = new SessionVolumeTracker();
|
||||||
|
private priceTick: number = 0.1;
|
||||||
|
private qtyStep: number = 0.001;
|
||||||
|
private precisionSync: Promise<void> | null = null;
|
||||||
|
|
||||||
private timer: ReturnType<typeof setInterval> | null = null;
|
private timer: ReturnType<typeof setInterval> | null = null;
|
||||||
private processing = false;
|
private processing = false;
|
||||||
@@ -114,6 +117,9 @@ export class MakerEngine {
|
|||||||
this.rateLimit = new RateLimitController(this.config.refreshIntervalMs, (type, detail) =>
|
this.rateLimit = new RateLimitController(this.config.refreshIntervalMs, (type, detail) =>
|
||||||
this.tradeLog.push(type, detail)
|
this.tradeLog.push(type, detail)
|
||||||
);
|
);
|
||||||
|
this.priceTick = Math.max(1e-9, this.config.priceTick);
|
||||||
|
this.qtyStep = Math.max(1e-9, this.qtyStep);
|
||||||
|
this.syncPrecision();
|
||||||
this.bootstrap();
|
this.bootstrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +296,7 @@ export class MakerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 直接使用orderbook价格,格式化为字符串避免精度问题
|
// 直接使用orderbook价格,格式化为字符串避免精度问题
|
||||||
const priceDecimals = Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick)));
|
const priceDecimals = this.getPriceDecimals();
|
||||||
const closeBidPrice = formatPriceToString(topBid, priceDecimals);
|
const closeBidPrice = formatPriceToString(topBid, priceDecimals);
|
||||||
const closeAskPrice = formatPriceToString(topAsk, priceDecimals);
|
const closeAskPrice = formatPriceToString(topAsk, priceDecimals);
|
||||||
const bidPrice = formatPriceToString(topBid - this.config.bidOffset, priceDecimals);
|
const bidPrice = formatPriceToString(topBid - this.config.bidOffset, priceDecimals);
|
||||||
@@ -340,7 +346,7 @@ export class MakerEngine {
|
|||||||
if (Math.abs(position.positionAmt) < EPS) return;
|
if (Math.abs(position.positionAmt) < EPS) return;
|
||||||
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
|
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
|
||||||
if (topBid == null || topAsk == null) return;
|
if (topBid == null || topAsk == null) return;
|
||||||
const priceDecimals = Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick)));
|
const priceDecimals = this.getPriceDecimals();
|
||||||
const closeBidPrice = formatPriceToString(topBid, priceDecimals);
|
const closeBidPrice = formatPriceToString(topBid, priceDecimals);
|
||||||
const closeAskPrice = formatPriceToString(topAsk, priceDecimals);
|
const closeAskPrice = formatPriceToString(topAsk, priceDecimals);
|
||||||
await this.checkRisk(position, Number(closeBidPrice), Number(closeAskPrice));
|
await this.checkRisk(position, Number(closeBidPrice), Number(closeAskPrice));
|
||||||
@@ -431,8 +437,8 @@ export class MakerEngine {
|
|||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
priceTick: this.config.priceTick,
|
priceTick: this.priceTick,
|
||||||
qtyStep: 0.001, // 默认数量步长
|
qtyStep: this.qtyStep,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -527,6 +533,48 @@ export class MakerEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private syncPrecision(): void {
|
||||||
|
if (this.precisionSync) return;
|
||||||
|
const getPrecision = this.exchange.getPrecision?.bind(this.exchange);
|
||||||
|
if (!getPrecision) return;
|
||||||
|
this.precisionSync = getPrecision()
|
||||||
|
.then((precision) => {
|
||||||
|
if (!precision) return;
|
||||||
|
let updated = false;
|
||||||
|
if (Number.isFinite(precision.priceTick) && precision.priceTick > 0) {
|
||||||
|
if (Math.abs(precision.priceTick - this.priceTick) > 1e-12) {
|
||||||
|
this.priceTick = precision.priceTick;
|
||||||
|
this.config.priceTick = precision.priceTick;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Number.isFinite(precision.qtyStep) && precision.qtyStep > 0) {
|
||||||
|
if (Math.abs(precision.qtyStep - this.qtyStep) > 1e-12) {
|
||||||
|
this.qtyStep = precision.qtyStep;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updated) {
|
||||||
|
this.tradeLog.push(
|
||||||
|
"info",
|
||||||
|
`已同步交易精度: priceTick=${precision.priceTick} qtyStep=${precision.qtyStep}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.tradeLog.push("error", `同步精度失败: ${extractMessage(error)}`);
|
||||||
|
this.precisionSync = null;
|
||||||
|
setTimeout(() => this.syncPrecision(), 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPriceDecimals(): number {
|
||||||
|
const tick = Math.max(1e-9, this.priceTick);
|
||||||
|
const raw = Math.log10(1 / tick);
|
||||||
|
if (!Number.isFinite(raw)) return 0;
|
||||||
|
return Math.max(0, Math.floor(raw + 1e-9));
|
||||||
|
}
|
||||||
|
|
||||||
private emitUpdate(): void {
|
private emitUpdate(): void {
|
||||||
try {
|
try {
|
||||||
const snapshot = this.buildSnapshot();
|
const snapshot = this.buildSnapshot();
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ export class OffsetMakerEngine {
|
|||||||
private readonly tradeLog: ReturnType<typeof createTradeLog>;
|
private readonly tradeLog: ReturnType<typeof createTradeLog>;
|
||||||
private readonly events = new StrategyEventEmitter<MakerEvent, OffsetMakerEngineSnapshot>();
|
private readonly events = new StrategyEventEmitter<MakerEvent, OffsetMakerEngineSnapshot>();
|
||||||
private readonly sessionVolume = new SessionVolumeTracker();
|
private readonly sessionVolume = new SessionVolumeTracker();
|
||||||
|
private priceTick: number = 0.1;
|
||||||
|
private qtyStep: number = 0.001;
|
||||||
|
private precisionSync: Promise<void> | null = null;
|
||||||
|
|
||||||
private timer: ReturnType<typeof setInterval> | null = null;
|
private timer: ReturnType<typeof setInterval> | null = null;
|
||||||
private processing = false;
|
private processing = false;
|
||||||
@@ -93,6 +96,9 @@ export class OffsetMakerEngine {
|
|||||||
this.rateLimit = new RateLimitController(this.config.refreshIntervalMs, (type, detail) =>
|
this.rateLimit = new RateLimitController(this.config.refreshIntervalMs, (type, detail) =>
|
||||||
this.tradeLog.push(type, detail)
|
this.tradeLog.push(type, detail)
|
||||||
);
|
);
|
||||||
|
this.priceTick = Math.max(1e-9, this.config.priceTick);
|
||||||
|
this.qtyStep = Math.max(1e-9, this.qtyStep);
|
||||||
|
this.syncPrecision();
|
||||||
// Debounce window defaults to 3x refresh interval, min 1s
|
// Debounce window defaults to 3x refresh interval, min 1s
|
||||||
this.repriceDwellMs = Math.max(1000, this.config.refreshIntervalMs * 3);
|
this.repriceDwellMs = Math.max(1000, this.config.refreshIntervalMs * 3);
|
||||||
this.bootstrap();
|
this.bootstrap();
|
||||||
@@ -275,7 +281,7 @@ export class OffsetMakerEngine {
|
|||||||
const finalAsk = latestAsk ?? topAsk!;
|
const finalAsk = latestAsk ?? topAsk!;
|
||||||
|
|
||||||
// 直接使用orderbook价格,格式化为字符串避免精度问题
|
// 直接使用orderbook价格,格式化为字符串避免精度问题
|
||||||
const priceDecimals = Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick)));
|
const priceDecimals = this.getPriceDecimals();
|
||||||
const closeBidPrice = formatPriceToString(finalBid, priceDecimals);
|
const closeBidPrice = formatPriceToString(finalBid, priceDecimals);
|
||||||
const closeAskPrice = formatPriceToString(finalAsk, priceDecimals);
|
const closeAskPrice = formatPriceToString(finalAsk, priceDecimals);
|
||||||
const bidPrice = formatPriceToString(finalBid - this.config.bidOffset, priceDecimals);
|
const bidPrice = formatPriceToString(finalBid - this.config.bidOffset, priceDecimals);
|
||||||
@@ -326,7 +332,7 @@ export class OffsetMakerEngine {
|
|||||||
const absPosition = Math.abs(position.positionAmt);
|
const absPosition = Math.abs(position.positionAmt);
|
||||||
const side: "BUY" | "SELL" = position.positionAmt > 0 ? "SELL" : "BUY";
|
const side: "BUY" | "SELL" = position.positionAmt > 0 ? "SELL" : "BUY";
|
||||||
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
|
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
|
||||||
const priceDecimals = Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick)));
|
const priceDecimals = this.getPriceDecimals();
|
||||||
const closeBidPrice = topBid != null ? formatPriceToString(topBid, priceDecimals) : null;
|
const closeBidPrice = topBid != null ? formatPriceToString(topBid, priceDecimals) : null;
|
||||||
const closeAskPrice = topAsk != null ? formatPriceToString(topAsk, priceDecimals) : null;
|
const closeAskPrice = topAsk != null ? formatPriceToString(topAsk, priceDecimals) : null;
|
||||||
try {
|
try {
|
||||||
@@ -464,7 +470,7 @@ export class OffsetMakerEngine {
|
|||||||
const newPrice = Number(t.price);
|
const newPrice = Number(t.price);
|
||||||
const oldPrice = Number(existing.price);
|
const oldPrice = Number(existing.price);
|
||||||
if (!Number.isFinite(newPrice) || !Number.isFinite(oldPrice)) continue;
|
if (!Number.isFinite(newPrice) || !Number.isFinite(oldPrice)) continue;
|
||||||
const ticksDiff = Math.abs(newPrice - oldPrice) / this.config.priceTick;
|
const ticksDiff = Math.abs(newPrice - oldPrice) / this.priceTick;
|
||||||
const recentPlaced = this.lastEntryOrderBySide[t.side]?.ts ?? 0;
|
const recentPlaced = this.lastEntryOrderBySide[t.side]?.ts ?? 0;
|
||||||
const withinDwell = Date.now() - recentPlaced < this.repriceDwellMs;
|
const withinDwell = Date.now() - recentPlaced < this.repriceDwellMs;
|
||||||
if (ticksDiff < this.minRepriceTicks || withinDwell) {
|
if (ticksDiff < this.minRepriceTicks || withinDwell) {
|
||||||
@@ -529,8 +535,8 @@ export class OffsetMakerEngine {
|
|||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
priceTick: this.config.priceTick,
|
priceTick: this.priceTick,
|
||||||
qtyStep: 0.001, // 默认数量步长
|
qtyStep: this.qtyStep,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
// Record last placed entry order timing and price
|
// Record last placed entry order timing and price
|
||||||
@@ -620,6 +626,48 @@ export class OffsetMakerEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private syncPrecision(): void {
|
||||||
|
if (this.precisionSync) return;
|
||||||
|
const getPrecision = this.exchange.getPrecision?.bind(this.exchange);
|
||||||
|
if (!getPrecision) return;
|
||||||
|
this.precisionSync = getPrecision()
|
||||||
|
.then((precision) => {
|
||||||
|
if (!precision) return;
|
||||||
|
let updated = false;
|
||||||
|
if (Number.isFinite(precision.priceTick) && precision.priceTick > 0) {
|
||||||
|
if (Math.abs(precision.priceTick - this.priceTick) > 1e-12) {
|
||||||
|
this.priceTick = precision.priceTick;
|
||||||
|
this.config.priceTick = precision.priceTick;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Number.isFinite(precision.qtyStep) && precision.qtyStep > 0) {
|
||||||
|
if (Math.abs(precision.qtyStep - this.qtyStep) > 1e-12) {
|
||||||
|
this.qtyStep = precision.qtyStep;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updated) {
|
||||||
|
this.tradeLog.push(
|
||||||
|
"info",
|
||||||
|
`已同步交易精度: priceTick=${precision.priceTick} qtyStep=${precision.qtyStep}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.tradeLog.push("error", `同步精度失败: ${String(error)}`);
|
||||||
|
this.precisionSync = null;
|
||||||
|
setTimeout(() => this.syncPrecision(), 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPriceDecimals(): number {
|
||||||
|
const tick = Math.max(1e-9, this.priceTick);
|
||||||
|
const raw = Math.log10(1 / tick);
|
||||||
|
if (!Number.isFinite(raw)) return 0;
|
||||||
|
return Math.max(0, Math.floor(raw + 1e-9));
|
||||||
|
}
|
||||||
|
|
||||||
private emitUpdate(): void {
|
private emitUpdate(): void {
|
||||||
try {
|
try {
|
||||||
const snapshot = this.buildSnapshot();
|
const snapshot = this.buildSnapshot();
|
||||||
|
|||||||
@@ -123,12 +123,14 @@ export class TrendEngine {
|
|||||||
.digest("hex");
|
.digest("hex");
|
||||||
|
|
||||||
private readonly listeners = new Map<TrendEngineEvent, Set<TrendEngineListener>>();
|
private readonly listeners = new Map<TrendEngineEvent, Set<TrendEngineListener>>();
|
||||||
|
private precisionSync: Promise<void> | null = null;
|
||||||
|
|
||||||
constructor(private readonly config: TradingConfig, private readonly exchange: ExchangeAdapter) {
|
constructor(private readonly config: TradingConfig, private readonly exchange: ExchangeAdapter) {
|
||||||
this.tradeLog = createTradeLog(this.config.maxLogEntries);
|
this.tradeLog = createTradeLog(this.config.maxLogEntries);
|
||||||
this.rateLimit = new RateLimitController(this.config.pollIntervalMs, (type, detail) =>
|
this.rateLimit = new RateLimitController(this.config.pollIntervalMs, (type, detail) =>
|
||||||
this.tradeLog.push(type, detail)
|
this.tradeLog.push(type, detail)
|
||||||
);
|
);
|
||||||
|
this.syncPrecision();
|
||||||
this.bootstrap();
|
this.bootstrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -929,6 +931,42 @@ export class TrendEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private syncPrecision(): void {
|
||||||
|
if (this.precisionSync) return;
|
||||||
|
const getPrecision = this.exchange.getPrecision?.bind(this.exchange);
|
||||||
|
if (!getPrecision) return;
|
||||||
|
this.precisionSync = getPrecision()
|
||||||
|
.then((precision) => {
|
||||||
|
if (!precision) return;
|
||||||
|
let updated = false;
|
||||||
|
if (Number.isFinite(precision.priceTick) && precision.priceTick > 0) {
|
||||||
|
const delta = Math.abs(precision.priceTick - this.config.priceTick);
|
||||||
|
if (delta > 1e-12) {
|
||||||
|
this.config.priceTick = precision.priceTick;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Number.isFinite(precision.qtyStep) && precision.qtyStep > 0) {
|
||||||
|
const delta = Math.abs(precision.qtyStep - this.config.qtyStep);
|
||||||
|
if (delta > 1e-12) {
|
||||||
|
this.config.qtyStep = precision.qtyStep;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updated) {
|
||||||
|
this.tradeLog.push(
|
||||||
|
"info",
|
||||||
|
`已同步交易精度: priceTick=${precision.priceTick} qtyStep=${precision.qtyStep}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.tradeLog.push("error", `同步精度失败: ${extractMessage(error)}`);
|
||||||
|
this.precisionSync = null;
|
||||||
|
setTimeout(() => this.syncPrecision(), 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private emitUpdate(): void {
|
private emitUpdate(): void {
|
||||||
try {
|
try {
|
||||||
const snapshot = this.buildSnapshot();
|
const snapshot = this.buildSnapshot();
|
||||||
|
|||||||
Reference in New Issue
Block a user