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:
discountry
2026-01-20 00:51:15 +08:00
parent 9629c22496
commit 168d8cbb08
2 changed files with 118 additions and 30 deletions
+12
View File
@@ -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**
+106 -30
View File
@@ -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) {