mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 16:28:06 +00:00
更新交易引擎,添加最大平仓滑点百分比配置以优化价格操控保护逻辑,确保在市价平仓时价格与标记价格的偏差在允许范围内。
This commit is contained in:
@@ -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)) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)}`);
|
||||
|
||||
Reference in New Issue
Block a user