mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-12 09:48:07 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a32efa2ba0 | ||
|
|
76704b6bdd | ||
|
|
168d8cbb08 |
@@ -0,0 +1,12 @@
|
|||||||
|
# RitMEX Bot - Claude Instructions
|
||||||
|
|
||||||
|
## Package Manager
|
||||||
|
|
||||||
|
**必须使用 Bun** - 这个项目使用 Bun 作为包管理器和运行时。所有能用 bun 执行的命令都必须使用 bun:
|
||||||
|
|
||||||
|
- 安装依赖: `bun install`
|
||||||
|
- 运行脚本: `bun run <script>`
|
||||||
|
- 执行测试: `bun test`
|
||||||
|
- 类型检查: `bun run typecheck`
|
||||||
|
|
||||||
|
**不要使用 npm、yarn 或 npx**
|
||||||
@@ -157,6 +157,8 @@ export interface MakerConfig {
|
|||||||
maxLogEntries: number;
|
maxLogEntries: number;
|
||||||
maxCloseSlippagePct: number;
|
maxCloseSlippagePct: number;
|
||||||
priceTick: number;
|
priceTick: number;
|
||||||
|
/** 开仓挂单档位:1=买1/卖1,2=买2/卖2,以此类推。仅影响无仓位时的开仓挂单,平仓逻辑不受影响。默认1 */
|
||||||
|
entryDepthLevel: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makerConfig: MakerConfig = {
|
export const makerConfig: MakerConfig = {
|
||||||
@@ -172,6 +174,7 @@ export const makerConfig: MakerConfig = {
|
|||||||
0.05
|
0.05
|
||||||
),
|
),
|
||||||
priceTick: parseNumber(process.env.MAKER_PRICE_TICK ?? process.env.PRICE_TICK, 0.1),
|
priceTick: parseNumber(process.env.MAKER_PRICE_TICK ?? process.env.PRICE_TICK, 0.1),
|
||||||
|
entryDepthLevel: Math.max(1, Math.floor(parseNumber(process.env.MAKER_ENTRY_DEPTH_LEVEL, 1))),
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface MakerPointsConfig {
|
export interface MakerPointsConfig {
|
||||||
@@ -347,6 +350,8 @@ export interface LiquidityMakerConfig {
|
|||||||
closeTickOffset: number;
|
closeTickOffset: number;
|
||||||
/** 偏移判断阈值倍数,当一侧深度超出另一侧此倍数时取消薄端订单,默认2 */
|
/** 偏移判断阈值倍数,当一侧深度超出另一侧此倍数时取消薄端订单,默认2 */
|
||||||
depthImbalanceRatio: number;
|
depthImbalanceRatio: number;
|
||||||
|
/** 开仓挂单档位:1=买1/卖1,2=买2/卖2,以此类推。仅影响无仓位时的开仓挂单,平仓逻辑不受影响。默认1 */
|
||||||
|
entryDepthLevel: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const liquidityMakerConfig: LiquidityMakerConfig = {
|
export const liquidityMakerConfig: LiquidityMakerConfig = {
|
||||||
@@ -364,6 +369,7 @@ export const liquidityMakerConfig: LiquidityMakerConfig = {
|
|||||||
priceTick: parseNumber(process.env.LIQUIDITY_MAKER_PRICE_TICK ?? process.env.MAKER_PRICE_TICK ?? process.env.PRICE_TICK, 0.1),
|
priceTick: parseNumber(process.env.LIQUIDITY_MAKER_PRICE_TICK ?? process.env.MAKER_PRICE_TICK ?? process.env.PRICE_TICK, 0.1),
|
||||||
closeTickOffset: Math.max(1, Math.floor(parseNumber(process.env.LIQUIDITY_MAKER_CLOSE_TICK_OFFSET, 1))),
|
closeTickOffset: Math.max(1, Math.floor(parseNumber(process.env.LIQUIDITY_MAKER_CLOSE_TICK_OFFSET, 1))),
|
||||||
depthImbalanceRatio: Math.max(1.1, parseNumber(process.env.LIQUIDITY_MAKER_DEPTH_IMBALANCE_RATIO, 2)),
|
depthImbalanceRatio: Math.max(1.1, parseNumber(process.env.LIQUIDITY_MAKER_DEPTH_IMBALANCE_RATIO, 2)),
|
||||||
|
entryDepthLevel: Math.max(1, Math.floor(parseNumber(process.env.MAKER_ENTRY_DEPTH_LEVEL, 1))),
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isBasisStrategyEnabled(): boolean {
|
export function isBasisStrategyEnabled(): boolean {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { isOrderActiveStatus } from "../utils/order-status";
|
|||||||
import { getPosition, parseSymbolParts } from "../utils/strategy";
|
import { getPosition, parseSymbolParts } from "../utils/strategy";
|
||||||
import type { PositionSnapshot } from "../utils/strategy";
|
import type { PositionSnapshot } from "../utils/strategy";
|
||||||
import { computePositionPnl } from "../utils/pnl";
|
import { computePositionPnl } from "../utils/pnl";
|
||||||
import { getTopPrices, getMidOrLast } from "../utils/price";
|
import { getTopPrices, getPricesAtLevel, getMidOrLast } from "../utils/price";
|
||||||
import { shouldStopLoss } from "../utils/risk";
|
import { shouldStopLoss } from "../utils/risk";
|
||||||
import {
|
import {
|
||||||
marketClose,
|
marketClose,
|
||||||
@@ -430,10 +430,18 @@ export class LiquidityMakerEngine {
|
|||||||
|
|
||||||
// 直接使用orderbook价格,格式化为字符串避免精度问题
|
// 直接使用orderbook价格,格式化为字符串避免精度问题
|
||||||
const priceDecimals = this.getPriceDecimals();
|
const priceDecimals = this.getPriceDecimals();
|
||||||
|
// 平仓价格始终使用买1/卖1
|
||||||
const closeBidPrice = formatPriceToString(finalBid, priceDecimals);
|
const closeBidPrice = formatPriceToString(finalBid, priceDecimals);
|
||||||
const closeAskPrice = formatPriceToString(finalAsk, priceDecimals);
|
const closeAskPrice = formatPriceToString(finalAsk, priceDecimals);
|
||||||
const rawBidPrice = finalBid - this.config.bidOffset;
|
|
||||||
const rawAskPrice = finalAsk + this.config.askOffset;
|
// 开仓价格根据 entryDepthLevel 使用指定档位
|
||||||
|
const entryLevel = this.config.entryDepthLevel ?? 1;
|
||||||
|
const { bidAtLevel: entryBid, askAtLevel: entryAsk } = getPricesAtLevel(latestDepth, entryLevel);
|
||||||
|
const entryBidBase = entryBid ?? finalBid;
|
||||||
|
const entryAskBase = entryAsk ?? finalAsk;
|
||||||
|
|
||||||
|
const rawBidPrice = entryBidBase - this.config.bidOffset;
|
||||||
|
const rawAskPrice = entryAskBase + this.config.askOffset;
|
||||||
const safeBid = this.ensureMakerPrice("BUY", rawBidPrice, finalBid, finalAsk);
|
const safeBid = this.ensureMakerPrice("BUY", rawBidPrice, finalBid, finalAsk);
|
||||||
const safeAsk = this.ensureMakerPrice("SELL", rawAskPrice, finalBid, finalAsk);
|
const safeAsk = this.ensureMakerPrice("SELL", rawAskPrice, finalBid, finalAsk);
|
||||||
const bidPrice = safeBid != null ? formatPriceToString(safeBid, priceDecimals) : null;
|
const bidPrice = safeBid != null ? formatPriceToString(safeBid, priceDecimals) : null;
|
||||||
@@ -627,18 +635,23 @@ export class LiquidityMakerEngine {
|
|||||||
|
|
||||||
let targetPrice: number;
|
let targetPrice: number;
|
||||||
|
|
||||||
// 基于最近成交或orderbook计算目标价
|
// 基于最近成交或入场价计算目标价,应用 closeTickOffset
|
||||||
if (this.lastFill && Date.now() - this.lastFill.timestamp < 60000) {
|
if (this.lastFill && Date.now() - this.lastFill.timestamp < 60000) {
|
||||||
// 最近1分钟内有成交,基于成交价计算
|
// 最近1分钟内有成交,基于成交价计算
|
||||||
if (closeSide === "SELL") {
|
if (closeSide === "SELL") {
|
||||||
// 多头平仓:在成交价上方挂卖单
|
|
||||||
targetPrice = this.lastFill.price + tickOffset;
|
targetPrice = this.lastFill.price + tickOffset;
|
||||||
} else {
|
} else {
|
||||||
// 空头平仓:在成交价下方挂买单
|
|
||||||
targetPrice = this.lastFill.price - tickOffset;
|
targetPrice = this.lastFill.price - tickOffset;
|
||||||
}
|
}
|
||||||
|
} else if (entryPrice && Number.isFinite(entryPrice) && entryPrice > 0) {
|
||||||
|
// 没有最近成交但有入场价,基于入场价计算
|
||||||
|
if (closeSide === "SELL") {
|
||||||
|
targetPrice = entryPrice + tickOffset;
|
||||||
|
} else {
|
||||||
|
targetPrice = entryPrice - tickOffset;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 没有最近成交,使用orderbook价格
|
// 没有成交也没有入场价,使用orderbook价格
|
||||||
if (closeSide === "SELL") {
|
if (closeSide === "SELL") {
|
||||||
targetPrice = topAsk;
|
targetPrice = topAsk;
|
||||||
} else {
|
} else {
|
||||||
@@ -646,18 +659,18 @@ export class LiquidityMakerEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保不亏本
|
// 确保不亏本(仅当目标价低于入场价时调整)
|
||||||
if (entryPrice && Number.isFinite(entryPrice) && entryPrice > 0) {
|
if (entryPrice && Number.isFinite(entryPrice) && entryPrice > 0) {
|
||||||
if (closeSide === "SELL") {
|
if (closeSide === "SELL") {
|
||||||
// 多头平仓:卖价必须 >= 入场价
|
// 多头平仓:卖价必须 >= 入场价
|
||||||
if (targetPrice < entryPrice) {
|
if (targetPrice < entryPrice) {
|
||||||
targetPrice = entryPrice + this.priceTick; // 至少盈利1个tick
|
targetPrice = entryPrice + this.priceTick;
|
||||||
this.tradeLog.push("info", `平仓价调整为入场价+1tick以确保不亏本: ${targetPrice.toFixed(priceDecimals)}`);
|
this.tradeLog.push("info", `平仓价调整为入场价+1tick以确保不亏本: ${targetPrice.toFixed(priceDecimals)}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 空头平仓:买价必须 <= 入场价
|
// 空头平仓:买价必须 <= 入场价
|
||||||
if (targetPrice > entryPrice) {
|
if (targetPrice > entryPrice) {
|
||||||
targetPrice = entryPrice - this.priceTick; // 至少盈利1个tick
|
targetPrice = entryPrice - this.priceTick;
|
||||||
this.tradeLog.push("info", `平仓价调整为入场价-1tick以确保不亏本: ${targetPrice.toFixed(priceDecimals)}`);
|
this.tradeLog.push("info", `平仓价调整为入场价-1tick以确保不亏本: ${targetPrice.toFixed(priceDecimals)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { isOrderActiveStatus } from "../utils/order-status";
|
|||||||
import { getPosition } from "../utils/strategy";
|
import { getPosition } from "../utils/strategy";
|
||||||
import type { PositionSnapshot } from "../utils/strategy";
|
import type { PositionSnapshot } from "../utils/strategy";
|
||||||
import { computePositionPnl } from "../utils/pnl";
|
import { computePositionPnl } from "../utils/pnl";
|
||||||
import { getTopPrices, getMidOrLast } from "../utils/price";
|
import { getTopPrices, getPricesAtLevel, getMidOrLast } from "../utils/price";
|
||||||
import { shouldStopLoss } from "../utils/risk";
|
import { shouldStopLoss } from "../utils/risk";
|
||||||
import {
|
import {
|
||||||
marketClose,
|
marketClose,
|
||||||
@@ -305,10 +305,18 @@ export class MakerEngine {
|
|||||||
|
|
||||||
// 直接使用orderbook价格,格式化为字符串避免精度问题
|
// 直接使用orderbook价格,格式化为字符串避免精度问题
|
||||||
const priceDecimals = this.getPriceDecimals();
|
const priceDecimals = this.getPriceDecimals();
|
||||||
|
// 平仓价格始终使用买1/卖1
|
||||||
const closeBidPrice = formatPriceToString(topBid, priceDecimals);
|
const closeBidPrice = formatPriceToString(topBid, priceDecimals);
|
||||||
const closeAskPrice = formatPriceToString(topAsk, priceDecimals);
|
const closeAskPrice = formatPriceToString(topAsk, priceDecimals);
|
||||||
const bidPrice = formatPriceToString(topBid - this.config.bidOffset, priceDecimals);
|
|
||||||
const askPrice = formatPriceToString(topAsk + this.config.askOffset, priceDecimals);
|
// 开仓价格根据 entryDepthLevel 使用指定档位
|
||||||
|
const entryLevel = this.config.entryDepthLevel ?? 1;
|
||||||
|
const { bidAtLevel: entryBid, askAtLevel: entryAsk } = getPricesAtLevel(depth, entryLevel);
|
||||||
|
const entryBidBase = entryBid ?? topBid;
|
||||||
|
const entryAskBase = entryAsk ?? topAsk;
|
||||||
|
|
||||||
|
const bidPrice = formatPriceToString(entryBidBase - this.config.bidOffset, priceDecimals);
|
||||||
|
const askPrice = formatPriceToString(entryAskBase + this.config.askOffset, priceDecimals);
|
||||||
const position = getPosition(this.accountSnapshot, this.config.symbol);
|
const position = getPosition(this.accountSnapshot, this.config.symbol);
|
||||||
const absPosition = Math.abs(position.positionAmt);
|
const absPosition = Math.abs(position.positionAmt);
|
||||||
const desired: DesiredOrder[] = [];
|
const desired: DesiredOrder[] = [];
|
||||||
|
|||||||
@@ -83,7 +83,9 @@ type MakerPointsListener = (snapshot: MakerPointsSnapshot) => void;
|
|||||||
|
|
||||||
const EPS = 1e-5;
|
const EPS = 1e-5;
|
||||||
const INSUFFICIENT_BALANCE_COOLDOWN_MS = 15_000;
|
const INSUFFICIENT_BALANCE_COOLDOWN_MS = 15_000;
|
||||||
const STOP_LOSS_COOLDOWN_MS = 10_000;
|
const STOP_LOSS_COOLDOWN_MS = 5_000;
|
||||||
|
const STOP_LOSS_CHECK_INTERVAL_MS = 250; // 止损检查最大间隔
|
||||||
|
const STOP_LOSS_RETRY_INTERVAL_MS = 500; // 止损失败后重试间隔
|
||||||
|
|
||||||
export class MakerPointsEngine {
|
export class MakerPointsEngine {
|
||||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
private accountSnapshot: AsterAccountSnapshot | null = null;
|
||||||
@@ -187,7 +189,7 @@ export class MakerPointsEngine {
|
|||||||
if (!this.stopLossTimer) {
|
if (!this.stopLossTimer) {
|
||||||
this.stopLossTimer = setInterval(() => {
|
this.stopLossTimer = setInterval(() => {
|
||||||
void this.checkStopLoss();
|
void this.checkStopLoss();
|
||||||
}, Math.max(500, this.config.refreshIntervalMs));
|
}, Math.min(STOP_LOSS_CHECK_INTERVAL_MS, this.config.refreshIntervalMs));
|
||||||
}
|
}
|
||||||
this.binanceDepth.start();
|
this.binanceDepth.start();
|
||||||
}
|
}
|
||||||
@@ -821,62 +823,132 @@ export class MakerPointsEngine {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用实时深度数据计算仓位的未实现盈亏
|
||||||
|
* 优先使用实时数据,回退到账户快照数据
|
||||||
|
*/
|
||||||
|
private computeRealtimePnl(position: PositionSnapshot): number | null {
|
||||||
|
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
|
||||||
|
// 使用实时深度计算 PnL
|
||||||
|
if (topBid != null && topAsk != null) {
|
||||||
|
return computePositionPnl(position, topBid, topAsk);
|
||||||
|
}
|
||||||
|
// 回退到账户推送的数据
|
||||||
|
if (Number.isFinite(position.unrealizedProfit)) {
|
||||||
|
return position.unrealizedProfit;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private async checkStopLoss(): Promise<void> {
|
private async checkStopLoss(): Promise<void> {
|
||||||
if (this.stopLossProcessing) return;
|
if (this.stopLossProcessing) return;
|
||||||
const lossLimit = Number(this.config.stopLossUsd);
|
const lossLimit = Number(this.config.stopLossUsd);
|
||||||
if (!Number.isFinite(lossLimit) || lossLimit <= 0) return;
|
if (!Number.isFinite(lossLimit) || lossLimit <= 0) return;
|
||||||
if (!this.accountSnapshot) return;
|
if (!this.accountSnapshot) return;
|
||||||
|
|
||||||
const position = getPosition(this.accountSnapshot, this.config.symbol);
|
const position = getPosition(this.accountSnapshot, this.config.symbol);
|
||||||
const absPosition = Math.abs(position.positionAmt);
|
const absPosition = Math.abs(position.positionAmt);
|
||||||
if (absPosition < EPS) return;
|
if (absPosition < EPS) return;
|
||||||
if (!Number.isFinite(position.unrealizedProfit)) return;
|
|
||||||
|
// 使用实时计算的 PnL
|
||||||
|
const realtimePnl = this.computeRealtimePnl(position);
|
||||||
|
if (realtimePnl == null) return;
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now < this.stopLossCooldownUntil) return;
|
if (now < this.stopLossCooldownUntil) return;
|
||||||
if (position.unrealizedProfit > -lossLimit) return;
|
if (realtimePnl > -lossLimit) return;
|
||||||
|
|
||||||
this.stopLossProcessing = true;
|
this.stopLossProcessing = true;
|
||||||
this.stopLossCooldownUntil = now + STOP_LOSS_COOLDOWN_MS;
|
// 不在这里设置冷却期,只有成功平仓后才设置
|
||||||
this.tradeLog.push(
|
this.tradeLog.push(
|
||||||
"stop",
|
"stop",
|
||||||
`触发止损: 未实现亏损 ${position.unrealizedProfit.toFixed(4)} USDT`
|
`触发止损: 实时未实现亏损 ${realtimePnl.toFixed(4)} USDT`
|
||||||
);
|
);
|
||||||
this.notify({
|
this.notify({
|
||||||
type: "stop_loss",
|
type: "stop_loss",
|
||||||
level: "error",
|
level: "error",
|
||||||
symbol: this.config.symbol,
|
symbol: this.config.symbol,
|
||||||
title: "止损触发",
|
title: "止损触发",
|
||||||
message: `未实现亏损 ${position.unrealizedProfit.toFixed(4)} USDT,强制平仓`,
|
message: `实时未实现亏损 ${realtimePnl.toFixed(4)} USDT,强制平仓`,
|
||||||
details: {
|
details: {
|
||||||
side: position.positionAmt > 0 ? "LONG" : "SHORT",
|
side: position.positionAmt > 0 ? "LONG" : "SHORT",
|
||||||
size: absPosition,
|
size: absPosition,
|
||||||
unrealizedPnl: position.unrealizedProfit,
|
unrealizedPnl: realtimePnl,
|
||||||
lossLimit: -lossLimit,
|
lossLimit: -lossLimit,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 循环重试止损,直到仓位为0
|
||||||
|
await this.executeStopLossWithRetry(position.positionAmt > 0 ? "SELL" : "BUY");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行止损平仓,失败后自动重试直到仓位为0
|
||||||
|
*/
|
||||||
|
private async executeStopLossWithRetry(side: "BUY" | "SELL"): Promise<void> {
|
||||||
|
const maxRetries = 10;
|
||||||
|
let retryCount = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.flushOrders();
|
while (retryCount < maxRetries) {
|
||||||
await marketClose(
|
// 每次重试前重新检查仓位
|
||||||
this.exchange,
|
const currentPosition = getPosition(this.accountSnapshot, this.config.symbol);
|
||||||
this.config.symbol,
|
const currentAbsPosition = Math.abs(currentPosition.positionAmt);
|
||||||
this.openOrders,
|
|
||||||
this.locks,
|
// 仓位已清零,止损成功
|
||||||
this.timers,
|
if (currentAbsPosition < EPS) {
|
||||||
this.pending,
|
this.tradeLog.push("stop", "止损成功: 仓位已清零");
|
||||||
position.positionAmt > 0 ? "SELL" : "BUY",
|
this.stopLossCooldownUntil = Date.now() + STOP_LOSS_COOLDOWN_MS;
|
||||||
absPosition,
|
break;
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
}
|
||||||
undefined,
|
|
||||||
{ qtyStep: this.qtyStep }
|
try {
|
||||||
);
|
// 强制解锁 MARKET 类型,确保不被之前的操作阻塞
|
||||||
} catch (error) {
|
unlockOperating(this.locks, this.timers, this.pending, "MARKET");
|
||||||
if (isUnknownOrderError(error)) {
|
|
||||||
this.tradeLog.push("order", "止损平仓时订单已不存在");
|
// 先取消所有挂单
|
||||||
} else if (isPrecisionError(error)) {
|
await this.flushOrders();
|
||||||
this.tradeLog.push("warn", `止损平仓精度错误,重新同步: ${extractMessage(error)}`);
|
|
||||||
this.syncPrecision(true);
|
// 执行市价平仓
|
||||||
} else {
|
await marketClose(
|
||||||
this.tradeLog.push("error", `止损平仓失败: ${extractMessage(error)}`);
|
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.qtyStep }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 等待一小段时间让账户数据更新
|
||||||
|
await this.sleep(STOP_LOSS_RETRY_INTERVAL_MS);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
retryCount++;
|
||||||
|
if (isUnknownOrderError(error)) {
|
||||||
|
this.tradeLog.push("order", "止损平仓时订单已不存在,继续检查仓位");
|
||||||
|
} else if (isPrecisionError(error)) {
|
||||||
|
this.tradeLog.push("warn", `止损平仓精度错误,重新同步: ${extractMessage(error)}`);
|
||||||
|
this.syncPrecision(true);
|
||||||
|
} else {
|
||||||
|
this.tradeLog.push("error", `止损平仓失败 (重试 ${retryCount}/${maxRetries}): ${extractMessage(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 失败后等待一段时间再重试
|
||||||
|
if (retryCount < maxRetries) {
|
||||||
|
await this.sleep(STOP_LOSS_RETRY_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retryCount >= maxRetries) {
|
||||||
|
this.tradeLog.push("error", `止损重试已达上限 (${maxRetries} 次),请手动检查仓位`);
|
||||||
|
// 达到重试上限后设置冷却期,避免持续重试
|
||||||
|
this.stopLossCooldownUntil = Date.now() + STOP_LOSS_COOLDOWN_MS;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.stopLossProcessing = false;
|
this.stopLossProcessing = false;
|
||||||
@@ -884,6 +956,10 @@ export class MakerPointsEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
private async flushOrders(): Promise<void> {
|
private async flushOrders(): Promise<void> {
|
||||||
if (!this.openOrders.length) return;
|
if (!this.openOrders.length) return;
|
||||||
for (const order of this.openOrders) {
|
for (const order of this.openOrders) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { getPosition, parseSymbolParts } from "../utils/strategy";
|
|||||||
import type { PositionSnapshot } from "../utils/strategy";
|
import type { PositionSnapshot } from "../utils/strategy";
|
||||||
import { computeDepthStats } from "../utils/depth";
|
import { computeDepthStats } from "../utils/depth";
|
||||||
import { computePositionPnl } from "../utils/pnl";
|
import { computePositionPnl } from "../utils/pnl";
|
||||||
import { getTopPrices, getMidOrLast } from "../utils/price";
|
import { getTopPrices, getPricesAtLevel, getMidOrLast } from "../utils/price";
|
||||||
import { shouldStopLoss } from "../utils/risk";
|
import { shouldStopLoss } from "../utils/risk";
|
||||||
import {
|
import {
|
||||||
marketClose,
|
marketClose,
|
||||||
@@ -356,10 +356,18 @@ export class OffsetMakerEngine {
|
|||||||
|
|
||||||
// 直接使用orderbook价格,格式化为字符串避免精度问题
|
// 直接使用orderbook价格,格式化为字符串避免精度问题
|
||||||
const priceDecimals = this.getPriceDecimals();
|
const priceDecimals = this.getPriceDecimals();
|
||||||
|
// 平仓价格始终使用买1/卖1
|
||||||
const closeBidPrice = formatPriceToString(finalBid, priceDecimals);
|
const closeBidPrice = formatPriceToString(finalBid, priceDecimals);
|
||||||
const closeAskPrice = formatPriceToString(finalAsk, priceDecimals);
|
const closeAskPrice = formatPriceToString(finalAsk, priceDecimals);
|
||||||
const rawBidPrice = finalBid - this.config.bidOffset;
|
|
||||||
const rawAskPrice = finalAsk + this.config.askOffset;
|
// 开仓价格根据 entryDepthLevel 使用指定档位
|
||||||
|
const entryLevel = this.config.entryDepthLevel ?? 1;
|
||||||
|
const { bidAtLevel: entryBid, askAtLevel: entryAsk } = getPricesAtLevel(latestDepth, entryLevel);
|
||||||
|
const entryBidBase = entryBid ?? finalBid;
|
||||||
|
const entryAskBase = entryAsk ?? finalAsk;
|
||||||
|
|
||||||
|
const rawBidPrice = entryBidBase - this.config.bidOffset;
|
||||||
|
const rawAskPrice = entryAskBase + this.config.askOffset;
|
||||||
const safeBid = this.ensureMakerPrice("BUY", rawBidPrice, finalBid, finalAsk);
|
const safeBid = this.ensureMakerPrice("BUY", rawBidPrice, finalBid, finalAsk);
|
||||||
const safeAsk = this.ensureMakerPrice("SELL", rawAskPrice, finalBid, finalAsk);
|
const safeAsk = this.ensureMakerPrice("SELL", rawAskPrice, finalBid, finalAsk);
|
||||||
const bidPrice = safeBid != null ? formatPriceToString(safeBid, priceDecimals) : null;
|
const bidPrice = safeBid != null ? formatPriceToString(safeBid, priceDecimals) : null;
|
||||||
|
|||||||
@@ -9,6 +9,46 @@ export function getTopPrices(depth?: AsterDepth | null): { topBid: number | null
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定档位的盘口价格
|
||||||
|
* @param depth 深度数据
|
||||||
|
* @param level 档位(1=买1/卖1,2=买2/卖2,以此类推)
|
||||||
|
* @returns 指定档位的买卖价格,如果该档位不存在则回退到最近的有效档位
|
||||||
|
*/
|
||||||
|
export function getPricesAtLevel(
|
||||||
|
depth?: AsterDepth | null,
|
||||||
|
level: number = 1
|
||||||
|
): { bidAtLevel: number | null; askAtLevel: number | null } {
|
||||||
|
const index = Math.max(0, level - 1);
|
||||||
|
|
||||||
|
// 尝试获取指定档位,如果不存在则回退到最近的有效档位
|
||||||
|
const bids = depth?.bids ?? [];
|
||||||
|
const asks = depth?.asks ?? [];
|
||||||
|
|
||||||
|
let bidAtLevel: number | null = null;
|
||||||
|
let askAtLevel: number | null = null;
|
||||||
|
|
||||||
|
// 从指定档位向前查找第一个有效的买价
|
||||||
|
for (let i = Math.min(index, bids.length - 1); i >= 0; i--) {
|
||||||
|
const bid = Number(bids[i]?.[0]);
|
||||||
|
if (Number.isFinite(bid)) {
|
||||||
|
bidAtLevel = bid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从指定档位向前查找第一个有效的卖价
|
||||||
|
for (let i = Math.min(index, asks.length - 1); i >= 0; i--) {
|
||||||
|
const ask = Number(asks[i]?.[0]);
|
||||||
|
if (Number.isFinite(ask)) {
|
||||||
|
askAtLevel = ask;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { bidAtLevel, askAtLevel };
|
||||||
|
}
|
||||||
|
|
||||||
export function getMidOrLast(depth?: AsterDepth | null, ticker?: AsterTicker | null): number | null {
|
export function getMidOrLast(depth?: AsterDepth | null, ticker?: AsterTicker | null): number | null {
|
||||||
const { topBid, topAsk } = getTopPrices(depth);
|
const { topBid, topAsk } = getTopPrices(depth);
|
||||||
if (topBid != null && topAsk != null) return (topBid + topAsk) / 2;
|
if (topBid != null && topAsk != null) return (topBid + topAsk) / 2;
|
||||||
|
|||||||
Reference in New Issue
Block a user