mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-16 01:06:59 +08:00
Feature: Add position holding duration to AI decision context
Track and display how long each position has been held to help AI make better timing decisions. **Implementation**: - Added UpdateTime field to PositionInfo struct (decision/engine.go:26) - Added positionFirstSeenTime map to AutoTrader for tracking (trader/auto_trader.go:60) - Record opening time when position is created successfully: - executeOpenLongWithRecord: Records timestamp for long positions (trader/auto_trader.go:540-541) - executeOpenShortWithRecord: Records timestamp for short positions (trader/auto_trader.go:593-594) - Fallback tracking in buildTradingContext for program restart scenarios (trader/auto_trader.go:386-392) - Auto-cleanup closed positions from tracking map (trader/auto_trader.go:409-414) - Display duration in user prompt with smart formatting: - Under 60 min: "持仓时长25分钟" - Over 60 min: "持仓时长2小时15分钟" **Example Output**: ``` 1. TAOUSDT LONG | 入场价435.5300 当前价433.1900 | 盈亏-0.54% | 杠杆20x | 保证金25 | 强平价418.1528 | 持仓时长2小时15分钟 ``` **Benefits**: - AI can see how long positions have been held - Helps enforce minimum holding period (30-60 min) from system prompt - Simple implementation with minimal overhead - Auto-cleanup prevents memory leaks Co-Authored-By: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
@@ -23,6 +23,7 @@ type PositionInfo struct {
|
|||||||
UnrealizedPnLPct float64 `json:"unrealized_pnl_pct"`
|
UnrealizedPnLPct float64 `json:"unrealized_pnl_pct"`
|
||||||
LiquidationPrice float64 `json:"liquidation_price"`
|
LiquidationPrice float64 `json:"liquidation_price"`
|
||||||
MarginUsed float64 `json:"margin_used"`
|
MarginUsed float64 `json:"margin_used"`
|
||||||
|
UpdateTime int64 `json:"update_time"` // 持仓更新时间戳(毫秒)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountInfo 账户信息
|
// AccountInfo 账户信息
|
||||||
@@ -335,10 +336,24 @@ func buildUserPrompt(ctx *Context) string {
|
|||||||
if len(ctx.Positions) > 0 {
|
if len(ctx.Positions) > 0 {
|
||||||
sb.WriteString("## 当前持仓\n")
|
sb.WriteString("## 当前持仓\n")
|
||||||
for i, pos := range ctx.Positions {
|
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),
|
i+1, pos.Symbol, strings.ToUpper(pos.Side),
|
||||||
pos.EntryPrice, pos.MarkPrice, pos.UnrealizedPnLPct,
|
pos.EntryPrice, pos.MarkPrice, pos.UnrealizedPnLPct,
|
||||||
pos.Leverage, pos.MarginUsed, pos.LiquidationPrice))
|
pos.Leverage, pos.MarginUsed, pos.LiquidationPrice, holdingDuration))
|
||||||
|
|
||||||
// 使用FormatMarketData输出完整市场数据
|
// 使用FormatMarketData输出完整市场数据
|
||||||
if marketData, ok := ctx.MarketDataMap[pos.Symbol]; ok {
|
if marketData, ok := ctx.MarketDataMap[pos.Symbol]; ok {
|
||||||
|
|||||||
@@ -44,19 +44,20 @@ type AutoTraderConfig struct {
|
|||||||
|
|
||||||
// AutoTrader 自动交易器
|
// AutoTrader 自动交易器
|
||||||
type AutoTrader struct {
|
type AutoTrader struct {
|
||||||
id string // Trader唯一标识
|
id string // Trader唯一标识
|
||||||
name string // Trader显示名称
|
name string // Trader显示名称
|
||||||
aiModel string // AI模型名称
|
aiModel string // AI模型名称
|
||||||
config AutoTraderConfig
|
config AutoTraderConfig
|
||||||
trader *FuturesTrader
|
trader *FuturesTrader
|
||||||
decisionLogger *logger.DecisionLogger // 决策日志记录器
|
decisionLogger *logger.DecisionLogger // 决策日志记录器
|
||||||
initialBalance float64
|
initialBalance float64
|
||||||
dailyPnL float64
|
dailyPnL float64
|
||||||
lastResetTime time.Time
|
lastResetTime time.Time
|
||||||
stopUntil time.Time
|
stopUntil time.Time
|
||||||
isRunning bool
|
isRunning bool
|
||||||
startTime time.Time // 系统启动时间
|
startTime time.Time // 系统启动时间
|
||||||
callCount int // AI调用次数
|
callCount int // AI调用次数
|
||||||
|
positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAutoTrader 创建自动交易器
|
// NewAutoTrader 创建自动交易器
|
||||||
@@ -103,17 +104,18 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
|
|||||||
decisionLogger := logger.NewDecisionLogger(logDir)
|
decisionLogger := logger.NewDecisionLogger(logDir)
|
||||||
|
|
||||||
return &AutoTrader{
|
return &AutoTrader{
|
||||||
id: config.ID,
|
id: config.ID,
|
||||||
name: config.Name,
|
name: config.Name,
|
||||||
aiModel: config.AIModel,
|
aiModel: config.AIModel,
|
||||||
config: config,
|
config: config,
|
||||||
trader: trader,
|
trader: trader,
|
||||||
decisionLogger: decisionLogger,
|
decisionLogger: decisionLogger,
|
||||||
initialBalance: config.InitialBalance,
|
initialBalance: config.InitialBalance,
|
||||||
lastResetTime: time.Now(),
|
lastResetTime: time.Now(),
|
||||||
startTime: time.Now(),
|
startTime: time.Now(),
|
||||||
callCount: 0,
|
callCount: 0,
|
||||||
isRunning: false,
|
isRunning: false,
|
||||||
|
positionFirstSeenTime: make(map[string]int64),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,6 +351,9 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
|
|||||||
var positionInfos []decision.PositionInfo
|
var positionInfos []decision.PositionInfo
|
||||||
totalMarginUsed := 0.0
|
totalMarginUsed := 0.0
|
||||||
|
|
||||||
|
// 当前持仓的key集合(用于清理已平仓的记录)
|
||||||
|
currentPositionKeys := make(map[string]bool)
|
||||||
|
|
||||||
for _, pos := range positions {
|
for _, pos := range positions {
|
||||||
symbol := pos["symbol"].(string)
|
symbol := pos["symbol"].(string)
|
||||||
side := pos["side"].(string)
|
side := pos["side"].(string)
|
||||||
@@ -377,6 +382,15 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
|
|||||||
marginUsed := (quantity * markPrice) / float64(leverage)
|
marginUsed := (quantity * markPrice) / float64(leverage)
|
||||||
totalMarginUsed += marginUsed
|
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{
|
positionInfos = append(positionInfos, decision.PositionInfo{
|
||||||
Symbol: symbol,
|
Symbol: symbol,
|
||||||
Side: side,
|
Side: side,
|
||||||
@@ -388,9 +402,17 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
|
|||||||
UnrealizedPnLPct: pnlPct,
|
UnrealizedPnLPct: pnlPct,
|
||||||
LiquidationPrice: liquidationPrice,
|
LiquidationPrice: liquidationPrice,
|
||||||
MarginUsed: marginUsed,
|
MarginUsed: marginUsed,
|
||||||
|
UpdateTime: updateTime,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理已平仓的持仓记录
|
||||||
|
for key := range at.positionFirstSeenTime {
|
||||||
|
if !currentPositionKeys[key] {
|
||||||
|
delete(at.positionFirstSeenTime, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 获取合并的候选币种池(AI500 + OI Top,去重)
|
// 3. 获取合并的候选币种池(AI500 + OI Top,去重)
|
||||||
// 无论有没有持仓,都分析相同数量的币种(让AI看到所有好机会)
|
// 无论有没有持仓,都分析相同数量的币种(让AI看到所有好机会)
|
||||||
// AI会根据保证金使用率和现有持仓情况,自己决定是否要换仓
|
// AI会根据保证金使用率和现有持仓情况,自己决定是否要换仓
|
||||||
@@ -514,6 +536,10 @@ func (at *AutoTrader) executeOpenLongWithRecord(decision *decision.Decision, act
|
|||||||
|
|
||||||
log.Printf(" ✓ 开仓成功,订单ID: %v, 数量: %.4f", order["orderId"], quantity)
|
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 {
|
if err := at.trader.SetStopLoss(decision.Symbol, "LONG", quantity, decision.StopLoss); err != nil {
|
||||||
log.Printf(" ⚠ 设置止损失败: %v", err)
|
log.Printf(" ⚠ 设置止损失败: %v", err)
|
||||||
@@ -563,6 +589,10 @@ func (at *AutoTrader) executeOpenShortWithRecord(decision *decision.Decision, ac
|
|||||||
|
|
||||||
log.Printf(" ✓ 开仓成功,订单ID: %v, 数量: %.4f", order["orderId"], quantity)
|
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 {
|
if err := at.trader.SetStopLoss(decision.Symbol, "SHORT", quantity, decision.StopLoss); err != nil {
|
||||||
log.Printf(" ⚠ 设置止损失败: %v", err)
|
log.Printf(" ⚠ 设置止损失败: %v", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user