mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18:07 +00:00
Add Claude instructions and enhance stop-loss logic
- Introduced a new `CLAUDE.md` file with instructions for using Bun as the package manager. - Adjusted stop-loss cooldown and check intervals in `MakerPointsEngine` for improved responsiveness. - Implemented a new method to compute real-time PnL using live depth data, enhancing stop-loss decision-making. - Added retry logic for stop-loss execution to ensure positions are closed effectively, with detailed logging for failures.
This commit is contained in:
@@ -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**
|
||||
@@ -83,7 +83,9 @@ type MakerPointsListener = (snapshot: MakerPointsSnapshot) => void;
|
||||
|
||||
const EPS = 1e-5;
|
||||
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 {
|
||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
||||
@@ -187,7 +189,7 @@ export class MakerPointsEngine {
|
||||
if (!this.stopLossTimer) {
|
||||
this.stopLossTimer = setInterval(() => {
|
||||
void this.checkStopLoss();
|
||||
}, Math.max(500, this.config.refreshIntervalMs));
|
||||
}, Math.min(STOP_LOSS_CHECK_INTERVAL_MS, this.config.refreshIntervalMs));
|
||||
}
|
||||
this.binanceDepth.start();
|
||||
}
|
||||
@@ -821,62 +823,132 @@ export class MakerPointsEngine {
|
||||
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> {
|
||||
if (this.stopLossProcessing) return;
|
||||
const lossLimit = Number(this.config.stopLossUsd);
|
||||
if (!Number.isFinite(lossLimit) || lossLimit <= 0) return;
|
||||
if (!this.accountSnapshot) return;
|
||||
|
||||
const position = getPosition(this.accountSnapshot, this.config.symbol);
|
||||
const absPosition = Math.abs(position.positionAmt);
|
||||
if (absPosition < EPS) return;
|
||||
if (!Number.isFinite(position.unrealizedProfit)) return;
|
||||
|
||||
// 使用实时计算的 PnL
|
||||
const realtimePnl = this.computeRealtimePnl(position);
|
||||
if (realtimePnl == null) return;
|
||||
|
||||
const now = Date.now();
|
||||
if (now < this.stopLossCooldownUntil) return;
|
||||
if (position.unrealizedProfit > -lossLimit) return;
|
||||
if (realtimePnl > -lossLimit) return;
|
||||
|
||||
this.stopLossProcessing = true;
|
||||
this.stopLossCooldownUntil = now + STOP_LOSS_COOLDOWN_MS;
|
||||
// 不在这里设置冷却期,只有成功平仓后才设置
|
||||
this.tradeLog.push(
|
||||
"stop",
|
||||
`触发止损: 未实现亏损 ${position.unrealizedProfit.toFixed(4)} USDT`
|
||||
`触发止损: 实时未实现亏损 ${realtimePnl.toFixed(4)} USDT`
|
||||
);
|
||||
this.notify({
|
||||
type: "stop_loss",
|
||||
level: "error",
|
||||
symbol: this.config.symbol,
|
||||
title: "止损触发",
|
||||
message: `未实现亏损 ${position.unrealizedProfit.toFixed(4)} USDT,强制平仓`,
|
||||
message: `实时未实现亏损 ${realtimePnl.toFixed(4)} USDT,强制平仓`,
|
||||
details: {
|
||||
side: position.positionAmt > 0 ? "LONG" : "SHORT",
|
||||
size: absPosition,
|
||||
unrealizedPnl: position.unrealizedProfit,
|
||||
unrealizedPnl: realtimePnl,
|
||||
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 {
|
||||
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),
|
||||
undefined,
|
||||
{ qtyStep: this.qtyStep }
|
||||
);
|
||||
} catch (error) {
|
||||
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", `止损平仓失败: ${extractMessage(error)}`);
|
||||
while (retryCount < maxRetries) {
|
||||
// 每次重试前重新检查仓位
|
||||
const currentPosition = getPosition(this.accountSnapshot, this.config.symbol);
|
||||
const currentAbsPosition = Math.abs(currentPosition.positionAmt);
|
||||
|
||||
// 仓位已清零,止损成功
|
||||
if (currentAbsPosition < EPS) {
|
||||
this.tradeLog.push("stop", "止损成功: 仓位已清零");
|
||||
this.stopLossCooldownUntil = Date.now() + STOP_LOSS_COOLDOWN_MS;
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
// 强制解锁 MARKET 类型,确保不被之前的操作阻塞
|
||||
unlockOperating(this.locks, this.timers, this.pending, "MARKET");
|
||||
|
||||
// 先取消所有挂单
|
||||
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.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 {
|
||||
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> {
|
||||
if (!this.openOrders.length) return;
|
||||
for (const order of this.openOrders) {
|
||||
|
||||
Reference in New Issue
Block a user