mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-14 16:26:57 +08:00
fix: detect and record stop-loss/take-profit auto-close trades
Fixes #200 Previously, positions closed via stop-loss or take-profit were not recorded in decision logs, causing inaccurate performance metrics (win rate, profit factor, etc.) displayed in the frontend. This commit implements a position snapshot mechanism to detect auto-closed positions by comparing positions between trading cycles. Changes: - Add PositionSnapshot struct to track position state - Add detectAutoClosedPositions() to detect disappeared positions - Update position snapshots at cycle end (after AI execution) - Support auto_close_long/auto_close_short action types in logger - Ensure accurate performance analysis including auto-closed trades Key fix: - Snapshots are updated AFTER AI execution to avoid false positives - AI manual closes won't be mistakenly detected as auto-closes
This commit is contained in:
@@ -243,7 +243,7 @@ func (l *DecisionLogger) GetStatistics() (*Statistics, error) {
|
|||||||
switch action.Action {
|
switch action.Action {
|
||||||
case "open_long", "open_short":
|
case "open_long", "open_short":
|
||||||
stats.TotalOpenPositions++
|
stats.TotalOpenPositions++
|
||||||
case "close_long", "close_short":
|
case "close_long", "close_short", "auto_close_long", "auto_close_short":
|
||||||
stats.TotalClosePositions++
|
stats.TotalClosePositions++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,9 +348,9 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
|
|||||||
|
|
||||||
symbol := action.Symbol
|
symbol := action.Symbol
|
||||||
side := ""
|
side := ""
|
||||||
if action.Action == "open_long" || action.Action == "close_long" {
|
if action.Action == "open_long" || action.Action == "close_long" || action.Action == "auto_close_long" {
|
||||||
side = "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"
|
side = "short"
|
||||||
}
|
}
|
||||||
posKey := symbol + "_" + side
|
posKey := symbol + "_" + side
|
||||||
@@ -365,7 +365,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
|
|||||||
"quantity": action.Quantity,
|
"quantity": action.Quantity,
|
||||||
"leverage": action.Leverage,
|
"leverage": action.Leverage,
|
||||||
}
|
}
|
||||||
case "close_long", "close_short":
|
case "close_long", "close_short", "auto_close_long", "auto_close_short":
|
||||||
// 移除已平仓记录
|
// 移除已平仓记录
|
||||||
delete(openPositions, posKey)
|
delete(openPositions, posKey)
|
||||||
}
|
}
|
||||||
@@ -382,9 +382,9 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
|
|||||||
|
|
||||||
symbol := action.Symbol
|
symbol := action.Symbol
|
||||||
side := ""
|
side := ""
|
||||||
if action.Action == "open_long" || action.Action == "close_long" {
|
if action.Action == "open_long" || action.Action == "close_long" || action.Action == "auto_close_long" {
|
||||||
side = "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"
|
side = "short"
|
||||||
}
|
}
|
||||||
posKey := symbol + "_" + side // 使用symbol_side作为key,区分多空持仓
|
posKey := symbol + "_" + side // 使用symbol_side作为key,区分多空持仓
|
||||||
@@ -400,7 +400,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
|
|||||||
"leverage": action.Leverage,
|
"leverage": action.Leverage,
|
||||||
}
|
}
|
||||||
|
|
||||||
case "close_long", "close_short":
|
case "close_long", "close_short", "auto_close_long", "auto_close_short":
|
||||||
// 查找对应的开仓记录(可能来自预填充或当前窗口)
|
// 查找对应的开仓记录(可能来自预填充或当前窗口)
|
||||||
if openPos, exists := openPositions[posKey]; exists {
|
if openPos, exists := openPositions[posKey]; exists {
|
||||||
openPrice := openPos["openPrice"].(float64)
|
openPrice := openPos["openPrice"].(float64)
|
||||||
|
|||||||
@@ -75,6 +75,15 @@ type AutoTraderConfig struct {
|
|||||||
SystemPromptTemplate string // 系统提示词模板名称(如 "default", "aggressive")
|
SystemPromptTemplate string // 系统提示词模板名称(如 "default", "aggressive")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PositionSnapshot 持仓快照(用于检测自动平仓)
|
||||||
|
type PositionSnapshot struct {
|
||||||
|
Symbol string
|
||||||
|
Side string
|
||||||
|
Quantity float64
|
||||||
|
EntryPrice float64
|
||||||
|
Leverage int
|
||||||
|
}
|
||||||
|
|
||||||
// AutoTrader 自动交易器
|
// AutoTrader 自动交易器
|
||||||
type AutoTrader struct {
|
type AutoTrader struct {
|
||||||
id string // Trader唯一标识
|
id string // Trader唯一标识
|
||||||
@@ -98,6 +107,7 @@ type AutoTrader struct {
|
|||||||
startTime time.Time // 系统启动时间
|
startTime time.Time // 系统启动时间
|
||||||
callCount int // AI调用次数
|
callCount int // AI调用次数
|
||||||
positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒)
|
positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒)
|
||||||
|
lastPositions map[string]*PositionSnapshot // 上一个周期的持仓快照 (symbol_side -> snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAutoTrader 创建自动交易器
|
// NewAutoTrader 创建自动交易器
|
||||||
@@ -216,6 +226,7 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
|
|||||||
callCount: 0,
|
callCount: 0,
|
||||||
isRunning: false,
|
isRunning: false,
|
||||||
positionFirstSeenTime: make(map[string]int64),
|
positionFirstSeenTime: make(map[string]int64),
|
||||||
|
lastPositions: make(map[string]*PositionSnapshot),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,9 +268,9 @@ func (at *AutoTrader) Stop() {
|
|||||||
func (at *AutoTrader) runCycle() error {
|
func (at *AutoTrader) runCycle() error {
|
||||||
at.callCount++
|
at.callCount++
|
||||||
|
|
||||||
log.Printf("\n" + strings.Repeat("=", 70))
|
log.Print("\n" + strings.Repeat("=", 70))
|
||||||
log.Printf("⏰ %s - AI决策周期 #%d", time.Now().Format("2006-01-02 15:04:05"), at.callCount)
|
log.Printf("⏰ %s - AI决策周期 #%d", time.Now().Format("2006-01-02 15:04:05"), at.callCount)
|
||||||
log.Printf(strings.Repeat("=", 70))
|
log.Print(strings.Repeat("=", 70))
|
||||||
|
|
||||||
// 创建决策记录
|
// 创建决策记录
|
||||||
record := &logger.DecisionRecord{
|
record := &logger.DecisionRecord{
|
||||||
@@ -293,6 +304,15 @@ func (at *AutoTrader) runCycle() error {
|
|||||||
return fmt.Errorf("构建交易上下文失败: %w", err)
|
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{
|
record.AccountState = logger.AccountSnapshot{
|
||||||
TotalBalance: ctx.Account.TotalEquity,
|
TotalBalance: ctx.Account.TotalEquity,
|
||||||
@@ -346,19 +366,19 @@ func (at *AutoTrader) runCycle() error {
|
|||||||
// 打印系统提示词和AI思维链(即使有错误,也要输出以便调试)
|
// 打印系统提示词和AI思维链(即使有错误,也要输出以便调试)
|
||||||
if decision != nil {
|
if decision != nil {
|
||||||
if decision.SystemPrompt != "" {
|
if decision.SystemPrompt != "" {
|
||||||
log.Printf("\n" + strings.Repeat("=", 70))
|
log.Print("\n" + strings.Repeat("=", 70))
|
||||||
log.Printf("📋 系统提示词 [模板: %s] (错误情况)", at.systemPromptTemplate)
|
log.Printf("📋 系统提示词 [模板: %s] (错误情况)", at.systemPromptTemplate)
|
||||||
log.Println(strings.Repeat("=", 70))
|
log.Println(strings.Repeat("=", 70))
|
||||||
log.Println(decision.SystemPrompt)
|
log.Println(decision.SystemPrompt)
|
||||||
log.Printf(strings.Repeat("=", 70) + "\n")
|
log.Print(strings.Repeat("=", 70) + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if decision.CoTTrace != "" {
|
if decision.CoTTrace != "" {
|
||||||
log.Printf("\n" + strings.Repeat("-", 70))
|
log.Print("\n" + strings.Repeat("-", 70))
|
||||||
log.Println("💭 AI思维链分析(错误情况):")
|
log.Println("💭 AI思维链分析(错误情况):")
|
||||||
log.Println(strings.Repeat("-", 70))
|
log.Println(strings.Repeat("-", 70))
|
||||||
log.Println(decision.CoTTrace)
|
log.Println(decision.CoTTrace)
|
||||||
log.Printf(strings.Repeat("-", 70) + "\n")
|
log.Print(strings.Repeat("-", 70) + "\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +446,47 @@ func (at *AutoTrader) runCycle() error {
|
|||||||
record.Decisions = append(record.Decisions, actionRecord)
|
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 {
|
if err := at.decisionLogger.LogDecision(record); err != nil {
|
||||||
log.Printf("⚠ 保存决策记录失败: %v", err)
|
log.Printf("⚠ 保存决策记录失败: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1081,3 +1141,74 @@ func normalizeSymbol(symbol string) string {
|
|||||||
|
|
||||||
return symbol
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user