From d4a5c0534dfb52b35b0d766cf0d6de5d2ec7860b Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Wed, 29 Oct 2025 14:20:40 +0800 Subject: [PATCH] =?UTF-8?q?Feature:=20Add=20position=20holding=20duration?= =?UTF-8?q?=20to=20AI=20decision=20context=20Track=20and=20display=20how?= =?UTF-8?q?=20long=20each=20position=20has=20been=20held=20to=20help=20AI?= =?UTF-8?q?=20make=20better=20timing=20decisions.=20**Implementation**:=20?= =?UTF-8?q?-=20Added=20UpdateTime=20field=20to=20PositionInfo=20struct=20(?= =?UTF-8?q?decision/engine.go:26)=20-=20Added=20positionFirstSeenTime=20ma?= =?UTF-8?q?p=20to=20AutoTrader=20for=20tracking=20(trader/auto=5Ftrader.go?= =?UTF-8?q?:60)=20-=20Record=20opening=20time=20when=20position=20is=20cre?= =?UTF-8?q?ated=20successfully:=20=20=20-=20executeOpenLongWithRecord:=20R?= =?UTF-8?q?ecords=20timestamp=20for=20long=20positions=20(trader/auto=5Ftr?= =?UTF-8?q?ader.go:540-541)=20=20=20-=20executeOpenShortWithRecord:=20Reco?= =?UTF-8?q?rds=20timestamp=20for=20short=20positions=20(trader/auto=5Ftrad?= =?UTF-8?q?er.go:593-594)=20-=20Fallback=20tracking=20in=20buildTradingCon?= =?UTF-8?q?text=20for=20program=20restart=20scenarios=20(trader/auto=5Ftra?= =?UTF-8?q?der.go:386-392)=20-=20Auto-cleanup=20closed=20positions=20from?= =?UTF-8?q?=20tracking=20map=20(trader/auto=5Ftrader.go:409-414)=20-=20Dis?= =?UTF-8?q?play=20duration=20in=20user=20prompt=20with=20smart=20formattin?= =?UTF-8?q?g:=20=20=20-=20Under=2060=20min:=20"=E6=8C=81=E4=BB=93=E6=97=B6?= =?UTF-8?q?=E9=95=BF25=E5=88=86=E9=92=9F"=20=20=20-=20Over=2060=20min:=20"?= =?UTF-8?q?=E6=8C=81=E4=BB=93=E6=97=B6=E9=95=BF2=E5=B0=8F=E6=97=B615?= =?UTF-8?q?=E5=88=86=E9=92=9F"=20**Example=20Output**:=20```=201.=20TAOUSD?= =?UTF-8?q?T=20LONG=20|=20=E5=85=A5=E5=9C=BA=E4=BB=B7435.5300=20=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E4=BB=B7433.1900=20|=20=E7=9B=88=E4=BA=8F-0.54%=20|?= =?UTF-8?q?=20=E6=9D=A0=E6=9D=8620x=20|=20=E4=BF=9D=E8=AF=81=E9=87=9125=20?= =?UTF-8?q?|=20=E5=BC=BA=E5=B9=B3=E4=BB=B7418.1528=20|=20=E6=8C=81?= =?UTF-8?q?=E4=BB=93=E6=97=B6=E9=95=BF2=E5=B0=8F=E6=97=B615=E5=88=86?= =?UTF-8?q?=E9=92=9F=20```=20**Benefits**:=20-=20AI=20can=20see=20how=20lo?= =?UTF-8?q?ng=20positions=20have=20been=20held=20-=20Helps=20enforce=20min?= =?UTF-8?q?imum=20holding=20period=20(30-60=20min)=20from=20system=20promp?= =?UTF-8?q?t=20-=20Simple=20implementation=20with=20minimal=20overhead=20-?= =?UTF-8?q?=20Auto-cleanup=20prevents=20memory=20leaks=20Co-Authored-By:?= =?UTF-8?q?=20tinkle-community=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decision/engine.go | 19 +++++++++-- trader/auto_trader.go | 78 ++++++++++++++++++++++++++++++------------- 2 files changed, 71 insertions(+), 26 deletions(-) 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)