From 3f67b9929162bd7efa3a797d8b9eba4a80498076 Mon Sep 17 00:00:00 2001 From: discountry Date: Sun, 8 Feb 2026 00:08:38 +0800 Subject: [PATCH] Add immediate reprice logic to MakerPointsEngine - Enhanced the MakerPointsEngine by introducing a new method `shouldTriggerImmediateReprice` to trigger an immediate tick when the market depth deviates beyond a specified minimum reprice basis points threshold. - Updated the existing depth protection logic to include this new reprice condition. - Added a comprehensive test suite to validate the immediate reprice functionality and its integration with the MakerPoints engine. --- src/strategy/maker-points-engine.ts | 20 +++- tests/maker-points-immediate-reprice.test.ts | 116 +++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 tests/maker-points-immediate-reprice.test.ts diff --git a/src/strategy/maker-points-engine.ts b/src/strategy/maker-points-engine.ts index 87164b2..7084ba2 100644 --- a/src/strategy/maker-points-engine.ts +++ b/src/strategy/maker-points-engine.ts @@ -346,7 +346,7 @@ export class MakerPointsEngine { this.lastStandxDepthTime = Date.now(); this.feedStatus.depth = true; this.emitUpdate(); - if (this.shouldTriggerImmediateDepthProtection(depth)) { + if (this.shouldTriggerImmediateDepthProtection(depth) || this.shouldTriggerImmediateReprice(depth)) { this.forceTickRequested = true; void this.tick(); } @@ -914,6 +914,24 @@ export class MakerPointsEngine { return false; } + /** + * 当盘口相对上次报价偏移超过 minRepriceBps 时,立即触发一次主循环,优先撤销旧报价。 + */ + private shouldTriggerImmediateReprice(depth: AsterDepth | null): boolean { + if (!depth) return false; + if (this.defenseMode || this.reconnectResetPending || this.stopLossProcessing) return false; + + const hasActiveEntryOrders = this.openOrders.some( + (order) => order.symbol === this.config.symbol && !order.reduceOnly && isOrderActiveStatus(order.status) + ); + if (!hasActiveEntryOrders) return false; + + const { topBid, topAsk } = getTopPrices(depth); + if (topBid == null || topAsk == null) return false; + + return this.shouldReprice(topBid, topAsk); + } + private buildCloseOnlyOrders( position: PositionSnapshot, bid1: number, diff --git a/tests/maker-points-immediate-reprice.test.ts b/tests/maker-points-immediate-reprice.test.ts new file mode 100644 index 0000000..0f050f4 --- /dev/null +++ b/tests/maker-points-immediate-reprice.test.ts @@ -0,0 +1,116 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { ExchangeAdapter } from "../src/exchanges/adapter"; +import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types"; +import { MakerPointsEngine } from "../src/strategy/maker-points-engine"; + +class StubAdapter implements ExchangeAdapter { + id = "standx"; + + private depthListeners: Array<(depth: AsterDepth) => void> = []; + + supportsTrailingStops(): boolean { + return false; + } + + watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {} + watchOrders(_cb: (orders: AsterOrder[]) => void): void {} + watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {} + watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {} + + watchDepth(_symbol: string, cb: (depth: AsterDepth) => void): void { + this.depthListeners.push(cb); + } + + emitDepth(depth: AsterDepth): void { + for (const listener of this.depthListeners) { + listener(depth); + } + } + + async createOrder(): Promise { + throw new Error("not implemented"); + } + + async cancelOrder(): Promise {} + async cancelOrders(): Promise {} + async cancelAllOrders(): Promise {} + + async queryAccountSnapshot(): Promise { + return null; + } +} + +afterEach(() => { + vi.restoreAllMocks(); + vi.useRealTimers(); +}); + +describe("MakerPointsEngine immediate reprice", () => { + it("triggers an immediate tick when min reprice bps threshold is reached", () => { + vi.useFakeTimers(); + const adapter = new StubAdapter(); + + const engine = new MakerPointsEngine( + { + symbol: "BTC-USD", + perOrderAmount: 0.01, + closeThreshold: 0, + stopLossUsd: 1, + refreshIntervalMs: 10_000, + maxLogEntries: 20, + maxCloseSlippagePct: 0.05, + priceTick: 0.1, + qtyStep: 0.001, + enableBand0To10: true, + enableBand10To30: false, + enableBand30To100: false, + band0To10Amount: 0.01, + band10To30Amount: 0.01, + band30To100Amount: 0.01, + minRepriceBps: 3, + enableBinanceDepthCancel: false, + filterMinDepth: 0, + }, + adapter + ); + + (engine as any).feedStatus = { account: true, depth: true, ticker: true, orders: true, binance: true }; + (engine as any).initialOrderSnapshotReady = true; + (engine as any).defenseMode = false; + (engine as any).reconnectResetPending = false; + (engine as any).stopLossProcessing = false; + (engine as any).lastQuoteBid1 = 100; + (engine as any).lastQuoteAsk1 = 101; + (engine as any).openOrders = [ + { + orderId: 1, + clientOrderId: "entry-order", + symbol: "BTC-USD", + side: "BUY", + type: "LIMIT", + status: "NEW", + price: "99.0", + origQty: "0.01", + executedQty: "0", + stopPrice: "0", + time: Date.now(), + updateTime: Date.now(), + reduceOnly: false, + closePosition: false, + }, + ]; + + const tickSpy = vi.spyOn(engine as any, "tick").mockResolvedValue(undefined); + + adapter.emitDepth({ + lastUpdateId: 1, + bids: [["99.9", "1"]], + asks: [["100.9", "1"]], + eventTime: Date.now(), + symbol: "BTC-USD", + }); + + expect(tickSpy).toHaveBeenCalledTimes(1); + engine.stop(); + }); +});