mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-16 01:06:59 +08:00
fix(trader): add backend safety checks for partial_close (#713)
* fix(trader): add backend safety checks for partial_close After PR #415 added partial_close functionality, production users reported two critical issues: 1. **Exchange minimum value error**: "Order must have minimum value of $10" when remaining position value falls below exchange threshold 2. **Unprotected positions after partial close**: Exchanges auto-cancel TP/SL orders when position size changes, leaving remaining position exposed to liquidation risk This PR adds **backend safety checks** as a safety net layer that complements the prompt-based rules from PR #712. **Protection**: Before executing partial_close, verify remaining position value > $10 ```go const MIN_POSITION_VALUE = 10.0 // Exchange底线 remainingValue := remainingQuantity * markPrice if remainingValue > 0 && remainingValue <= MIN_POSITION_VALUE { // 🔄 Auto-correct to full close decision.Action = "close_long" // or "close_short" return at.executeCloseLongWithRecord(decision, actionRecord) } ``` **Behavior**: - Position $20 → partial_close 50% → remaining $10 ≤ $10 → Auto full close ✅ - Position $30 → partial_close 50% → remaining $15 > $10 → Allow partial close ✅ **Protection**: Restore TP/SL orders for remaining position if AI provides new_stop_loss/new_take_profit ```go // Exchanges auto-cancel TP/SL when position size changes if decision.NewStopLoss > 0 { at.trader.SetStopLoss(symbol, side, remainingQuantity, decision.NewStopLoss) } if decision.NewTakeProfit > 0 { at.trader.SetTakeProfit(symbol, side, remainingQuantity, decision.NewTakeProfit) } // Warning if AI didn't provide new TP/SL if decision.NewStopLoss <= 0 && decision.NewTakeProfit <= 0 { log.Printf("⚠️⚠️⚠️ Warning: Remaining position has NO TP/SL protection") } ``` **Improvement**: Show position quantity and value to help AI make better decisions ``` Before: | 入场价100.00 当前价105.00 | 盈亏+5.00% | ... After: | 入场价100.00 当前价105.00 | 数量0.5000 | 仓位价值52.50 USDT | 盈亏+5.00% | ... ``` **Benefits**: - AI can now calculate: remaining_value = current_value × (1 - close_percentage/100) - AI can proactively avoid decisions that would violate $10 threshold - Added MIN_POSITION_VALUE check before execution - Auto-correct to full close if remaining value ≤ $10 - Restore TP/SL for remaining position - Warning logs when AI doesn't provide new TP/SL - Import "math" package - Calculate and display position value - Add quantity and position value to prompt - Complements PR #712 (Prompt v6.0.0 safety rules) - Addresses #301 (Backend layer) - Based on PR #415 (Core functionality) | Layer | Location | Purpose | |-------|----------|---------| | **Layer 1: AI Prompt** | PR #712 | Prevent bad decisions before they happen | | **Layer 2: Backend** | This PR | Auto-correct and safety net | **Together they provide**: - ✅ AI makes better decisions (sees position value, knows rules) - ✅ Backend catches edge cases (auto-corrects violations) - ✅ User-friendly warnings (explains what happened) - [x] Compiles successfully (`go build ./...`) - [x] MIN_POSITION_VALUE logic correct - [x] TP/SL restoration logic correct - [x] Position value display format validated - [x] Auto-correction flow tested This PR can be merged **independently** of PR #712, or together. Suggested merge order: 1. PR #712 (Prompt v6.0.0) - AI layer improvements 2. This PR (Backend safety) - Safety net layer Or merge together for complete two-layer protection. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: add error handling for markPrice type assertion - Check type assertion success before using markPrice - Return error if markPrice is invalid or <= 0 - Addresses code review feedback from @xqliu in PR #713 * test(trader): add comprehensive unit tests for partial_close safety checks - Test minimum position value check (< 10 USDT triggers full close) - Test boundary condition (exactly 10 USDT also triggers full close) - Test stop-loss/take-profit recovery after partial close - Test edge cases (invalid close percentages) - Test integration scenarios with mock trader All 14 test cases passed, covering: 1. MinPositionCheck (5 cases): normal, small remainder, boundary, edge cases 2. StopLossTakeProfitRecovery (4 cases): both/SL only/TP only/none 3. EdgeCases (4 cases): zero/over 100/negative/normal percentages 4. Integration (2 cases): LONG with SL/TP, SHORT with auto full close 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * style: apply go fmt after rebase Only formatting changes: - api/server.go: fix indentation - manager/trader_manager.go: add blank line - trader/partial_close_test.go: align struct fields 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(test): rename MockTrader to MockPartialCloseTrader to avoid conflict Problem: - trader/partial_close_test.go defined MockTrader - trader/auto_trader_test.go already has MockTrader - Methods CloseLong, CloseShort, SetStopLoss, SetTakeProfit were declared twice - Compilation failed with 'already declared' errors Solution: - Rename MockTrader to MockPartialCloseTrader in partial_close_test.go - This avoids naming conflict while keeping test logic independent Test Results: - All partial close tests pass - All trader tests pass Related: PR #713 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: the-dev-z <the-dev-z@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
b907155f54
commit
6df2bf202c
@@ -1180,6 +1180,37 @@ func (at *AutoTrader) executePartialCloseWithRecord(decision *decision.Decision,
|
||||
closeQuantity := totalQuantity * (decision.ClosePercentage / 100.0)
|
||||
actionRecord.Quantity = closeQuantity
|
||||
|
||||
// ✅ Layer 2: 最小仓位检查(防止产生小额剩余)
|
||||
markPrice, ok := targetPosition["markPrice"].(float64)
|
||||
if !ok || markPrice <= 0 {
|
||||
return fmt.Errorf("无法解析当前价格,无法执行最小仓位检查")
|
||||
}
|
||||
|
||||
currentPositionValue := totalQuantity * markPrice
|
||||
remainingQuantity := totalQuantity - closeQuantity
|
||||
remainingValue := remainingQuantity * markPrice
|
||||
|
||||
const MIN_POSITION_VALUE = 10.0 // 最小持仓价值 10 USDT(對齊交易所底线,小仓位建议直接全平)
|
||||
|
||||
if remainingValue > 0 && remainingValue <= MIN_POSITION_VALUE {
|
||||
log.Printf("⚠️ 检测到 partial_close 后剩余仓位 %.2f USDT < %.0f USDT",
|
||||
remainingValue, MIN_POSITION_VALUE)
|
||||
log.Printf(" → 当前仓位价值: %.2f USDT, 平仓 %.1f%%, 剩余: %.2f USDT",
|
||||
currentPositionValue, decision.ClosePercentage, remainingValue)
|
||||
log.Printf(" → 自动修正为全部平仓,避免产生无法平仓的小额剩余")
|
||||
|
||||
// 🔄 自动修正为全部平仓
|
||||
if positionSide == "LONG" {
|
||||
decision.Action = "close_long"
|
||||
log.Printf(" ✓ 已修正为: close_long")
|
||||
return at.executeCloseLongWithRecord(decision, actionRecord)
|
||||
} else {
|
||||
decision.Action = "close_short"
|
||||
log.Printf(" ✓ 已修正为: close_short")
|
||||
return at.executeCloseShortWithRecord(decision, actionRecord)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行平仓
|
||||
var order map[string]interface{}
|
||||
if positionSide == "LONG" {
|
||||
@@ -1197,10 +1228,35 @@ func (at *AutoTrader) executePartialCloseWithRecord(decision *decision.Decision,
|
||||
actionRecord.OrderID = orderID
|
||||
}
|
||||
|
||||
remainingQuantity := totalQuantity - closeQuantity
|
||||
log.Printf(" ✓ 部分平仓成功: 平仓 %.4f (%.1f%%), 剩余 %.4f",
|
||||
closeQuantity, decision.ClosePercentage, remainingQuantity)
|
||||
|
||||
// ✅ Step 4: 恢复止盈止损(防止剩余仓位裸奔)
|
||||
// 重要:币安等交易所在部分平仓后会自动取消原有的 TP/SL 订单(因为数量不匹配)
|
||||
// 如果 AI 提供了新的止损止盈价格,则为剩余仓位重新设置保护
|
||||
if decision.NewStopLoss > 0 {
|
||||
log.Printf(" → 为剩余仓位 %.4f 恢复止损单: %.2f", remainingQuantity, decision.NewStopLoss)
|
||||
err = at.trader.SetStopLoss(decision.Symbol, positionSide, remainingQuantity, decision.NewStopLoss)
|
||||
if err != nil {
|
||||
log.Printf(" ⚠️ 恢复止损失败: %v(不影响平仓结果)", err)
|
||||
}
|
||||
}
|
||||
|
||||
if decision.NewTakeProfit > 0 {
|
||||
log.Printf(" → 为剩余仓位 %.4f 恢复止盈单: %.2f", remainingQuantity, decision.NewTakeProfit)
|
||||
err = at.trader.SetTakeProfit(decision.Symbol, positionSide, remainingQuantity, decision.NewTakeProfit)
|
||||
if err != nil {
|
||||
log.Printf(" ⚠️ 恢复止盈失败: %v(不影响平仓结果)", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果 AI 没有提供新的止盈止损,记录警告
|
||||
if decision.NewStopLoss <= 0 && decision.NewTakeProfit <= 0 {
|
||||
log.Printf(" ⚠️⚠️⚠️ 警告: 部分平仓后AI未提供新的止盈止损价格")
|
||||
log.Printf(" → 剩余仓位 %.4f (价值 %.2f USDT) 目前没有止盈止损保护", remainingQuantity, remainingValue)
|
||||
log.Printf(" → 建议: 在 partial_close 决策中包含 new_stop_loss 和 new_take_profit 字段")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user