From 6998afebb1eeb9fab4fdec216f73632315909d8a Mon Sep 17 00:00:00 2001 From: discountry Date: Sat, 7 Feb 2026 23:27:42 +0800 Subject: [PATCH] Refactor price calculation in MakerPointsEngine for improved accuracy - Replaced direct price calculations with a new method `normalizeDepthTargetPrice` to ensure valid target prices for buy and sell orders. - Updated all instances of price calculations in the MakerPointsEngine to utilize the new normalization method. - Added boundary tests for `getDepthBetweenPrices` to validate behavior when prices are exactly at the target. --- src/strategy/maker-points-engine.ts | 54 ++++++++++++++--------- tests/price-depth-filter-boundary.test.ts | 37 ++++++++++++++++ 2 files changed, 69 insertions(+), 22 deletions(-) create mode 100644 tests/price-depth-filter-boundary.test.ts diff --git a/src/strategy/maker-points-engine.ts b/src/strategy/maker-points-engine.ts index 0513b27..3251c80 100644 --- a/src/strategy/maker-points-engine.ts +++ b/src/strategy/maker-points-engine.ts @@ -772,17 +772,17 @@ export class MakerPointsEngine { const shouldCheckDepth = minDepth > 0; if (!skipBuy) { - const price = bid1 * (1 - bps / 10000); - if (Number.isFinite(price) && price > 0) { + const targetPrice = this.normalizeDepthTargetPrice(bid1 * (1 - bps / 10000), priceDecimals); + if (targetPrice != null) { if (shouldCheckDepth) { - const depthQty = getDepthBetweenPrices(depth, "BUY", price); + const depthQty = getDepthBetweenPrices(depth, "BUY", targetPrice); if (depthQty < minDepth) { this.logThinDepthSkip("BUY", bps, depthQty, minDepth); } else { this.resetThinDepthSkip("BUY", bps); desired.push({ side: "BUY", - price: formatPriceToString(price, priceDecimals), + price: formatPriceToString(targetPrice, priceDecimals), amount, reduceOnly: false, }); @@ -790,7 +790,7 @@ export class MakerPointsEngine { } else { desired.push({ side: "BUY", - price: formatPriceToString(price, priceDecimals), + price: formatPriceToString(targetPrice, priceDecimals), amount, reduceOnly: false, }); @@ -798,17 +798,17 @@ export class MakerPointsEngine { } } if (!skipSell) { - const price = ask1 * (1 + bps / 10000); - if (Number.isFinite(price) && price > 0) { + const targetPrice = this.normalizeDepthTargetPrice(ask1 * (1 + bps / 10000), priceDecimals); + if (targetPrice != null) { if (shouldCheckDepth) { - const depthQty = getDepthBetweenPrices(depth, "SELL", price); + const depthQty = getDepthBetweenPrices(depth, "SELL", targetPrice); if (depthQty < minDepth) { this.logThinDepthSkip("SELL", bps, depthQty, minDepth); } else { this.resetThinDepthSkip("SELL", bps); desired.push({ side: "SELL", - price: formatPriceToString(price, priceDecimals), + price: formatPriceToString(targetPrice, priceDecimals), amount, reduceOnly: false, }); @@ -816,7 +816,7 @@ export class MakerPointsEngine { } else { desired.push({ side: "SELL", - price: formatPriceToString(price, priceDecimals), + price: formatPriceToString(targetPrice, priceDecimals), amount, reduceOnly: false, }); @@ -839,6 +839,7 @@ export class MakerPointsEngine { ): boolean { const minDepth = this.config.filterMinDepth; if (minDepth <= 0) return false; + const priceDecimals = this.getPriceDecimals(); // 获取启用的所有档位 const targets = buildBpsTargets({ @@ -850,11 +851,11 @@ export class MakerPointsEngine { let changed = false; for (const bps of targets) { - const buyPrice = bid1 * (1 - bps / 10000); - const sellPrice = ask1 * (1 + bps / 10000); + const buyTargetPrice = this.normalizeDepthTargetPrice(bid1 * (1 - bps / 10000), priceDecimals); + const sellTargetPrice = this.normalizeDepthTargetPrice(ask1 * (1 + bps / 10000), priceDecimals); - const buyDepthQty = getDepthBetweenPrices(depth, "BUY", buyPrice); - const sellDepthQty = getDepthBetweenPrices(depth, "SELL", sellPrice); + const buyDepthQty = getDepthBetweenPrices(depth, "BUY", buyTargetPrice ?? 0); + const sellDepthQty = getDepthBetweenPrices(depth, "SELL", sellTargetPrice ?? 0); const currentBuyOk = buyDepthQty >= minDepth; const currentSellOk = sellDepthQty >= minDepth; @@ -889,15 +890,16 @@ export class MakerPointsEngine { 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 buyPrice = topBid * (1 - bps / 10000); - const sellPrice = topAsk * (1 + bps / 10000); - const buyDepthQty = getDepthBetweenPrices(depth, "BUY", buyPrice); - const sellDepthQty = getDepthBetweenPrices(depth, "SELL", sellPrice); + 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; @@ -1344,6 +1346,13 @@ 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(); @@ -1398,12 +1407,13 @@ export class MakerPointsEngine { if (!this.depthSnapshot || topBid == null || topAsk == null) { return bands; } + const priceDecimals = this.getPriceDecimals(); return bands.map((band) => { - 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); + 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); return { ...band, buyDepth, sellDepth }; }); } diff --git a/tests/price-depth-filter-boundary.test.ts b/tests/price-depth-filter-boundary.test.ts new file mode 100644 index 0000000..9274f1f --- /dev/null +++ b/tests/price-depth-filter-boundary.test.ts @@ -0,0 +1,37 @@ +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 + }); +});