feat: Add position snapshot to track auto-closed positions (PR #231)

- Add position_snapshots table to track all closed positions
- Record both manual and auto-closed positions
- Fix data integrity issue where auto-closed positions were not recorded
- Improve trade history tracking and analysis
- Merge with existing partial_close functionality
This commit is contained in:
ZhouYongyou
2025-11-03 22:15:12 +08:00
2 changed files with 139 additions and 8 deletions

View File

@@ -243,7 +243,7 @@ func (l *DecisionLogger) GetStatistics() (*Statistics, error) {
switch action.Action {
case "open_long", "open_short":
stats.TotalOpenPositions++
case "close_long", "close_short", "partial_close":
case "close_long", "close_short", "partial_close", "auto_close_long", "auto_close_short":
stats.TotalClosePositions++
// update_stop_loss 和 update_take_profit 不計入統計
}
@@ -349,9 +349,9 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
symbol := action.Symbol
side := ""
if action.Action == "open_long" || action.Action == "close_long" || action.Action == "partial_close" {
if action.Action == "open_long" || action.Action == "close_long" || action.Action == "partial_close" || action.Action == "auto_close_long" {
side = "long"
} else if action.Action == "open_short" || action.Action == "close_short" {
} else if action.Action == "open_short" || action.Action == "close_short" || action.Action == "auto_close_short" {
side = "short"
}
@@ -377,7 +377,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
"quantity": action.Quantity,
"leverage": action.Leverage,
}
case "close_long", "close_short":
case "close_long", "close_short", "auto_close_long", "auto_close_short":
// 移除已平仓记录
delete(openPositions, posKey)
// partial_close 不處理,保留持倉記錄
@@ -395,9 +395,9 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
symbol := action.Symbol
side := ""
if action.Action == "open_long" || action.Action == "close_long" || action.Action == "partial_close" {
if action.Action == "open_long" || action.Action == "close_long" || action.Action == "partial_close" || action.Action == "auto_close_long" {
side = "long"
} else if action.Action == "open_short" || action.Action == "close_short" {
} else if action.Action == "open_short" || action.Action == "close_short" || action.Action == "auto_close_short" {
side = "short"
}
@@ -425,7 +425,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
"leverage": action.Leverage,
}
case "close_long", "close_short", "partial_close":
case "close_long", "close_short", "partial_close", "auto_close_long", "auto_close_short":
// 查找对应的开仓记录(可能来自预填充或当前窗口)
if openPos, exists := openPositions[posKey]; exists {
openPrice := openPos["openPrice"].(float64)

View File

@@ -76,6 +76,15 @@ type AutoTraderConfig struct {
SystemPromptTemplate string // 系统提示词模板名称(如 "default", "aggressive"
}
// PositionSnapshot 持仓快照(用于检测自动平仓)
type PositionSnapshot struct {
Symbol string
Side string
Quantity float64
EntryPrice float64
Leverage int
}
// AutoTrader 自动交易器
type AutoTrader struct {
id string // Trader唯一标识
@@ -99,6 +108,7 @@ type AutoTrader struct {
startTime time.Time // 系统启动时间
callCount int // AI调用次数
positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒)
lastPositions map[string]*PositionSnapshot // 上一个周期的持仓快照 (symbol_side -> snapshot)
}
// NewAutoTrader 创建自动交易器
@@ -218,6 +228,7 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
callCount: 0,
isRunning: false,
positionFirstSeenTime: make(map[string]int64),
lastPositions: make(map[string]*PositionSnapshot),
}, nil
}
@@ -295,6 +306,15 @@ func (at *AutoTrader) runCycle() error {
return fmt.Errorf("构建交易上下文失败: %w", err)
}
// 3.1 检测自动平仓(止损/止盈触发)
autoClosedActions := at.detectAutoClosedPositions(ctx.Positions)
for _, action := range autoClosedActions {
log.Printf("[AUTO-CLOSE] 检测到自动平仓: %s %s (价格: %.4f)", action.Symbol, action.Action, action.Price)
record.Decisions = append(record.Decisions, action)
record.ExecutionLog = append(record.ExecutionLog,
fmt.Sprintf("[AUTO-CLOSE] 自动平仓: %s %s (止损/止盈触发)", action.Symbol, action.Action))
}
// 保存账户状态快照
record.AccountState = logger.AccountSnapshot{
TotalBalance: ctx.Account.TotalEquity,
@@ -428,7 +448,47 @@ func (at *AutoTrader) runCycle() error {
record.Decisions = append(record.Decisions, actionRecord)
}
// 9. 保存决策记录
// 9. 更新持仓快照(用于下一周期检测自动平仓)
// 注意:需要重新获取当前持仓,因为 AI 可能在本周期执行了平仓操作
// ctx.Positions 是周期开始时的持仓,不反映本周期的变化
currentPositionsAfterExecution, err := at.trader.GetPositions()
if err != nil {
log.Printf("⚠ 更新持仓快照失败,无法获取当前持仓: %v", err)
// 即使失败也不影响主流程,下一周期可能会有误报但不会崩溃
} else {
// 将原始持仓数据转换为快照格式
snapshots := make([]PositionSnapshot, 0, len(currentPositionsAfterExecution))
for _, pos := range currentPositionsAfterExecution {
symbol := pos["symbol"].(string)
side := pos["side"].(string)
entryPrice := pos["entryPrice"].(float64)
quantity := pos["positionAmt"].(float64)
if quantity < 0 {
quantity = -quantity // 空仓数量为负,转为正数
}
leverage := 10
if lev, ok := pos["leverage"].(float64); ok {
leverage = int(lev)
}
snapshots = append(snapshots, PositionSnapshot{
Symbol: symbol,
Side: side,
EntryPrice: entryPrice,
Quantity: quantity,
Leverage: leverage,
})
}
at.lastPositions = make(map[string]*PositionSnapshot)
for _, snap := range snapshots {
posKey := snap.Symbol + "_" + snap.Side
// 创建副本避免指针问题
snapshot := snap
at.lastPositions[posKey] = &snapshot
}
}
// 10. 保存决策记录
if err := at.decisionLogger.LogDecision(record); err != nil {
log.Printf("⚠ 保存决策记录失败: %v", err)
}
@@ -1306,3 +1366,74 @@ func normalizeSymbol(symbol string) string {
return symbol
}
// detectAutoClosedPositions 检测自动平仓的持仓(止损/止盈触发)
func (at *AutoTrader) detectAutoClosedPositions(currentPositions []decision.PositionInfo) []logger.DecisionAction {
var autoClosedActions []logger.DecisionAction
// 创建当前持仓的map便于查找
currentPosMap := make(map[string]bool)
for _, pos := range currentPositions {
posKey := pos.Symbol + "_" + pos.Side
currentPosMap[posKey] = true
}
// 检查上一个周期的持仓,哪些现在消失了
for posKey, lastPos := range at.lastPositions {
if !currentPosMap[posKey] {
// 这个持仓消失了,说明被自动平仓了(止损/止盈触发)
// 获取当前价格作为平仓价格的近似值
marketData, err := market.Get(lastPos.Symbol)
closePrice := 0.0
if err == nil {
closePrice = marketData.CurrentPrice
} else {
// 如果无法获取当前价格使用入场价作为fallback
closePrice = lastPos.EntryPrice
}
// 确定是平多仓还是平空仓
action := "auto_close_long"
if lastPos.Side == "short" {
action = "auto_close_short"
}
// 创建自动平仓记录
autoClosedAction := logger.DecisionAction{
Action: action,
Symbol: lastPos.Symbol,
Quantity: lastPos.Quantity,
Leverage: lastPos.Leverage,
Price: closePrice,
OrderID: 0, // 自动平仓没有特定的订单ID
Timestamp: time.Now(),
Success: true,
Error: "",
}
autoClosedActions = append(autoClosedActions, autoClosedAction)
log.Printf("[AUTO-CLOSE] 检测到自动平仓: %s %s @ %.4f (可能由止损/止盈触发)",
lastPos.Symbol, action, closePrice)
}
}
return autoClosedActions
}
// updatePositionSnapshots 更新持仓快照(用于下一周期检测自动平仓)
func (at *AutoTrader) updatePositionSnapshots(currentPositions []decision.PositionInfo) {
// 清空旧的快照
at.lastPositions = make(map[string]*PositionSnapshot)
// 记录当前所有持仓
for _, pos := range currentPositions {
posKey := pos.Symbol + "_" + pos.Side
at.lastPositions[posKey] = &PositionSnapshot{
Symbol: pos.Symbol,
Side: pos.Side,
Quantity: pos.Quantity,
EntryPrice: pos.EntryPrice,
Leverage: pos.Leverage,
}
}
}