From 00388f91664476a712eaa95fd9eaa8357f5c4c96 Mon Sep 17 00:00:00 2001 From: discountry Date: Wed, 21 Jan 2026 11:11:58 +0800 Subject: [PATCH] add filter --- src/config.ts | 3 + src/strategy/maker-points-engine.ts | 109 ++++++++++++++++++++++++---- src/utils/price.ts | 54 ++++++++++++++ 3 files changed, 152 insertions(+), 14 deletions(-) diff --git a/src/config.ts b/src/config.ts index a3211cf..a7efb77 100644 --- a/src/config.ts +++ b/src/config.ts @@ -199,6 +199,8 @@ export interface MakerPointsConfig { minRepriceBps: number; /** 是否根据 Binance 盘口深度失衡自动取消单边挂单,默认 true */ enableBinanceDepthCancel: boolean; + /** 0-10 bps 档位最小深度阈值 (BTC),盘口到目标价之间的挂单量低于此值则跳过该档位,默认 1 */ + band0To10MinDepth: number; } const defaultMakerPointsAmount = parseNumber(process.env.MAKER_POINTS_ORDER_AMOUNT, parseNumber(process.env.TRADE_AMOUNT, 0.001)); @@ -224,6 +226,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), + band0To10MinDepth: parseNumber(process.env.MAKER_POINTS_BAND_0_10_MIN_DEPTH, 1), }; export interface BasisArbConfig { diff --git a/src/strategy/maker-points-engine.ts b/src/strategy/maker-points-engine.ts index 7569c85..33bdbdb 100644 --- a/src/strategy/maker-points-engine.ts +++ b/src/strategy/maker-points-engine.ts @@ -13,7 +13,7 @@ import { isOrderActiveStatus } from "../utils/order-status"; import { getPosition, parseSymbolParts } from "../utils/strategy"; import type { PositionSnapshot } from "../utils/strategy"; import { computePositionPnl } from "../utils/pnl"; -import { getMidOrLast, getTopPrices } from "../utils/price"; +import { getDepthBetweenPrices, getMidOrLast, getTopPrices } from "../utils/price"; import { marketClose, placeOrder, @@ -504,6 +504,7 @@ export class MakerPointsEngine { ask1: topAsk, skipBuy, skipSell, + depth, }) : this.desiredOrders; @@ -542,8 +543,9 @@ export class MakerPointsEngine { ask1: number; skipBuy: boolean; skipSell: boolean; + depth: AsterDepth | null; }): DesiredOrder[] { - const { bid1, ask1, skipBuy, skipSell } = params; + const { bid1, ask1, skipBuy, skipSell, depth } = params; const targets = buildBpsTargets({ band0To10: this.config.enableBand0To10, @@ -555,6 +557,7 @@ export class MakerPointsEngine { const priceDecimals = this.getPriceDecimals(); const desired: DesiredOrder[] = []; + const minDepth = this.config.band0To10MinDepth; const getAmountForBps = (bps: number): number => { if (bps <= 10) return Number(this.config.band0To10Amount); @@ -566,26 +569,63 @@ export class MakerPointsEngine { const amount = getAmountForBps(bps); if (!Number.isFinite(amount) || amount <= 0) continue; + // 对于 0-10 bps 档位,检查深度是否足够 + const isBand0To10 = bps <= 10; + if (!skipBuy) { const price = bid1 * (1 - bps / 10000); if (Number.isFinite(price) && price > 0) { - desired.push({ - side: "BUY", - price: formatPriceToString(price, priceDecimals), - amount, - reduceOnly: false, - }); + // 0-10 bps 档位需要检查深度 + if (isBand0To10 && minDepth > 0) { + const depthQty = getDepthBetweenPrices(depth, "BUY", price); + if (depthQty < minDepth) { + this.logThinDepthSkip("BUY", bps, depthQty, minDepth); + // 跳过该档位的 BUY 挂单 + } else { + this.resetThinDepthSkip("BUY"); + desired.push({ + side: "BUY", + price: formatPriceToString(price, priceDecimals), + amount, + reduceOnly: false, + }); + } + } else { + desired.push({ + side: "BUY", + price: formatPriceToString(price, priceDecimals), + amount, + reduceOnly: false, + }); + } } } if (!skipSell) { const price = ask1 * (1 + bps / 10000); if (Number.isFinite(price) && price > 0) { - desired.push({ - side: "SELL", - price: formatPriceToString(price, priceDecimals), - amount, - reduceOnly: false, - }); + // 0-10 bps 档位需要检查深度 + if (isBand0To10 && minDepth > 0) { + const depthQty = getDepthBetweenPrices(depth, "SELL", price); + if (depthQty < minDepth) { + this.logThinDepthSkip("SELL", bps, depthQty, minDepth); + // 跳过该档位的 SELL 挂单 + } else { + this.resetThinDepthSkip("SELL"); + desired.push({ + side: "SELL", + price: formatPriceToString(price, priceDecimals), + amount, + reduceOnly: false, + }); + } + } else { + desired.push({ + side: "SELL", + price: formatPriceToString(price, priceDecimals), + amount, + reduceOnly: false, + }); + } } } } @@ -1127,6 +1167,47 @@ export class MakerPointsEngine { } } + private lastThinDepthSkipBuy = false; + private lastThinDepthSkipSell = false; + + /** + * 记录因深度不足而跳过挂单的日志 + * 使用状态跟踪避免重复日志 + */ + private logThinDepthSkip(side: "BUY" | "SELL", bps: number, depthQty: number, minDepth: number): void { + const isBuy = side === "BUY"; + const alreadySkipped = isBuy ? this.lastThinDepthSkipBuy : this.lastThinDepthSkipSell; + + if (!alreadySkipped) { + this.tradeLog.push( + "info", + `跳过 ${side} ${bps}bps 挂单: 深度 ${depthQty.toFixed(4)} BTC < ${minDepth} BTC` + ); + if (isBuy) { + this.lastThinDepthSkipBuy = true; + } else { + this.lastThinDepthSkipSell = true; + } + } + } + + /** + * 当深度恢复时重置跳过状态,允许下次再次记录 + */ + private resetThinDepthSkip(side: "BUY" | "SELL"): void { + if (side === "BUY") { + if (this.lastThinDepthSkipBuy) { + this.tradeLog.push("info", "BUY 深度恢复,继续挂单"); + this.lastThinDepthSkipBuy = false; + } + } else { + if (this.lastThinDepthSkipSell) { + this.tradeLog.push("info", "SELL 深度恢复,继续挂单"); + this.lastThinDepthSkipSell = false; + } + } + } + private registerInsufficientBalance(error: unknown): void { const now = Date.now(); const detail = extractMessage(error); diff --git a/src/utils/price.ts b/src/utils/price.ts index ec02379..273a628 100644 --- a/src/utils/price.ts +++ b/src/utils/price.ts @@ -56,4 +56,58 @@ export function getMidOrLast(depth?: AsterDepth | null, ticker?: AsterTicker | n return Number.isFinite(last) ? last : null; } +/** + * 计算从盘口一档到目标价格之间的挂单总量 + * @param depth 深度数据 + * @param side 挂单方向: BUY 检查 bids, SELL 检查 asks + * @param targetPrice 目标挂单价格 + * @returns 从一档到目标价格之间的挂单总量 (不包含目标价格本身) + */ +export function getDepthBetweenPrices( + depth: AsterDepth | null | undefined, + side: "BUY" | "SELL", + targetPrice: number +): number { + if (!depth) return 0; + if (!Number.isFinite(targetPrice) || targetPrice <= 0) return 0; + + let total = 0; + + if (side === "BUY") { + // BUY 订单挂在 bid 侧,检查从 bid1 到目标价格之间的所有 bids + // bids 按价格从高到低排序,目标价格 < bid1 + const bids = depth.bids ?? []; + for (const level of bids) { + const price = Number(level[0]); + const qty = Number(level[1]); + if (!Number.isFinite(price) || !Number.isFinite(qty)) continue; + // 只计算价格 > 目标价格的档位 (目标价格以上的挂单) + if (price > targetPrice) { + total += qty; + } else { + // bids 是从高到低排序,一旦 price <= targetPrice 就停止 + break; + } + } + } else { + // SELL 订单挂在 ask 侧,检查从 ask1 到目标价格之间的所有 asks + // asks 按价格从低到高排序,目标价格 > ask1 + const asks = depth.asks ?? []; + for (const level of asks) { + const price = Number(level[0]); + const qty = Number(level[1]); + if (!Number.isFinite(price) || !Number.isFinite(qty)) continue; + // 只计算价格 < 目标价格的档位 (目标价格以下的挂单) + if (price < targetPrice) { + total += qty; + } else { + // asks 是从低到高排序,一旦 price >= targetPrice 就停止 + break; + } + } + } + + return total; +} +