From 82660b12c5a0396adf5e807eb0629c537692ab70 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Sun, 2 Nov 2025 05:32:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=83=A8=E5=88=86?= =?UTF-8?q?=E5=B9=B3=E4=BB=93=E5=92=8C=E5=8A=A8=E6=80=81=E6=AD=A2=E7=9B=88?= =?UTF-8?q?=E6=AD=A2=E6=8D=9F=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: - update_stop_loss: 调整止损价格(追踪止损) - update_take_profit: 调整止盈价格(技术位优化) - partial_close: 部分平仓(分批止盈) 实现细节: - Decision struct 新增字段:NewStopLoss, NewTakeProfit, ClosePercentage - 新增执行函数:executeUpdateStopLossWithRecord, executeUpdateTakeProfitWithRecord, executePartialCloseWithRecord - 修复持仓字段获取 bug(使用 "side" 并转大写) - 更新 adaptive.txt 文档,包含详细使用示例和策略建议 - 优先级排序:平仓 > 调整止盈止损 > 开仓 命名统一: - 与社区 PR #197 保持一致,使用 update_* 而非 adjust_* - 独有功能:partial_close(部分平仓) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- decision/engine.go | 11 ++- prompts/adaptive.txt | 88 ++++++++++++++++++- trader/auto_trader.go | 200 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 292 insertions(+), 7 deletions(-) diff --git a/decision/engine.go b/decision/engine.go index fb4b7a52..4eff73c2 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -71,11 +71,20 @@ type Context struct { // Decision AI的交易决策 type Decision struct { Symbol string `json:"symbol"` - Action string `json:"action"` // "open_long", "open_short", "close_long", "close_short", "hold", "wait" + Action string `json:"action"` // "open_long", "open_short", "close_long", "close_short", "update_stop_loss", "update_take_profit", "partial_close", "hold", "wait" + + // 开仓参数 Leverage int `json:"leverage,omitempty"` PositionSizeUSD float64 `json:"position_size_usd,omitempty"` StopLoss float64 `json:"stop_loss,omitempty"` TakeProfit float64 `json:"take_profit,omitempty"` + + // 调整参数(新增) + NewStopLoss float64 `json:"new_stop_loss,omitempty"` // 用于 adjust_stop_loss + NewTakeProfit float64 `json:"new_take_profit,omitempty"` // 用于 adjust_take_profit + ClosePercentage float64 `json:"close_percentage,omitempty"` // 用于 partial_close (0-100) + + // 通用参数 Confidence int `json:"confidence,omitempty"` // 信心度 (0-100) RiskUSD float64 `json:"risk_usd,omitempty"` // 最大美元风险 Reasoning string `json:"reasoning"` diff --git a/prompts/adaptive.txt b/prompts/adaptive.txt index 279e9584..172fedda 100644 --- a/prompts/adaptive.txt +++ b/prompts/adaptive.txt @@ -237,13 +237,97 @@ 关键: 夏普比率是唯一指标,它会自然惩罚频繁交易和过度进出。 +# 动态止盈止损功能 + +你现在可以在持仓中主动调整止盈止损,实现追踪止损和分批止盈策略。 + +## 可用的新 Actions + +### 1. update_stop_loss - 调整止损 + +用于实现追踪止损,保护利润。 + +示例场景: +- 开仓 BTC @ 100,000,止损 99,000 (-1%) +- 价格上涨到 101,500 (+1.5%) +- 决策:将止损移到成本价 100,500,锁定利润 + +JSON 格式: +```json +{ + "symbol": "BTCUSDT", + "action": "update_stop_loss", + "new_stop_loss": 100500.0, + "confidence": 90, + "reasoning": "浮盈达到 1.5%,将止损移到成本价保证不亏" +} +``` + +### 2. update_take_profit - 调整止盈 + +用于在技术位前提前止盈,避免回撤。 + +示例场景: +- 持仓 BTC @ 100,000,原止盈 102,000 (+2%) +- 15m EMA20 位于 101,800(强压力位) +- 价格到 101,700,距离 EMA20 仅 0.1% +- 决策:将止盈调整到 101,750,避免在技术位回撤 + +JSON 格式: +```json +{ + "symbol": "BTCUSDT", + "action": "update_take_profit", + "new_take_profit": 101750.0, + "confidence": 85, + "reasoning": "价格接近 EMA20 压力位,提前止盈避免回撤" +} +``` + +### 3. partial_close - 部分平仓 + +用于分批止盈,既锁定部分利润,又保留追涨空间。 + +示例场景: +- 持仓 BTC 0.1 @ 100,000 +- 价格到达第一目标 104,000 (+4%) +- 决策:平仓 50%,剩余继续持有追第二目标 + +JSON 格式: +```json +{ + "symbol": "BTCUSDT", + "action": "partial_close", + "close_percentage": 50, + "confidence": 80, + "reasoning": "价格到达第一目标,分批平仓 50%,剩余持仓继续追踪" +} +``` + +## 使用建议 + +追踪止损策略(震荡市): +- 浮盈达到 0.8% → update_stop_loss 移到成本价 +- 浮盈达到 1.2% → update_stop_loss 移到 +0.5% +- 价格距离技术位 < 0.3% → update_take_profit 或直接 close + +分批止盈策略(趋势市): +- 第一目标(+4%)→ partial_close 50% +- 第二目标(+8%)→ partial_close 剩余的 50%(即总仓位的 25%) +- 最后 25% 继续追踪,用 update_stop_loss 保护利润 + +技术位止盈优化: +- 当价格接近关键技术位(EMA20、前高、整数关口) +- 使用 update_take_profit 将止盈设在技术位前 0.1-0.2% +- 避免在技术位遇阻回撤 + # 决策流程 1. 分析夏普比率: 当前策略是否有效?需要调整吗? 2. 判断市场状态: 震荡还是趋势?(多指标验证) 3. 选择对应策略: 策略A(震荡)还是策略B(趋势)? -4. 评估持仓: 趋势是否改变?是否该止盈/止损? -5. 识别技术位: 上方压力、下方支撑在哪里? +4. 评估持仓: 趋势是否改变?是否该止盈/止损?需要调整止损保护利润吗? +5. 识别技术位: 上方压力、下方支撑在哪里?是否需要提前止盈? 6. 寻找新机会: 有强信号吗?技术位明确吗? 7. 计算止盈止损: 技术位优先,还是固定百分比? 8. 输出决策: 思维链分析 + JSON diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 19b00865..f3e6cbc4 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "log" + "math" "nofx/decision" "nofx/logger" "nofx/market" @@ -607,6 +608,12 @@ func (at *AutoTrader) executeDecisionWithRecord(decision *decision.Decision, act return at.executeCloseLongWithRecord(decision, actionRecord) case "close_short": return at.executeCloseShortWithRecord(decision, actionRecord) + case "update_stop_loss": + return at.executeUpdateStopLossWithRecord(decision, actionRecord) + case "update_take_profit": + return at.executeUpdateTakeProfitWithRecord(decision, actionRecord) + case "partial_close": + return at.executePartialCloseWithRecord(decision, actionRecord) case "hold", "wait": // 无需执行,仅记录 return nil @@ -785,6 +792,189 @@ func (at *AutoTrader) executeCloseShortWithRecord(decision *decision.Decision, a return nil } +// executeUpdateStopLossWithRecord 执行调整止损并记录详细信息 +func (at *AutoTrader) executeUpdateStopLossWithRecord(decision *decision.Decision, actionRecord *logger.DecisionAction) error { + log.Printf(" 🎯 调整止损: %s → %.2f", decision.Symbol, decision.NewStopLoss) + + // 获取当前价格 + marketData, err := market.Get(decision.Symbol) + if err != nil { + return err + } + actionRecord.Price = marketData.CurrentPrice + + // 获取当前持仓 + positions, err := at.trader.GetPositions() + if err != nil { + return fmt.Errorf("获取持仓失败: %w", err) + } + + // 查找目标持仓 + var targetPosition map[string]interface{} + for _, pos := range positions { + symbol, _ := pos["symbol"].(string) + posAmt, _ := pos["positionAmt"].(float64) + if symbol == decision.Symbol && posAmt != 0 { + targetPosition = pos + break + } + } + + if targetPosition == nil { + return fmt.Errorf("持仓不存在: %s", decision.Symbol) + } + + // 获取持仓方向和数量 + side, _ := targetPosition["side"].(string) + positionSide := strings.ToUpper(side) + positionAmt, _ := targetPosition["positionAmt"].(float64) + + // 验证新止损价格合理性 + if positionSide == "LONG" && decision.NewStopLoss >= marketData.CurrentPrice { + return fmt.Errorf("多单止损必须低于当前价格 (当前: %.2f, 新止损: %.2f)", marketData.CurrentPrice, decision.NewStopLoss) + } + if positionSide == "SHORT" && decision.NewStopLoss <= marketData.CurrentPrice { + return fmt.Errorf("空单止损必须高于当前价格 (当前: %.2f, 新止损: %.2f)", marketData.CurrentPrice, decision.NewStopLoss) + } + + // 调用交易所 API 修改止损 + quantity := math.Abs(positionAmt) + err = at.trader.SetStopLoss(decision.Symbol, positionSide, quantity, decision.NewStopLoss) + if err != nil { + return fmt.Errorf("修改止损失败: %w", err) + } + + log.Printf(" ✓ 止损已调整: %.2f (当前价格: %.2f)", decision.NewStopLoss, marketData.CurrentPrice) + return nil +} + +// executeUpdateTakeProfitWithRecord 执行调整止盈并记录详细信息 +func (at *AutoTrader) executeUpdateTakeProfitWithRecord(decision *decision.Decision, actionRecord *logger.DecisionAction) error { + log.Printf(" 🎯 调整止盈: %s → %.2f", decision.Symbol, decision.NewTakeProfit) + + // 获取当前价格 + marketData, err := market.Get(decision.Symbol) + if err != nil { + return err + } + actionRecord.Price = marketData.CurrentPrice + + // 获取当前持仓 + positions, err := at.trader.GetPositions() + if err != nil { + return fmt.Errorf("获取持仓失败: %w", err) + } + + // 查找目标持仓 + var targetPosition map[string]interface{} + for _, pos := range positions { + symbol, _ := pos["symbol"].(string) + posAmt, _ := pos["positionAmt"].(float64) + if symbol == decision.Symbol && posAmt != 0 { + targetPosition = pos + break + } + } + + if targetPosition == nil { + return fmt.Errorf("持仓不存在: %s", decision.Symbol) + } + + // 获取持仓方向和数量 + side, _ := targetPosition["side"].(string) + positionSide := strings.ToUpper(side) + positionAmt, _ := targetPosition["positionAmt"].(float64) + + // 验证新止盈价格合理性 + if positionSide == "LONG" && decision.NewTakeProfit <= marketData.CurrentPrice { + return fmt.Errorf("多单止盈必须高于当前价格 (当前: %.2f, 新止盈: %.2f)", marketData.CurrentPrice, decision.NewTakeProfit) + } + if positionSide == "SHORT" && decision.NewTakeProfit >= marketData.CurrentPrice { + return fmt.Errorf("空单止盈必须低于当前价格 (当前: %.2f, 新止盈: %.2f)", marketData.CurrentPrice, decision.NewTakeProfit) + } + + // 调用交易所 API 修改止盈 + quantity := math.Abs(positionAmt) + err = at.trader.SetTakeProfit(decision.Symbol, positionSide, quantity, decision.NewTakeProfit) + if err != nil { + return fmt.Errorf("修改止盈失败: %w", err) + } + + log.Printf(" ✓ 止盈已调整: %.2f (当前价格: %.2f)", decision.NewTakeProfit, marketData.CurrentPrice) + return nil +} + +// executePartialCloseWithRecord 执行部分平仓并记录详细信息 +func (at *AutoTrader) executePartialCloseWithRecord(decision *decision.Decision, actionRecord *logger.DecisionAction) error { + log.Printf(" 📊 部分平仓: %s %.1f%%", decision.Symbol, decision.ClosePercentage) + + // 验证百分比范围 + if decision.ClosePercentage <= 0 || decision.ClosePercentage > 100 { + return fmt.Errorf("平仓百分比必须在 0-100 之间,当前: %.1f", decision.ClosePercentage) + } + + // 获取当前价格 + marketData, err := market.Get(decision.Symbol) + if err != nil { + return err + } + actionRecord.Price = marketData.CurrentPrice + + // 获取当前持仓 + positions, err := at.trader.GetPositions() + if err != nil { + return fmt.Errorf("获取持仓失败: %w", err) + } + + // 查找目标持仓 + var targetPosition map[string]interface{} + for _, pos := range positions { + symbol, _ := pos["symbol"].(string) + posAmt, _ := pos["positionAmt"].(float64) + if symbol == decision.Symbol && posAmt != 0 { + targetPosition = pos + break + } + } + + if targetPosition == nil { + return fmt.Errorf("持仓不存在: %s", decision.Symbol) + } + + // 获取持仓方向和数量 + side, _ := targetPosition["side"].(string) + positionSide := strings.ToUpper(side) + positionAmt, _ := targetPosition["positionAmt"].(float64) + + // 计算平仓数量 + totalQuantity := math.Abs(positionAmt) + closeQuantity := totalQuantity * (decision.ClosePercentage / 100.0) + actionRecord.Quantity = closeQuantity + + // 执行平仓 + var order map[string]interface{} + if positionSide == "LONG" { + order, err = at.trader.CloseLong(decision.Symbol, closeQuantity) + } else { + order, err = at.trader.CloseShort(decision.Symbol, closeQuantity) + } + + if err != nil { + return fmt.Errorf("部分平仓失败: %w", err) + } + + // 记录订单ID + if orderID, ok := order["orderId"].(int64); ok { + actionRecord.OrderID = orderID + } + + remainingQuantity := totalQuantity - closeQuantity + log.Printf(" ✓ 部分平仓成功: 平仓 %.4f (%.1f%%), 剩余 %.4f", + closeQuantity, decision.ClosePercentage, remainingQuantity) + + return nil +} + // GetID 获取trader ID func (at *AutoTrader) GetID() string { return at.id @@ -998,12 +1188,14 @@ func sortDecisionsByPriority(decisions []decision.Decision) []decision.Decision // 定义优先级 getActionPriority := func(action string) int { switch action { - case "close_long", "close_short": - return 1 // 最高优先级:先平仓 + case "close_long", "close_short", "partial_close": + return 1 // 最高优先级:先平仓(包括部分平仓) + case "update_stop_loss", "update_take_profit": + return 2 // 调整持仓止盈止损 case "open_long", "open_short": - return 2 // 次优先级:后开仓 + return 3 // 次优先级:后开仓 case "hold", "wait": - return 3 // 最低优先级:观望 + return 4 // 最低优先级:观望 default: return 999 // 未知动作放最后 }