2 Commits
Author SHA1 Message Date
discountry 3b935b7979 Refactor MakerPoints configuration and depth handling
- Renamed `band0To10MinDepth` to `filterMinDepth` in `config.ts` for clarity.
- Updated `MakerPointsEngine` to utilize the new `filterMinDepth` for depth checks across all bands.
- Introduced a method to track depth status changes, enhancing order placement logic based on market depth.
- Improved logging for depth-related order skips to provide clearer insights into trading decisions.
2026-01-21 11:26:21 +08:00
discountry 00388f9166 add filter 2026-01-21 11:11:58 +08:00
3 changed files with 185 additions and 14 deletions
+3
View File
@@ -199,6 +199,8 @@ export interface MakerPointsConfig {
minRepriceBps: number; minRepriceBps: number;
/** 是否根据 Binance 盘口深度失衡自动取消单边挂单,默认 true */ /** 是否根据 Binance 盘口深度失衡自动取消单边挂单,默认 true */
enableBinanceDepthCancel: boolean; enableBinanceDepthCancel: boolean;
/** 各档位最小深度阈值 (BTC),盘口到目标价之间的挂单量低于此值则跳过该档位,默认 1 */
filterMinDepth: number;
} }
const defaultMakerPointsAmount = parseNumber(process.env.MAKER_POINTS_ORDER_AMOUNT, parseNumber(process.env.TRADE_AMOUNT, 0.001)); 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), band30To100Amount: parseNumber(process.env.MAKER_POINTS_BAND_30_100_AMOUNT, defaultMakerPointsAmount),
minRepriceBps: parseNumber(process.env.MAKER_POINTS_MIN_REPRICE_BPS, 3), minRepriceBps: parseNumber(process.env.MAKER_POINTS_MIN_REPRICE_BPS, 3),
enableBinanceDepthCancel: parseBoolean(process.env.MAKER_POINTS_BINANCE_DEPTH_CANCEL, true), enableBinanceDepthCancel: parseBoolean(process.env.MAKER_POINTS_BINANCE_DEPTH_CANCEL, true),
filterMinDepth: parseNumber(process.env.MAKER_POINTS_FILTER_MIN_DEPTH, 1),
}; };
export interface BasisArbConfig { export interface BasisArbConfig {
+116 -2
View File
@@ -13,7 +13,7 @@ import { isOrderActiveStatus } from "../utils/order-status";
import { getPosition, parseSymbolParts } from "../utils/strategy"; import { getPosition, parseSymbolParts } from "../utils/strategy";
import type { PositionSnapshot } from "../utils/strategy"; import type { PositionSnapshot } from "../utils/strategy";
import { computePositionPnl } from "../utils/pnl"; import { computePositionPnl } from "../utils/pnl";
import { getMidOrLast, getTopPrices } from "../utils/price"; import { getDepthBetweenPrices, getMidOrLast, getTopPrices } from "../utils/price";
import { import {
marketClose, marketClose,
placeOrder, placeOrder,
@@ -124,6 +124,8 @@ export class MakerPointsEngine {
private lastSkipSell = false; private lastSkipSell = false;
private lastQuoteBid1: number | null = null; private lastQuoteBid1: number | null = null;
private lastQuoteAsk1: number | null = null; private lastQuoteAsk1: number | null = null;
// 跟踪各档位深度是否足够的状态 (按 bps 值索引)
private lastDepthOkStatus: Record<number, { buy: boolean; sell: boolean }> = {};
private readinessLogged = { private readinessLogged = {
account: false, account: false,
@@ -489,11 +491,13 @@ export class MakerPointsEngine {
const closeOnlyChanged = closeOnly !== prevCloseOnly; const closeOnlyChanged = closeOnly !== prevCloseOnly;
const skipChanged = skipBuy !== prevSkipBuy || skipSell !== prevSkipSell; const skipChanged = skipBuy !== prevSkipBuy || skipSell !== prevSkipSell;
const repriceNeeded = closeOnly ? true : this.shouldReprice(topBid, topAsk); const repriceNeeded = closeOnly ? true : this.shouldReprice(topBid, topAsk);
const depthStatusChanged = this.checkDepthStatusChanged(depth, topBid, topAsk);
const shouldRecompute = const shouldRecompute =
closeOnly || closeOnly ||
repriceNeeded || repriceNeeded ||
closeOnlyChanged || closeOnlyChanged ||
skipChanged || skipChanged ||
depthStatusChanged ||
this.desiredOrders.length === 0; this.desiredOrders.length === 0;
const desired = shouldRecompute const desired = shouldRecompute
@@ -504,6 +508,7 @@ export class MakerPointsEngine {
ask1: topAsk, ask1: topAsk,
skipBuy, skipBuy,
skipSell, skipSell,
depth,
}) })
: this.desiredOrders; : this.desiredOrders;
@@ -542,8 +547,9 @@ export class MakerPointsEngine {
ask1: number; ask1: number;
skipBuy: boolean; skipBuy: boolean;
skipSell: boolean; skipSell: boolean;
depth: AsterDepth | null;
}): DesiredOrder[] { }): DesiredOrder[] {
const { bid1, ask1, skipBuy, skipSell } = params; const { bid1, ask1, skipBuy, skipSell, depth } = params;
const targets = buildBpsTargets({ const targets = buildBpsTargets({
band0To10: this.config.enableBand0To10, band0To10: this.config.enableBand0To10,
@@ -555,6 +561,7 @@ export class MakerPointsEngine {
const priceDecimals = this.getPriceDecimals(); const priceDecimals = this.getPriceDecimals();
const desired: DesiredOrder[] = []; const desired: DesiredOrder[] = [];
const minDepth = this.config.filterMinDepth;
const getAmountForBps = (bps: number): number => { const getAmountForBps = (bps: number): number => {
if (bps <= 10) return Number(this.config.band0To10Amount); if (bps <= 10) return Number(this.config.band0To10Amount);
@@ -566,9 +573,26 @@ export class MakerPointsEngine {
const amount = getAmountForBps(bps); const amount = getAmountForBps(bps);
if (!Number.isFinite(amount) || amount <= 0) continue; if (!Number.isFinite(amount) || amount <= 0) continue;
// 所有档位都检查深度
const shouldCheckDepth = minDepth > 0;
if (!skipBuy) { if (!skipBuy) {
const price = bid1 * (1 - bps / 10000); const price = bid1 * (1 - bps / 10000);
if (Number.isFinite(price) && price > 0) { if (Number.isFinite(price) && price > 0) {
if (shouldCheckDepth) {
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(price, priceDecimals),
amount,
reduceOnly: false,
});
}
} else {
desired.push({ desired.push({
side: "BUY", side: "BUY",
price: formatPriceToString(price, priceDecimals), price: formatPriceToString(price, priceDecimals),
@@ -577,9 +601,24 @@ export class MakerPointsEngine {
}); });
} }
} }
}
if (!skipSell) { if (!skipSell) {
const price = ask1 * (1 + bps / 10000); const price = ask1 * (1 + bps / 10000);
if (Number.isFinite(price) && price > 0) { if (Number.isFinite(price) && price > 0) {
if (shouldCheckDepth) {
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(price, priceDecimals),
amount,
reduceOnly: false,
});
}
} else {
desired.push({ desired.push({
side: "SELL", side: "SELL",
price: formatPriceToString(price, priceDecimals), price: formatPriceToString(price, priceDecimals),
@@ -589,10 +628,54 @@ export class MakerPointsEngine {
} }
} }
} }
}
return desired; return desired;
} }
/**
* 检查各档位的深度状态是否发生变化
* 当深度从足够变为不足,或从不足变为足够时,需要触发重新计算
*/
private checkDepthStatusChanged(
depth: AsterDepth | null,
bid1: number,
ask1: number
): boolean {
const minDepth = this.config.filterMinDepth;
if (minDepth <= 0) return false;
// 获取启用的所有档位
const targets = buildBpsTargets({
band0To10: this.config.enableBand0To10,
band10To30: this.config.enableBand10To30,
band30To100: this.config.enableBand30To100,
});
let changed = false;
for (const bps of targets) {
const buyPrice = bid1 * (1 - bps / 10000);
const sellPrice = ask1 * (1 + bps / 10000);
const buyDepthQty = getDepthBetweenPrices(depth, "BUY", buyPrice);
const sellDepthQty = getDepthBetweenPrices(depth, "SELL", sellPrice);
const currentBuyOk = buyDepthQty >= minDepth;
const currentSellOk = sellDepthQty >= minDepth;
const lastStatus = this.lastDepthOkStatus[bps];
if (lastStatus) {
if (lastStatus.buy !== currentBuyOk || lastStatus.sell !== currentSellOk) {
changed = true;
}
}
this.lastDepthOkStatus[bps] = { buy: currentBuyOk, sell: currentSellOk };
}
return changed;
}
private buildCloseOnlyOrders( private buildCloseOnlyOrders(
position: PositionSnapshot, position: PositionSnapshot,
bid1: number, bid1: number,
@@ -1127,6 +1210,37 @@ export class MakerPointsEngine {
} }
} }
// 跟踪各档位的深度跳过状态 (按 bps 和 side 索引)
private thinDepthSkipStatus: Record<string, boolean> = {};
/**
* 记录因深度不足而跳过挂单的日志
* 使用状态跟踪避免重复日志
*/
private logThinDepthSkip(side: "BUY" | "SELL", bps: number, depthQty: number, minDepth: number): void {
const key = `${side}_${bps}`;
const alreadySkipped = this.thinDepthSkipStatus[key];
if (!alreadySkipped) {
this.tradeLog.push(
"info",
`跳过 ${side} ${bps}bps 挂单: 深度 ${depthQty.toFixed(4)} BTC < ${minDepth} BTC`
);
this.thinDepthSkipStatus[key] = true;
}
}
/**
* 当深度恢复时重置跳过状态,允许下次再次记录
*/
private resetThinDepthSkip(side: "BUY" | "SELL", bps: number): void {
const key = `${side}_${bps}`;
if (this.thinDepthSkipStatus[key]) {
this.tradeLog.push("info", `${side} ${bps}bps 深度恢复,继续挂单`);
this.thinDepthSkipStatus[key] = false;
}
}
private registerInsufficientBalance(error: unknown): void { private registerInsufficientBalance(error: unknown): void {
const now = Date.now(); const now = Date.now();
const detail = extractMessage(error); const detail = extractMessage(error);
+54
View File
@@ -56,4 +56,58 @@ export function getMidOrLast(depth?: AsterDepth | null, ticker?: AsterTicker | n
return Number.isFinite(last) ? last : null; 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;
}