mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 17:28:08 +00:00
Add immediate depth protection logic to MakerPointsEngine
- Introduced a new private property `forceTickRequested` to manage immediate tick requests. - Implemented `shouldTriggerImmediateDepthProtection` method to trigger a tick when depth falls below the configured threshold. - Updated the tick processing logic to accommodate immediate depth protection. - Added unit tests to validate the immediate tick triggering behavior based on depth changes.
This commit is contained in:
@@ -128,6 +128,7 @@ export class MakerPointsEngine {
|
|||||||
private processing = false;
|
private processing = false;
|
||||||
private stopLossProcessing = false;
|
private stopLossProcessing = false;
|
||||||
private stopLossCooldownUntil = 0;
|
private stopLossCooldownUntil = 0;
|
||||||
|
private forceTickRequested = false;
|
||||||
private desiredOrders: DesiredOrder[] = [];
|
private desiredOrders: DesiredOrder[] = [];
|
||||||
private accountUnrealized = 0;
|
private accountUnrealized = 0;
|
||||||
private initialOrderSnapshotReady = false;
|
private initialOrderSnapshotReady = false;
|
||||||
@@ -341,6 +342,10 @@ export class MakerPointsEngine {
|
|||||||
this.lastStandxDepthTime = Date.now();
|
this.lastStandxDepthTime = Date.now();
|
||||||
this.feedStatus.depth = true;
|
this.feedStatus.depth = true;
|
||||||
this.emitUpdate();
|
this.emitUpdate();
|
||||||
|
if (this.shouldTriggerImmediateDepthProtection(depth)) {
|
||||||
|
this.forceTickRequested = true;
|
||||||
|
void this.tick();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
log,
|
log,
|
||||||
{
|
{
|
||||||
@@ -568,7 +573,9 @@ export class MakerPointsEngine {
|
|||||||
this.processing = true;
|
this.processing = true;
|
||||||
let hadRateLimit = false;
|
let hadRateLimit = false;
|
||||||
try {
|
try {
|
||||||
const decision = this.rateLimit.beforeCycle();
|
const forceRun = this.forceTickRequested;
|
||||||
|
this.forceTickRequested = false;
|
||||||
|
const decision = forceRun ? "run" : this.rateLimit.beforeCycle();
|
||||||
if (decision === "paused") {
|
if (decision === "paused") {
|
||||||
this.emitUpdate();
|
this.emitUpdate();
|
||||||
return;
|
return;
|
||||||
@@ -864,6 +871,43 @@ export class MakerPointsEngine {
|
|||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当深度从“满足阈值”切换到“不满足阈值”时,立即触发一次主循环,优先撤销不再安全的挂单。
|
||||||
|
*/
|
||||||
|
private shouldTriggerImmediateDepthProtection(depth: AsterDepth | null): boolean {
|
||||||
|
if (!depth) return false;
|
||||||
|
if (this.defenseMode || this.reconnectResetPending || this.stopLossProcessing) return false;
|
||||||
|
|
||||||
|
const minDepth = this.config.filterMinDepth;
|
||||||
|
if (minDepth <= 0) return false;
|
||||||
|
|
||||||
|
const { topBid, topAsk } = getTopPrices(depth);
|
||||||
|
if (topBid == null || topAsk == null) return false;
|
||||||
|
|
||||||
|
const targets = buildBpsTargets({
|
||||||
|
band0To10: this.config.enableBand0To10,
|
||||||
|
band10To30: this.config.enableBand10To30,
|
||||||
|
band30To100: this.config.enableBand30To100,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const bps of targets) {
|
||||||
|
const lastStatus = this.lastDepthOkStatus[bps];
|
||||||
|
if (!lastStatus) continue;
|
||||||
|
|
||||||
|
const buyPrice = topBid * (1 - bps / 10000);
|
||||||
|
const sellPrice = topAsk * (1 + bps / 10000);
|
||||||
|
const buyDepthQty = getDepthBetweenPrices(depth, "BUY", buyPrice);
|
||||||
|
const sellDepthQty = getDepthBetweenPrices(depth, "SELL", sellPrice);
|
||||||
|
const currentBuyOk = buyDepthQty >= minDepth;
|
||||||
|
const currentSellOk = sellDepthQty >= minDepth;
|
||||||
|
|
||||||
|
if (lastStatus.buy && !currentBuyOk) return true;
|
||||||
|
if (lastStatus.sell && !currentSellOk) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private buildCloseOnlyOrders(
|
private buildCloseOnlyOrders(
|
||||||
position: PositionSnapshot,
|
position: PositionSnapshot,
|
||||||
bid1: number,
|
bid1: number,
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
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<AsterOrder> {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelOrder(): Promise<void> {}
|
||||||
|
async cancelOrders(): Promise<void> {}
|
||||||
|
async cancelAllOrders(): Promise<void> {}
|
||||||
|
|
||||||
|
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("MakerPointsEngine immediate depth protection", () => {
|
||||||
|
it("triggers an immediate tick when depth drops below threshold", () => {
|
||||||
|
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: 10,
|
||||||
|
},
|
||||||
|
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).lastDepthOkStatus[9] = { buy: true, sell: true };
|
||||||
|
|
||||||
|
const tickSpy = vi.spyOn(engine as any, "tick").mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
adapter.emitDepth({
|
||||||
|
lastUpdateId: 1,
|
||||||
|
bids: [["100", "1"]],
|
||||||
|
asks: [["101", "1"]],
|
||||||
|
eventTime: Date.now(),
|
||||||
|
symbol: "BTC-USD",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tickSpy).toHaveBeenCalledTimes(1);
|
||||||
|
engine.stop();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user