mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 17:28:08 +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 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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user