mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18:07 +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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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(
|
||||
side: "BUY" | "SELL",
|
||||
toCheckPrice: number | null | undefined,
|
||||
@@ -48,6 +111,13 @@ function enforceMarkPriceGuard(
|
||||
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 {
|
||||
return Boolean(locks[type]);
|
||||
}
|
||||
@@ -86,16 +156,12 @@ export function unlockOperating(
|
||||
}
|
||||
|
||||
export async function deduplicateOrders(
|
||||
adapter: ExchangeAdapter,
|
||||
symbol: string,
|
||||
ctx: OrderContext,
|
||||
openOrders: Order[],
|
||||
locks: OrderLockMap,
|
||||
timers: OrderTimerMap,
|
||||
pendings: OrderPendingMap,
|
||||
type: string,
|
||||
side: string,
|
||||
log: LogHandler
|
||||
side: string
|
||||
): Promise<void> {
|
||||
const { adapter, symbol, locks, timers, pendings, log } = ctx;
|
||||
// Treat STOP orders on some exchanges (e.g., Lighter) as LIMIT with stopPrice populated.
|
||||
const sameTypeOrders = openOrders.filter((o) => {
|
||||
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(
|
||||
adapter: ExchangeAdapter,
|
||||
symbol: string,
|
||||
openOrders: Order[],
|
||||
locks: OrderLockMap,
|
||||
timers: OrderTimerMap,
|
||||
pendings: OrderPendingMap,
|
||||
side: "BUY" | "SELL",
|
||||
price: string, // 改为字符串价格
|
||||
amount: number,
|
||||
log: LogHandler,
|
||||
reduceOnly = false,
|
||||
guard?: OrderGuardOptions,
|
||||
opts?: PlaceOrderOptions
|
||||
ctx: OrderContext,
|
||||
request: LimitOrderRequest
|
||||
): Promise<Order | undefined> {
|
||||
const { locks, timers, pendings, log } = ctx;
|
||||
const { side, openOrders, guard, reduceOnly = false } = request;
|
||||
const type = "LIMIT";
|
||||
if (isOperating(locks, type)) return;
|
||||
const priceNum = Number(price);
|
||||
const priceNum = Number(request.price);
|
||||
if (!enforceMarkPriceGuard(side, priceNum, guard, log, "限价单")) return;
|
||||
const qtyStep = opts?.qtyStep ?? 0.001;
|
||||
const rawQuantity = Math.abs(amount);
|
||||
const roundedQuantity = roundQtyDownToStep(rawQuantity, qtyStep);
|
||||
const quantity = roundedQuantity > 0 ? roundedQuantity : rawQuantity;
|
||||
const quantity = normalizeQuantity(request.amount, request.qtyStep ?? DEFAULT_QTY_STEP);
|
||||
if (quantity <= 0) {
|
||||
log("error", "限价单数量无效,跳过下单");
|
||||
return;
|
||||
}
|
||||
if (!opts?.skipDedupe) {
|
||||
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
||||
if (!request.skipDedupe) {
|
||||
await deduplicateOrders(ctx, openOrders, type, side);
|
||||
}
|
||||
lockOperating(locks, timers, pendings, type, log);
|
||||
try {
|
||||
const closePosition = reduceOnly ? true : undefined;
|
||||
const order = await routeLimitOrder({
|
||||
adapter,
|
||||
symbol,
|
||||
adapter: ctx.adapter,
|
||||
symbol: ctx.symbol,
|
||||
side,
|
||||
quantity,
|
||||
price: priceNum,
|
||||
timeInForce: reduceOnly ? "GTC" : "GTX",
|
||||
reduceOnly: reduceOnly ? true : undefined,
|
||||
closePosition,
|
||||
slPrice: opts?.slPrice,
|
||||
tpPrice: opts?.tpPrice,
|
||||
clientOrderId: opts?.clientOrderId,
|
||||
slPrice: request.slPrice,
|
||||
tpPrice: request.tpPrice,
|
||||
clientOrderId: request.clientOrderId,
|
||||
});
|
||||
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;
|
||||
} catch (err) {
|
||||
unlockOperating(locks, timers, pendings, type);
|
||||
@@ -197,37 +242,26 @@ export async function placeOrder(
|
||||
}
|
||||
|
||||
export async function placeMarketOrder(
|
||||
adapter: ExchangeAdapter,
|
||||
symbol: string,
|
||||
openOrders: Order[],
|
||||
locks: OrderLockMap,
|
||||
timers: OrderTimerMap,
|
||||
pendings: OrderPendingMap,
|
||||
side: "BUY" | "SELL",
|
||||
amount: number,
|
||||
log: LogHandler,
|
||||
reduceOnly = false,
|
||||
guard?: OrderGuardOptions,
|
||||
opts?: { qtyStep: number }
|
||||
ctx: OrderContext,
|
||||
request: MarketOrderRequest
|
||||
): Promise<Order | undefined> {
|
||||
const { locks, timers, pendings, log } = ctx;
|
||||
const { side, openOrders, guard, reduceOnly = false } = request;
|
||||
const type = "MARKET";
|
||||
if (isOperating(locks, type)) return;
|
||||
if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价单")) return;
|
||||
const qtyStep = opts?.qtyStep ?? 0.001;
|
||||
const rawQuantity = Math.abs(amount);
|
||||
const roundedQuantity = roundQtyDownToStep(rawQuantity, qtyStep);
|
||||
const quantity = roundedQuantity > 0 ? roundedQuantity : rawQuantity;
|
||||
const quantity = normalizeQuantity(request.amount, request.qtyStep ?? DEFAULT_QTY_STEP);
|
||||
if (quantity <= 0) {
|
||||
log("error", "市价单数量无效,跳过下单");
|
||||
return;
|
||||
}
|
||||
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
||||
await deduplicateOrders(ctx, openOrders, type, side);
|
||||
lockOperating(locks, timers, pendings, type, log);
|
||||
try {
|
||||
const closePosition = reduceOnly ? true : undefined;
|
||||
const order = await routeMarketOrder({
|
||||
adapter,
|
||||
symbol,
|
||||
adapter: ctx.adapter,
|
||||
symbol: ctx.symbol,
|
||||
side,
|
||||
quantity,
|
||||
reduceOnly: reduceOnly ? true : undefined,
|
||||
@@ -247,20 +281,11 @@ export async function placeMarketOrder(
|
||||
}
|
||||
|
||||
export async function placeStopLossOrder(
|
||||
adapter: ExchangeAdapter,
|
||||
symbol: string,
|
||||
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 }
|
||||
ctx: OrderContext,
|
||||
request: StopLossOrderRequest
|
||||
): Promise<Order | undefined> {
|
||||
const { locks, timers, pendings, log } = ctx;
|
||||
const { side, openOrders, guard, stopPrice, lastPrice } = request;
|
||||
const type = "STOP_MARKET";
|
||||
if (isOperating(locks, type)) return;
|
||||
if (!enforceMarkPriceGuard(side, stopPrice, guard, log, "止损单")) return;
|
||||
@@ -274,24 +299,20 @@ export async function placeStopLossOrder(
|
||||
return;
|
||||
}
|
||||
}
|
||||
const priceTick = opts?.priceTick ?? 0.1;
|
||||
const qtyStep = opts?.qtyStep ?? 0.001;
|
||||
const normalizedStop = roundDownToTick(stopPrice, priceTick);
|
||||
const rawQuantity = Math.abs(quantity);
|
||||
const roundedQuantity = roundQtyDownToStep(rawQuantity, qtyStep);
|
||||
const normalizedQty = roundedQuantity > 0 ? roundedQuantity : rawQuantity;
|
||||
const normalizedStop = roundDownToTick(stopPrice, request.priceTick ?? DEFAULT_PRICE_TICK);
|
||||
const normalizedQty = normalizeQuantity(request.quantity, request.qtyStep ?? DEFAULT_QTY_STEP);
|
||||
if (normalizedQty <= 0) {
|
||||
log("error", "止损单数量无效,跳过下单");
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
try {
|
||||
const order = await routeStopOrder({
|
||||
adapter,
|
||||
symbol,
|
||||
adapter: ctx.adapter,
|
||||
symbol: ctx.symbol,
|
||||
side,
|
||||
quantity: normalizedQty,
|
||||
stopPrice: normalizedStop,
|
||||
@@ -314,20 +335,11 @@ export async function placeStopLossOrder(
|
||||
}
|
||||
|
||||
export async function placeTrailingStopOrder(
|
||||
adapter: ExchangeAdapter,
|
||||
symbol: string,
|
||||
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 }
|
||||
ctx: OrderContext,
|
||||
request: TrailingStopOrderRequest
|
||||
): Promise<Order | undefined> {
|
||||
const { adapter, locks, timers, pendings, log } = ctx;
|
||||
const { side, openOrders, guard, activationPrice, callbackRate } = request;
|
||||
const type = "TRAILING_STOP_MARKET";
|
||||
if (isOperating(locks, type)) return;
|
||||
if (!adapter.supportsTrailingStops()) {
|
||||
@@ -335,22 +347,18 @@ export async function placeTrailingStopOrder(
|
||||
return;
|
||||
}
|
||||
if (!enforceMarkPriceGuard(side, activationPrice, guard, log, "动态止盈单")) return;
|
||||
const priceTick = opts?.priceTick ?? 0.1;
|
||||
const qtyStep = opts?.qtyStep ?? 0.001;
|
||||
const normalizedActivation = roundDownToTick(activationPrice, priceTick);
|
||||
const rawQuantity = Math.abs(quantity);
|
||||
const roundedQuantity = roundQtyDownToStep(rawQuantity, qtyStep);
|
||||
const normalizedQty = roundedQuantity > 0 ? roundedQuantity : rawQuantity;
|
||||
const normalizedActivation = roundDownToTick(activationPrice, request.priceTick ?? DEFAULT_PRICE_TICK);
|
||||
const normalizedQty = normalizeQuantity(request.quantity, request.qtyStep ?? DEFAULT_QTY_STEP);
|
||||
if (normalizedQty <= 0) {
|
||||
log("error", "动态止盈单数量无效,跳过下单");
|
||||
return;
|
||||
}
|
||||
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
||||
await deduplicateOrders(ctx, openOrders, type, side);
|
||||
lockOperating(locks, timers, pendings, type, log);
|
||||
try {
|
||||
const order = await routeTrailingStopOrder({
|
||||
adapter,
|
||||
symbol,
|
||||
symbol: ctx.symbol,
|
||||
side,
|
||||
quantity: normalizedQty,
|
||||
activationPrice: normalizedActivation,
|
||||
@@ -374,28 +382,18 @@ export async function placeTrailingStopOrder(
|
||||
}
|
||||
}
|
||||
|
||||
export async function marketClose(
|
||||
adapter: ExchangeAdapter,
|
||||
symbol: string,
|
||||
openOrders: Order[],
|
||||
locks: OrderLockMap,
|
||||
timers: OrderTimerMap,
|
||||
pendings: OrderPendingMap,
|
||||
side: "BUY" | "SELL",
|
||||
quantity: number,
|
||||
log: LogHandler,
|
||||
guard?: OrderGuardOptions,
|
||||
opts?: { qtyStep: number }
|
||||
): Promise<void> {
|
||||
export async function marketClose(ctx: OrderContext, request: MarketCloseRequest): Promise<void> {
|
||||
const { locks, timers, pendings, log } = ctx;
|
||||
const { side, openOrders, guard, qtyStep } = request;
|
||||
const type = "MARKET";
|
||||
if (isOperating(locks, type)) return;
|
||||
if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价平仓")) return;
|
||||
|
||||
const qtyStep = opts?.qtyStep;
|
||||
const rawQuantity = Math.abs(quantity);
|
||||
const normalizedQtyRaw = qtyStep != null ? roundQtyDownToStep(rawQuantity, qtyStep) : rawQuantity;
|
||||
let normalizedQty = normalizedQtyRaw > 0 ? normalizedQtyRaw : rawQuantity;
|
||||
const rawQuantity = Math.abs(request.quantity);
|
||||
let normalizedQty = qtyStep != null ? normalizeQuantity(rawQuantity, qtyStep) : rawQuantity;
|
||||
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);
|
||||
if (Math.abs(rawQuantity - normalizedQty) <= epsilon) {
|
||||
normalizedQty = rawQuantity;
|
||||
@@ -406,12 +404,12 @@ export async function marketClose(
|
||||
return;
|
||||
}
|
||||
|
||||
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
||||
await deduplicateOrders(ctx, openOrders, type, side);
|
||||
lockOperating(locks, timers, pendings, type, log);
|
||||
try {
|
||||
const order = await routeCloseOrder({
|
||||
adapter,
|
||||
symbol,
|
||||
adapter: ctx.adapter,
|
||||
symbol: ctx.symbol,
|
||||
side,
|
||||
quantity: normalizedQty,
|
||||
reduceOnly: true,
|
||||
|
||||
+46
-81
@@ -10,11 +10,13 @@ import {
|
||||
placeOrder,
|
||||
placeStopLossOrder,
|
||||
unlockOperating,
|
||||
type OrderContext,
|
||||
type OrderLockMap,
|
||||
type OrderPendingMap,
|
||||
type OrderTimerMap,
|
||||
} from "../core/order-coordinator";
|
||||
import { StrategyEventEmitter } from "./common/event-emitter";
|
||||
import { createPrecisionSyncer, type PrecisionSyncer } from "./common/precision-syncer";
|
||||
import { safeSubscribe, type LogHandler } from "./common/subscriptions";
|
||||
import { clearGridState, loadGridState, saveGridState } from "./common/grid-storage";
|
||||
import {
|
||||
@@ -170,7 +172,7 @@ export class GridEngine {
|
||||
private savePending = false;
|
||||
private uncoveredQty = 0;
|
||||
private desiredOrders: DesiredGridOrder[] = [];
|
||||
private precisionSync: Promise<void> | null = null;
|
||||
private readonly precision: PrecisionSyncer;
|
||||
|
||||
constructor(
|
||||
private readonly config: GridConfig,
|
||||
@@ -187,11 +189,25 @@ export class GridEngine {
|
||||
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.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 {
|
||||
if (this.timer || !this.running) {
|
||||
if (!this.timer && !this.running) {
|
||||
@@ -209,6 +225,7 @@ export class GridEngine {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
this.precision.stop();
|
||||
}
|
||||
|
||||
on(event: GridEvent, listener: GridListener): void {
|
||||
@@ -277,37 +294,6 @@ export class GridEngine {
|
||||
// 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
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -744,26 +730,16 @@ export class GridEngine {
|
||||
const ordersVersionBeforePlace = this.ordersVersion;
|
||||
try {
|
||||
this.lastLimitAttemptAt = now;
|
||||
placed = await placeOrder(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pendings,
|
||||
action.side,
|
||||
action.price,
|
||||
action.qty,
|
||||
this.log,
|
||||
isEntry ? false : this.config.useReduceOnlyForExit,
|
||||
undefined,
|
||||
{
|
||||
priceTick: this.config.priceTick,
|
||||
qtyStep: this.config.qtyStep,
|
||||
skipDedupe: true,
|
||||
clientOrderId,
|
||||
}
|
||||
);
|
||||
placed = await placeOrder(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: action.side,
|
||||
price: action.price,
|
||||
amount: action.qty,
|
||||
reduceOnly: isEntry ? false : this.config.useReduceOnlyForExit,
|
||||
qtyStep: this.config.qtyStep,
|
||||
skipDedupe: true,
|
||||
clientOrderId,
|
||||
});
|
||||
} catch (error) {
|
||||
this.log("error", `挂单失败 (${action.side} @ ${action.price}): ${extractMessage(error)}`);
|
||||
}
|
||||
@@ -829,23 +805,17 @@ export class GridEngine {
|
||||
}
|
||||
}
|
||||
try {
|
||||
await marketClose(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pendings,
|
||||
side,
|
||||
qty,
|
||||
this.log,
|
||||
{
|
||||
await marketClose(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: side,
|
||||
quantity: qty,
|
||||
guard: {
|
||||
markPrice: mark,
|
||||
expectedPrice: Number.isFinite(closeSidePrice) ? closeSidePrice : null,
|
||||
maxPct: limitPct > 0 ? limitPct : undefined,
|
||||
},
|
||||
{ qtyStep: this.config.qtyStep }
|
||||
);
|
||||
qtyStep: this.config.qtyStep
|
||||
});
|
||||
this.log("close", `市价平仓 ${side} ${qty} (${reason})`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -971,21 +941,16 @@ export class GridEngine {
|
||||
|
||||
const lastPrice = Number(this.tickerSnapshot?.lastPrice);
|
||||
try {
|
||||
const placed = await placeStopLossOrder(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pendings,
|
||||
desired.side,
|
||||
desired.stopPrice,
|
||||
Math.abs(this.position.positionAmt),
|
||||
Number.isFinite(lastPrice) ? lastPrice : price,
|
||||
this.log,
|
||||
undefined,
|
||||
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
|
||||
);
|
||||
const placed = await placeStopLossOrder(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: desired.side,
|
||||
stopPrice: desired.stopPrice,
|
||||
quantity: Math.abs(this.position.positionAmt),
|
||||
lastPrice: Number.isFinite(lastPrice) ? lastPrice : price,
|
||||
guard: undefined,
|
||||
priceTick: this.config.priceTick,
|
||||
qtyStep: this.config.qtyStep
|
||||
});
|
||||
if (placed?.orderId != null) {
|
||||
state.exchangeStop = {
|
||||
orderId: String(placed.orderId),
|
||||
|
||||
@@ -15,7 +15,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 { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
||||
import { extractMessage, isUnknownOrderError } from "../utils/errors";
|
||||
import { formatPriceToString } from "../utils/math";
|
||||
@@ -76,6 +76,19 @@ export class GuardianEngine {
|
||||
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(() => {
|
||||
@@ -390,24 +403,19 @@ export class GuardianEngine {
|
||||
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 = side;
|
||||
this.lastStopAttempt.price = stopPrice;
|
||||
this.lastStopAttempt.at = now;
|
||||
@@ -449,24 +457,19 @@ export class GuardianEngine {
|
||||
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",
|
||||
@@ -484,24 +487,19 @@ export class GuardianEngine {
|
||||
if (quantity <= minQty) {
|
||||
return;
|
||||
}
|
||||
const restored = await placeStopLossOrder(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pending,
|
||||
side,
|
||||
Number.isFinite(existingStopPrice) ? existingStopPrice : nextStopPrice,
|
||||
quantity,
|
||||
lastPrice,
|
||||
(type, detail) => this.tradeLog.push(type, detail),
|
||||
{
|
||||
const restored = await placeStopLossOrder(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: side,
|
||||
stopPrice: Number.isFinite(existingStopPrice) ? existingStopPrice : 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 (restored && Number.isFinite(existingStopPrice)) {
|
||||
this.tradeLog.push(
|
||||
"order",
|
||||
@@ -525,24 +523,19 @@ export class GuardianEngine {
|
||||
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.guardian.trailingFail", { error: String(err) }));
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
placeOrder,
|
||||
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 type { MakerEngineSnapshot } from "./maker-engine";
|
||||
import { makeOrderPlan } from "../core/lib/order-plan";
|
||||
import { safeCancelOrder } from "../core/lib/orders";
|
||||
@@ -151,6 +151,19 @@ export class LiquidityMakerEngine {
|
||||
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(() => {
|
||||
@@ -698,17 +711,11 @@ export class LiquidityMakerEngine {
|
||||
const closeBidPrice = topBid != null ? formatPriceToString(topBid, priceDecimals) : null;
|
||||
const closeAskPrice = topAsk != null ? formatPriceToString(topAsk, priceDecimals) : null;
|
||||
try {
|
||||
await marketClose(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pending,
|
||||
side,
|
||||
absPosition,
|
||||
(type, detail) => this.tradeLog.push(type, detail),
|
||||
{
|
||||
await marketClose(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: side,
|
||||
quantity: absPosition,
|
||||
guard: {
|
||||
markPrice: position.markPrice,
|
||||
expectedPrice:
|
||||
side === "SELL"
|
||||
@@ -716,8 +723,8 @@ export class LiquidityMakerEngine {
|
||||
: (closeBidPrice != null ? Number(closeBidPrice) : null),
|
||||
maxPct: this.config.maxCloseSlippagePct,
|
||||
},
|
||||
{ qtyStep: this.precision.qtyStep }
|
||||
);
|
||||
qtyStep: this.precision.qtyStep
|
||||
});
|
||||
} catch (error) {
|
||||
if (isUnknownOrderError(error)) {
|
||||
this.tradeLog.push("order", "限频强制平仓时订单已不存在");
|
||||
@@ -886,27 +893,18 @@ export class LiquidityMakerEngine {
|
||||
}
|
||||
try {
|
||||
const reduceOnlyFlag = this.marketType === "spot" ? false : target.reduceOnly;
|
||||
await placeOrder(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pending,
|
||||
target.side,
|
||||
target.price, // 已经是字符串价格
|
||||
target.amount,
|
||||
(type, detail) => this.tradeLog.push(type, detail),
|
||||
reduceOnlyFlag,
|
||||
{
|
||||
await placeOrder(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: target.side,
|
||||
price: target.price,
|
||||
amount: target.amount,
|
||||
reduceOnly: reduceOnlyFlag,
|
||||
guard: {
|
||||
markPrice: this.getPositionSnapshot().markPrice,
|
||||
maxPct: this.config.maxCloseSlippagePct,
|
||||
},
|
||||
{
|
||||
priceTick: this.precision.priceTick,
|
||||
qtyStep: this.precision.qtyStep,
|
||||
}
|
||||
);
|
||||
qtyStep: this.precision.qtyStep
|
||||
});
|
||||
// Record last placed entry order timing and price
|
||||
if (!target.reduceOnly) {
|
||||
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.flushOrders();
|
||||
await marketClose(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pending,
|
||||
"SELL",
|
||||
absPosition,
|
||||
(type, detail) => this.tradeLog.push(type, detail),
|
||||
{
|
||||
await marketClose(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: "SELL",
|
||||
quantity: absPosition,
|
||||
guard: {
|
||||
markPrice: position.markPrice,
|
||||
expectedPrice: bidPrice || null,
|
||||
maxPct: this.config.maxCloseSlippagePct,
|
||||
},
|
||||
{ qtyStep: this.precision.qtyStep }
|
||||
);
|
||||
qtyStep: this.precision.qtyStep
|
||||
});
|
||||
} catch (error) {
|
||||
if (isRateLimitError(error)) throw error;
|
||||
if (isUnknownOrderError(error)) {
|
||||
@@ -1005,23 +997,17 @@ export class LiquidityMakerEngine {
|
||||
);
|
||||
try {
|
||||
await this.flushOrders();
|
||||
await marketClose(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pending,
|
||||
position.positionAmt > 0 ? "SELL" : "BUY",
|
||||
absPosition,
|
||||
(type, detail) => this.tradeLog.push(type, detail),
|
||||
{
|
||||
await marketClose(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: position.positionAmt > 0 ? "SELL" : "BUY",
|
||||
quantity: absPosition,
|
||||
guard: {
|
||||
markPrice: position.markPrice,
|
||||
expectedPrice: Number(position.positionAmt > 0 ? bidPrice : askPrice) || null,
|
||||
maxPct: this.config.maxCloseSlippagePct,
|
||||
},
|
||||
{ qtyStep: this.precision.qtyStep }
|
||||
);
|
||||
qtyStep: this.precision.qtyStep
|
||||
});
|
||||
} catch (error) {
|
||||
if (isUnknownOrderError(error)) {
|
||||
this.tradeLog.push("order", "止损平仓时订单已不存在");
|
||||
@@ -1253,17 +1239,11 @@ export class LiquidityMakerEngine {
|
||||
if (absQty < EPS) return false;
|
||||
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
|
||||
try {
|
||||
await marketClose(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pending,
|
||||
target.side,
|
||||
absQty,
|
||||
(type, detail) => this.tradeLog.push(type, detail),
|
||||
{
|
||||
await marketClose(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: target.side,
|
||||
quantity: absQty,
|
||||
guard: {
|
||||
markPrice: position.markPrice,
|
||||
expectedPrice:
|
||||
target.side === "SELL"
|
||||
@@ -1271,8 +1251,8 @@ export class LiquidityMakerEngine {
|
||||
: (topAsk != null ? Number(topAsk) : null),
|
||||
maxPct: this.config.maxCloseSlippagePct,
|
||||
},
|
||||
{ qtyStep: this.precision.qtyStep }
|
||||
);
|
||||
qtyStep: this.precision.qtyStep
|
||||
});
|
||||
this.tradeLog.push("order", `小额仓位使用市价平仓 ${target.side} 数量 ${absQty.toFixed(6)}`);
|
||||
return true;
|
||||
} catch (closeError) {
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
placeOrder,
|
||||
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 { makeOrderPlan } from "../core/lib/order-plan";
|
||||
import { safeCancelOrder } from "../core/lib/orders";
|
||||
import { RateLimitController } from "../core/lib/rate-limit";
|
||||
@@ -127,6 +127,19 @@ export class MakerEngine {
|
||||
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(() => {
|
||||
@@ -439,27 +452,18 @@ export class MakerEngine {
|
||||
if (!target) continue;
|
||||
if (target.amount < EPS) continue;
|
||||
try {
|
||||
await placeOrder(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pending,
|
||||
target.side,
|
||||
target.price, // 已经是字符串价格
|
||||
target.amount,
|
||||
(type, detail) => this.tradeLog.push(type, detail),
|
||||
target.reduceOnly,
|
||||
{
|
||||
await placeOrder(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: target.side,
|
||||
price: target.price,
|
||||
amount: target.amount,
|
||||
reduceOnly: target.reduceOnly,
|
||||
guard: {
|
||||
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
|
||||
maxPct: this.config.maxCloseSlippagePct,
|
||||
},
|
||||
{
|
||||
priceTick: this.precision.priceTick,
|
||||
qtyStep: this.precision.qtyStep,
|
||||
}
|
||||
);
|
||||
qtyStep: this.precision.qtyStep
|
||||
});
|
||||
} catch (error) {
|
||||
if (isInsufficientBalanceError(error)) {
|
||||
this.registerInsufficientBalance(error);
|
||||
@@ -507,23 +511,17 @@ export class MakerEngine {
|
||||
);
|
||||
try {
|
||||
await this.flushOrders();
|
||||
await marketClose(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pending,
|
||||
position.positionAmt > 0 ? "SELL" : "BUY",
|
||||
absPosition,
|
||||
(type, detail) => this.tradeLog.push(type, detail),
|
||||
{
|
||||
await marketClose(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: position.positionAmt > 0 ? "SELL" : "BUY",
|
||||
quantity: absPosition,
|
||||
guard: {
|
||||
markPrice: position.markPrice,
|
||||
expectedPrice: Number(closeSidePrice) || null,
|
||||
maxPct: this.config.maxCloseSlippagePct,
|
||||
},
|
||||
{ qtyStep: this.precision.qtyStep }
|
||||
);
|
||||
qtyStep: this.precision.qtyStep
|
||||
});
|
||||
} catch (error) {
|
||||
if (isUnknownOrderError(error)) {
|
||||
this.tradeLog.push("order", t("log.maker.stopOrderMissing"));
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
placeOrder,
|
||||
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 { makeOrderPlan } from "../core/lib/order-plan";
|
||||
import { safeCancelOrder } from "../core/lib/orders";
|
||||
import { RateLimitController } from "../core/lib/rate-limit";
|
||||
@@ -244,6 +244,19 @@ export class MakerPointsEngine {
|
||||
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;
|
||||
// 初始化数据时间戳
|
||||
@@ -1046,26 +1059,17 @@ export class MakerPointsEngine {
|
||||
: target.side === "BUY"
|
||||
? priceNum - 1
|
||||
: priceNum + 1;
|
||||
await placeOrder(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pending,
|
||||
target.side,
|
||||
target.price,
|
||||
target.amount,
|
||||
(type, detail) => this.tradeLog.push(type, detail),
|
||||
target.reduceOnly,
|
||||
undefined,
|
||||
{
|
||||
priceTick: this.precision.priceTick,
|
||||
qtyStep: this.precision.qtyStep,
|
||||
skipDedupe: true,
|
||||
slPrice,
|
||||
}
|
||||
);
|
||||
await placeOrder(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: target.side,
|
||||
price: target.price,
|
||||
amount: target.amount,
|
||||
reduceOnly: target.reduceOnly,
|
||||
guard: undefined,
|
||||
qtyStep: this.precision.qtyStep,
|
||||
skipDedupe: true,
|
||||
slPrice
|
||||
});
|
||||
} catch (error) {
|
||||
if (isInsufficientBalanceError(error)) {
|
||||
this.registerInsufficientBalance(error);
|
||||
@@ -1231,19 +1235,13 @@ export class MakerPointsEngine {
|
||||
await this.flushOrders();
|
||||
|
||||
// 执行市价平仓
|
||||
await marketClose(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pending,
|
||||
side,
|
||||
currentAbsPosition,
|
||||
(type, detail) => this.tradeLog.push(type, detail),
|
||||
undefined,
|
||||
{ qtyStep: this.precision.qtyStep }
|
||||
);
|
||||
await marketClose(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: side,
|
||||
quantity: currentAbsPosition,
|
||||
guard: undefined,
|
||||
qtyStep: this.precision.qtyStep
|
||||
});
|
||||
|
||||
// 等待一小段时间让账户数据更新
|
||||
await this.sleep(STOP_LOSS_RETRY_INTERVAL_MS);
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
placeOrder,
|
||||
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 type { MakerEngineSnapshot } from "./maker-engine";
|
||||
import { makeOrderPlan } from "../core/lib/order-plan";
|
||||
import { safeCancelOrder } from "../core/lib/orders";
|
||||
@@ -141,6 +141,19 @@ export class OffsetMakerEngine {
|
||||
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(() => {
|
||||
@@ -551,17 +564,11 @@ export class OffsetMakerEngine {
|
||||
const closeBidPrice = topBid != null ? formatPriceToString(topBid, priceDecimals) : null;
|
||||
const closeAskPrice = topAsk != null ? formatPriceToString(topAsk, priceDecimals) : null;
|
||||
try {
|
||||
await marketClose(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pending,
|
||||
side,
|
||||
absPosition,
|
||||
(type, detail) => this.tradeLog.push(type, detail),
|
||||
{
|
||||
await marketClose(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: side,
|
||||
quantity: absPosition,
|
||||
guard: {
|
||||
markPrice: position.markPrice,
|
||||
expectedPrice:
|
||||
side === "SELL"
|
||||
@@ -569,8 +576,8 @@ export class OffsetMakerEngine {
|
||||
: (closeBidPrice != null ? Number(closeBidPrice) : null),
|
||||
maxPct: this.config.maxCloseSlippagePct,
|
||||
},
|
||||
{ qtyStep: this.precision.qtyStep }
|
||||
);
|
||||
qtyStep: this.precision.qtyStep
|
||||
});
|
||||
} catch (error) {
|
||||
if (isUnknownOrderError(error)) {
|
||||
this.tradeLog.push("order", "限频强制平仓时订单已不存在");
|
||||
@@ -644,23 +651,17 @@ export class OffsetMakerEngine {
|
||||
);
|
||||
try {
|
||||
await this.flushOrders();
|
||||
await marketClose(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pending,
|
||||
side,
|
||||
absPosition,
|
||||
(type, detail) => this.tradeLog.push(type, detail),
|
||||
{
|
||||
await marketClose(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: side,
|
||||
quantity: absPosition,
|
||||
guard: {
|
||||
markPrice: position.markPrice,
|
||||
expectedPrice: Number(closeSidePrice) || null,
|
||||
maxPct: this.config.maxCloseSlippagePct,
|
||||
},
|
||||
{ qtyStep: this.precision.qtyStep }
|
||||
);
|
||||
qtyStep: this.precision.qtyStep
|
||||
});
|
||||
} catch (error) {
|
||||
if (isUnknownOrderError(error)) {
|
||||
this.tradeLog.push("order", "深度不平衡平仓时订单已不存在");
|
||||
@@ -747,27 +748,18 @@ export class OffsetMakerEngine {
|
||||
}
|
||||
try {
|
||||
const reduceOnlyFlag = this.marketType === "spot" ? false : target.reduceOnly;
|
||||
await placeOrder(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pending,
|
||||
target.side,
|
||||
target.price, // 已经是字符串价格
|
||||
target.amount,
|
||||
(type, detail) => this.tradeLog.push(type, detail),
|
||||
reduceOnlyFlag,
|
||||
{
|
||||
await placeOrder(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: target.side,
|
||||
price: target.price,
|
||||
amount: target.amount,
|
||||
reduceOnly: reduceOnlyFlag,
|
||||
guard: {
|
||||
markPrice: this.getPositionSnapshot().markPrice,
|
||||
maxPct: this.config.maxCloseSlippagePct,
|
||||
},
|
||||
{
|
||||
priceTick: this.precision.priceTick,
|
||||
qtyStep: this.precision.qtyStep,
|
||||
}
|
||||
);
|
||||
qtyStep: this.precision.qtyStep
|
||||
});
|
||||
// Record last placed entry order timing and price
|
||||
if (!target.reduceOnly) {
|
||||
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.flushOrders();
|
||||
await marketClose(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pending,
|
||||
"SELL",
|
||||
absPosition,
|
||||
(type, detail) => this.tradeLog.push(type, detail),
|
||||
{
|
||||
await marketClose(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: "SELL",
|
||||
quantity: absPosition,
|
||||
guard: {
|
||||
markPrice: position.markPrice,
|
||||
expectedPrice: bidPrice || null,
|
||||
maxPct: this.config.maxCloseSlippagePct,
|
||||
},
|
||||
{ qtyStep: this.precision.qtyStep }
|
||||
);
|
||||
qtyStep: this.precision.qtyStep
|
||||
});
|
||||
} catch (error) {
|
||||
if (isRateLimitError(error)) throw error;
|
||||
if (isUnknownOrderError(error)) {
|
||||
@@ -866,23 +852,17 @@ export class OffsetMakerEngine {
|
||||
);
|
||||
try {
|
||||
await this.flushOrders();
|
||||
await marketClose(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pending,
|
||||
position.positionAmt > 0 ? "SELL" : "BUY",
|
||||
absPosition,
|
||||
(type, detail) => this.tradeLog.push(type, detail),
|
||||
{
|
||||
await marketClose(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: position.positionAmt > 0 ? "SELL" : "BUY",
|
||||
quantity: absPosition,
|
||||
guard: {
|
||||
markPrice: position.markPrice,
|
||||
expectedPrice: Number(position.positionAmt > 0 ? bidPrice : askPrice) || null,
|
||||
maxPct: this.config.maxCloseSlippagePct,
|
||||
},
|
||||
{ qtyStep: this.precision.qtyStep }
|
||||
);
|
||||
qtyStep: this.precision.qtyStep
|
||||
});
|
||||
} catch (error) {
|
||||
if (isUnknownOrderError(error)) {
|
||||
this.tradeLog.push("order", "止损平仓时订单已不存在");
|
||||
@@ -1123,17 +1103,11 @@ export class OffsetMakerEngine {
|
||||
if (absQty < EPS) return false;
|
||||
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
|
||||
try {
|
||||
await marketClose(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pending,
|
||||
target.side,
|
||||
absQty,
|
||||
(type, detail) => this.tradeLog.push(type, detail),
|
||||
{
|
||||
await marketClose(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: target.side,
|
||||
quantity: absQty,
|
||||
guard: {
|
||||
markPrice: position.markPrice,
|
||||
expectedPrice:
|
||||
target.side === "SELL"
|
||||
@@ -1141,8 +1115,8 @@ export class OffsetMakerEngine {
|
||||
: (topAsk != null ? Number(topAsk) : null),
|
||||
maxPct: this.config.maxCloseSlippagePct,
|
||||
},
|
||||
{ qtyStep: this.precision.qtyStep }
|
||||
);
|
||||
qtyStep: this.precision.qtyStep
|
||||
});
|
||||
this.tradeLog.push("order", `小额仓位使用市价平仓 ${target.side} 数量 ${absQty.toFixed(6)}`);
|
||||
return true;
|
||||
} catch (closeError) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||
import type { AccountSnapshot, Depth, Order, Ticker } from "../exchanges/types";
|
||||
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
||||
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 { getPosition, type PositionSnapshot } from "../utils/strategy";
|
||||
import { computePositionPnl } from "../utils/pnl";
|
||||
@@ -136,6 +136,19 @@ export class SwingEngine {
|
||||
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(() => {
|
||||
@@ -348,24 +361,18 @@ export class SwingEngine {
|
||||
if (Math.abs(position.positionAmt) > EPS) {
|
||||
return;
|
||||
}
|
||||
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: position.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} (market)`);
|
||||
} catch (err) {
|
||||
this.tradeLog.push("error", `Open failed: ${extractMessage(err)}`);
|
||||
@@ -380,23 +387,17 @@ export class SwingEngine {
|
||||
side === "SELL"
|
||||
? Number(this.depthSnapshot?.bids?.[0]?.[0])
|
||||
: Number(this.depthSnapshot?.asks?.[0]?.[0]);
|
||||
await marketClose(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pending,
|
||||
side,
|
||||
Math.abs(position.positionAmt),
|
||||
(type, detail) => this.tradeLog.push(type, detail),
|
||||
{
|
||||
await marketClose(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: side,
|
||||
quantity: Math.abs(position.positionAmt),
|
||||
guard: {
|
||||
markPrice: position.markPrice,
|
||||
expectedPrice: Number.isFinite(expected) ? expected : Number(this.tickerSnapshot?.lastPrice) || null,
|
||||
maxPct: this.config.maxCloseSlippagePct,
|
||||
},
|
||||
{ qtyStep: this.config.qtyStep }
|
||||
);
|
||||
qtyStep: this.config.qtyStep
|
||||
});
|
||||
this.tradeLog.push("close", `${reason}: ${side} (market close)`);
|
||||
} catch (err) {
|
||||
if (isUnknownOrderError(err)) {
|
||||
@@ -459,24 +460,19 @@ export class SwingEngine {
|
||||
|
||||
try {
|
||||
const qty = Math.abs(position.positionAmt);
|
||||
await placeStopLossOrder(
|
||||
this.exchange,
|
||||
this.config.symbol,
|
||||
this.openOrders,
|
||||
this.locks,
|
||||
this.timers,
|
||||
this.pending,
|
||||
stopSide,
|
||||
stopPrice,
|
||||
qty,
|
||||
lastPrice,
|
||||
(type, detail) => this.tradeLog.push(type, detail),
|
||||
{
|
||||
await placeStopLossOrder(this.orderContext, {
|
||||
openOrders: this.openOrders,
|
||||
side: stopSide,
|
||||
stopPrice: stopPrice,
|
||||
quantity: qty,
|
||||
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: stopSide, price: stopPrice, at: Date.now() };
|
||||
} catch (err) {
|
||||
this.lastStopAttempt = { side: stopSide, price: stopPrice, at: Date.now() };
|
||||
|
||||
@@ -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) }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user