更新交易引擎,添加最大平仓滑点百分比配置以优化价格操控保护逻辑,确保在市价平仓时价格与标记价格的偏差在允许范围内。

This commit is contained in:
discountry
2025-09-24 03:49:37 +08:00
parent 9c45458507
commit dc6f9ee72e
6 changed files with 161 additions and 16 deletions
+14 -2
View File
@@ -309,7 +309,11 @@ export class MakerEngine {
target.price,
target.amount,
(type, detail) => this.tradeLog.push(type, detail),
target.reduceOnly
target.reduceOnly,
{
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
maxPct: this.config.maxCloseSlippagePct,
}
);
} catch (error) {
this.tradeLog.push("error", `挂单失败(${target.side} ${target.price}): ${String(error)}`);
@@ -345,6 +349,9 @@ export class MakerEngine {
);
if (derivedLoss || snapshotLoss) {
// 价格操纵保护:只有平仓方向价格与标记价格在阈值内才允许市价平仓
const closeSideIsSell = position.positionAmt > 0;
const closeSidePrice = closeSideIsSell ? bidPrice : askPrice;
this.tradeLog.push(
"stop",
`触发止损,方向=${position.positionAmt > 0 ? "多" : "空"} 当前亏损=${pnl.toFixed(4)} USDT`
@@ -360,7 +367,12 @@ export class MakerEngine {
this.pending,
position.positionAmt > 0 ? "SELL" : "BUY",
absPosition,
(type, detail) => this.tradeLog.push(type, detail)
(type, detail) => this.tradeLog.push(type, detail),
{
markPrice: position.markPrice,
expectedPrice: Number(closeSidePrice) || null,
maxPct: this.config.maxCloseSlippagePct,
}
);
} catch (error) {
if (isUnknownOrderError(error)) {
+20 -3
View File
@@ -306,6 +306,9 @@ export class OffsetMakerEngine {
if (!longExitRequired && !shortExitRequired) return false;
const side: "BUY" | "SELL" = position.positionAmt > 0 ? "SELL" : "BUY";
const bid = Number(this.depthSnapshot?.bids?.[0]?.[0]);
const ask = Number(this.depthSnapshot?.asks?.[0]?.[0]);
const closeSidePrice = side === "SELL" ? bid : ask;
this.tradeLog.push(
"stop",
`深度极端不平衡(${buySum.toFixed(4)} vs ${sellSum.toFixed(4)}), 市价平仓 ${side}`
@@ -321,7 +324,12 @@ export class OffsetMakerEngine {
this.pending,
side,
absPosition,
(type, detail) => this.tradeLog.push(type, detail)
(type, detail) => this.tradeLog.push(type, detail),
{
markPrice: position.markPrice,
expectedPrice: Number(closeSidePrice) || null,
maxPct: this.config.maxCloseSlippagePct,
}
);
} catch (error) {
if (isUnknownOrderError(error)) {
@@ -396,7 +404,11 @@ export class OffsetMakerEngine {
target.price,
target.amount,
(type, detail) => this.tradeLog.push(type, detail),
target.reduceOnly
target.reduceOnly,
{
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
maxPct: this.config.maxCloseSlippagePct,
}
);
} catch (error) {
this.tradeLog.push("error", `挂单失败(${target.side} ${target.price}): ${String(error)}`);
@@ -447,7 +459,12 @@ export class OffsetMakerEngine {
this.pending,
position.positionAmt > 0 ? "SELL" : "BUY",
absPosition,
(type, detail) => this.tradeLog.push(type, detail)
(type, detail) => this.tradeLog.push(type, detail),
{
markPrice: position.markPrice,
expectedPrice: Number(position.positionAmt > 0 ? bidPrice : askPrice) || null,
maxPct: this.config.maxCloseSlippagePct,
}
);
} catch (error) {
if (isUnknownOrderError(error)) {
+48 -5
View File
@@ -2,12 +2,45 @@ import type { ExchangeAdapter } from "../exchanges/adapter";
import type { AsterOrder, CreateOrderParams } from "../exchanges/types";
import { toPrice1Decimal, toQty3Decimal } from "../utils/math";
import { isUnknownOrderError } from "../utils/errors";
import { isOrderPriceAllowedByMark } from "../utils/strategy";
export type OrderLockMap = Record<string, boolean>;
export type OrderTimerMap = Record<string, ReturnType<typeof setTimeout> | null>;
export type OrderPendingMap = Record<string, string | null>;
export type LogHandler = (type: string, detail: string) => void;
type OrderGuardOptions = {
markPrice?: number | null;
expectedPrice?: number | null;
maxPct?: number;
};
function enforceMarkPriceGuard(
side: "BUY" | "SELL",
toCheckPrice: number | null | undefined,
guard: OrderGuardOptions | undefined,
log: LogHandler,
context: string
): boolean {
if (!guard || guard.maxPct == null) return true;
const allowed = isOrderPriceAllowedByMark({
side,
orderPrice: toCheckPrice,
markPrice: guard.markPrice,
maxPct: guard.maxPct,
});
if (!allowed) {
const priceStr = Number.isFinite(Number(toCheckPrice)) ? Number(toCheckPrice).toFixed(2) : String(toCheckPrice);
const markStr = Number.isFinite(Number(guard.markPrice)) ? Number(guard.markPrice).toFixed(2) : String(guard.markPrice);
log(
"info",
`${context} 保护触发:side=${side} price=${priceStr} mark=${markStr} 超过 ${(guard.maxPct! * 100).toFixed(2)}%`
);
return false;
}
return true;
}
export function isOperating(locks: OrderLockMap, type: string): boolean {
return Boolean(locks[type]);
}
@@ -92,10 +125,12 @@ export async function placeOrder(
price: number,
amount: number,
log: LogHandler,
reduceOnly = false
reduceOnly = false,
guard?: OrderGuardOptions
): Promise<AsterOrder | undefined> {
const type = "LIMIT";
if (isOperating(locks, type)) return;
if (!enforceMarkPriceGuard(side, price, guard, log, "限价单")) return;
const params: CreateOrderParams = {
symbol,
side,
@@ -132,10 +167,12 @@ export async function placeMarketOrder(
side: "BUY" | "SELL",
amount: number,
log: LogHandler,
reduceOnly = false
reduceOnly = false,
guard?: OrderGuardOptions
): Promise<AsterOrder | undefined> {
const type = "MARKET";
if (isOperating(locks, type)) return;
if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价单")) return;
const params: CreateOrderParams = {
symbol,
side,
@@ -171,10 +208,12 @@ export async function placeStopLossOrder(
stopPrice: number,
quantity: number,
lastPrice: number | null,
log: LogHandler
log: LogHandler,
guard?: OrderGuardOptions
): Promise<AsterOrder | undefined> {
const type = "STOP_MARKET";
if (isOperating(locks, type)) return;
if (!enforceMarkPriceGuard(side, stopPrice, guard, log, "止损单")) return;
if (lastPrice != null) {
if (side === "SELL" && stopPrice >= lastPrice) {
log("error", `止损价 ${stopPrice} 高于或等于当前价 ${lastPrice},取消挂单`);
@@ -222,10 +261,12 @@ export async function placeTrailingStopOrder(
activationPrice: number,
quantity: number,
callbackRate: number,
log: LogHandler
log: LogHandler,
guard?: OrderGuardOptions
): Promise<AsterOrder | undefined> {
const type = "TRAILING_STOP_MARKET";
if (isOperating(locks, type)) return;
if (!enforceMarkPriceGuard(side, activationPrice, guard, log, "动态止盈单")) return;
const params: CreateOrderParams = {
symbol,
side,
@@ -265,10 +306,12 @@ export async function marketClose(
pendings: OrderPendingMap,
side: "BUY" | "SELL",
quantity: number,
log: LogHandler
log: LogHandler,
guard?: OrderGuardOptions
): Promise<void> {
const type = "MARKET";
if (isOperating(locks, type)) return;
if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价平仓")) return;
const params: CreateOrderParams = {
symbol,
side,
+44 -4
View File
@@ -286,7 +286,13 @@ export class TrendEngine {
this.pending,
side,
this.config.tradeAmount,
(type, detail) => this.tradeLog.push(type, detail)
(type, detail) => this.tradeLog.push(type, detail),
false,
{
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
expectedPrice: Number(this.tickerSnapshot?.lastPrice) || null,
maxPct: this.config.maxCloseSlippagePct,
}
);
this.tradeLog.push("open", `${reason}: ${side} @ ${price}`);
this.lastOpenPlan = { side, price };
@@ -395,6 +401,23 @@ export class TrendEngine {
}
}
}
// 价格操纵保护:仅当平仓方向价格与标记价格偏离在阈值内才执行市价平仓
const mark = getPosition(this.accountSnapshot, this.config.symbol).markPrice;
const limitPct = this.config.maxCloseSlippagePct;
const sideIsSell = direction === "long";
const depthBid = Number(this.depthSnapshot?.bids?.[0]?.[0]);
const depthAsk = Number(this.depthSnapshot?.asks?.[0]?.[0]);
const closeSidePrice = sideIsSell ? depthBid : depthAsk;
if (mark != null && Number.isFinite(mark) && mark > 0 && Number.isFinite(closeSidePrice)) {
const pctDiff = Math.abs(closeSidePrice - mark) / mark;
if (pctDiff > limitPct) {
this.tradeLog.push(
"info",
`市价平仓保护触发:closePx=${Number(closeSidePrice).toFixed(2)} mark=${mark.toFixed(2)} 偏离 ${(pctDiff * 100).toFixed(2)}% > ${(limitPct * 100).toFixed(2)}%`
);
return { closed: false, pnl };
}
}
await marketClose(
this.exchange,
this.config.symbol,
@@ -404,7 +427,16 @@ export class TrendEngine {
this.pending,
direction === "long" ? "SELL" : "BUY",
Math.abs(position.positionAmt),
(type, detail) => this.tradeLog.push(type, detail)
(type, detail) => this.tradeLog.push(type, detail),
{
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
expectedPrice: Number(
direction === "long"
? this.depthSnapshot?.bids?.[0]?.[0]
: this.depthSnapshot?.asks?.[0]?.[0]
) || null,
maxPct: this.config.maxCloseSlippagePct,
}
);
this.tradeLog.push("close", `止损平仓: ${direction === "long" ? "SELL" : "BUY"}`);
} catch (err) {
@@ -439,7 +471,11 @@ export class TrendEngine {
stopPrice,
quantity,
lastPrice,
(type, detail) => this.tradeLog.push(type, detail)
(type, detail) => this.tradeLog.push(type, detail),
{
markPrice: position.markPrice,
maxPct: this.config.maxCloseSlippagePct,
}
);
} catch (err) {
this.tradeLog.push("error", `挂止损单失败: ${String(err)}`);
@@ -484,7 +520,11 @@ export class TrendEngine {
activationPrice,
quantity,
this.config.trailingCallbackRate,
(type, detail) => this.tradeLog.push(type, detail)
(type, detail) => this.tradeLog.push(type, detail),
{
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
maxPct: this.config.maxCloseSlippagePct,
}
);
} catch (err) {
this.tradeLog.push("error", `挂动态止盈失败: ${String(err)}`);