Skip to content

Commit c719b15

Browse files
committed
chore: sync with upstream/dev
Includes: - PR NoFxAiOS#931: Fix Go formatting for test files - PR NoFxAiOS#800: Data staleness detection (Part 2/3) - already in z-dev-v2 - PR NoFxAiOS#918: Improve UX messages for empty states and error feedback - PR NoFxAiOS#922: Fix missing system_prompt_template field in trader edit - PR NoFxAiOS#921: Remove duplicate exchange config fields (Aster & Hyperliquid) - PR NoFxAiOS#917: Fix two-stage private key input validation (0x prefix support) - PR NoFxAiOS#713: Add backend safety checks for partial_close - PR NoFxAiOS#908: Web Crypto environment check (0xEmberZz) - PR NoFxAiOS#638: Decision limit selector with 5/10/20/50 options (xqliu) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> # Conflicts: # market/data.go # web/src/components/TwoStageKeyModal.tsx
2 parents 59f7ccc + f4c95c0 commit c719b15

File tree

12 files changed

+839
-58
lines changed

12 files changed

+839
-58
lines changed

api/server.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1569,7 +1569,15 @@ func (s *Server) handleLatestDecisions(c *gin.Context) {
15691569
return
15701570
}
15711571

1572-
records, err := trader.GetDecisionLogger().GetLatestRecords(5)
1572+
// 从 query 参数读取 limit,默认 5,最大 50
1573+
limit := 5
1574+
if limitStr := c.Query("limit"); limitStr != "" {
1575+
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 50 {
1576+
limit = l
1577+
}
1578+
}
1579+
1580+
records, err := trader.GetDecisionLogger().GetLatestRecords(limit)
15731581
if err != nil {
15741582
c.JSON(http.StatusInternalServerError, gin.H{
15751583
"error": fmt.Sprintf("获取决策日志失败: %v", err),

decision/engine.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"fmt"
66
"log"
7+
"math"
78
"nofx/market"
89
"nofx/mcp"
910
"nofx/pool"
@@ -413,9 +414,12 @@ func buildUserPrompt(ctx *Context) string {
413414
}
414415
}
415416

416-
sb.WriteString(fmt.Sprintf("%d. %s %s | 入场价%.4f 当前价%.4f | 盈亏%+.2f%% | 盈亏金额%+.2f USDT | 最高收益率%.2f%% | 杠杆%dx | 保证金%.0f | 强平价%.4f%s\n\n",
417+
// 计算仓位价值(用于 partial_close 检查)
418+
positionValue := math.Abs(pos.Quantity) * pos.MarkPrice
419+
420+
sb.WriteString(fmt.Sprintf("%d. %s %s | 入场价%.4f 当前价%.4f | 数量%.4f | 仓位价值%.2f USDT | 盈亏%+.2f%% | 盈亏金额%+.2f USDT | 最高收益率%.2f%% | 杠杆%dx | 保证金%.0f | 强平价%.4f%s\n\n",
417421
i+1, pos.Symbol, strings.ToUpper(pos.Side),
418-
pos.EntryPrice, pos.MarkPrice, pos.UnrealizedPnLPct, pos.UnrealizedPnL, pos.PeakPnLPct,
422+
pos.EntryPrice, pos.MarkPrice, pos.Quantity, positionValue, pos.UnrealizedPnLPct, pos.UnrealizedPnL, pos.PeakPnLPct,
419423
pos.Leverage, pos.MarginUsed, pos.LiquidationPrice, holdingDuration))
420424

421425
// 使用FormatMarketData输出完整市场数据

trader/auto_trader.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1203,6 +1203,37 @@ func (at *AutoTrader) executePartialCloseWithRecord(decision *decision.Decision,
12031203
closeQuantity := totalQuantity * (decision.ClosePercentage / 100.0)
12041204
actionRecord.Quantity = closeQuantity
12051205

1206+
// ✅ Layer 2: 最小仓位检查(防止产生小额剩余)
1207+
markPrice, ok := targetPosition["markPrice"].(float64)
1208+
if !ok || markPrice <= 0 {
1209+
return fmt.Errorf("无法解析当前价格,无法执行最小仓位检查")
1210+
}
1211+
1212+
currentPositionValue := totalQuantity * markPrice
1213+
remainingQuantity := totalQuantity - closeQuantity
1214+
remainingValue := remainingQuantity * markPrice
1215+
1216+
const MIN_POSITION_VALUE = 10.0 // 最小持仓价值 10 USDT(對齊交易所底线,小仓位建议直接全平)
1217+
1218+
if remainingValue > 0 && remainingValue <= MIN_POSITION_VALUE {
1219+
log.Printf("⚠️ 检测到 partial_close 后剩余仓位 %.2f USDT < %.0f USDT",
1220+
remainingValue, MIN_POSITION_VALUE)
1221+
log.Printf(" → 当前仓位价值: %.2f USDT, 平仓 %.1f%%, 剩余: %.2f USDT",
1222+
currentPositionValue, decision.ClosePercentage, remainingValue)
1223+
log.Printf(" → 自动修正为全部平仓,避免产生无法平仓的小额剩余")
1224+
1225+
// 🔄 自动修正为全部平仓
1226+
if positionSide == "LONG" {
1227+
decision.Action = "close_long"
1228+
log.Printf(" ✓ 已修正为: close_long")
1229+
return at.executeCloseLongWithRecord(decision, actionRecord)
1230+
} else {
1231+
decision.Action = "close_short"
1232+
log.Printf(" ✓ 已修正为: close_short")
1233+
return at.executeCloseShortWithRecord(decision, actionRecord)
1234+
}
1235+
}
1236+
12061237
// 执行平仓
12071238
var order map[string]interface{}
12081239
if positionSide == "LONG" {
@@ -1220,10 +1251,35 @@ func (at *AutoTrader) executePartialCloseWithRecord(decision *decision.Decision,
12201251
actionRecord.OrderID = orderID
12211252
}
12221253

1223-
remainingQuantity := totalQuantity - closeQuantity
12241254
log.Printf(" ✓ 部分平仓成功: 平仓 %.4f (%.1f%%), 剩余 %.4f",
12251255
closeQuantity, decision.ClosePercentage, remainingQuantity)
12261256

1257+
// ✅ Step 4: 恢复止盈止损(防止剩余仓位裸奔)
1258+
// 重要:币安等交易所在部分平仓后会自动取消原有的 TP/SL 订单(因为数量不匹配)
1259+
// 如果 AI 提供了新的止损止盈价格,则为剩余仓位重新设置保护
1260+
if decision.NewStopLoss > 0 {
1261+
log.Printf(" → 为剩余仓位 %.4f 恢复止损单: %.2f", remainingQuantity, decision.NewStopLoss)
1262+
err = at.trader.SetStopLoss(decision.Symbol, positionSide, remainingQuantity, decision.NewStopLoss)
1263+
if err != nil {
1264+
log.Printf(" ⚠️ 恢复止损失败: %v(不影响平仓结果)", err)
1265+
}
1266+
}
1267+
1268+
if decision.NewTakeProfit > 0 {
1269+
log.Printf(" → 为剩余仓位 %.4f 恢复止盈单: %.2f", remainingQuantity, decision.NewTakeProfit)
1270+
err = at.trader.SetTakeProfit(decision.Symbol, positionSide, remainingQuantity, decision.NewTakeProfit)
1271+
if err != nil {
1272+
log.Printf(" ⚠️ 恢复止盈失败: %v(不影响平仓结果)", err)
1273+
}
1274+
}
1275+
1276+
// 如果 AI 没有提供新的止盈止损,记录警告
1277+
if decision.NewStopLoss <= 0 && decision.NewTakeProfit <= 0 {
1278+
log.Printf(" ⚠️⚠️⚠️ 警告: 部分平仓后AI未提供新的止盈止损价格")
1279+
log.Printf(" → 剩余仓位 %.4f (价值 %.2f USDT) 目前没有止盈止损保护", remainingQuantity, remainingValue)
1280+
log.Printf(" → 建议: 在 partial_close 决策中包含 new_stop_loss 和 new_take_profit 字段")
1281+
}
1282+
12271283
return nil
12281284
}
12291285

trader/hyperliquid_trader_race_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ func TestGetSzDecimals_ValidMeta(t *testing.T) {
123123
}
124124

125125
tests := []struct {
126-
coin string
126+
coin string
127127
expectedDecimals int
128128
}{
129129
{"BTC", 5},

0 commit comments

Comments
 (0)