diff --git a/decision/engine.go b/decision/engine.go index df48d534..fa3e5233 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"` // 用于 update_stop_loss + NewTakeProfit float64 `json:"new_take_profit,omitempty"` // 用于 update_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"` @@ -504,12 +513,15 @@ func findMatchingBracket(s string, start int) int { func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error { // 验证action validActions := map[string]bool{ - "open_long": true, - "open_short": true, - "close_long": true, - "close_short": true, - "hold": true, - "wait": true, + "open_long": true, + "open_short": true, + "close_long": true, + "close_short": true, + "update_stop_loss": true, + "update_take_profit": true, + "partial_close": true, + "hold": true, + "wait": true, } if !validActions[d.Action] { @@ -589,5 +601,26 @@ func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoi } } + // 动态调整止损验证 + if d.Action == "update_stop_loss" { + if d.NewStopLoss <= 0 { + return fmt.Errorf("新止损价格必须大于0: %.2f", d.NewStopLoss) + } + } + + // 动态调整止盈验证 + if d.Action == "update_take_profit" { + if d.NewTakeProfit <= 0 { + return fmt.Errorf("新止盈价格必须大于0: %.2f", d.NewTakeProfit) + } + } + + // 部分平仓验证 + if d.Action == "partial_close" { + if d.ClosePercentage <= 0 || d.ClosePercentage > 100 { + return fmt.Errorf("平仓百分比必须在0-100之间: %.1f", d.ClosePercentage) + } + } + return nil } diff --git a/logger/decision_logger.go b/logger/decision_logger.go index efa5ab74..746f58ad 100644 --- a/logger/decision_logger.go +++ b/logger/decision_logger.go @@ -409,18 +409,24 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna quantity := openPos["quantity"].(float64) leverage := openPos["leverage"].(int) + // 对于 partial_close,使用实际平仓数量;否则使用完整仓位数量 + actualQuantity := quantity + if action.Action == "partial_close" { + actualQuantity = action.Quantity + } + // 计算实际盈亏(USDT) - // 合约交易 PnL 计算:quantity × 价格差 + // 合约交易 PnL 计算:actualQuantity × 价格差 // 注意:杠杆不影响绝对盈亏,只影响保证金需求 var pnl float64 if side == "long" { - pnl = quantity * (action.Price - openPrice) + pnl = actualQuantity * (action.Price - openPrice) } else { - pnl = quantity * (openPrice - action.Price) + pnl = actualQuantity * (openPrice - action.Price) } // 计算盈亏百分比(相对保证金) - positionValue := quantity * openPrice + positionValue := actualQuantity * openPrice marginUsed := positionValue / float64(leverage) pnlPct := 0.0 if marginUsed > 0 { @@ -431,7 +437,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna outcome := TradeOutcome{ Symbol: symbol, Side: side, - Quantity: quantity, + Quantity: actualQuantity, Leverage: leverage, OpenPrice: openPrice, ClosePrice: action.Price, diff --git a/trader/aster_trader.go b/trader/aster_trader.go index 15128e13..2a393430 100644 --- a/trader/aster_trader.go +++ b/trader/aster_trader.go @@ -1005,6 +1005,61 @@ func (t *AsterTrader) CancelAllOrders(symbol string) error { return err } +// CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置) +func (t *AsterTrader) CancelStopOrders(symbol string) error { + // 获取该币种的所有未完成订单 + params := map[string]interface{}{ + "symbol": symbol, + } + + body, err := t.request("GET", "/fapi/v3/openOrders", params) + if err != nil { + return fmt.Errorf("获取未完成订单失败: %w", err) + } + + var orders []map[string]interface{} + if err := json.Unmarshal(body, &orders); err != nil { + return fmt.Errorf("解析订单数据失败: %w", err) + } + + // 过滤出止盈止损单并取消 + canceledCount := 0 + for _, order := range orders { + orderType, _ := order["type"].(string) + + // 只取消止损和止盈订单 + if orderType == "STOP_MARKET" || + orderType == "TAKE_PROFIT_MARKET" || + orderType == "STOP" || + orderType == "TAKE_PROFIT" { + + orderID, _ := order["orderId"].(float64) + cancelParams := map[string]interface{}{ + "symbol": symbol, + "orderId": int64(orderID), + } + + _, err := t.request("DELETE", "/fapi/v3/order", cancelParams) + if err != nil { + log.Printf(" ⚠ 取消订单 %d 失败: %v", int64(orderID), err) + continue + } + + canceledCount++ + log.Printf(" ✓ 已取消 %s 的止盈/止损单 (订单ID: %d, 类型: %s)", + symbol, int64(orderID), orderType) + } + } + + if canceledCount == 0 { + log.Printf(" ℹ %s 没有止盈/止损单需要取消", symbol) + } else { + log.Printf(" ✓ 已取消 %s 的 %d 个止盈/止损单", symbol, canceledCount) + } + + return nil +} + // FormatQuantity 格式化数量(实现Trader接口) func (t *AsterTrader) FormatQuantity(symbol string, quantity float64) (string, error) { formatted, err := t.formatQuantity(symbol, quantity) diff --git a/trader/auto_trader.go b/trader/auto_trader.go index c489fcc3..998eea8b 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" @@ -593,6 +594,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 @@ -771,6 +778,201 @@ 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) + } + + // 取消旧的止损单(避免多个止损单共存) + if err := at.trader.CancelStopOrders(decision.Symbol); err != nil { + log.Printf(" ⚠ 取消旧止损单失败: %v", err) + // 不中断执行,继续设置新止损 + } + + // 调用交易所 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) + } + + // 取消旧的止盈单(避免多个止盈单共存) + if err := at.trader.CancelStopOrders(decision.Symbol); err != nil { + log.Printf(" ⚠ 取消旧止盈单失败: %v", err) + // 不中断执行,继续设置新止盈 + } + + // 调用交易所 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 @@ -984,12 +1186,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 // 未知动作放最后 } diff --git a/trader/binance_futures.go b/trader/binance_futures.go index 354415a0..abaf5c9a 100644 --- a/trader/binance_futures.go +++ b/trader/binance_futures.go @@ -425,6 +425,53 @@ func (t *FuturesTrader) CancelAllOrders(symbol string) error { return nil } +// CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置) +func (t *FuturesTrader) CancelStopOrders(symbol string) error { + // 获取该币种的所有未完成订单 + orders, err := t.client.NewListOpenOrdersService(). + Symbol(symbol). + Do(context.Background()) + + if err != nil { + return fmt.Errorf("获取未完成订单失败: %w", err) + } + + // 过滤出止盈止损单并取消 + canceledCount := 0 + for _, order := range orders { + orderType := order.Type + + // 只取消止损和止盈订单 + if orderType == futures.OrderTypeStopMarket || + orderType == futures.OrderTypeTakeProfitMarket || + orderType == futures.OrderTypeStop || + orderType == futures.OrderTypeTakeProfit { + + _, err := t.client.NewCancelOrderService(). + Symbol(symbol). + OrderID(order.OrderID). + Do(context.Background()) + + if err != nil { + log.Printf(" ⚠ 取消订单 %d 失败: %v", order.OrderID, err) + continue + } + + canceledCount++ + log.Printf(" ✓ 已取消 %s 的止盈/止损单 (订单ID: %d, 类型: %s)", + symbol, order.OrderID, orderType) + } + } + + if canceledCount == 0 { + log.Printf(" ℹ %s 没有止盈/止损单需要取消", symbol) + } else { + log.Printf(" ✓ 已取消 %s 的 %d 个止盈/止损单", symbol, canceledCount) + } + + return nil +} + // GetMarketPrice 获取市场价格 func (t *FuturesTrader) GetMarketPrice(symbol string) (float64, error) { prices, err := t.client.NewListPricesService().Symbol(symbol).Do(context.Background()) diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index 5ae48a16..6e85940f 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -511,6 +511,40 @@ func (t *HyperliquidTrader) CancelAllOrders(symbol string) error { return nil } +// CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置) +func (t *HyperliquidTrader) CancelStopOrders(symbol string) error { + coin := convertSymbolToHyperliquid(symbol) + + // 获取所有挂单 + openOrders, err := t.exchange.Info().OpenOrders(t.ctx, t.walletAddr) + if err != nil { + return fmt.Errorf("获取挂单失败: %w", err) + } + + // 注意:Hyperliquid SDK 的 OpenOrder 结构不暴露 trigger 字段 + // 因此暂时取消该币种的所有挂单(包括止盈止损单) + // 这是安全的,因为在设置新的止盈止损之前,应该清理所有旧订单 + canceledCount := 0 + for _, order := range openOrders { + if order.Coin == coin { + _, err := t.exchange.Cancel(t.ctx, coin, order.Oid) + if err != nil { + log.Printf(" ⚠ 取消订单失败 (oid=%d): %v", order.Oid, err) + continue + } + canceledCount++ + } + } + + if canceledCount == 0 { + log.Printf(" ℹ %s 没有挂单需要取消", symbol) + } else { + log.Printf(" ✓ 已取消 %s 的 %d 个挂单(包括止盈/止损单)", symbol, canceledCount) + } + + return nil +} + // GetMarketPrice 获取市场价格 func (t *HyperliquidTrader) GetMarketPrice(symbol string) (float64, error) { coin := convertSymbolToHyperliquid(symbol) diff --git a/trader/interface.go b/trader/interface.go index 18d75ee7..edf70d32 100644 --- a/trader/interface.go +++ b/trader/interface.go @@ -39,6 +39,9 @@ type Trader interface { // CancelAllOrders 取消该币种的所有挂单 CancelAllOrders(symbol string) error + // CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置) + CancelStopOrders(symbol string) error + // FormatQuantity 格式化数量到正确的精度 FormatQuantity(symbol string, quantity float64) (string, error) }