refactor(core): give order functions a parameter object

placeOrder took 13 positional arguments; the other five order functions took
9-13. The leading six — adapter, symbol, openOrders, locks, timers, pendings —
were the same values at all 36 call sites, and every engine spelled them out
again for each order it placed.

Introduce Parameter Object: OrderContext holds what is fixed for an engine's
lifetime (exposed once via a lazily-built this.orderContext), and each function
takes a named request. A wrong argument order is now a compile error rather than
a silently misrouted order.

The type change surfaced dead weight: placeOrder's opts.priceTick was never read
by its body, yet five engines passed it. Removed.

Also finishes the PrecisionSyncer migration — grid-engine was the ninth copy and
was missed last round, so it still carried the uncleared retry timer.

Extract Function: normalizeQuantity replaces the round-down-but-never-to-zero
block that appeared in all five order functions.

250 pass; tsc and oxlint clean.
This commit is contained in:
discountry
2026-07-29 21:17:58 +08:00
parent 6f64dd5a0a
commit 5aecaabb16
10 changed files with 542 additions and 708 deletions
+69 -88
View File
@@ -25,7 +25,7 @@ import {
placeTrailingStopOrder,
unlockOperating,
} from "../core/order-coordinator";
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator";
import type { OrderContext, OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator";
import { extractMessage, isUnknownOrderError } from "../utils/errors";
import { formatPriceToString } from "../utils/math";
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
@@ -139,6 +139,19 @@ export class TrendEngine {
this.bootstrap();
}
/** Bundles the fixed order-routing state; rebuilt lazily on first use. */
private get orderContext(): OrderContext {
return (this.orderContextCache ??= {
adapter: this.exchange,
symbol: this.config.symbol,
locks: this.locks,
timers: this.timers,
pendings: this.pending,
log: (type, detail) => this.tradeLog.push(type, detail),
});
}
private orderContextCache: OrderContext | null = null;
start(): void {
if (this.timer) return;
this.timer = setInterval(() => {
@@ -490,24 +503,18 @@ export class TrendEngine {
private async submitMarketOrder(side: "BUY" | "SELL", price: number, reason: string): Promise<void> {
try {
await placeMarketOrder(
this.exchange,
this.config.symbol,
this.openOrders,
this.locks,
this.timers,
this.pending,
side,
this.config.tradeAmount,
(type, detail) => this.tradeLog.push(type, detail),
false,
{
await placeMarketOrder(this.orderContext, {
openOrders: this.openOrders,
side: side,
amount: this.config.tradeAmount,
reduceOnly: false,
guard: {
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
expectedPrice: Number(this.tickerSnapshot?.lastPrice) || null,
maxPct: this.config.maxCloseSlippagePct,
},
{ qtyStep: this.config.qtyStep }
);
qtyStep: this.config.qtyStep
});
this.tradeLog.push("open", `${reason}: ${side} @ ${price}`);
this.lastOpenPlan = { side, price };
} catch (err) {
@@ -748,17 +755,11 @@ export class TrendEngine {
return { closed: false, pnl };
}
}
await marketClose(
this.exchange,
this.config.symbol,
this.openOrders,
this.locks,
this.timers,
this.pending,
direction === "long" ? "SELL" : "BUY",
Math.abs(position.positionAmt),
(type, detail) => this.tradeLog.push(type, detail),
{
await marketClose(this.orderContext, {
openOrders: this.openOrders,
side: direction === "long" ? "SELL" : "BUY",
quantity: Math.abs(position.positionAmt),
guard: {
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
expectedPrice: Number(
direction === "long"
@@ -767,8 +768,8 @@ export class TrendEngine {
) || null,
maxPct: this.config.maxCloseSlippagePct,
},
{ qtyStep: this.config.qtyStep }
);
qtyStep: this.config.qtyStep
});
result.closed = true;
this.tradeLog.push("close", t("log.trend.stopClose", { side: direction === "long" ? "SELL" : "BUY" }));
// 记录止损时间以便短期内抑制再次入场
@@ -811,24 +812,19 @@ export class TrendEngine {
if (quantity <= minQty) {
return;
}
await placeStopLossOrder(
this.exchange,
this.config.symbol,
this.openOrders,
this.locks,
this.timers,
this.pending,
side,
stopPrice,
quantity,
lastPrice,
(type, detail) => this.tradeLog.push(type, detail),
{
await placeStopLossOrder(this.orderContext, {
openOrders: this.openOrders,
side: side,
stopPrice: stopPrice,
quantity: quantity,
lastPrice: lastPrice,
guard: {
markPrice: position.markPrice,
maxPct: this.config.maxCloseSlippagePct,
},
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
);
priceTick: this.config.priceTick,
qtyStep: this.config.qtyStep
});
this.lastStopAttempt = { side, price: stopPrice, at: Date.now() };
} catch (err) {
this.tradeLog.push("error", t("log.trend.placeStopFail", { error: String(err) }));
@@ -871,24 +867,19 @@ export class TrendEngine {
if (quantity <= minQty) {
return;
}
const order = await placeStopLossOrder(
this.exchange,
this.config.symbol,
this.openOrders,
this.locks,
this.timers,
this.pending,
side,
nextStopPrice,
quantity,
lastPrice,
(type, detail) => this.tradeLog.push(type, detail),
{
const order = await placeStopLossOrder(this.orderContext, {
openOrders: this.openOrders,
side: side,
stopPrice: nextStopPrice,
quantity: quantity,
lastPrice: lastPrice,
guard: {
markPrice: position.markPrice,
maxPct: this.config.maxCloseSlippagePct,
},
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
);
priceTick: this.config.priceTick,
qtyStep: this.config.qtyStep
});
if (order) {
this.tradeLog.push(
"stop",
@@ -914,24 +905,19 @@ export class TrendEngine {
(side === "SELL" && existingStopPrice >= lastPrice) ||
(side === "BUY" && existingStopPrice <= lastPrice);
if (!restoreInvalid) {
const restored = await placeStopLossOrder(
this.exchange,
this.config.symbol,
this.openOrders,
this.locks,
this.timers,
this.pending,
side,
existingStopPrice,
quantity,
lastPrice,
(t, d) => this.tradeLog.push(t, d),
{
const restored = await placeStopLossOrder(this.orderContext, {
openOrders: this.openOrders,
side: side,
stopPrice: existingStopPrice,
quantity: quantity,
lastPrice: lastPrice,
guard: {
markPrice: position.markPrice,
maxPct: this.config.maxCloseSlippagePct,
},
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
);
priceTick: this.config.priceTick,
qtyStep: this.config.qtyStep
});
if (restored) {
this.tradeLog.push(
"order",
@@ -959,24 +945,19 @@ export class TrendEngine {
return;
}
try {
await placeTrailingStopOrder(
this.exchange,
this.config.symbol,
this.openOrders,
this.locks,
this.timers,
this.pending,
side,
activationPrice,
quantity,
this.config.trailingCallbackRate,
(type, detail) => this.tradeLog.push(type, detail),
{
await placeTrailingStopOrder(this.orderContext, {
openOrders: this.openOrders,
side: side,
activationPrice: activationPrice,
quantity: quantity,
callbackRate: this.config.trailingCallbackRate,
guard: {
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
maxPct: this.config.maxCloseSlippagePct,
},
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
);
priceTick: this.config.priceTick,
qtyStep: this.config.qtyStep
});
} catch (err) {
this.tradeLog.push("error", t("log.trend.trailingFail", { error: String(err) }));
}