Author SHA1 Message Date
discountry 2a1cf24510 Enhance Binance depth health monitoring and defense mode logic
- Updated translations for Binance depth status messages to include depth window information.
- Modified `MakerPointsEngine` to incorporate health checks for the Binance depth tracker, including handling of unhealthy states.
- Improved defense mode activation logic to respond to Binance depth health status, ensuring appropriate logging and notifications.
- Added integration tests for defense mode behavior based on Binance depth health, validating transitions into and out of defense mode.
- Refactored `BinanceDepthTracker` to support health checks and improved connection management.
2026-02-07 21:54:22 +08:00
12 changed files with 27 additions and 482 deletions
-4
View File
@@ -59,10 +59,6 @@ MAKER_REFRESH_INTERVAL_MS=500 # Maker refresh cadence (ms)
MAKER_MAX_CLOSE_SLIPPAGE_PCT=0.05 # Maker close slippage guard (fallbacks to MAX_CLOSE_SLIPPAGE_PCT)
MAKER_PRICE_TICK=0.1 # Maker price tick size (defaults to PRICE_TICK)
# Maker-points Binance depth imbalance monitor
MAKER_POINTS_BINANCE_DEPTH_WINDOW_BPS=3 # Binance depth monitor window around best bid/ask (bps)
MAKER_POINTS_BINANCE_DEPTH_IMBALANCE_RATIO=9 # Imbalance threshold ratio (e.g. 9 => one side >= 9x)
# Grid strategy defaults
GRID_LOWER_PRICE=25000 # Grid lower bound price (quote currency)
GRID_UPPER_PRICE=35000 # Grid upper bound price
-6
View File
@@ -132,8 +132,6 @@ MAKER_POINTS_ORDER_AMOUNT=0.01
MAKER_POINTS_CLOSE_THRESHOLD=0.1
MAKER_POINTS_STOP_LOSS_USD=0
MAKER_POINTS_MIN_REPRICE_BPS=3
MAKER_POINTS_BINANCE_DEPTH_WINDOW_BPS=3
MAKER_POINTS_BINANCE_DEPTH_IMBALANCE_RATIO=9
# ===== 挂单档位开关 =====
MAKER_POINTS_BAND_0_10=true
@@ -175,8 +173,6 @@ MAKER_POINTS_ORDER_AMOUNT=0.01
MAKER_POINTS_CLOSE_THRESHOLD=0.1
MAKER_POINTS_STOP_LOSS_USD=0
MAKER_POINTS_MIN_REPRICE_BPS=3
MAKER_POINTS_BINANCE_DEPTH_WINDOW_BPS=3
MAKER_POINTS_BINANCE_DEPTH_IMBALANCE_RATIO=9
MAKER_POINTS_BAND_0_10=true
MAKER_POINTS_BAND_10_30=true
MAKER_POINTS_BAND_30_100=true
@@ -218,8 +214,6 @@ bun run pm2:start:maker-points
| `MAKER_POINTS_ORDER_AMOUNT` | 每笔挂单数量 | 建议 `0.01` 起步 |
| `MAKER_POINTS_CLOSE_THRESHOLD` | 持仓达到多少开始平仓 | 设为 `0` 表示不自动平仓 |
| `MAKER_POINTS_STOP_LOSS_USD` | 亏损多少美元强制平仓 | 设为 `0` 表示关闭止损 |
| `MAKER_POINTS_BINANCE_DEPTH_WINDOW_BPS` | Binance 失衡检测窗口(bps | 默认 `3` |
| `MAKER_POINTS_BINANCE_DEPTH_IMBALANCE_RATIO` | Binance 失衡比例阈值 | 默认 `9` |
| `MAKER_POINTS_BAND_*` | 三个挂单档位的开关 | 全部 `true` 即可 |
| `STANDX_TOKEN_CREATE_DATE` | Token 创建日期 | 推荐配置,格式 YYYY-MM-DD |
| `STANDX_TOKEN_VALIDITY_DAYS` | Token 有效期天数 | 推荐配置,与创建日期配合使用 |
+2 -8
View File
@@ -227,11 +227,7 @@ export interface MakerPointsConfig {
minRepriceBps: number;
/** 是否根据 Binance 盘口深度失衡自动取消单边挂单,默认 true */
enableBinanceDepthCancel: boolean;
/** Binance 深度监控窗口(bps,默认 3 */
binanceDepthWindowBps?: number;
/** Binance 深度失衡比例阈值,默认 9 */
binanceDepthImbalanceRatio?: number;
/** 各档位最小深度阈值 (BTC),盘口到目标价之间的挂单量低于此值则跳过该档位,默认 10 */
/** 各档位最小深度阈值 (BTC),盘口到目标价之间的挂单量低于此值则跳过该档位,默认 50 */
filterMinDepth: number;
}
@@ -258,9 +254,7 @@ export const makerPointsConfig: MakerPointsConfig = {
band30To100Amount: parseNumber(process.env.MAKER_POINTS_BAND_30_100_AMOUNT, defaultMakerPointsAmount),
minRepriceBps: parseNumber(process.env.MAKER_POINTS_MIN_REPRICE_BPS, 3),
enableBinanceDepthCancel: parseBoolean(process.env.MAKER_POINTS_BINANCE_DEPTH_CANCEL, true),
binanceDepthWindowBps: parseNumber(process.env.MAKER_POINTS_BINANCE_DEPTH_WINDOW_BPS, 3),
binanceDepthImbalanceRatio: parseNumber(process.env.MAKER_POINTS_BINANCE_DEPTH_IMBALANCE_RATIO, 9),
filterMinDepth: parseNumber(process.env.MAKER_POINTS_FILTER_MIN_DEPTH, 10),
filterMinDepth: parseNumber(process.env.MAKER_POINTS_FILTER_MIN_DEPTH, 50),
};
export interface BasisArbConfig {
+2 -2
View File
@@ -248,8 +248,8 @@ const translations: Record<string, TranslationEntry> = {
en: "Quote mode: {mode} | BUY {buy} | SELL {sell}",
},
"makerPoints.binanceLine": {
zh: "Binance 深度(±{windowBps}bps): 买 {buy} 卖 {sell} 状态: {status}",
en: "Binance depth (±{windowBps}bps): bid {buy} | ask {sell} | Status: {status}",
zh: "Binance 深度(±9bps): 买 {buy} 卖 {sell} 状态: {status}",
en: "Binance depth (±9bps): bid {buy} | ask {sell} | Status: {status}",
},
"makerPoints.bandDepthLine": {
zh: "StandX 档位 {band}bps 深度: 买 {buy} 卖 {sell}",
+1 -1
View File
@@ -19,7 +19,7 @@ const RECONNECT_DELAY_MAX_MS = 60_000;
const DEFAULT_REFRESH_SYNC_INTERVAL_MS = 30_000;
const DEFAULT_DEPTH_WINDOW_BPS = 9;
const DEFAULT_IMBALANCE_RATIO = 2;
const DEFAULT_IMBALANCE_RATIO = 9;
const MAX_BUFFER_SIZE = 5000;
const SYNC_SNAPSHOT_MAX_RETRIES = 5;
const REST_FAILURE_DEFENSE_THRESHOLD = 1;
+21 -97
View File
@@ -128,7 +128,6 @@ export class MakerPointsEngine {
private processing = false;
private stopLossProcessing = false;
private stopLossCooldownUntil = 0;
private forceTickRequested = false;
private desiredOrders: DesiredOrder[] = [];
private accountUnrealized = 0;
private initialOrderSnapshotReady = false;
@@ -206,12 +205,8 @@ export class MakerPointsEngine {
baseUrl: process.env.BINANCE_SPOT_WS_URL ?? process.env.BINANCE_WS_URL,
restBaseUrl: process.env.BINANCE_REST_URL,
levels: 20,
ratio: Number.isFinite(this.config.binanceDepthImbalanceRatio)
? Math.max(1.01, Number(this.config.binanceDepthImbalanceRatio))
: 9,
depthWindowBps: Number.isFinite(this.config.binanceDepthWindowBps)
? Math.max(1, Number(this.config.binanceDepthWindowBps))
: 3,
ratio: 9,
depthWindowBps: 9,
speedMs: 100,
logger: (context, error) => {
this.tradeLog.push("warn", `Binance ${context} 异常: ${extractMessage(error)}`);
@@ -346,10 +341,6 @@ export class MakerPointsEngine {
this.lastStandxDepthTime = Date.now();
this.feedStatus.depth = true;
this.emitUpdate();
if (this.shouldTriggerImmediateDepthProtection(depth) || this.shouldTriggerImmediateReprice(depth)) {
this.forceTickRequested = true;
void this.tick();
}
},
log,
{
@@ -577,9 +568,7 @@ export class MakerPointsEngine {
this.processing = true;
let hadRateLimit = false;
try {
const forceRun = this.forceTickRequested;
this.forceTickRequested = false;
const decision = forceRun ? "run" : this.rateLimit.beforeCycle();
const decision = this.rateLimit.beforeCycle();
if (decision === "paused") {
this.emitUpdate();
return;
@@ -776,17 +765,17 @@ export class MakerPointsEngine {
const shouldCheckDepth = minDepth > 0;
if (!skipBuy) {
const targetPrice = this.normalizeDepthTargetPrice(bid1 * (1 - bps / 10000), priceDecimals);
if (targetPrice != null) {
const price = bid1 * (1 - bps / 10000);
if (Number.isFinite(price) && price > 0) {
if (shouldCheckDepth) {
const depthQty = getDepthBetweenPrices(depth, "BUY", targetPrice);
const depthQty = getDepthBetweenPrices(depth, "BUY", price);
if (depthQty < minDepth) {
this.logThinDepthSkip("BUY", bps, depthQty, minDepth);
} else {
this.resetThinDepthSkip("BUY", bps);
desired.push({
side: "BUY",
price: formatPriceToString(targetPrice, priceDecimals),
price: formatPriceToString(price, priceDecimals),
amount,
reduceOnly: false,
});
@@ -794,7 +783,7 @@ export class MakerPointsEngine {
} else {
desired.push({
side: "BUY",
price: formatPriceToString(targetPrice, priceDecimals),
price: formatPriceToString(price, priceDecimals),
amount,
reduceOnly: false,
});
@@ -802,17 +791,17 @@ export class MakerPointsEngine {
}
}
if (!skipSell) {
const targetPrice = this.normalizeDepthTargetPrice(ask1 * (1 + bps / 10000), priceDecimals);
if (targetPrice != null) {
const price = ask1 * (1 + bps / 10000);
if (Number.isFinite(price) && price > 0) {
if (shouldCheckDepth) {
const depthQty = getDepthBetweenPrices(depth, "SELL", targetPrice);
const depthQty = getDepthBetweenPrices(depth, "SELL", price);
if (depthQty < minDepth) {
this.logThinDepthSkip("SELL", bps, depthQty, minDepth);
} else {
this.resetThinDepthSkip("SELL", bps);
desired.push({
side: "SELL",
price: formatPriceToString(targetPrice, priceDecimals),
price: formatPriceToString(price, priceDecimals),
amount,
reduceOnly: false,
});
@@ -820,7 +809,7 @@ export class MakerPointsEngine {
} else {
desired.push({
side: "SELL",
price: formatPriceToString(targetPrice, priceDecimals),
price: formatPriceToString(price, priceDecimals),
amount,
reduceOnly: false,
});
@@ -843,7 +832,6 @@ export class MakerPointsEngine {
): boolean {
const minDepth = this.config.filterMinDepth;
if (minDepth <= 0) return false;
const priceDecimals = this.getPriceDecimals();
// 获取启用的所有档位
const targets = buildBpsTargets({
@@ -855,11 +843,11 @@ export class MakerPointsEngine {
let changed = false;
for (const bps of targets) {
const buyTargetPrice = this.normalizeDepthTargetPrice(bid1 * (1 - bps / 10000), priceDecimals);
const sellTargetPrice = this.normalizeDepthTargetPrice(ask1 * (1 + bps / 10000), priceDecimals);
const buyPrice = bid1 * (1 - bps / 10000);
const sellPrice = ask1 * (1 + bps / 10000);
const buyDepthQty = getDepthBetweenPrices(depth, "BUY", buyTargetPrice ?? 0);
const sellDepthQty = getDepthBetweenPrices(depth, "SELL", sellTargetPrice ?? 0);
const buyDepthQty = getDepthBetweenPrices(depth, "BUY", buyPrice);
const sellDepthQty = getDepthBetweenPrices(depth, "SELL", sellPrice);
const currentBuyOk = buyDepthQty >= minDepth;
const currentSellOk = sellDepthQty >= minDepth;
@@ -876,62 +864,6 @@ export class MakerPointsEngine {
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,
});
const priceDecimals = this.getPriceDecimals();
for (const bps of targets) {
const lastStatus = this.lastDepthOkStatus[bps];
if (!lastStatus) continue;
const buyTargetPrice = this.normalizeDepthTargetPrice(topBid * (1 - bps / 10000), priceDecimals);
const sellTargetPrice = this.normalizeDepthTargetPrice(topAsk * (1 + bps / 10000), priceDecimals);
const buyDepthQty = getDepthBetweenPrices(depth, "BUY", buyTargetPrice ?? 0);
const sellDepthQty = getDepthBetweenPrices(depth, "SELL", sellTargetPrice ?? 0);
const currentBuyOk = buyDepthQty >= minDepth;
const currentSellOk = sellDepthQty >= minDepth;
if (lastStatus.buy && !currentBuyOk) return true;
if (lastStatus.sell && !currentSellOk) return true;
}
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,
@@ -1368,13 +1300,6 @@ export class MakerPointsEngine {
return Math.max(0, Math.floor(raw + 1e-9));
}
private normalizeDepthTargetPrice(price: number, priceDecimals: number): number | null {
if (!Number.isFinite(price) || price <= 0) return null;
const normalized = Number(formatPriceToString(price, priceDecimals));
if (!Number.isFinite(normalized) || normalized <= 0) return null;
return normalized;
}
private emitUpdate(): void {
try {
const snapshot = this.buildSnapshot();
@@ -1429,13 +1354,12 @@ export class MakerPointsEngine {
if (!this.depthSnapshot || topBid == null || topAsk == null) {
return bands;
}
const priceDecimals = this.getPriceDecimals();
return bands.map((band) => {
const buyTargetPrice = this.normalizeDepthTargetPrice(topBid * (1 - band.bps / 10000), priceDecimals);
const sellTargetPrice = this.normalizeDepthTargetPrice(topAsk * (1 + band.bps / 10000), priceDecimals);
const buyDepth = getDepthBetweenPrices(this.depthSnapshot, "BUY", buyTargetPrice ?? 0);
const sellDepth = getDepthBetweenPrices(this.depthSnapshot, "SELL", sellTargetPrice ?? 0);
const buyPrice = topBid * (1 - band.bps / 10000);
const sellPrice = topAsk * (1 + band.bps / 10000);
const buyDepth = getDepthBetweenPrices(this.depthSnapshot, "BUY", buyPrice);
const sellDepth = getDepthBetweenPrices(this.depthSnapshot, "SELL", sellPrice);
return { ...band, buyDepth, sellDepth };
});
}
-1
View File
@@ -162,7 +162,6 @@ export function MakerPointsApp({ onExit }: MakerPointsAppProps) {
{t("makerPoints.binanceLine", {
buy: formatNumber(snapshot.binanceDepth?.buySum ?? 0, 4),
sell: formatNumber(snapshot.binanceDepth?.sellSum ?? 0, 4),
windowBps: snapshot.binanceDepth?.windowBps ?? 5,
status: imbalanceLabel,
})}
</Text>
+1 -4
View File
@@ -28,14 +28,10 @@ describe("config env parsing", () => {
process.env.EXCHANGE = "standx";
process.env.MAKER_POINTS_STOP_LOSS_USD = "1 # comment";
process.env.MAKER_POINTS_CLOSE_THRESHOLD = "2 ; comment";
process.env.MAKER_POINTS_BINANCE_DEPTH_WINDOW_BPS = "6 # comment";
process.env.MAKER_POINTS_BINANCE_DEPTH_IMBALANCE_RATIO = "9 ; comment";
const { makerPointsConfig } = await loadConfig();
expect(makerPointsConfig.stopLossUsd).toBe(1);
expect(makerPointsConfig.closeThreshold).toBe(2);
expect(makerPointsConfig.binanceDepthWindowBps).toBe(6);
expect(makerPointsConfig.binanceDepthImbalanceRatio).toBe(9);
});
it("parses boolean maker-points env values with inline comments", async () => {
@@ -46,3 +42,4 @@ describe("config env parsing", () => {
expect(makerPointsConfig.enableBand10To30).toBe(false);
});
});
@@ -1,109 +0,0 @@
import { describe, expect, it } from "vitest";
import type { ExchangeAdapter } from "../src/exchanges/adapter";
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
import { t } from "../src/i18n";
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
class StubAdapter implements ExchangeAdapter {
id = "standx";
supportsTrailingStops(): boolean {
return false;
}
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {}
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
async createOrder(): Promise<AsterOrder> {
throw new Error("not implemented");
}
async cancelOrder(): Promise<void> {}
async cancelOrders(): Promise<void> {}
async cancelAllOrders(): Promise<void> {}
}
describe("MakerPointsEngine Binance depth monitor config", () => {
it("uses default 3bps window and ratio 9", () => {
const engine = new MakerPointsEngine(
{
symbol: "BTC-USD",
perOrderAmount: 0.01,
closeThreshold: 0,
stopLossUsd: 1,
refreshIntervalMs: 500,
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: true,
filterMinDepth: 0,
},
new StubAdapter()
);
const trackerOptions = ((engine as any).binanceDepth as { options?: { depthWindowBps?: number; ratio?: number } })
.options;
expect(trackerOptions?.depthWindowBps).toBe(3);
expect(trackerOptions?.ratio).toBe(9);
engine.stop();
});
it("uses configured window and ratio", () => {
const engine = new MakerPointsEngine(
{
symbol: "BTC-USD",
perOrderAmount: 0.01,
closeThreshold: 0,
stopLossUsd: 1,
refreshIntervalMs: 500,
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: true,
binanceDepthWindowBps: 7,
binanceDepthImbalanceRatio: 11,
filterMinDepth: 0,
},
new StubAdapter()
);
const trackerOptions = ((engine as any).binanceDepth as { options?: { depthWindowBps?: number; ratio?: number } })
.options;
expect(trackerOptions?.depthWindowBps).toBe(7);
expect(trackerOptions?.ratio).toBe(11);
engine.stop();
});
it("renders binance depth line with dynamic window bps", () => {
const line = t(
"makerPoints.binanceLine",
{ windowBps: 5, buy: "1.23", sell: "1.11", status: "Balanced" },
"en"
);
expect(line).toContain("±5bps");
});
});
@@ -1,97 +0,0 @@
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();
});
});
@@ -1,116 +0,0 @@
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 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();
});
});
-37
View File
@@ -1,37 +0,0 @@
import { describe, expect, it } from "vitest";
import { getDepthBetweenPrices } from "../src/utils/price";
import type { AsterDepth } from "../src/exchanges/types";
describe("getDepthBetweenPrices boundary", () => {
it("SELL side excludes quantity exactly at target price", () => {
const depth: AsterDepth = {
lastUpdateId: 1,
bids: [],
asks: [
["69345", "1"],
["69349", "2"],
["69350", "999"],
["69351", "3"],
],
};
const total = getDepthBetweenPrices(depth, "SELL", 69350);
expect(total).toBe(3); // 仅 69345 + 69349
});
it("BUY side excludes quantity exactly at target price", () => {
const depth: AsterDepth = {
lastUpdateId: 1,
bids: [
["69355", "1"],
["69351", "2"],
["69350", "999"],
["69349", "3"],
],
asks: [],
};
const total = getDepthBetweenPrices(depth, "BUY", 69350);
expect(total).toBe(3); // 仅 69355 + 69351
});
});