From d5c4b047462193ca0ec5573e9823b33c274f7e45 Mon Sep 17 00:00:00 2001 From: discountry Date: Fri, 3 Oct 2025 07:15:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E5=B0=86=E4=BB=B7?= =?UTF-8?q?=E6=A0=BC=E7=B1=BB=E5=9E=8B=E6=94=B9=E4=B8=BA=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E4=B8=B2=E4=BB=A5=E9=81=BF=E5=85=8D=E7=B2=BE=E5=BA=A6=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=8C=E5=B9=B6=E6=B7=BB=E5=8A=A0=E4=BB=B7=E6=A0=BC?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/lib/order-plan.ts | 10 +++------ src/core/order-coordinator.ts | 9 ++++---- src/exchanges/grvt/gateway.ts | 2 +- src/strategy/maker-engine.ts | 29 ++++++++++++++---------- src/strategy/offset-maker-engine.ts | 34 ++++++++++++++++++++--------- src/strategy/trend-engine.ts | 12 +++++----- src/utils/math.ts | 11 ++++++++++ 7 files changed, 68 insertions(+), 39 deletions(-) diff --git a/src/core/lib/order-plan.ts b/src/core/lib/order-plan.ts index 18712a1..3e3d7c2 100644 --- a/src/core/lib/order-plan.ts +++ b/src/core/lib/order-plan.ts @@ -2,7 +2,7 @@ import type { AsterOrder } from "../../exchanges/types"; export interface OrderTarget { side: "BUY" | "SELL"; - price: number; + price: string; // 改为字符串避免精度问题 amount: number; reduceOnly: boolean; } @@ -15,18 +15,14 @@ export function makeOrderPlan( const toCancel: AsterOrder[] = []; for (const order of openOrders) { - const price = Number(order.price); - if (!Number.isFinite(price)) { - toCancel.push(order); - continue; - } + const orderPrice = String(order.price); const reduceOnly = order.reduceOnly === true; const matchedIndex = targets.findIndex((target, index) => { return ( unmatched.has(index) && target.side === order.side && target.reduceOnly === reduceOnly && - price === target.price + orderPrice === target.price // 直接使用字符串比较 ); }); if (matchedIndex >= 0) { diff --git a/src/core/order-coordinator.ts b/src/core/order-coordinator.ts index cb3046a..198a0be 100644 --- a/src/core/order-coordinator.ts +++ b/src/core/order-coordinator.ts @@ -1,6 +1,6 @@ import type { ExchangeAdapter } from "../exchanges/adapter"; import type { AsterOrder, CreateOrderParams } from "../exchanges/types"; -import { roundDownToTick, roundQtyDownToStep } from "../utils/math"; +import { roundDownToTick, roundQtyDownToStep, formatPriceToString } from "../utils/math"; import { isUnknownOrderError } from "../utils/errors"; import { isOrderPriceAllowedByMark } from "../utils/strategy"; @@ -122,7 +122,7 @@ export async function placeOrder( timers: OrderTimerMap, pendings: OrderPendingMap, side: "BUY" | "SELL", - price: number, + price: string, // 改为字符串价格 amount: number, log: LogHandler, reduceOnly = false, @@ -131,7 +131,8 @@ export async function placeOrder( ): Promise { const type = "LIMIT"; if (isOperating(locks, type)) return; - if (!enforceMarkPriceGuard(side, price, guard, log, "限价单")) return; + const priceNum = Number(price); + if (!enforceMarkPriceGuard(side, priceNum, guard, log, "限价单")) return; const priceTick = opts?.priceTick ?? 0.1; const qtyStep = opts?.qtyStep ?? 0.001; const params: CreateOrderParams = { @@ -139,7 +140,7 @@ export async function placeOrder( side, type, quantity: roundQtyDownToStep(amount, qtyStep), - price: roundDownToTick(price, priceTick), + price: priceNum, // 直接使用字符串转换的数字,不再格式化 timeInForce: "GTX", }; if (reduceOnly) params.reduceOnly = "true"; diff --git a/src/exchanges/grvt/gateway.ts b/src/exchanges/grvt/gateway.ts index 9ab7318..922d73c 100644 --- a/src/exchanges/grvt/gateway.ts +++ b/src/exchanges/grvt/gateway.ts @@ -41,7 +41,7 @@ import type { const DEFAULT_ACCOUNT_POLL_INTERVAL_MS = 5000; const DEFAULT_ORDERS_POLL_INTERVAL_MS = 2500; -const DEFAULT_DEPTH_POLL_INTERVAL_MS = 750; +const DEFAULT_DEPTH_POLL_INTERVAL_MS = 400; const DEFAULT_TICKER_POLL_INTERVAL_MS = 1000; const DEFAULT_KLINE_POLL_INTERVAL_MS = 15000; diff --git a/src/strategy/maker-engine.ts b/src/strategy/maker-engine.ts index 7916a0c..d1ac368 100644 --- a/src/strategy/maker-engine.ts +++ b/src/strategy/maker-engine.ts @@ -7,7 +7,7 @@ import type { AsterOrder, AsterTicker, } from "../exchanges/types"; -import { roundDownToTick } from "../utils/math"; +import { formatPriceToString } from "../utils/math"; import { createTradeLog, type TradeLogEntry } from "../logging/trade-log"; import { isUnknownOrderError, isRateLimitError } from "../utils/errors"; import { getPosition } from "../utils/strategy"; @@ -30,7 +30,7 @@ import { SessionVolumeTracker } from "./common/session-volume"; interface DesiredOrder { side: "BUY" | "SELL"; - price: number; + price: string; // 改为字符串价格 amount: number; reduceOnly: boolean; } @@ -244,10 +244,12 @@ export class MakerEngine { return; } - const closeBidPrice = roundDownToTick(topBid, this.config.priceTick); - const closeAskPrice = roundDownToTick(topAsk, this.config.priceTick); - const bidPrice = roundDownToTick(topBid - this.config.bidOffset, this.config.priceTick); - const askPrice = roundDownToTick(topAsk + this.config.askOffset, this.config.priceTick); + // 直接使用orderbook价格,格式化为字符串避免精度问题 + const priceDecimals = Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick))); + const closeBidPrice = formatPriceToString(topBid, priceDecimals); + const closeAskPrice = formatPriceToString(topAsk, priceDecimals); + const bidPrice = formatPriceToString(topBid - this.config.bidOffset, priceDecimals); + const askPrice = formatPriceToString(topAsk + this.config.askOffset, priceDecimals); const position = getPosition(this.accountSnapshot, this.config.symbol); const absPosition = Math.abs(position.positionAmt); const desired: DesiredOrder[] = []; @@ -268,7 +270,7 @@ export class MakerEngine { this.desiredOrders = desired; this.sessionVolume.update(position, this.getReferencePrice()); await this.syncOrders(desired); - await this.checkRisk(position, closeBidPrice, closeAskPrice); + await this.checkRisk(position, Number(closeBidPrice), Number(closeAskPrice)); this.emitUpdate(); } catch (error) { if (isRateLimitError(error)) { @@ -291,9 +293,10 @@ export class MakerEngine { if (Math.abs(position.positionAmt) < EPS) return; const { topBid, topAsk } = getTopPrices(this.depthSnapshot); if (topBid == null || topAsk == null) return; - const closeBidPrice = roundDownToTick(topBid, this.config.priceTick); - const closeAskPrice = roundDownToTick(topAsk, this.config.priceTick); - await this.checkRisk(position, closeBidPrice, closeAskPrice); + const priceDecimals = Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick))); + const closeBidPrice = formatPriceToString(topBid, priceDecimals); + const closeAskPrice = formatPriceToString(topAsk, priceDecimals); + await this.checkRisk(position, Number(closeBidPrice), Number(closeAskPrice)); await this.flushOrders(); } @@ -368,13 +371,17 @@ export class MakerEngine { this.timers, this.pending, target.side, - target.price, + target.price, // 已经是字符串价格 target.amount, (type, detail) => this.tradeLog.push(type, detail), target.reduceOnly, { markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice, maxPct: this.config.maxCloseSlippagePct, + }, + { + priceTick: this.config.priceTick, + qtyStep: 0.001, // 默认数量步长 } ); } catch (error) { diff --git a/src/strategy/offset-maker-engine.ts b/src/strategy/offset-maker-engine.ts index 5075660..5b5e517 100644 --- a/src/strategy/offset-maker-engine.ts +++ b/src/strategy/offset-maker-engine.ts @@ -7,7 +7,7 @@ import type { AsterOrder, AsterTicker, } from "../exchanges/types"; -import { roundDownToTick } from "../utils/math"; +import { formatPriceToString } from "../utils/math"; import { createTradeLog } from "../logging/trade-log"; import { isUnknownOrderError, isRateLimitError } from "../utils/errors"; import { getPosition } from "../utils/strategy"; @@ -32,7 +32,7 @@ import { SessionVolumeTracker } from "./common/session-volume"; interface DesiredOrder { side: "BUY" | "SELL"; - price: number; + price: string; // 改为字符串价格 amount: number; reduceOnly: boolean; } @@ -236,6 +236,7 @@ export class OffsetMakerEngine { return; } + // 确保使用最新的深度数据 const depth = this.depthSnapshot!; const { topBid, topAsk } = getTopPrices(depth); if (topBid == null || topAsk == null) { @@ -257,10 +258,18 @@ export class OffsetMakerEngine { return; } - const closeBidPrice = roundDownToTick(topBid!, this.config.priceTick); - const closeAskPrice = roundDownToTick(topAsk!, this.config.priceTick); - const bidPrice = roundDownToTick(topBid! - this.config.bidOffset, this.config.priceTick); - const askPrice = roundDownToTick(topAsk! + this.config.askOffset, this.config.priceTick); + // 在计算挂单价格前,重新获取最新的深度数据以确保价格同步 + const latestDepth = this.depthSnapshot!; + const { topBid: latestBid, topAsk: latestAsk } = getTopPrices(latestDepth); + const finalBid = latestBid ?? topBid!; + const finalAsk = latestAsk ?? topAsk!; + + // 直接使用orderbook价格,格式化为字符串避免精度问题 + const priceDecimals = Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick))); + const closeBidPrice = formatPriceToString(finalBid, priceDecimals); + const closeAskPrice = formatPriceToString(finalAsk, priceDecimals); + const bidPrice = formatPriceToString(finalBid - this.config.bidOffset, priceDecimals); + const askPrice = formatPriceToString(finalAsk + this.config.askOffset, priceDecimals); const absPosition = Math.abs(position.positionAmt); const desired: DesiredOrder[] = []; const canEnter = !this.rateLimit.shouldBlockEntries(); @@ -282,7 +291,7 @@ export class OffsetMakerEngine { this.desiredOrders = desired; this.sessionVolume.update(position, this.getReferencePrice()); await this.syncOrders(desired); - await this.checkRisk(position, closeBidPrice, closeAskPrice); + await this.checkRisk(position, Number(closeBidPrice), Number(closeAskPrice)); this.emitUpdate(); } catch (error) { if (isRateLimitError(error)) { @@ -307,8 +316,9 @@ export class OffsetMakerEngine { const absPosition = Math.abs(position.positionAmt); const side: "BUY" | "SELL" = position.positionAmt > 0 ? "SELL" : "BUY"; const { topBid, topAsk } = getTopPrices(this.depthSnapshot); - const closeBidPrice = topBid != null ? roundDownToTick(topBid, this.config.priceTick) : null; - const closeAskPrice = topAsk != null ? roundDownToTick(topAsk, this.config.priceTick) : null; + const priceDecimals = Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick))); + const closeBidPrice = topBid != null ? formatPriceToString(topBid, priceDecimals) : null; + const closeAskPrice = topAsk != null ? formatPriceToString(topAsk, priceDecimals) : null; try { await marketClose( this.exchange, @@ -471,13 +481,17 @@ export class OffsetMakerEngine { this.timers, this.pending, target.side, - target.price, + target.price, // 已经是字符串价格 target.amount, (type, detail) => this.tradeLog.push(type, detail), target.reduceOnly, { markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice, maxPct: this.config.maxCloseSlippagePct, + }, + { + priceTick: this.config.priceTick, + qtyStep: 0.001, // 默认数量步长 } ); } catch (error) { diff --git a/src/strategy/trend-engine.ts b/src/strategy/trend-engine.ts index 33a8b5a..eead68f 100644 --- a/src/strategy/trend-engine.ts +++ b/src/strategy/trend-engine.ts @@ -27,7 +27,7 @@ import { } from "../core/order-coordinator"; import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator"; import { extractMessage, isUnknownOrderError } from "../utils/errors"; -import { roundDownToTick } from "../utils/math"; +import { formatPriceToString } from "../utils/math"; import { createTradeLog, type TradeLogEntry } from "../logging/trade-log"; import { decryptCopyright } from "../utils/copyright"; import { isRateLimitError } from "../utils/errors"; @@ -557,7 +557,7 @@ export class TrendEngine { const rawTarget = direction === "long" ? position.entryPrice + steps * stepPx : position.entryPrice - steps * stepPx; - let targetStop = roundDownToTick(rawTarget, this.config.priceTick); + let targetStop = Number(formatPriceToString(rawTarget, Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick))))); // 不允许下一次移动超过动态止盈订单的激活价 if (Number.isFinite(trailingActivate)) { @@ -639,13 +639,13 @@ export class TrendEngine { } if (!currentStop) { - await this.tryPlaceStopLoss(stopSide, roundDownToTick(stopPrice, this.config.priceTick), price); + await this.tryPlaceStopLoss(stopSide, Number(formatPriceToString(stopPrice, Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick))))), price); } if (!currentTrailing && this.exchange.supportsTrailingStops()) { await this.tryPlaceTrailingStop( stopSide, - roundDownToTick(activationPrice, this.config.priceTick), + Number(formatPriceToString(activationPrice, Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick))))), Math.abs(position.positionAmt) ); } @@ -815,7 +815,7 @@ export class TrendEngine { { priceTick: this.config.priceTick, qtyStep: this.config.qtyStep } ); if (order) { - this.tradeLog.push("stop", `移动止损到 ${roundDownToTick(nextStopPrice, this.config.priceTick)}`); + this.tradeLog.push("stop", `移动止损到 ${formatPriceToString(nextStopPrice, Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick))))}`); } } catch (err) { this.tradeLog.push("error", `移动止损失败: ${String(err)}`); @@ -846,7 +846,7 @@ export class TrendEngine { { priceTick: this.config.priceTick, qtyStep: this.config.qtyStep } ); if (restored) { - this.tradeLog.push("order", `恢复原止损 @ ${roundDownToTick(existingStopPrice, this.config.priceTick)}`); + this.tradeLog.push("order", `恢复原止损 @ ${formatPriceToString(existingStopPrice, Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick))))}`); } } } catch (recoverErr) { diff --git a/src/utils/math.ts b/src/utils/math.ts index 12c67a5..45e5db4 100644 --- a/src/utils/math.ts +++ b/src/utils/math.ts @@ -21,3 +21,14 @@ export function decimalsOf(step: number): number { export function isNearlyZero(value: number, epsilon = 1e-5): boolean { return Math.abs(value) < epsilon; } + +/** + * 将价格格式化为指定小数位数的字符串 + * @param price 原始价格 + * @param decimals 小数位数 + * @returns 格式化后的价格字符串 + */ +export function formatPriceToString(price: number, decimals: number): string { + if (!Number.isFinite(price)) return "0"; + return price.toFixed(decimals); +}