diff --git a/decision/engine.go b/decision/engine.go index c3d0dace..0379d766 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -23,6 +23,7 @@ type PositionInfo struct { UnrealizedPnLPct float64 `json:"unrealized_pnl_pct"` LiquidationPrice float64 `json:"liquidation_price"` MarginUsed float64 `json:"margin_used"` + UpdateTime int64 `json:"update_time"` // 持仓更新时间戳(毫秒) } // AccountInfo 账户信息 @@ -335,10 +336,24 @@ func buildUserPrompt(ctx *Context) string { if len(ctx.Positions) > 0 { sb.WriteString("## 当前持仓\n") for i, pos := range ctx.Positions { - sb.WriteString(fmt.Sprintf("%d. %s %s | 入场价%.4f 当前价%.4f | 盈亏%+.2f%% | 杠杆%dx | 保证金%.0f | 强平价%.4f\n\n", + // 计算持仓时长 + holdingDuration := "" + if pos.UpdateTime > 0 { + durationMs := time.Now().UnixMilli() - pos.UpdateTime + durationMin := durationMs / (1000 * 60) // 转换为分钟 + if durationMin < 60 { + holdingDuration = fmt.Sprintf(" | 持仓时长%d分钟", durationMin) + } else { + durationHour := durationMin / 60 + durationMinRemainder := durationMin % 60 + holdingDuration = fmt.Sprintf(" | 持仓时长%d小时%d分钟", durationHour, durationMinRemainder) + } + } + + sb.WriteString(fmt.Sprintf("%d. %s %s | 入场价%.4f 当前价%.4f | 盈亏%+.2f%% | 杠杆%dx | 保证金%.0f | 强平价%.4f%s\n\n", i+1, pos.Symbol, strings.ToUpper(pos.Side), pos.EntryPrice, pos.MarkPrice, pos.UnrealizedPnLPct, - pos.Leverage, pos.MarginUsed, pos.LiquidationPrice)) + pos.Leverage, pos.MarginUsed, pos.LiquidationPrice, holdingDuration)) // 使用FormatMarketData输出完整市场数据 if marketData, ok := ctx.MarketDataMap[pos.Symbol]; ok { diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 7b8c4b8a..34f49475 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -44,19 +44,20 @@ type AutoTraderConfig struct { // AutoTrader 自动交易器 type AutoTrader struct { - id string // Trader唯一标识 - name string // Trader显示名称 - aiModel string // AI模型名称 - config AutoTraderConfig - trader *FuturesTrader - decisionLogger *logger.DecisionLogger // 决策日志记录器 - initialBalance float64 - dailyPnL float64 - lastResetTime time.Time - stopUntil time.Time - isRunning bool - startTime time.Time // 系统启动时间 - callCount int // AI调用次数 + id string // Trader唯一标识 + name string // Trader显示名称 + aiModel string // AI模型名称 + config AutoTraderConfig + trader *FuturesTrader + decisionLogger *logger.DecisionLogger // 决策日志记录器 + initialBalance float64 + dailyPnL float64 + lastResetTime time.Time + stopUntil time.Time + isRunning bool + startTime time.Time // 系统启动时间 + callCount int // AI调用次数 + positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒) } // NewAutoTrader 创建自动交易器 @@ -103,17 +104,18 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { decisionLogger := logger.NewDecisionLogger(logDir) return &AutoTrader{ - id: config.ID, - name: config.Name, - aiModel: config.AIModel, - config: config, - trader: trader, - decisionLogger: decisionLogger, - initialBalance: config.InitialBalance, - lastResetTime: time.Now(), - startTime: time.Now(), - callCount: 0, - isRunning: false, + id: config.ID, + name: config.Name, + aiModel: config.AIModel, + config: config, + trader: trader, + decisionLogger: decisionLogger, + initialBalance: config.InitialBalance, + lastResetTime: time.Now(), + startTime: time.Now(), + callCount: 0, + isRunning: false, + positionFirstSeenTime: make(map[string]int64), }, nil } @@ -349,6 +351,9 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { var positionInfos []decision.PositionInfo totalMarginUsed := 0.0 + // 当前持仓的key集合(用于清理已平仓的记录) + currentPositionKeys := make(map[string]bool) + for _, pos := range positions { symbol := pos["symbol"].(string) side := pos["side"].(string) @@ -377,6 +382,15 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { marginUsed := (quantity * markPrice) / float64(leverage) totalMarginUsed += marginUsed + // 跟踪持仓首次出现时间 + posKey := symbol + "_" + side + currentPositionKeys[posKey] = true + if _, exists := at.positionFirstSeenTime[posKey]; !exists { + // 新持仓,记录当前时间 + at.positionFirstSeenTime[posKey] = time.Now().UnixMilli() + } + updateTime := at.positionFirstSeenTime[posKey] + positionInfos = append(positionInfos, decision.PositionInfo{ Symbol: symbol, Side: side, @@ -388,9 +402,17 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { UnrealizedPnLPct: pnlPct, LiquidationPrice: liquidationPrice, MarginUsed: marginUsed, + UpdateTime: updateTime, }) } + // 清理已平仓的持仓记录 + for key := range at.positionFirstSeenTime { + if !currentPositionKeys[key] { + delete(at.positionFirstSeenTime, key) + } + } + // 3. 获取合并的候选币种池(AI500 + OI Top,去重) // 无论有没有持仓,都分析相同数量的币种(让AI看到所有好机会) // AI会根据保证金使用率和现有持仓情况,自己决定是否要换仓 @@ -514,6 +536,10 @@ func (at *AutoTrader) executeOpenLongWithRecord(decision *decision.Decision, act log.Printf(" ✓ 开仓成功,订单ID: %v, 数量: %.4f", order["orderId"], quantity) + // 记录开仓时间 + posKey := decision.Symbol + "_long" + at.positionFirstSeenTime[posKey] = time.Now().UnixMilli() + // 设置止损止盈 if err := at.trader.SetStopLoss(decision.Symbol, "LONG", quantity, decision.StopLoss); err != nil { log.Printf(" ⚠ 设置止损失败: %v", err) @@ -563,6 +589,10 @@ func (at *AutoTrader) executeOpenShortWithRecord(decision *decision.Decision, ac log.Printf(" ✓ 开仓成功,订单ID: %v, 数量: %.4f", order["orderId"], quantity) + // 记录开仓时间 + posKey := decision.Symbol + "_short" + at.positionFirstSeenTime[posKey] = time.Now().UnixMilli() + // 设置止损止盈 if err := at.trader.SetStopLoss(decision.Symbol, "SHORT", quantity, decision.StopLoss); err != nil { log.Printf(" ⚠ 设置止损失败: %v", err)