feat: enhance precision synchronization in trading strategies and improve order quantity normalization logic

This commit is contained in:
discountry
2025-10-27 18:42:44 +08:00
parent 295c6a47b7
commit 5bba8169b5
8 changed files with 325 additions and 14 deletions
+49 -1
View File
@@ -80,7 +80,7 @@ export class GridEngine {
private readonly locks: OrderLockMap = {};
private readonly timers: OrderTimerMap = {};
private readonly pendings: OrderPendingMap = {};
private readonly priceDecimals: number;
private priceDecimals: number;
private readonly now: () => number;
private readonly configValid: boolean;
private readonly gridLevels: number[];
@@ -134,6 +134,7 @@ export class GridEngine {
};
private readonly log: LogHandler;
private precisionSync: Promise<void> | null = null;
private timer: ReturnType<typeof setInterval> | null = null;
private processing = false;
@@ -155,6 +156,7 @@ export class GridEngine {
this.configValid = this.validateConfig();
this.gridLevels = this.computeGridLevels();
this.buildLevelMeta();
this.syncPrecision();
this.running = this.configValid;
if (!this.configValid) {
this.stopReason = "配置无效,已暂停网格";
@@ -200,6 +202,52 @@ export class GridEngine {
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 {
if (this.config.lowerPrice <= 0 || this.config.upperPrice <= 0) {
return false;
+52 -4
View File
@@ -77,6 +77,9 @@ export class MakerEngine {
private readonly tradeLog: ReturnType<typeof createTradeLog>;
private readonly events = new StrategyEventEmitter<MakerEvent, MakerEngineSnapshot>();
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 processing = false;
@@ -114,6 +117,9 @@ export class MakerEngine {
this.rateLimit = new RateLimitController(this.config.refreshIntervalMs, (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();
}
@@ -290,7 +296,7 @@ export class MakerEngine {
}
// 直接使用orderbook价格,格式化为字符串避免精度问题
const priceDecimals = Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick)));
const priceDecimals = this.getPriceDecimals();
const closeBidPrice = formatPriceToString(topBid, priceDecimals);
const closeAskPrice = formatPriceToString(topAsk, priceDecimals);
const bidPrice = formatPriceToString(topBid - this.config.bidOffset, priceDecimals);
@@ -340,7 +346,7 @@ export class MakerEngine {
if (Math.abs(position.positionAmt) < EPS) return;
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
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 closeAskPrice = formatPriceToString(topAsk, priceDecimals);
await this.checkRisk(position, Number(closeBidPrice), Number(closeAskPrice));
@@ -431,8 +437,8 @@ export class MakerEngine {
maxPct: this.config.maxCloseSlippagePct,
},
{
priceTick: this.config.priceTick,
qtyStep: 0.001, // 默认数量步长
priceTick: this.priceTick,
qtyStep: this.qtyStep,
}
);
} 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 {
try {
const snapshot = this.buildSnapshot();
+53 -5
View File
@@ -64,6 +64,9 @@ export class OffsetMakerEngine {
private readonly tradeLog: ReturnType<typeof createTradeLog>;
private readonly events = new StrategyEventEmitter<MakerEvent, OffsetMakerEngineSnapshot>();
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 processing = false;
@@ -93,6 +96,9 @@ export class OffsetMakerEngine {
this.rateLimit = new RateLimitController(this.config.refreshIntervalMs, (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
this.repriceDwellMs = Math.max(1000, this.config.refreshIntervalMs * 3);
this.bootstrap();
@@ -275,7 +281,7 @@ export class OffsetMakerEngine {
const finalAsk = latestAsk ?? topAsk!;
// 直接使用orderbook价格,格式化为字符串避免精度问题
const priceDecimals = Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick)));
const priceDecimals = this.getPriceDecimals();
const closeBidPrice = formatPriceToString(finalBid, priceDecimals);
const closeAskPrice = formatPriceToString(finalAsk, priceDecimals);
const bidPrice = formatPriceToString(finalBid - this.config.bidOffset, priceDecimals);
@@ -326,7 +332,7 @@ export class OffsetMakerEngine {
const absPosition = Math.abs(position.positionAmt);
const side: "BUY" | "SELL" = position.positionAmt > 0 ? "SELL" : "BUY";
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 closeAskPrice = topAsk != null ? formatPriceToString(topAsk, priceDecimals) : null;
try {
@@ -464,7 +470,7 @@ export class OffsetMakerEngine {
const newPrice = Number(t.price);
const oldPrice = Number(existing.price);
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 withinDwell = Date.now() - recentPlaced < this.repriceDwellMs;
if (ticksDiff < this.minRepriceTicks || withinDwell) {
@@ -529,8 +535,8 @@ export class OffsetMakerEngine {
maxPct: this.config.maxCloseSlippagePct,
},
{
priceTick: this.config.priceTick,
qtyStep: 0.001, // 默认数量步长
priceTick: this.priceTick,
qtyStep: this.qtyStep,
}
);
// 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 {
try {
const snapshot = this.buildSnapshot();
+38
View File
@@ -123,12 +123,14 @@ export class TrendEngine {
.digest("hex");
private readonly listeners = new Map<TrendEngineEvent, Set<TrendEngineListener>>();
private precisionSync: Promise<void> | null = null;
constructor(private readonly config: TradingConfig, private readonly exchange: ExchangeAdapter) {
this.tradeLog = createTradeLog(this.config.maxLogEntries);
this.rateLimit = new RateLimitController(this.config.pollIntervalMs, (type, detail) =>
this.tradeLog.push(type, detail)
);
this.syncPrecision();
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 {
try {
const snapshot = this.buildSnapshot();