mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 09:18:08 +00:00
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:
+122
-124
@@ -22,6 +22,69 @@ type OrderGuardOptions = {
|
|||||||
maxPct?: number;
|
maxPct?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Everything about *where* an order goes, fixed for an engine's lifetime.
|
||||||
|
* These six values always travelled together as the leading positional
|
||||||
|
* parameters of every order function; bundling them keeps call sites readable
|
||||||
|
* and makes an argument-order mistake impossible.
|
||||||
|
*/
|
||||||
|
export interface OrderContext {
|
||||||
|
adapter: ExchangeAdapter;
|
||||||
|
symbol: string;
|
||||||
|
locks: OrderLockMap;
|
||||||
|
timers: OrderTimerMap;
|
||||||
|
pendings: OrderPendingMap;
|
||||||
|
log: LogHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrderRequestBase {
|
||||||
|
/** Live orders, used to cancel same-type duplicates before placing. */
|
||||||
|
openOrders: Order[];
|
||||||
|
side: "BUY" | "SELL";
|
||||||
|
/** Rejects the order when its price strays too far from the mark price. */
|
||||||
|
guard?: OrderGuardOptions;
|
||||||
|
qtyStep?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LimitOrderRequest extends OrderRequestBase {
|
||||||
|
/** String to preserve the exact tick the caller computed. */
|
||||||
|
price: string;
|
||||||
|
amount: number;
|
||||||
|
reduceOnly?: boolean;
|
||||||
|
skipDedupe?: boolean;
|
||||||
|
slPrice?: number;
|
||||||
|
tpPrice?: number;
|
||||||
|
clientOrderId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketOrderRequest extends OrderRequestBase {
|
||||||
|
amount: number;
|
||||||
|
reduceOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StopLossOrderRequest extends OrderRequestBase {
|
||||||
|
stopPrice: number;
|
||||||
|
quantity: number;
|
||||||
|
/** Latest traded price; the stop is rejected when it is already through it. */
|
||||||
|
lastPrice: number | null;
|
||||||
|
priceTick?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrailingStopOrderRequest extends OrderRequestBase {
|
||||||
|
activationPrice: number;
|
||||||
|
quantity: number;
|
||||||
|
callbackRate: number;
|
||||||
|
priceTick?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketCloseRequest extends OrderRequestBase {
|
||||||
|
quantity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Step assumed when the caller does not know the venue's own. */
|
||||||
|
const DEFAULT_QTY_STEP = 0.001;
|
||||||
|
const DEFAULT_PRICE_TICK = 0.1;
|
||||||
|
|
||||||
function enforceMarkPriceGuard(
|
function enforceMarkPriceGuard(
|
||||||
side: "BUY" | "SELL",
|
side: "BUY" | "SELL",
|
||||||
toCheckPrice: number | null | undefined,
|
toCheckPrice: number | null | undefined,
|
||||||
@@ -48,6 +111,13 @@ function enforceMarkPriceGuard(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Rounds down to the venue's step, but never to zero — a sub-step size is kept as-is. */
|
||||||
|
function normalizeQuantity(amount: number, qtyStep: number): number {
|
||||||
|
const raw = Math.abs(amount);
|
||||||
|
const rounded = roundQtyDownToStep(raw, qtyStep);
|
||||||
|
return rounded > 0 ? rounded : raw;
|
||||||
|
}
|
||||||
|
|
||||||
export function isOperating(locks: OrderLockMap, type: string): boolean {
|
export function isOperating(locks: OrderLockMap, type: string): boolean {
|
||||||
return Boolean(locks[type]);
|
return Boolean(locks[type]);
|
||||||
}
|
}
|
||||||
@@ -86,16 +156,12 @@ export function unlockOperating(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function deduplicateOrders(
|
export async function deduplicateOrders(
|
||||||
adapter: ExchangeAdapter,
|
ctx: OrderContext,
|
||||||
symbol: string,
|
|
||||||
openOrders: Order[],
|
openOrders: Order[],
|
||||||
locks: OrderLockMap,
|
|
||||||
timers: OrderTimerMap,
|
|
||||||
pendings: OrderPendingMap,
|
|
||||||
type: string,
|
type: string,
|
||||||
side: string,
|
side: string
|
||||||
log: LogHandler
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const { adapter, symbol, locks, timers, pendings, log } = ctx;
|
||||||
// Treat STOP orders on some exchanges (e.g., Lighter) as LIMIT with stopPrice populated.
|
// Treat STOP orders on some exchanges (e.g., Lighter) as LIMIT with stopPrice populated.
|
||||||
const sameTypeOrders = openOrders.filter((o) => {
|
const sameTypeOrders = openOrders.filter((o) => {
|
||||||
const normalizedType = String(o.type).toUpperCase();
|
const normalizedType = String(o.type).toUpperCase();
|
||||||
@@ -128,63 +194,42 @@ export async function deduplicateOrders(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlaceOrderOptions = {
|
|
||||||
priceTick: number;
|
|
||||||
qtyStep: number;
|
|
||||||
skipDedupe?: boolean;
|
|
||||||
slPrice?: number;
|
|
||||||
tpPrice?: number;
|
|
||||||
clientOrderId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function placeOrder(
|
export async function placeOrder(
|
||||||
adapter: ExchangeAdapter,
|
ctx: OrderContext,
|
||||||
symbol: string,
|
request: LimitOrderRequest
|
||||||
openOrders: Order[],
|
|
||||||
locks: OrderLockMap,
|
|
||||||
timers: OrderTimerMap,
|
|
||||||
pendings: OrderPendingMap,
|
|
||||||
side: "BUY" | "SELL",
|
|
||||||
price: string, // 改为字符串价格
|
|
||||||
amount: number,
|
|
||||||
log: LogHandler,
|
|
||||||
reduceOnly = false,
|
|
||||||
guard?: OrderGuardOptions,
|
|
||||||
opts?: PlaceOrderOptions
|
|
||||||
): Promise<Order | undefined> {
|
): Promise<Order | undefined> {
|
||||||
|
const { locks, timers, pendings, log } = ctx;
|
||||||
|
const { side, openOrders, guard, reduceOnly = false } = request;
|
||||||
const type = "LIMIT";
|
const type = "LIMIT";
|
||||||
if (isOperating(locks, type)) return;
|
if (isOperating(locks, type)) return;
|
||||||
const priceNum = Number(price);
|
const priceNum = Number(request.price);
|
||||||
if (!enforceMarkPriceGuard(side, priceNum, guard, log, "限价单")) return;
|
if (!enforceMarkPriceGuard(side, priceNum, guard, log, "限价单")) return;
|
||||||
const qtyStep = opts?.qtyStep ?? 0.001;
|
const quantity = normalizeQuantity(request.amount, request.qtyStep ?? DEFAULT_QTY_STEP);
|
||||||
const rawQuantity = Math.abs(amount);
|
|
||||||
const roundedQuantity = roundQtyDownToStep(rawQuantity, qtyStep);
|
|
||||||
const quantity = roundedQuantity > 0 ? roundedQuantity : rawQuantity;
|
|
||||||
if (quantity <= 0) {
|
if (quantity <= 0) {
|
||||||
log("error", "限价单数量无效,跳过下单");
|
log("error", "限价单数量无效,跳过下单");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!opts?.skipDedupe) {
|
if (!request.skipDedupe) {
|
||||||
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
await deduplicateOrders(ctx, openOrders, type, side);
|
||||||
}
|
}
|
||||||
lockOperating(locks, timers, pendings, type, log);
|
lockOperating(locks, timers, pendings, type, log);
|
||||||
try {
|
try {
|
||||||
const closePosition = reduceOnly ? true : undefined;
|
const closePosition = reduceOnly ? true : undefined;
|
||||||
const order = await routeLimitOrder({
|
const order = await routeLimitOrder({
|
||||||
adapter,
|
adapter: ctx.adapter,
|
||||||
symbol,
|
symbol: ctx.symbol,
|
||||||
side,
|
side,
|
||||||
quantity,
|
quantity,
|
||||||
price: priceNum,
|
price: priceNum,
|
||||||
timeInForce: reduceOnly ? "GTC" : "GTX",
|
timeInForce: reduceOnly ? "GTC" : "GTX",
|
||||||
reduceOnly: reduceOnly ? true : undefined,
|
reduceOnly: reduceOnly ? true : undefined,
|
||||||
closePosition,
|
closePosition,
|
||||||
slPrice: opts?.slPrice,
|
slPrice: request.slPrice,
|
||||||
tpPrice: opts?.tpPrice,
|
tpPrice: request.tpPrice,
|
||||||
clientOrderId: opts?.clientOrderId,
|
clientOrderId: request.clientOrderId,
|
||||||
});
|
});
|
||||||
pendings[type] = String(order.orderId);
|
pendings[type] = String(order.orderId);
|
||||||
log("order", `挂限价单: ${side} @ ${priceNum} 数量 ${quantity} reduceOnly=${reduceOnly}${opts?.slPrice ? ` sl=${opts.slPrice}` : ""}`);
|
log("order", `挂限价单: ${side} @ ${priceNum} 数量 ${quantity} reduceOnly=${reduceOnly}${request.slPrice ? ` sl=${request.slPrice}` : ""}`);
|
||||||
return order;
|
return order;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
unlockOperating(locks, timers, pendings, type);
|
unlockOperating(locks, timers, pendings, type);
|
||||||
@@ -197,37 +242,26 @@ export async function placeOrder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function placeMarketOrder(
|
export async function placeMarketOrder(
|
||||||
adapter: ExchangeAdapter,
|
ctx: OrderContext,
|
||||||
symbol: string,
|
request: MarketOrderRequest
|
||||||
openOrders: Order[],
|
|
||||||
locks: OrderLockMap,
|
|
||||||
timers: OrderTimerMap,
|
|
||||||
pendings: OrderPendingMap,
|
|
||||||
side: "BUY" | "SELL",
|
|
||||||
amount: number,
|
|
||||||
log: LogHandler,
|
|
||||||
reduceOnly = false,
|
|
||||||
guard?: OrderGuardOptions,
|
|
||||||
opts?: { qtyStep: number }
|
|
||||||
): Promise<Order | undefined> {
|
): Promise<Order | undefined> {
|
||||||
|
const { locks, timers, pendings, log } = ctx;
|
||||||
|
const { side, openOrders, guard, reduceOnly = false } = request;
|
||||||
const type = "MARKET";
|
const type = "MARKET";
|
||||||
if (isOperating(locks, type)) return;
|
if (isOperating(locks, type)) return;
|
||||||
if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价单")) return;
|
if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价单")) return;
|
||||||
const qtyStep = opts?.qtyStep ?? 0.001;
|
const quantity = normalizeQuantity(request.amount, request.qtyStep ?? DEFAULT_QTY_STEP);
|
||||||
const rawQuantity = Math.abs(amount);
|
|
||||||
const roundedQuantity = roundQtyDownToStep(rawQuantity, qtyStep);
|
|
||||||
const quantity = roundedQuantity > 0 ? roundedQuantity : rawQuantity;
|
|
||||||
if (quantity <= 0) {
|
if (quantity <= 0) {
|
||||||
log("error", "市价单数量无效,跳过下单");
|
log("error", "市价单数量无效,跳过下单");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
await deduplicateOrders(ctx, openOrders, type, side);
|
||||||
lockOperating(locks, timers, pendings, type, log);
|
lockOperating(locks, timers, pendings, type, log);
|
||||||
try {
|
try {
|
||||||
const closePosition = reduceOnly ? true : undefined;
|
const closePosition = reduceOnly ? true : undefined;
|
||||||
const order = await routeMarketOrder({
|
const order = await routeMarketOrder({
|
||||||
adapter,
|
adapter: ctx.adapter,
|
||||||
symbol,
|
symbol: ctx.symbol,
|
||||||
side,
|
side,
|
||||||
quantity,
|
quantity,
|
||||||
reduceOnly: reduceOnly ? true : undefined,
|
reduceOnly: reduceOnly ? true : undefined,
|
||||||
@@ -247,20 +281,11 @@ export async function placeMarketOrder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function placeStopLossOrder(
|
export async function placeStopLossOrder(
|
||||||
adapter: ExchangeAdapter,
|
ctx: OrderContext,
|
||||||
symbol: string,
|
request: StopLossOrderRequest
|
||||||
openOrders: Order[],
|
|
||||||
locks: OrderLockMap,
|
|
||||||
timers: OrderTimerMap,
|
|
||||||
pendings: OrderPendingMap,
|
|
||||||
side: "BUY" | "SELL",
|
|
||||||
stopPrice: number,
|
|
||||||
quantity: number,
|
|
||||||
lastPrice: number | null,
|
|
||||||
log: LogHandler,
|
|
||||||
guard?: OrderGuardOptions,
|
|
||||||
opts?: { priceTick: number; qtyStep: number }
|
|
||||||
): Promise<Order | undefined> {
|
): Promise<Order | undefined> {
|
||||||
|
const { locks, timers, pendings, log } = ctx;
|
||||||
|
const { side, openOrders, guard, stopPrice, lastPrice } = request;
|
||||||
const type = "STOP_MARKET";
|
const type = "STOP_MARKET";
|
||||||
if (isOperating(locks, type)) return;
|
if (isOperating(locks, type)) return;
|
||||||
if (!enforceMarkPriceGuard(side, stopPrice, guard, log, "止损单")) return;
|
if (!enforceMarkPriceGuard(side, stopPrice, guard, log, "止损单")) return;
|
||||||
@@ -274,24 +299,20 @@ export async function placeStopLossOrder(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const priceTick = opts?.priceTick ?? 0.1;
|
const normalizedStop = roundDownToTick(stopPrice, request.priceTick ?? DEFAULT_PRICE_TICK);
|
||||||
const qtyStep = opts?.qtyStep ?? 0.001;
|
const normalizedQty = normalizeQuantity(request.quantity, request.qtyStep ?? DEFAULT_QTY_STEP);
|
||||||
const normalizedStop = roundDownToTick(stopPrice, priceTick);
|
|
||||||
const rawQuantity = Math.abs(quantity);
|
|
||||||
const roundedQuantity = roundQtyDownToStep(rawQuantity, qtyStep);
|
|
||||||
const normalizedQty = roundedQuantity > 0 ? roundedQuantity : rawQuantity;
|
|
||||||
if (normalizedQty <= 0) {
|
if (normalizedQty <= 0) {
|
||||||
log("error", "止损单数量无效,跳过下单");
|
log("error", "止损单数量无效,跳过下单");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avoid forcing price for STOP_MARKET globally; keep this exchange-specific in gateways
|
// Avoid forcing price for STOP_MARKET globally; keep this exchange-specific in gateways
|
||||||
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
await deduplicateOrders(ctx, openOrders, type, side);
|
||||||
lockOperating(locks, timers, pendings, type, log);
|
lockOperating(locks, timers, pendings, type, log);
|
||||||
try {
|
try {
|
||||||
const order = await routeStopOrder({
|
const order = await routeStopOrder({
|
||||||
adapter,
|
adapter: ctx.adapter,
|
||||||
symbol,
|
symbol: ctx.symbol,
|
||||||
side,
|
side,
|
||||||
quantity: normalizedQty,
|
quantity: normalizedQty,
|
||||||
stopPrice: normalizedStop,
|
stopPrice: normalizedStop,
|
||||||
@@ -314,20 +335,11 @@ export async function placeStopLossOrder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function placeTrailingStopOrder(
|
export async function placeTrailingStopOrder(
|
||||||
adapter: ExchangeAdapter,
|
ctx: OrderContext,
|
||||||
symbol: string,
|
request: TrailingStopOrderRequest
|
||||||
openOrders: Order[],
|
|
||||||
locks: OrderLockMap,
|
|
||||||
timers: OrderTimerMap,
|
|
||||||
pendings: OrderPendingMap,
|
|
||||||
side: "BUY" | "SELL",
|
|
||||||
activationPrice: number,
|
|
||||||
quantity: number,
|
|
||||||
callbackRate: number,
|
|
||||||
log: LogHandler,
|
|
||||||
guard?: OrderGuardOptions,
|
|
||||||
opts?: { priceTick: number; qtyStep: number }
|
|
||||||
): Promise<Order | undefined> {
|
): Promise<Order | undefined> {
|
||||||
|
const { adapter, locks, timers, pendings, log } = ctx;
|
||||||
|
const { side, openOrders, guard, activationPrice, callbackRate } = request;
|
||||||
const type = "TRAILING_STOP_MARKET";
|
const type = "TRAILING_STOP_MARKET";
|
||||||
if (isOperating(locks, type)) return;
|
if (isOperating(locks, type)) return;
|
||||||
if (!adapter.supportsTrailingStops()) {
|
if (!adapter.supportsTrailingStops()) {
|
||||||
@@ -335,22 +347,18 @@ export async function placeTrailingStopOrder(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!enforceMarkPriceGuard(side, activationPrice, guard, log, "动态止盈单")) return;
|
if (!enforceMarkPriceGuard(side, activationPrice, guard, log, "动态止盈单")) return;
|
||||||
const priceTick = opts?.priceTick ?? 0.1;
|
const normalizedActivation = roundDownToTick(activationPrice, request.priceTick ?? DEFAULT_PRICE_TICK);
|
||||||
const qtyStep = opts?.qtyStep ?? 0.001;
|
const normalizedQty = normalizeQuantity(request.quantity, request.qtyStep ?? DEFAULT_QTY_STEP);
|
||||||
const normalizedActivation = roundDownToTick(activationPrice, priceTick);
|
|
||||||
const rawQuantity = Math.abs(quantity);
|
|
||||||
const roundedQuantity = roundQtyDownToStep(rawQuantity, qtyStep);
|
|
||||||
const normalizedQty = roundedQuantity > 0 ? roundedQuantity : rawQuantity;
|
|
||||||
if (normalizedQty <= 0) {
|
if (normalizedQty <= 0) {
|
||||||
log("error", "动态止盈单数量无效,跳过下单");
|
log("error", "动态止盈单数量无效,跳过下单");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
await deduplicateOrders(ctx, openOrders, type, side);
|
||||||
lockOperating(locks, timers, pendings, type, log);
|
lockOperating(locks, timers, pendings, type, log);
|
||||||
try {
|
try {
|
||||||
const order = await routeTrailingStopOrder({
|
const order = await routeTrailingStopOrder({
|
||||||
adapter,
|
adapter,
|
||||||
symbol,
|
symbol: ctx.symbol,
|
||||||
side,
|
side,
|
||||||
quantity: normalizedQty,
|
quantity: normalizedQty,
|
||||||
activationPrice: normalizedActivation,
|
activationPrice: normalizedActivation,
|
||||||
@@ -374,28 +382,18 @@ export async function placeTrailingStopOrder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function marketClose(
|
export async function marketClose(ctx: OrderContext, request: MarketCloseRequest): Promise<void> {
|
||||||
adapter: ExchangeAdapter,
|
const { locks, timers, pendings, log } = ctx;
|
||||||
symbol: string,
|
const { side, openOrders, guard, qtyStep } = request;
|
||||||
openOrders: Order[],
|
|
||||||
locks: OrderLockMap,
|
|
||||||
timers: OrderTimerMap,
|
|
||||||
pendings: OrderPendingMap,
|
|
||||||
side: "BUY" | "SELL",
|
|
||||||
quantity: number,
|
|
||||||
log: LogHandler,
|
|
||||||
guard?: OrderGuardOptions,
|
|
||||||
opts?: { qtyStep: number }
|
|
||||||
): Promise<void> {
|
|
||||||
const type = "MARKET";
|
const type = "MARKET";
|
||||||
if (isOperating(locks, type)) return;
|
if (isOperating(locks, type)) return;
|
||||||
if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价平仓")) return;
|
if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价平仓")) return;
|
||||||
|
|
||||||
const qtyStep = opts?.qtyStep;
|
const rawQuantity = Math.abs(request.quantity);
|
||||||
const rawQuantity = Math.abs(quantity);
|
let normalizedQty = qtyStep != null ? normalizeQuantity(rawQuantity, qtyStep) : rawQuantity;
|
||||||
const normalizedQtyRaw = qtyStep != null ? roundQtyDownToStep(rawQuantity, qtyStep) : rawQuantity;
|
|
||||||
let normalizedQty = normalizedQtyRaw > 0 ? normalizedQtyRaw : rawQuantity;
|
|
||||||
if (qtyStep != null) {
|
if (qtyStep != null) {
|
||||||
|
// A step-rounded close that is within rounding noise of the real position
|
||||||
|
// would leave dust behind; close the exact amount instead.
|
||||||
const epsilon = Math.max(qtyStep * 1e-4, 1e-10);
|
const epsilon = Math.max(qtyStep * 1e-4, 1e-10);
|
||||||
if (Math.abs(rawQuantity - normalizedQty) <= epsilon) {
|
if (Math.abs(rawQuantity - normalizedQty) <= epsilon) {
|
||||||
normalizedQty = rawQuantity;
|
normalizedQty = rawQuantity;
|
||||||
@@ -406,12 +404,12 @@ export async function marketClose(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
await deduplicateOrders(ctx, openOrders, type, side);
|
||||||
lockOperating(locks, timers, pendings, type, log);
|
lockOperating(locks, timers, pendings, type, log);
|
||||||
try {
|
try {
|
||||||
const order = await routeCloseOrder({
|
const order = await routeCloseOrder({
|
||||||
adapter,
|
adapter: ctx.adapter,
|
||||||
symbol,
|
symbol: ctx.symbol,
|
||||||
side,
|
side,
|
||||||
quantity: normalizedQty,
|
quantity: normalizedQty,
|
||||||
reduceOnly: true,
|
reduceOnly: true,
|
||||||
|
|||||||
+43
-78
@@ -10,11 +10,13 @@ import {
|
|||||||
placeOrder,
|
placeOrder,
|
||||||
placeStopLossOrder,
|
placeStopLossOrder,
|
||||||
unlockOperating,
|
unlockOperating,
|
||||||
|
type OrderContext,
|
||||||
type OrderLockMap,
|
type OrderLockMap,
|
||||||
type OrderPendingMap,
|
type OrderPendingMap,
|
||||||
type OrderTimerMap,
|
type OrderTimerMap,
|
||||||
} from "../core/order-coordinator";
|
} from "../core/order-coordinator";
|
||||||
import { StrategyEventEmitter } from "./common/event-emitter";
|
import { StrategyEventEmitter } from "./common/event-emitter";
|
||||||
|
import { createPrecisionSyncer, type PrecisionSyncer } from "./common/precision-syncer";
|
||||||
import { safeSubscribe, type LogHandler } from "./common/subscriptions";
|
import { safeSubscribe, type LogHandler } from "./common/subscriptions";
|
||||||
import { clearGridState, loadGridState, saveGridState } from "./common/grid-storage";
|
import { clearGridState, loadGridState, saveGridState } from "./common/grid-storage";
|
||||||
import {
|
import {
|
||||||
@@ -170,7 +172,7 @@ export class GridEngine {
|
|||||||
private savePending = false;
|
private savePending = false;
|
||||||
private uncoveredQty = 0;
|
private uncoveredQty = 0;
|
||||||
private desiredOrders: DesiredGridOrder[] = [];
|
private desiredOrders: DesiredGridOrder[] = [];
|
||||||
private precisionSync: Promise<void> | null = null;
|
private readonly precision: PrecisionSyncer;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly config: GridConfig,
|
private readonly config: GridConfig,
|
||||||
@@ -187,11 +189,25 @@ export class GridEngine {
|
|||||||
this.stopReason = "配置无效,已暂停网格";
|
this.stopReason = "配置无效,已暂停网格";
|
||||||
this.log("error", this.stopReason);
|
this.log("error", this.stopReason);
|
||||||
}
|
}
|
||||||
this.syncPrecision();
|
this.precision = createPrecisionSyncer(this.exchange, this.config, this.config.qtyStep, this.log);
|
||||||
|
this.precision.start();
|
||||||
this.bootstrap();
|
this.bootstrap();
|
||||||
this.setupConnectionProtection();
|
this.setupConnectionProtection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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.pendings,
|
||||||
|
log: (type, detail) => this.tradeLog.push(type, detail),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private orderContextCache: OrderContext | null = null;
|
||||||
|
|
||||||
start(): void {
|
start(): void {
|
||||||
if (this.timer || !this.running) {
|
if (this.timer || !this.running) {
|
||||||
if (!this.timer && !this.running) {
|
if (!this.timer && !this.running) {
|
||||||
@@ -209,6 +225,7 @@ export class GridEngine {
|
|||||||
clearInterval(this.timer);
|
clearInterval(this.timer);
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
}
|
}
|
||||||
|
this.precision.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
on(event: GridEvent, listener: GridListener): void {
|
on(event: GridEvent, listener: GridListener): void {
|
||||||
@@ -277,37 +294,6 @@ export class GridEngine {
|
|||||||
// Precision sync
|
// Precision sync
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
private syncPrecision(): void {
|
|
||||||
if (this.precisionSync) return;
|
|
||||||
const getPrecision = this.exchange.getPrecision?.bind(this.exchange);
|
|
||||||
if (!getPrecision) return;
|
|
||||||
this.precisionSync = getPrecision()
|
|
||||||
.then((precision) => {
|
|
||||||
if (!precision) return;
|
|
||||||
let updated = false;
|
|
||||||
if (Number.isFinite(precision.priceTick) && precision.priceTick > 0) {
|
|
||||||
if (Math.abs(precision.priceTick - this.config.priceTick) > 1e-12) {
|
|
||||||
this.config.priceTick = precision.priceTick;
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Number.isFinite(precision.qtyStep) && precision.qtyStep > 0) {
|
|
||||||
if (Math.abs(precision.qtyStep - this.config.qtyStep) > 1e-12) {
|
|
||||||
this.config.qtyStep = precision.qtyStep;
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (updated) {
|
|
||||||
this.log("info", `已同步交易精度: priceTick=${precision.priceTick} qtyStep=${precision.qtyStep}`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.log("error", `同步精度失败: ${extractMessage(error)}`);
|
|
||||||
this.precisionSync = null;
|
|
||||||
setTimeout(() => this.syncPrecision(), 2000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Feed subscriptions / connection events
|
// Feed subscriptions / connection events
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
@@ -744,26 +730,16 @@ export class GridEngine {
|
|||||||
const ordersVersionBeforePlace = this.ordersVersion;
|
const ordersVersionBeforePlace = this.ordersVersion;
|
||||||
try {
|
try {
|
||||||
this.lastLimitAttemptAt = now;
|
this.lastLimitAttemptAt = now;
|
||||||
placed = await placeOrder(
|
placed = await placeOrder(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: action.side,
|
||||||
this.openOrders,
|
price: action.price,
|
||||||
this.locks,
|
amount: action.qty,
|
||||||
this.timers,
|
reduceOnly: isEntry ? false : this.config.useReduceOnlyForExit,
|
||||||
this.pendings,
|
|
||||||
action.side,
|
|
||||||
action.price,
|
|
||||||
action.qty,
|
|
||||||
this.log,
|
|
||||||
isEntry ? false : this.config.useReduceOnlyForExit,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
priceTick: this.config.priceTick,
|
|
||||||
qtyStep: this.config.qtyStep,
|
qtyStep: this.config.qtyStep,
|
||||||
skipDedupe: true,
|
skipDedupe: true,
|
||||||
clientOrderId,
|
clientOrderId,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log("error", `挂单失败 (${action.side} @ ${action.price}): ${extractMessage(error)}`);
|
this.log("error", `挂单失败 (${action.side} @ ${action.price}): ${extractMessage(error)}`);
|
||||||
}
|
}
|
||||||
@@ -829,23 +805,17 @@ export class GridEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await marketClose(
|
await marketClose(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: side,
|
||||||
this.openOrders,
|
quantity: qty,
|
||||||
this.locks,
|
guard: {
|
||||||
this.timers,
|
|
||||||
this.pendings,
|
|
||||||
side,
|
|
||||||
qty,
|
|
||||||
this.log,
|
|
||||||
{
|
|
||||||
markPrice: mark,
|
markPrice: mark,
|
||||||
expectedPrice: Number.isFinite(closeSidePrice) ? closeSidePrice : null,
|
expectedPrice: Number.isFinite(closeSidePrice) ? closeSidePrice : null,
|
||||||
maxPct: limitPct > 0 ? limitPct : undefined,
|
maxPct: limitPct > 0 ? limitPct : undefined,
|
||||||
},
|
},
|
||||||
{ qtyStep: this.config.qtyStep }
|
qtyStep: this.config.qtyStep
|
||||||
);
|
});
|
||||||
this.log("close", `市价平仓 ${side} ${qty} (${reason})`);
|
this.log("close", `市价平仓 ${side} ${qty} (${reason})`);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -971,21 +941,16 @@ export class GridEngine {
|
|||||||
|
|
||||||
const lastPrice = Number(this.tickerSnapshot?.lastPrice);
|
const lastPrice = Number(this.tickerSnapshot?.lastPrice);
|
||||||
try {
|
try {
|
||||||
const placed = await placeStopLossOrder(
|
const placed = await placeStopLossOrder(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: desired.side,
|
||||||
this.openOrders,
|
stopPrice: desired.stopPrice,
|
||||||
this.locks,
|
quantity: Math.abs(this.position.positionAmt),
|
||||||
this.timers,
|
lastPrice: Number.isFinite(lastPrice) ? lastPrice : price,
|
||||||
this.pendings,
|
guard: undefined,
|
||||||
desired.side,
|
priceTick: this.config.priceTick,
|
||||||
desired.stopPrice,
|
qtyStep: this.config.qtyStep
|
||||||
Math.abs(this.position.positionAmt),
|
});
|
||||||
Number.isFinite(lastPrice) ? lastPrice : price,
|
|
||||||
this.log,
|
|
||||||
undefined,
|
|
||||||
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
|
|
||||||
);
|
|
||||||
if (placed?.orderId != null) {
|
if (placed?.orderId != null) {
|
||||||
state.exchangeStop = {
|
state.exchangeStop = {
|
||||||
orderId: String(placed.orderId),
|
orderId: String(placed.orderId),
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
placeTrailingStopOrder,
|
placeTrailingStopOrder,
|
||||||
unlockOperating,
|
unlockOperating,
|
||||||
} from "../core/order-coordinator";
|
} from "../core/order-coordinator";
|
||||||
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator";
|
import type { OrderContext, OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator";
|
||||||
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
||||||
import { extractMessage, isUnknownOrderError } from "../utils/errors";
|
import { extractMessage, isUnknownOrderError } from "../utils/errors";
|
||||||
import { formatPriceToString } from "../utils/math";
|
import { formatPriceToString } from "../utils/math";
|
||||||
@@ -76,6 +76,19 @@ export class GuardianEngine {
|
|||||||
this.bootstrap();
|
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 {
|
start(): void {
|
||||||
if (this.timer) return;
|
if (this.timer) return;
|
||||||
this.timer = setInterval(() => {
|
this.timer = setInterval(() => {
|
||||||
@@ -390,24 +403,19 @@ export class GuardianEngine {
|
|||||||
if (quantity <= minQty) {
|
if (quantity <= minQty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await placeStopLossOrder(
|
await placeStopLossOrder(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: side,
|
||||||
this.openOrders,
|
stopPrice: stopPrice,
|
||||||
this.locks,
|
quantity: quantity,
|
||||||
this.timers,
|
lastPrice: lastPrice,
|
||||||
this.pending,
|
guard: {
|
||||||
side,
|
|
||||||
stopPrice,
|
|
||||||
quantity,
|
|
||||||
lastPrice,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: position.markPrice,
|
markPrice: position.markPrice,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
|
priceTick: this.config.priceTick,
|
||||||
);
|
qtyStep: this.config.qtyStep
|
||||||
|
});
|
||||||
this.lastStopAttempt.side = side;
|
this.lastStopAttempt.side = side;
|
||||||
this.lastStopAttempt.price = stopPrice;
|
this.lastStopAttempt.price = stopPrice;
|
||||||
this.lastStopAttempt.at = now;
|
this.lastStopAttempt.at = now;
|
||||||
@@ -449,24 +457,19 @@ export class GuardianEngine {
|
|||||||
if (quantity <= minQty) {
|
if (quantity <= minQty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const order = await placeStopLossOrder(
|
const order = await placeStopLossOrder(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: side,
|
||||||
this.openOrders,
|
stopPrice: nextStopPrice,
|
||||||
this.locks,
|
quantity: quantity,
|
||||||
this.timers,
|
lastPrice: lastPrice,
|
||||||
this.pending,
|
guard: {
|
||||||
side,
|
|
||||||
nextStopPrice,
|
|
||||||
quantity,
|
|
||||||
lastPrice,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: position.markPrice,
|
markPrice: position.markPrice,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
|
priceTick: this.config.priceTick,
|
||||||
);
|
qtyStep: this.config.qtyStep
|
||||||
|
});
|
||||||
if (order) {
|
if (order) {
|
||||||
this.tradeLog.push(
|
this.tradeLog.push(
|
||||||
"stop",
|
"stop",
|
||||||
@@ -484,24 +487,19 @@ export class GuardianEngine {
|
|||||||
if (quantity <= minQty) {
|
if (quantity <= minQty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const restored = await placeStopLossOrder(
|
const restored = await placeStopLossOrder(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: side,
|
||||||
this.openOrders,
|
stopPrice: Number.isFinite(existingStopPrice) ? existingStopPrice : nextStopPrice,
|
||||||
this.locks,
|
quantity: quantity,
|
||||||
this.timers,
|
lastPrice: lastPrice,
|
||||||
this.pending,
|
guard: {
|
||||||
side,
|
|
||||||
Number.isFinite(existingStopPrice) ? existingStopPrice : nextStopPrice,
|
|
||||||
quantity,
|
|
||||||
lastPrice,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: position.markPrice,
|
markPrice: position.markPrice,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
|
priceTick: this.config.priceTick,
|
||||||
);
|
qtyStep: this.config.qtyStep
|
||||||
|
});
|
||||||
if (restored && Number.isFinite(existingStopPrice)) {
|
if (restored && Number.isFinite(existingStopPrice)) {
|
||||||
this.tradeLog.push(
|
this.tradeLog.push(
|
||||||
"order",
|
"order",
|
||||||
@@ -525,24 +523,19 @@ export class GuardianEngine {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await placeTrailingStopOrder(
|
await placeTrailingStopOrder(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: side,
|
||||||
this.openOrders,
|
activationPrice: activationPrice,
|
||||||
this.locks,
|
quantity: quantity,
|
||||||
this.timers,
|
callbackRate: this.config.trailingCallbackRate,
|
||||||
this.pending,
|
guard: {
|
||||||
side,
|
|
||||||
activationPrice,
|
|
||||||
quantity,
|
|
||||||
this.config.trailingCallbackRate,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
|
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
|
priceTick: this.config.priceTick,
|
||||||
);
|
qtyStep: this.config.qtyStep
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.tradeLog.push("error", t("log.guardian.trailingFail", { error: String(err) }));
|
this.tradeLog.push("error", t("log.guardian.trailingFail", { error: String(err) }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
placeOrder,
|
placeOrder,
|
||||||
unlockOperating,
|
unlockOperating,
|
||||||
} from "../core/order-coordinator";
|
} from "../core/order-coordinator";
|
||||||
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator";
|
import type { OrderContext, OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator";
|
||||||
import type { MakerEngineSnapshot } from "./maker-engine";
|
import type { MakerEngineSnapshot } from "./maker-engine";
|
||||||
import { makeOrderPlan } from "../core/lib/order-plan";
|
import { makeOrderPlan } from "../core/lib/order-plan";
|
||||||
import { safeCancelOrder } from "../core/lib/orders";
|
import { safeCancelOrder } from "../core/lib/orders";
|
||||||
@@ -151,6 +151,19 @@ export class LiquidityMakerEngine {
|
|||||||
this.bootstrap();
|
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 {
|
start(): void {
|
||||||
if (this.timer) return;
|
if (this.timer) return;
|
||||||
this.timer = setInterval(() => {
|
this.timer = setInterval(() => {
|
||||||
@@ -698,17 +711,11 @@ export class LiquidityMakerEngine {
|
|||||||
const closeBidPrice = topBid != null ? formatPriceToString(topBid, priceDecimals) : null;
|
const closeBidPrice = topBid != null ? formatPriceToString(topBid, priceDecimals) : null;
|
||||||
const closeAskPrice = topAsk != null ? formatPriceToString(topAsk, priceDecimals) : null;
|
const closeAskPrice = topAsk != null ? formatPriceToString(topAsk, priceDecimals) : null;
|
||||||
try {
|
try {
|
||||||
await marketClose(
|
await marketClose(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: side,
|
||||||
this.openOrders,
|
quantity: absPosition,
|
||||||
this.locks,
|
guard: {
|
||||||
this.timers,
|
|
||||||
this.pending,
|
|
||||||
side,
|
|
||||||
absPosition,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: position.markPrice,
|
markPrice: position.markPrice,
|
||||||
expectedPrice:
|
expectedPrice:
|
||||||
side === "SELL"
|
side === "SELL"
|
||||||
@@ -716,8 +723,8 @@ export class LiquidityMakerEngine {
|
|||||||
: (closeBidPrice != null ? Number(closeBidPrice) : null),
|
: (closeBidPrice != null ? Number(closeBidPrice) : null),
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ qtyStep: this.precision.qtyStep }
|
qtyStep: this.precision.qtyStep
|
||||||
);
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isUnknownOrderError(error)) {
|
if (isUnknownOrderError(error)) {
|
||||||
this.tradeLog.push("order", "限频强制平仓时订单已不存在");
|
this.tradeLog.push("order", "限频强制平仓时订单已不存在");
|
||||||
@@ -886,27 +893,18 @@ export class LiquidityMakerEngine {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const reduceOnlyFlag = this.marketType === "spot" ? false : target.reduceOnly;
|
const reduceOnlyFlag = this.marketType === "spot" ? false : target.reduceOnly;
|
||||||
await placeOrder(
|
await placeOrder(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: target.side,
|
||||||
this.openOrders,
|
price: target.price,
|
||||||
this.locks,
|
amount: target.amount,
|
||||||
this.timers,
|
reduceOnly: reduceOnlyFlag,
|
||||||
this.pending,
|
guard: {
|
||||||
target.side,
|
|
||||||
target.price, // 已经是字符串价格
|
|
||||||
target.amount,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
reduceOnlyFlag,
|
|
||||||
{
|
|
||||||
markPrice: this.getPositionSnapshot().markPrice,
|
markPrice: this.getPositionSnapshot().markPrice,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{
|
qtyStep: this.precision.qtyStep
|
||||||
priceTick: this.precision.priceTick,
|
});
|
||||||
qtyStep: this.precision.qtyStep,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// Record last placed entry order timing and price
|
// Record last placed entry order timing and price
|
||||||
if (!target.reduceOnly) {
|
if (!target.reduceOnly) {
|
||||||
this.lastEntryOrderBySide[target.side] = { price: target.price, ts: Date.now() };
|
this.lastEntryOrderBySide[target.side] = { price: target.price, ts: Date.now() };
|
||||||
@@ -955,23 +953,17 @@ export class LiquidityMakerEngine {
|
|||||||
// 尽力撤销所有未完成挂单,避免锁定基础资产导致余额不足
|
// 尽力撤销所有未完成挂单,避免锁定基础资产导致余额不足
|
||||||
await this.exchange.cancelAllOrders({ symbol: this.config.symbol }).catch(() => {});
|
await this.exchange.cancelAllOrders({ symbol: this.config.symbol }).catch(() => {});
|
||||||
await this.flushOrders();
|
await this.flushOrders();
|
||||||
await marketClose(
|
await marketClose(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: "SELL",
|
||||||
this.openOrders,
|
quantity: absPosition,
|
||||||
this.locks,
|
guard: {
|
||||||
this.timers,
|
|
||||||
this.pending,
|
|
||||||
"SELL",
|
|
||||||
absPosition,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: position.markPrice,
|
markPrice: position.markPrice,
|
||||||
expectedPrice: bidPrice || null,
|
expectedPrice: bidPrice || null,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ qtyStep: this.precision.qtyStep }
|
qtyStep: this.precision.qtyStep
|
||||||
);
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isRateLimitError(error)) throw error;
|
if (isRateLimitError(error)) throw error;
|
||||||
if (isUnknownOrderError(error)) {
|
if (isUnknownOrderError(error)) {
|
||||||
@@ -1005,23 +997,17 @@ export class LiquidityMakerEngine {
|
|||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await this.flushOrders();
|
await this.flushOrders();
|
||||||
await marketClose(
|
await marketClose(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: position.positionAmt > 0 ? "SELL" : "BUY",
|
||||||
this.openOrders,
|
quantity: absPosition,
|
||||||
this.locks,
|
guard: {
|
||||||
this.timers,
|
|
||||||
this.pending,
|
|
||||||
position.positionAmt > 0 ? "SELL" : "BUY",
|
|
||||||
absPosition,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: position.markPrice,
|
markPrice: position.markPrice,
|
||||||
expectedPrice: Number(position.positionAmt > 0 ? bidPrice : askPrice) || null,
|
expectedPrice: Number(position.positionAmt > 0 ? bidPrice : askPrice) || null,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ qtyStep: this.precision.qtyStep }
|
qtyStep: this.precision.qtyStep
|
||||||
);
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isUnknownOrderError(error)) {
|
if (isUnknownOrderError(error)) {
|
||||||
this.tradeLog.push("order", "止损平仓时订单已不存在");
|
this.tradeLog.push("order", "止损平仓时订单已不存在");
|
||||||
@@ -1253,17 +1239,11 @@ export class LiquidityMakerEngine {
|
|||||||
if (absQty < EPS) return false;
|
if (absQty < EPS) return false;
|
||||||
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
|
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
|
||||||
try {
|
try {
|
||||||
await marketClose(
|
await marketClose(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: target.side,
|
||||||
this.openOrders,
|
quantity: absQty,
|
||||||
this.locks,
|
guard: {
|
||||||
this.timers,
|
|
||||||
this.pending,
|
|
||||||
target.side,
|
|
||||||
absQty,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: position.markPrice,
|
markPrice: position.markPrice,
|
||||||
expectedPrice:
|
expectedPrice:
|
||||||
target.side === "SELL"
|
target.side === "SELL"
|
||||||
@@ -1271,8 +1251,8 @@ export class LiquidityMakerEngine {
|
|||||||
: (topAsk != null ? Number(topAsk) : null),
|
: (topAsk != null ? Number(topAsk) : null),
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ qtyStep: this.precision.qtyStep }
|
qtyStep: this.precision.qtyStep
|
||||||
);
|
});
|
||||||
this.tradeLog.push("order", `小额仓位使用市价平仓 ${target.side} 数量 ${absQty.toFixed(6)}`);
|
this.tradeLog.push("order", `小额仓位使用市价平仓 ${target.side} 数量 ${absQty.toFixed(6)}`);
|
||||||
return true;
|
return true;
|
||||||
} catch (closeError) {
|
} catch (closeError) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
placeOrder,
|
placeOrder,
|
||||||
unlockOperating,
|
unlockOperating,
|
||||||
} from "../core/order-coordinator";
|
} from "../core/order-coordinator";
|
||||||
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator";
|
import type { OrderContext, OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator";
|
||||||
import { makeOrderPlan } from "../core/lib/order-plan";
|
import { makeOrderPlan } from "../core/lib/order-plan";
|
||||||
import { safeCancelOrder } from "../core/lib/orders";
|
import { safeCancelOrder } from "../core/lib/orders";
|
||||||
import { RateLimitController } from "../core/lib/rate-limit";
|
import { RateLimitController } from "../core/lib/rate-limit";
|
||||||
@@ -127,6 +127,19 @@ export class MakerEngine {
|
|||||||
this.bootstrap();
|
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 {
|
start(): void {
|
||||||
if (this.timer) return;
|
if (this.timer) return;
|
||||||
this.timer = setInterval(() => {
|
this.timer = setInterval(() => {
|
||||||
@@ -439,27 +452,18 @@ export class MakerEngine {
|
|||||||
if (!target) continue;
|
if (!target) continue;
|
||||||
if (target.amount < EPS) continue;
|
if (target.amount < EPS) continue;
|
||||||
try {
|
try {
|
||||||
await placeOrder(
|
await placeOrder(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: target.side,
|
||||||
this.openOrders,
|
price: target.price,
|
||||||
this.locks,
|
amount: target.amount,
|
||||||
this.timers,
|
reduceOnly: target.reduceOnly,
|
||||||
this.pending,
|
guard: {
|
||||||
target.side,
|
|
||||||
target.price, // 已经是字符串价格
|
|
||||||
target.amount,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
target.reduceOnly,
|
|
||||||
{
|
|
||||||
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
|
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{
|
qtyStep: this.precision.qtyStep
|
||||||
priceTick: this.precision.priceTick,
|
});
|
||||||
qtyStep: this.precision.qtyStep,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isInsufficientBalanceError(error)) {
|
if (isInsufficientBalanceError(error)) {
|
||||||
this.registerInsufficientBalance(error);
|
this.registerInsufficientBalance(error);
|
||||||
@@ -507,23 +511,17 @@ export class MakerEngine {
|
|||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await this.flushOrders();
|
await this.flushOrders();
|
||||||
await marketClose(
|
await marketClose(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: position.positionAmt > 0 ? "SELL" : "BUY",
|
||||||
this.openOrders,
|
quantity: absPosition,
|
||||||
this.locks,
|
guard: {
|
||||||
this.timers,
|
|
||||||
this.pending,
|
|
||||||
position.positionAmt > 0 ? "SELL" : "BUY",
|
|
||||||
absPosition,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: position.markPrice,
|
markPrice: position.markPrice,
|
||||||
expectedPrice: Number(closeSidePrice) || null,
|
expectedPrice: Number(closeSidePrice) || null,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ qtyStep: this.precision.qtyStep }
|
qtyStep: this.precision.qtyStep
|
||||||
);
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isUnknownOrderError(error)) {
|
if (isUnknownOrderError(error)) {
|
||||||
this.tradeLog.push("order", t("log.maker.stopOrderMissing"));
|
this.tradeLog.push("order", t("log.maker.stopOrderMissing"));
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
placeOrder,
|
placeOrder,
|
||||||
unlockOperating,
|
unlockOperating,
|
||||||
} from "../core/order-coordinator";
|
} from "../core/order-coordinator";
|
||||||
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator";
|
import type { OrderContext, OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator";
|
||||||
import { makeOrderPlan } from "../core/lib/order-plan";
|
import { makeOrderPlan } from "../core/lib/order-plan";
|
||||||
import { safeCancelOrder } from "../core/lib/orders";
|
import { safeCancelOrder } from "../core/lib/orders";
|
||||||
import { RateLimitController } from "../core/lib/rate-limit";
|
import { RateLimitController } from "../core/lib/rate-limit";
|
||||||
@@ -244,6 +244,19 @@ export class MakerPointsEngine {
|
|||||||
this.bootstrap();
|
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 {
|
start(): void {
|
||||||
if (this.timer) return;
|
if (this.timer) return;
|
||||||
// 初始化数据时间戳
|
// 初始化数据时间戳
|
||||||
@@ -1046,26 +1059,17 @@ export class MakerPointsEngine {
|
|||||||
: target.side === "BUY"
|
: target.side === "BUY"
|
||||||
? priceNum - 1
|
? priceNum - 1
|
||||||
: priceNum + 1;
|
: priceNum + 1;
|
||||||
await placeOrder(
|
await placeOrder(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: target.side,
|
||||||
this.openOrders,
|
price: target.price,
|
||||||
this.locks,
|
amount: target.amount,
|
||||||
this.timers,
|
reduceOnly: target.reduceOnly,
|
||||||
this.pending,
|
guard: undefined,
|
||||||
target.side,
|
|
||||||
target.price,
|
|
||||||
target.amount,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
target.reduceOnly,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
priceTick: this.precision.priceTick,
|
|
||||||
qtyStep: this.precision.qtyStep,
|
qtyStep: this.precision.qtyStep,
|
||||||
skipDedupe: true,
|
skipDedupe: true,
|
||||||
slPrice,
|
slPrice
|
||||||
}
|
});
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isInsufficientBalanceError(error)) {
|
if (isInsufficientBalanceError(error)) {
|
||||||
this.registerInsufficientBalance(error);
|
this.registerInsufficientBalance(error);
|
||||||
@@ -1231,19 +1235,13 @@ export class MakerPointsEngine {
|
|||||||
await this.flushOrders();
|
await this.flushOrders();
|
||||||
|
|
||||||
// 执行市价平仓
|
// 执行市价平仓
|
||||||
await marketClose(
|
await marketClose(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: side,
|
||||||
this.openOrders,
|
quantity: currentAbsPosition,
|
||||||
this.locks,
|
guard: undefined,
|
||||||
this.timers,
|
qtyStep: this.precision.qtyStep
|
||||||
this.pending,
|
});
|
||||||
side,
|
|
||||||
currentAbsPosition,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
undefined,
|
|
||||||
{ qtyStep: this.precision.qtyStep }
|
|
||||||
);
|
|
||||||
|
|
||||||
// 等待一小段时间让账户数据更新
|
// 等待一小段时间让账户数据更新
|
||||||
await this.sleep(STOP_LOSS_RETRY_INTERVAL_MS);
|
await this.sleep(STOP_LOSS_RETRY_INTERVAL_MS);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
placeOrder,
|
placeOrder,
|
||||||
unlockOperating,
|
unlockOperating,
|
||||||
} from "../core/order-coordinator";
|
} from "../core/order-coordinator";
|
||||||
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator";
|
import type { OrderContext, OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator";
|
||||||
import type { MakerEngineSnapshot } from "./maker-engine";
|
import type { MakerEngineSnapshot } from "./maker-engine";
|
||||||
import { makeOrderPlan } from "../core/lib/order-plan";
|
import { makeOrderPlan } from "../core/lib/order-plan";
|
||||||
import { safeCancelOrder } from "../core/lib/orders";
|
import { safeCancelOrder } from "../core/lib/orders";
|
||||||
@@ -141,6 +141,19 @@ export class OffsetMakerEngine {
|
|||||||
this.bootstrap();
|
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 {
|
start(): void {
|
||||||
if (this.timer) return;
|
if (this.timer) return;
|
||||||
this.timer = setInterval(() => {
|
this.timer = setInterval(() => {
|
||||||
@@ -551,17 +564,11 @@ export class OffsetMakerEngine {
|
|||||||
const closeBidPrice = topBid != null ? formatPriceToString(topBid, priceDecimals) : null;
|
const closeBidPrice = topBid != null ? formatPriceToString(topBid, priceDecimals) : null;
|
||||||
const closeAskPrice = topAsk != null ? formatPriceToString(topAsk, priceDecimals) : null;
|
const closeAskPrice = topAsk != null ? formatPriceToString(topAsk, priceDecimals) : null;
|
||||||
try {
|
try {
|
||||||
await marketClose(
|
await marketClose(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: side,
|
||||||
this.openOrders,
|
quantity: absPosition,
|
||||||
this.locks,
|
guard: {
|
||||||
this.timers,
|
|
||||||
this.pending,
|
|
||||||
side,
|
|
||||||
absPosition,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: position.markPrice,
|
markPrice: position.markPrice,
|
||||||
expectedPrice:
|
expectedPrice:
|
||||||
side === "SELL"
|
side === "SELL"
|
||||||
@@ -569,8 +576,8 @@ export class OffsetMakerEngine {
|
|||||||
: (closeBidPrice != null ? Number(closeBidPrice) : null),
|
: (closeBidPrice != null ? Number(closeBidPrice) : null),
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ qtyStep: this.precision.qtyStep }
|
qtyStep: this.precision.qtyStep
|
||||||
);
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isUnknownOrderError(error)) {
|
if (isUnknownOrderError(error)) {
|
||||||
this.tradeLog.push("order", "限频强制平仓时订单已不存在");
|
this.tradeLog.push("order", "限频强制平仓时订单已不存在");
|
||||||
@@ -644,23 +651,17 @@ export class OffsetMakerEngine {
|
|||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await this.flushOrders();
|
await this.flushOrders();
|
||||||
await marketClose(
|
await marketClose(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: side,
|
||||||
this.openOrders,
|
quantity: absPosition,
|
||||||
this.locks,
|
guard: {
|
||||||
this.timers,
|
|
||||||
this.pending,
|
|
||||||
side,
|
|
||||||
absPosition,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: position.markPrice,
|
markPrice: position.markPrice,
|
||||||
expectedPrice: Number(closeSidePrice) || null,
|
expectedPrice: Number(closeSidePrice) || null,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ qtyStep: this.precision.qtyStep }
|
qtyStep: this.precision.qtyStep
|
||||||
);
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isUnknownOrderError(error)) {
|
if (isUnknownOrderError(error)) {
|
||||||
this.tradeLog.push("order", "深度不平衡平仓时订单已不存在");
|
this.tradeLog.push("order", "深度不平衡平仓时订单已不存在");
|
||||||
@@ -747,27 +748,18 @@ export class OffsetMakerEngine {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const reduceOnlyFlag = this.marketType === "spot" ? false : target.reduceOnly;
|
const reduceOnlyFlag = this.marketType === "spot" ? false : target.reduceOnly;
|
||||||
await placeOrder(
|
await placeOrder(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: target.side,
|
||||||
this.openOrders,
|
price: target.price,
|
||||||
this.locks,
|
amount: target.amount,
|
||||||
this.timers,
|
reduceOnly: reduceOnlyFlag,
|
||||||
this.pending,
|
guard: {
|
||||||
target.side,
|
|
||||||
target.price, // 已经是字符串价格
|
|
||||||
target.amount,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
reduceOnlyFlag,
|
|
||||||
{
|
|
||||||
markPrice: this.getPositionSnapshot().markPrice,
|
markPrice: this.getPositionSnapshot().markPrice,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{
|
qtyStep: this.precision.qtyStep
|
||||||
priceTick: this.precision.priceTick,
|
});
|
||||||
qtyStep: this.precision.qtyStep,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// Record last placed entry order timing and price
|
// Record last placed entry order timing and price
|
||||||
if (!target.reduceOnly) {
|
if (!target.reduceOnly) {
|
||||||
this.lastEntryOrderBySide[target.side] = { price: target.price, ts: Date.now() };
|
this.lastEntryOrderBySide[target.side] = { price: target.price, ts: Date.now() };
|
||||||
@@ -816,23 +808,17 @@ export class OffsetMakerEngine {
|
|||||||
// 尽力撤销所有未完成挂单,避免锁定基础资产导致余额不足
|
// 尽力撤销所有未完成挂单,避免锁定基础资产导致余额不足
|
||||||
await this.exchange.cancelAllOrders({ symbol: this.config.symbol }).catch(() => {});
|
await this.exchange.cancelAllOrders({ symbol: this.config.symbol }).catch(() => {});
|
||||||
await this.flushOrders();
|
await this.flushOrders();
|
||||||
await marketClose(
|
await marketClose(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: "SELL",
|
||||||
this.openOrders,
|
quantity: absPosition,
|
||||||
this.locks,
|
guard: {
|
||||||
this.timers,
|
|
||||||
this.pending,
|
|
||||||
"SELL",
|
|
||||||
absPosition,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: position.markPrice,
|
markPrice: position.markPrice,
|
||||||
expectedPrice: bidPrice || null,
|
expectedPrice: bidPrice || null,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ qtyStep: this.precision.qtyStep }
|
qtyStep: this.precision.qtyStep
|
||||||
);
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isRateLimitError(error)) throw error;
|
if (isRateLimitError(error)) throw error;
|
||||||
if (isUnknownOrderError(error)) {
|
if (isUnknownOrderError(error)) {
|
||||||
@@ -866,23 +852,17 @@ export class OffsetMakerEngine {
|
|||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await this.flushOrders();
|
await this.flushOrders();
|
||||||
await marketClose(
|
await marketClose(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: position.positionAmt > 0 ? "SELL" : "BUY",
|
||||||
this.openOrders,
|
quantity: absPosition,
|
||||||
this.locks,
|
guard: {
|
||||||
this.timers,
|
|
||||||
this.pending,
|
|
||||||
position.positionAmt > 0 ? "SELL" : "BUY",
|
|
||||||
absPosition,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: position.markPrice,
|
markPrice: position.markPrice,
|
||||||
expectedPrice: Number(position.positionAmt > 0 ? bidPrice : askPrice) || null,
|
expectedPrice: Number(position.positionAmt > 0 ? bidPrice : askPrice) || null,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ qtyStep: this.precision.qtyStep }
|
qtyStep: this.precision.qtyStep
|
||||||
);
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isUnknownOrderError(error)) {
|
if (isUnknownOrderError(error)) {
|
||||||
this.tradeLog.push("order", "止损平仓时订单已不存在");
|
this.tradeLog.push("order", "止损平仓时订单已不存在");
|
||||||
@@ -1123,17 +1103,11 @@ export class OffsetMakerEngine {
|
|||||||
if (absQty < EPS) return false;
|
if (absQty < EPS) return false;
|
||||||
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
|
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
|
||||||
try {
|
try {
|
||||||
await marketClose(
|
await marketClose(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: target.side,
|
||||||
this.openOrders,
|
quantity: absQty,
|
||||||
this.locks,
|
guard: {
|
||||||
this.timers,
|
|
||||||
this.pending,
|
|
||||||
target.side,
|
|
||||||
absQty,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: position.markPrice,
|
markPrice: position.markPrice,
|
||||||
expectedPrice:
|
expectedPrice:
|
||||||
target.side === "SELL"
|
target.side === "SELL"
|
||||||
@@ -1141,8 +1115,8 @@ export class OffsetMakerEngine {
|
|||||||
: (topAsk != null ? Number(topAsk) : null),
|
: (topAsk != null ? Number(topAsk) : null),
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ qtyStep: this.precision.qtyStep }
|
qtyStep: this.precision.qtyStep
|
||||||
);
|
});
|
||||||
this.tradeLog.push("order", `小额仓位使用市价平仓 ${target.side} 数量 ${absQty.toFixed(6)}`);
|
this.tradeLog.push("order", `小额仓位使用市价平仓 ${target.side} 数量 ${absQty.toFixed(6)}`);
|
||||||
return true;
|
return true;
|
||||||
} catch (closeError) {
|
} catch (closeError) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { ExchangeAdapter } from "../exchanges/adapter";
|
|||||||
import type { AccountSnapshot, Depth, Order, Ticker } from "../exchanges/types";
|
import type { AccountSnapshot, Depth, Order, Ticker } from "../exchanges/types";
|
||||||
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
||||||
import { marketClose, placeMarketOrder, placeStopLossOrder, unlockOperating } from "../core/order-coordinator";
|
import { marketClose, placeMarketOrder, placeStopLossOrder, 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, isRateLimitError, isUnknownOrderError } from "../utils/errors";
|
import { extractMessage, isRateLimitError, isUnknownOrderError } from "../utils/errors";
|
||||||
import { getPosition, type PositionSnapshot } from "../utils/strategy";
|
import { getPosition, type PositionSnapshot } from "../utils/strategy";
|
||||||
import { computePositionPnl } from "../utils/pnl";
|
import { computePositionPnl } from "../utils/pnl";
|
||||||
@@ -136,6 +136,19 @@ export class SwingEngine {
|
|||||||
this.bootstrap();
|
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 {
|
start(): void {
|
||||||
if (this.timer) return;
|
if (this.timer) return;
|
||||||
this.timer = setInterval(() => {
|
this.timer = setInterval(() => {
|
||||||
@@ -348,24 +361,18 @@ export class SwingEngine {
|
|||||||
if (Math.abs(position.positionAmt) > EPS) {
|
if (Math.abs(position.positionAmt) > EPS) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await placeMarketOrder(
|
await placeMarketOrder(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: side,
|
||||||
this.openOrders,
|
amount: this.config.tradeAmount,
|
||||||
this.locks,
|
reduceOnly: false,
|
||||||
this.timers,
|
guard: {
|
||||||
this.pending,
|
|
||||||
side,
|
|
||||||
this.config.tradeAmount,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
false,
|
|
||||||
{
|
|
||||||
markPrice: position.markPrice,
|
markPrice: position.markPrice,
|
||||||
expectedPrice: Number(this.tickerSnapshot?.lastPrice) || null,
|
expectedPrice: Number(this.tickerSnapshot?.lastPrice) || null,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ qtyStep: this.config.qtyStep }
|
qtyStep: this.config.qtyStep
|
||||||
);
|
});
|
||||||
this.tradeLog.push("open", `${reason}: ${side} (market)`);
|
this.tradeLog.push("open", `${reason}: ${side} (market)`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.tradeLog.push("error", `Open failed: ${extractMessage(err)}`);
|
this.tradeLog.push("error", `Open failed: ${extractMessage(err)}`);
|
||||||
@@ -380,23 +387,17 @@ export class SwingEngine {
|
|||||||
side === "SELL"
|
side === "SELL"
|
||||||
? Number(this.depthSnapshot?.bids?.[0]?.[0])
|
? Number(this.depthSnapshot?.bids?.[0]?.[0])
|
||||||
: Number(this.depthSnapshot?.asks?.[0]?.[0]);
|
: Number(this.depthSnapshot?.asks?.[0]?.[0]);
|
||||||
await marketClose(
|
await marketClose(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: side,
|
||||||
this.openOrders,
|
quantity: Math.abs(position.positionAmt),
|
||||||
this.locks,
|
guard: {
|
||||||
this.timers,
|
|
||||||
this.pending,
|
|
||||||
side,
|
|
||||||
Math.abs(position.positionAmt),
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: position.markPrice,
|
markPrice: position.markPrice,
|
||||||
expectedPrice: Number.isFinite(expected) ? expected : Number(this.tickerSnapshot?.lastPrice) || null,
|
expectedPrice: Number.isFinite(expected) ? expected : Number(this.tickerSnapshot?.lastPrice) || null,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ qtyStep: this.config.qtyStep }
|
qtyStep: this.config.qtyStep
|
||||||
);
|
});
|
||||||
this.tradeLog.push("close", `${reason}: ${side} (market close)`);
|
this.tradeLog.push("close", `${reason}: ${side} (market close)`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isUnknownOrderError(err)) {
|
if (isUnknownOrderError(err)) {
|
||||||
@@ -459,24 +460,19 @@ export class SwingEngine {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const qty = Math.abs(position.positionAmt);
|
const qty = Math.abs(position.positionAmt);
|
||||||
await placeStopLossOrder(
|
await placeStopLossOrder(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: stopSide,
|
||||||
this.openOrders,
|
stopPrice: stopPrice,
|
||||||
this.locks,
|
quantity: qty,
|
||||||
this.timers,
|
lastPrice: lastPrice,
|
||||||
this.pending,
|
guard: {
|
||||||
stopSide,
|
|
||||||
stopPrice,
|
|
||||||
qty,
|
|
||||||
lastPrice,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: position.markPrice,
|
markPrice: position.markPrice,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
|
priceTick: this.config.priceTick,
|
||||||
);
|
qtyStep: this.config.qtyStep
|
||||||
|
});
|
||||||
this.lastStopAttempt = { side: stopSide, price: stopPrice, at: Date.now() };
|
this.lastStopAttempt = { side: stopSide, price: stopPrice, at: Date.now() };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.lastStopAttempt = { side: stopSide, price: stopPrice, at: Date.now() };
|
this.lastStopAttempt = { side: stopSide, price: stopPrice, at: Date.now() };
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
placeTrailingStopOrder,
|
placeTrailingStopOrder,
|
||||||
unlockOperating,
|
unlockOperating,
|
||||||
} from "../core/order-coordinator";
|
} 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 { extractMessage, isUnknownOrderError } from "../utils/errors";
|
||||||
import { formatPriceToString } from "../utils/math";
|
import { formatPriceToString } from "../utils/math";
|
||||||
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
||||||
@@ -139,6 +139,19 @@ export class TrendEngine {
|
|||||||
this.bootstrap();
|
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 {
|
start(): void {
|
||||||
if (this.timer) return;
|
if (this.timer) return;
|
||||||
this.timer = setInterval(() => {
|
this.timer = setInterval(() => {
|
||||||
@@ -490,24 +503,18 @@ export class TrendEngine {
|
|||||||
|
|
||||||
private async submitMarketOrder(side: "BUY" | "SELL", price: number, reason: string): Promise<void> {
|
private async submitMarketOrder(side: "BUY" | "SELL", price: number, reason: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await placeMarketOrder(
|
await placeMarketOrder(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: side,
|
||||||
this.openOrders,
|
amount: this.config.tradeAmount,
|
||||||
this.locks,
|
reduceOnly: false,
|
||||||
this.timers,
|
guard: {
|
||||||
this.pending,
|
|
||||||
side,
|
|
||||||
this.config.tradeAmount,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
false,
|
|
||||||
{
|
|
||||||
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
|
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
|
||||||
expectedPrice: Number(this.tickerSnapshot?.lastPrice) || null,
|
expectedPrice: Number(this.tickerSnapshot?.lastPrice) || null,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ qtyStep: this.config.qtyStep }
|
qtyStep: this.config.qtyStep
|
||||||
);
|
});
|
||||||
this.tradeLog.push("open", `${reason}: ${side} @ ${price}`);
|
this.tradeLog.push("open", `${reason}: ${side} @ ${price}`);
|
||||||
this.lastOpenPlan = { side, price };
|
this.lastOpenPlan = { side, price };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -748,17 +755,11 @@ export class TrendEngine {
|
|||||||
return { closed: false, pnl };
|
return { closed: false, pnl };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await marketClose(
|
await marketClose(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: direction === "long" ? "SELL" : "BUY",
|
||||||
this.openOrders,
|
quantity: Math.abs(position.positionAmt),
|
||||||
this.locks,
|
guard: {
|
||||||
this.timers,
|
|
||||||
this.pending,
|
|
||||||
direction === "long" ? "SELL" : "BUY",
|
|
||||||
Math.abs(position.positionAmt),
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
|
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
|
||||||
expectedPrice: Number(
|
expectedPrice: Number(
|
||||||
direction === "long"
|
direction === "long"
|
||||||
@@ -767,8 +768,8 @@ export class TrendEngine {
|
|||||||
) || null,
|
) || null,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ qtyStep: this.config.qtyStep }
|
qtyStep: this.config.qtyStep
|
||||||
);
|
});
|
||||||
result.closed = true;
|
result.closed = true;
|
||||||
this.tradeLog.push("close", t("log.trend.stopClose", { side: direction === "long" ? "SELL" : "BUY" }));
|
this.tradeLog.push("close", t("log.trend.stopClose", { side: direction === "long" ? "SELL" : "BUY" }));
|
||||||
// 记录止损时间以便短期内抑制再次入场
|
// 记录止损时间以便短期内抑制再次入场
|
||||||
@@ -811,24 +812,19 @@ export class TrendEngine {
|
|||||||
if (quantity <= minQty) {
|
if (quantity <= minQty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await placeStopLossOrder(
|
await placeStopLossOrder(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: side,
|
||||||
this.openOrders,
|
stopPrice: stopPrice,
|
||||||
this.locks,
|
quantity: quantity,
|
||||||
this.timers,
|
lastPrice: lastPrice,
|
||||||
this.pending,
|
guard: {
|
||||||
side,
|
|
||||||
stopPrice,
|
|
||||||
quantity,
|
|
||||||
lastPrice,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: position.markPrice,
|
markPrice: position.markPrice,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
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() };
|
this.lastStopAttempt = { side, price: stopPrice, at: Date.now() };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.tradeLog.push("error", t("log.trend.placeStopFail", { error: String(err) }));
|
this.tradeLog.push("error", t("log.trend.placeStopFail", { error: String(err) }));
|
||||||
@@ -871,24 +867,19 @@ export class TrendEngine {
|
|||||||
if (quantity <= minQty) {
|
if (quantity <= minQty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const order = await placeStopLossOrder(
|
const order = await placeStopLossOrder(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: side,
|
||||||
this.openOrders,
|
stopPrice: nextStopPrice,
|
||||||
this.locks,
|
quantity: quantity,
|
||||||
this.timers,
|
lastPrice: lastPrice,
|
||||||
this.pending,
|
guard: {
|
||||||
side,
|
|
||||||
nextStopPrice,
|
|
||||||
quantity,
|
|
||||||
lastPrice,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: position.markPrice,
|
markPrice: position.markPrice,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
|
priceTick: this.config.priceTick,
|
||||||
);
|
qtyStep: this.config.qtyStep
|
||||||
|
});
|
||||||
if (order) {
|
if (order) {
|
||||||
this.tradeLog.push(
|
this.tradeLog.push(
|
||||||
"stop",
|
"stop",
|
||||||
@@ -914,24 +905,19 @@ export class TrendEngine {
|
|||||||
(side === "SELL" && existingStopPrice >= lastPrice) ||
|
(side === "SELL" && existingStopPrice >= lastPrice) ||
|
||||||
(side === "BUY" && existingStopPrice <= lastPrice);
|
(side === "BUY" && existingStopPrice <= lastPrice);
|
||||||
if (!restoreInvalid) {
|
if (!restoreInvalid) {
|
||||||
const restored = await placeStopLossOrder(
|
const restored = await placeStopLossOrder(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: side,
|
||||||
this.openOrders,
|
stopPrice: existingStopPrice,
|
||||||
this.locks,
|
quantity: quantity,
|
||||||
this.timers,
|
lastPrice: lastPrice,
|
||||||
this.pending,
|
guard: {
|
||||||
side,
|
|
||||||
existingStopPrice,
|
|
||||||
quantity,
|
|
||||||
lastPrice,
|
|
||||||
(t, d) => this.tradeLog.push(t, d),
|
|
||||||
{
|
|
||||||
markPrice: position.markPrice,
|
markPrice: position.markPrice,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
|
priceTick: this.config.priceTick,
|
||||||
);
|
qtyStep: this.config.qtyStep
|
||||||
|
});
|
||||||
if (restored) {
|
if (restored) {
|
||||||
this.tradeLog.push(
|
this.tradeLog.push(
|
||||||
"order",
|
"order",
|
||||||
@@ -959,24 +945,19 @@ export class TrendEngine {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await placeTrailingStopOrder(
|
await placeTrailingStopOrder(this.orderContext, {
|
||||||
this.exchange,
|
openOrders: this.openOrders,
|
||||||
this.config.symbol,
|
side: side,
|
||||||
this.openOrders,
|
activationPrice: activationPrice,
|
||||||
this.locks,
|
quantity: quantity,
|
||||||
this.timers,
|
callbackRate: this.config.trailingCallbackRate,
|
||||||
this.pending,
|
guard: {
|
||||||
side,
|
|
||||||
activationPrice,
|
|
||||||
quantity,
|
|
||||||
this.config.trailingCallbackRate,
|
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
|
||||||
{
|
|
||||||
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
|
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
},
|
},
|
||||||
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
|
priceTick: this.config.priceTick,
|
||||||
);
|
qtyStep: this.config.qtyStep
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.tradeLog.push("error", t("log.trend.trailingFail", { error: String(err) }));
|
this.tradeLog.push("error", t("log.trend.trailingFail", { error: String(err) }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { describe, expect, it, vi, beforeEach, afterAll } from "vitest";
|
import { describe, expect, it, vi, beforeEach, afterAll } from "vitest";
|
||||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||||
import type { Order } from "../src/exchanges/types";
|
import type { Order } from "../src/exchanges/types";
|
||||||
import type { OrderLockMap, OrderTimerMap, OrderPendingMap } from "../src/core/order-coordinator";
|
import type {
|
||||||
|
OrderContext,
|
||||||
|
OrderLockMap,
|
||||||
|
OrderPendingMap,
|
||||||
|
OrderTimerMap,
|
||||||
|
} from "../src/core/order-coordinator";
|
||||||
import {
|
import {
|
||||||
deduplicateOrders,
|
deduplicateOrders,
|
||||||
placeOrder,
|
placeOrder,
|
||||||
@@ -60,130 +65,76 @@ describe("order-coordinator", () => {
|
|||||||
process.env.EXCHANGE = originalExchange;
|
process.env.EXCHANGE = originalExchange;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("deduplicates orders by type and side", async () => {
|
/** One order context plus handles on the pieces the assertions poke at. */
|
||||||
|
function createContext() {
|
||||||
const adapter = createMockExchange();
|
const adapter = createMockExchange();
|
||||||
const locks: OrderLockMap = {};
|
const locks: OrderLockMap = {};
|
||||||
const timers: OrderTimerMap = {};
|
const timers: OrderTimerMap = {};
|
||||||
const pending: OrderPendingMap = {};
|
const pending: OrderPendingMap = {};
|
||||||
const log = vi.fn();
|
const log = vi.fn();
|
||||||
|
const ctx: OrderContext = { adapter, symbol: "BTCUSDT", locks, timers, pendings: pending, log };
|
||||||
|
return { ctx, adapter, locks, timers, pending, log };
|
||||||
|
}
|
||||||
|
|
||||||
|
it("deduplicates orders by type and side", async () => {
|
||||||
|
const { ctx, adapter, log } = createContext();
|
||||||
const openOrders: Order[] = [
|
const openOrders: Order[] = [
|
||||||
{ ...baseOrder, orderId: 1 },
|
{ ...baseOrder, orderId: 1 },
|
||||||
{ ...baseOrder, orderId: 2 },
|
{ ...baseOrder, orderId: 2 },
|
||||||
];
|
];
|
||||||
await deduplicateOrders(adapter, "BTCUSDT", openOrders, locks, timers, pending, "LIMIT", "BUY", log);
|
await deduplicateOrders(ctx, openOrders, "LIMIT", "BUY");
|
||||||
expect(adapter.cancelOrders).toHaveBeenCalledWith({ symbol: "BTCUSDT", orderIdList: [2] });
|
expect(adapter.cancelOrders).toHaveBeenCalledWith({ symbol: "BTCUSDT", orderIdList: [2] });
|
||||||
expect(log).toHaveBeenCalledWith("order", expect.stringContaining("去重撤销重复"));
|
expect(log).toHaveBeenCalledWith("order", expect.stringContaining("去重撤销重复"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("places limit orders and records pending id", async () => {
|
it("places limit orders and records pending id", async () => {
|
||||||
const adapter = createMockExchange();
|
const { ctx, adapter, pending } = createContext();
|
||||||
const locks: OrderLockMap = {};
|
await placeOrder(ctx, { openOrders: [], side: "BUY", price: "100", amount: 1, reduceOnly: false });
|
||||||
const timers: OrderTimerMap = {};
|
|
||||||
const pending: OrderPendingMap = {};
|
|
||||||
const log = vi.fn();
|
|
||||||
await placeOrder(
|
|
||||||
adapter,
|
|
||||||
"BTCUSDT",
|
|
||||||
[],
|
|
||||||
locks,
|
|
||||||
timers,
|
|
||||||
pending,
|
|
||||||
"BUY",
|
|
||||||
"100",
|
|
||||||
1,
|
|
||||||
log,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
expect(adapter.createOrder).toHaveBeenCalled();
|
expect(adapter.createOrder).toHaveBeenCalled();
|
||||||
expect(pending.MARKET).toBeUndefined();
|
expect(pending.MARKET).toBeUndefined();
|
||||||
expect(pending.LIMIT).toBe(String(baseOrder.orderId));
|
expect(pending.LIMIT).toBe(String(baseOrder.orderId));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("places market order and unlocks after completion", async () => {
|
it("places market order and unlocks after completion", async () => {
|
||||||
const adapter = createMockExchange();
|
const { ctx, adapter, pending } = createContext();
|
||||||
const locks: OrderLockMap = {};
|
await placeMarketOrder(ctx, { openOrders: [], side: "SELL", amount: 1, reduceOnly: true });
|
||||||
const timers: OrderTimerMap = {};
|
|
||||||
const pending: OrderPendingMap = {};
|
|
||||||
const log = vi.fn();
|
|
||||||
await placeMarketOrder(
|
|
||||||
adapter,
|
|
||||||
"BTCUSDT",
|
|
||||||
[],
|
|
||||||
locks,
|
|
||||||
timers,
|
|
||||||
pending,
|
|
||||||
"SELL",
|
|
||||||
1,
|
|
||||||
log,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
expect(adapter.createOrder).toHaveBeenCalled();
|
expect(adapter.createOrder).toHaveBeenCalled();
|
||||||
expect(pending.MARKET).toBe(String(baseOrder.orderId));
|
expect(pending.MARKET).toBe(String(baseOrder.orderId));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("places stop loss order only when valid", async () => {
|
it("places stop loss order only when valid", async () => {
|
||||||
const adapter = createMockExchange();
|
const { ctx, adapter, log } = createContext();
|
||||||
const locks: OrderLockMap = {};
|
await placeStopLossOrder(ctx, {
|
||||||
const timers: OrderTimerMap = {};
|
openOrders: [],
|
||||||
const pending: OrderPendingMap = {};
|
side: "SELL",
|
||||||
const log = vi.fn();
|
stopPrice: 99,
|
||||||
await placeStopLossOrder(
|
quantity: 1,
|
||||||
adapter,
|
lastPrice: 100,
|
||||||
"BTCUSDT",
|
});
|
||||||
[],
|
|
||||||
locks,
|
|
||||||
timers,
|
|
||||||
pending,
|
|
||||||
"SELL",
|
|
||||||
99,
|
|
||||||
1,
|
|
||||||
100,
|
|
||||||
log
|
|
||||||
);
|
|
||||||
expect(adapter.createOrder).toHaveBeenCalled();
|
expect(adapter.createOrder).toHaveBeenCalled();
|
||||||
expect(log).toHaveBeenCalledWith("stop", expect.stringContaining("STOP_MARKET"));
|
expect(log).toHaveBeenCalledWith("stop", expect.stringContaining("STOP_MARKET"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("places trailing stop order", async () => {
|
it("places trailing stop order", async () => {
|
||||||
const adapter = createMockExchange();
|
const { ctx, adapter, log } = createContext();
|
||||||
const locks: OrderLockMap = {};
|
await placeTrailingStopOrder(ctx, {
|
||||||
const timers: OrderTimerMap = {};
|
openOrders: [],
|
||||||
const pending: OrderPendingMap = {};
|
side: "SELL",
|
||||||
const log = vi.fn();
|
activationPrice: 101,
|
||||||
await placeTrailingStopOrder(
|
quantity: 1,
|
||||||
adapter,
|
callbackRate: 0.2,
|
||||||
"BTCUSDT",
|
});
|
||||||
[],
|
|
||||||
locks,
|
|
||||||
timers,
|
|
||||||
pending,
|
|
||||||
"SELL",
|
|
||||||
101,
|
|
||||||
1,
|
|
||||||
0.2,
|
|
||||||
log
|
|
||||||
);
|
|
||||||
expect(adapter.createOrder).toHaveBeenCalled();
|
expect(adapter.createOrder).toHaveBeenCalled();
|
||||||
expect(log).toHaveBeenCalledWith("order", expect.stringContaining("挂动态止盈单"));
|
expect(log).toHaveBeenCalledWith("order", expect.stringContaining("挂动态止盈单"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("market close cancels open orders before placing close order", async () => {
|
it("market close cancels open orders before placing close order", async () => {
|
||||||
const adapter = createMockExchange();
|
const { ctx, adapter, log } = createContext();
|
||||||
const locks: OrderLockMap = {};
|
await marketClose(ctx, {
|
||||||
const timers: OrderTimerMap = {};
|
openOrders: [{ ...baseOrder, orderId: 2 }],
|
||||||
const pending: OrderPendingMap = {};
|
side: "SELL",
|
||||||
const log = vi.fn();
|
quantity: 1,
|
||||||
await marketClose(
|
});
|
||||||
adapter,
|
|
||||||
"BTCUSDT",
|
|
||||||
[{ ...baseOrder, orderId: 2 }],
|
|
||||||
locks,
|
|
||||||
timers,
|
|
||||||
pending,
|
|
||||||
"SELL",
|
|
||||||
1,
|
|
||||||
log
|
|
||||||
);
|
|
||||||
expect(adapter.createOrder).toHaveBeenCalled();
|
expect(adapter.createOrder).toHaveBeenCalled();
|
||||||
expect(log).toHaveBeenCalledWith("close", expect.stringContaining("市价平仓"));
|
expect(log).toHaveBeenCalledWith("close", expect.stringContaining("市价平仓"));
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user