Files
nofx/logger/decision_logger.go
tinkle-community ceaedca253 Refactor: Improve AI decision system and Sharpe ratio calculation
Major improvements:
- Use period-level Sharpe ratio (range -2 to +2) instead of annualized
- Save full user prompt in decision logs for debugging
- Format complete market data (3m + 4h candles) for AI analysis
- Prevent position stacking with duplicate position checks
- Update Sharpe ratio interpretation thresholds
Market data enhancements:
- Display full technical indicators in user prompt
- Include 3-minute and 4-hour timeframe data
- Add OI (Open Interest) change and funding rate signals
Risk control:
- Block opening duplicate positions (same symbol + direction)
- Suggest close action first before opening new position
- Prevent margin usage from exceeding limits
UI improvements:
- Update multi-language translations
- Refine AI learning dashboard display
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
2025-10-29 04:44:17 +08:00

540 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package logger
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"time"
)
// DecisionRecord 决策记录
type DecisionRecord struct {
Timestamp time.Time `json:"timestamp"` // 决策时间
CycleNumber int `json:"cycle_number"` // 周期编号
InputPrompt string `json:"input_prompt"` // 发送给AI的输入prompt
CoTTrace string `json:"cot_trace"` // AI思维链输出
DecisionJSON string `json:"decision_json"` // 决策JSON
AccountState AccountSnapshot `json:"account_state"` // 账户状态快照
Positions []PositionSnapshot `json:"positions"` // 持仓快照
CandidateCoins []string `json:"candidate_coins"` // 候选币种列表
Decisions []DecisionAction `json:"decisions"` // 执行的决策
ExecutionLog []string `json:"execution_log"` // 执行日志
Success bool `json:"success"` // 是否成功
ErrorMessage string `json:"error_message"` // 错误信息(如果有)
}
// AccountSnapshot 账户状态快照
type AccountSnapshot struct {
TotalBalance float64 `json:"total_balance"`
AvailableBalance float64 `json:"available_balance"`
TotalUnrealizedProfit float64 `json:"total_unrealized_profit"`
PositionCount int `json:"position_count"`
MarginUsedPct float64 `json:"margin_used_pct"`
}
// PositionSnapshot 持仓快照
type PositionSnapshot struct {
Symbol string `json:"symbol"`
Side string `json:"side"`
PositionAmt float64 `json:"position_amt"`
EntryPrice float64 `json:"entry_price"`
MarkPrice float64 `json:"mark_price"`
UnrealizedProfit float64 `json:"unrealized_profit"`
Leverage float64 `json:"leverage"`
LiquidationPrice float64 `json:"liquidation_price"`
}
// DecisionAction 决策动作
type DecisionAction struct {
Action string `json:"action"` // open_long, open_short, close_long, close_short
Symbol string `json:"symbol"` // 币种
Quantity float64 `json:"quantity"` // 数量
Leverage int `json:"leverage"` // 杠杆(开仓时)
Price float64 `json:"price"` // 执行价格
OrderID int64 `json:"order_id"` // 订单ID
Timestamp time.Time `json:"timestamp"` // 执行时间
Success bool `json:"success"` // 是否成功
Error string `json:"error"` // 错误信息
}
// DecisionLogger 决策日志记录器
type DecisionLogger struct {
logDir string
cycleNumber int
}
// NewDecisionLogger 创建决策日志记录器
func NewDecisionLogger(logDir string) *DecisionLogger {
if logDir == "" {
logDir = "decision_logs"
}
// 确保日志目录存在
if err := os.MkdirAll(logDir, 0755); err != nil {
fmt.Printf("⚠ 创建日志目录失败: %v\n", err)
}
return &DecisionLogger{
logDir: logDir,
cycleNumber: 0,
}
}
// LogDecision 记录决策
func (l *DecisionLogger) LogDecision(record *DecisionRecord) error {
l.cycleNumber++
record.CycleNumber = l.cycleNumber
record.Timestamp = time.Now()
// 生成文件名decision_YYYYMMDD_HHMMSS_cycleN.json
filename := fmt.Sprintf("decision_%s_cycle%d.json",
record.Timestamp.Format("20060102_150405"),
record.CycleNumber)
filepath := filepath.Join(l.logDir, filename)
// 序列化为JSON带缩进方便阅读
data, err := json.MarshalIndent(record, "", " ")
if err != nil {
return fmt.Errorf("序列化决策记录失败: %w", err)
}
// 写入文件
if err := ioutil.WriteFile(filepath, data, 0644); err != nil {
return fmt.Errorf("写入决策记录失败: %w", err)
}
fmt.Printf("📝 决策记录已保存: %s\n", filename)
return nil
}
// GetLatestRecords 获取最近N条记录按时间正序从旧到新
func (l *DecisionLogger) GetLatestRecords(n int) ([]*DecisionRecord, error) {
files, err := ioutil.ReadDir(l.logDir)
if err != nil {
return nil, fmt.Errorf("读取日志目录失败: %w", err)
}
// 先按修改时间倒序收集(最新的在前)
var records []*DecisionRecord
count := 0
for i := len(files) - 1; i >= 0 && count < n; i-- {
file := files[i]
if file.IsDir() {
continue
}
filepath := filepath.Join(l.logDir, file.Name())
data, err := ioutil.ReadFile(filepath)
if err != nil {
continue
}
var record DecisionRecord
if err := json.Unmarshal(data, &record); err != nil {
continue
}
records = append(records, &record)
count++
}
// 反转数组,让时间从旧到新排列(用于图表显示)
for i, j := 0, len(records)-1; i < j; i, j = i+1, j-1 {
records[i], records[j] = records[j], records[i]
}
return records, nil
}
// GetRecordByDate 获取指定日期的所有记录
func (l *DecisionLogger) GetRecordByDate(date time.Time) ([]*DecisionRecord, error) {
dateStr := date.Format("20060102")
pattern := filepath.Join(l.logDir, fmt.Sprintf("decision_%s_*.json", dateStr))
files, err := filepath.Glob(pattern)
if err != nil {
return nil, fmt.Errorf("查找日志文件失败: %w", err)
}
var records []*DecisionRecord
for _, filepath := range files {
data, err := ioutil.ReadFile(filepath)
if err != nil {
continue
}
var record DecisionRecord
if err := json.Unmarshal(data, &record); err != nil {
continue
}
records = append(records, &record)
}
return records, nil
}
// CleanOldRecords 清理N天前的旧记录
func (l *DecisionLogger) CleanOldRecords(days int) error {
cutoffTime := time.Now().AddDate(0, 0, -days)
files, err := ioutil.ReadDir(l.logDir)
if err != nil {
return fmt.Errorf("读取日志目录失败: %w", err)
}
removedCount := 0
for _, file := range files {
if file.IsDir() {
continue
}
if file.ModTime().Before(cutoffTime) {
filepath := filepath.Join(l.logDir, file.Name())
if err := os.Remove(filepath); err != nil {
fmt.Printf("⚠ 删除旧记录失败 %s: %v\n", file.Name(), err)
continue
}
removedCount++
}
}
if removedCount > 0 {
fmt.Printf("🗑️ 已清理 %d 条旧记录(%d天前\n", removedCount, days)
}
return nil
}
// GetStatistics 获取统计信息
func (l *DecisionLogger) GetStatistics() (*Statistics, error) {
files, err := ioutil.ReadDir(l.logDir)
if err != nil {
return nil, fmt.Errorf("读取日志目录失败: %w", err)
}
stats := &Statistics{}
for _, file := range files {
if file.IsDir() {
continue
}
filepath := filepath.Join(l.logDir, file.Name())
data, err := ioutil.ReadFile(filepath)
if err != nil {
continue
}
var record DecisionRecord
if err := json.Unmarshal(data, &record); err != nil {
continue
}
stats.TotalCycles++
for _, action := range record.Decisions {
if action.Success {
switch action.Action {
case "open_long", "open_short":
stats.TotalOpenPositions++
case "close_long", "close_short":
stats.TotalClosePositions++
}
}
}
if record.Success {
stats.SuccessfulCycles++
} else {
stats.FailedCycles++
}
}
return stats, nil
}
// Statistics 统计信息
type Statistics struct {
TotalCycles int `json:"total_cycles"`
SuccessfulCycles int `json:"successful_cycles"`
FailedCycles int `json:"failed_cycles"`
TotalOpenPositions int `json:"total_open_positions"`
TotalClosePositions int `json:"total_close_positions"`
}
// TradeOutcome 单笔交易结果
type TradeOutcome struct {
Symbol string `json:"symbol"` // 币种
Side string `json:"side"` // long/short
OpenPrice float64 `json:"open_price"` // 开仓价
ClosePrice float64 `json:"close_price"` // 平仓价
PnL float64 `json:"pn_l"` // 盈亏USDT
PnLPct float64 `json:"pn_l_pct"` // 盈亏百分比
Duration string `json:"duration"` // 持仓时长
OpenTime time.Time `json:"open_time"` // 开仓时间
CloseTime time.Time `json:"close_time"` // 平仓时间
WasStopLoss bool `json:"was_stop_loss"` // 是否止损
}
// PerformanceAnalysis 交易表现分析
type PerformanceAnalysis struct {
TotalTrades int `json:"total_trades"` // 总交易数
WinningTrades int `json:"winning_trades"` // 盈利交易数
LosingTrades int `json:"losing_trades"` // 亏损交易数
WinRate float64 `json:"win_rate"` // 胜率
AvgWin float64 `json:"avg_win"` // 平均盈利
AvgLoss float64 `json:"avg_loss"` // 平均亏损
ProfitFactor float64 `json:"profit_factor"` // 盈亏比
SharpeRatio float64 `json:"sharpe_ratio"` // 夏普比率(风险调整后收益)
RecentTrades []TradeOutcome `json:"recent_trades"` // 最近N笔交易
SymbolStats map[string]*SymbolPerformance `json:"symbol_stats"` // 各币种表现
BestSymbol string `json:"best_symbol"` // 表现最好的币种
WorstSymbol string `json:"worst_symbol"` // 表现最差的币种
}
// SymbolPerformance 币种表现统计
type SymbolPerformance struct {
Symbol string `json:"symbol"` // 币种
TotalTrades int `json:"total_trades"` // 交易次数
WinningTrades int `json:"winning_trades"` // 盈利次数
LosingTrades int `json:"losing_trades"` // 亏损次数
WinRate float64 `json:"win_rate"` // 胜率
TotalPnL float64 `json:"total_pn_l"` // 总盈亏
AvgPnL float64 `json:"avg_pn_l"` // 平均盈亏
}
// AnalyzePerformance 分析最近N个周期的交易表现
func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAnalysis, error) {
records, err := l.GetLatestRecords(lookbackCycles)
if err != nil {
return nil, fmt.Errorf("读取历史记录失败: %w", err)
}
if len(records) == 0 {
return &PerformanceAnalysis{
RecentTrades: []TradeOutcome{},
SymbolStats: make(map[string]*SymbolPerformance),
}, nil
}
analysis := &PerformanceAnalysis{
RecentTrades: []TradeOutcome{},
SymbolStats: make(map[string]*SymbolPerformance),
}
// 追踪持仓状态symbol -> {side, openPrice, openTime}
openPositions := make(map[string]map[string]interface{})
// 遍历所有记录
for _, record := range records {
for _, action := range record.Decisions {
if !action.Success {
continue
}
symbol := action.Symbol
posKey := symbol // 使用symbol作为key假设同一时间一个币种只有一个方向的仓位
switch action.Action {
case "open_long", "open_short":
// 记录开仓
openPositions[posKey] = map[string]interface{}{
"side": action.Action[5:], // "long" or "short"
"openPrice": action.Price,
"openTime": action.Timestamp,
}
case "close_long", "close_short":
// 查找对应的开仓记录
if openPos, exists := openPositions[posKey]; exists {
openPrice := openPos["openPrice"].(float64)
openTime := openPos["openTime"].(time.Time)
side := openPos["side"].(string)
// 计算盈亏
pnl := 0.0
pnlPct := 0.0
if side == "long" {
pnlPct = ((action.Price - openPrice) / openPrice) * 100
} else {
pnlPct = ((openPrice - action.Price) / openPrice) * 100
}
pnl = pnlPct // 简化:用百分比代表盈亏
// 记录交易结果
outcome := TradeOutcome{
Symbol: symbol,
Side: side,
OpenPrice: openPrice,
ClosePrice: action.Price,
PnL: pnl,
PnLPct: pnlPct,
Duration: action.Timestamp.Sub(openTime).String(),
OpenTime: openTime,
CloseTime: action.Timestamp,
}
analysis.RecentTrades = append(analysis.RecentTrades, outcome)
analysis.TotalTrades++
if pnl > 0 {
analysis.WinningTrades++
analysis.AvgWin += pnl
} else {
analysis.LosingTrades++
analysis.AvgLoss += pnl
}
// 更新币种统计
if _, exists := analysis.SymbolStats[symbol]; !exists {
analysis.SymbolStats[symbol] = &SymbolPerformance{
Symbol: symbol,
}
}
stats := analysis.SymbolStats[symbol]
stats.TotalTrades++
stats.TotalPnL += pnl
if pnl > 0 {
stats.WinningTrades++
} else {
stats.LosingTrades++
}
// 移除已平仓记录
delete(openPositions, posKey)
}
}
}
}
// 计算统计指标
if analysis.TotalTrades > 0 {
analysis.WinRate = (float64(analysis.WinningTrades) / float64(analysis.TotalTrades)) * 100
if analysis.WinningTrades > 0 {
analysis.AvgWin /= float64(analysis.WinningTrades)
}
if analysis.LosingTrades > 0 {
analysis.AvgLoss /= float64(analysis.LosingTrades)
}
if analysis.AvgLoss != 0 {
analysis.ProfitFactor = analysis.AvgWin / (-analysis.AvgLoss)
}
}
// 计算各币种胜率和平均盈亏
bestPnL := -999999.0
worstPnL := 999999.0
for symbol, stats := range analysis.SymbolStats {
if stats.TotalTrades > 0 {
stats.WinRate = (float64(stats.WinningTrades) / float64(stats.TotalTrades)) * 100
stats.AvgPnL = stats.TotalPnL / float64(stats.TotalTrades)
if stats.TotalPnL > bestPnL {
bestPnL = stats.TotalPnL
analysis.BestSymbol = symbol
}
if stats.TotalPnL < worstPnL {
worstPnL = stats.TotalPnL
analysis.WorstSymbol = symbol
}
}
}
// 只保留最近的交易(倒序:最新的在前)
if len(analysis.RecentTrades) > 10 {
// 反转数组,让最新的在前
for i, j := 0, len(analysis.RecentTrades)-1; i < j; i, j = i+1, j-1 {
analysis.RecentTrades[i], analysis.RecentTrades[j] = analysis.RecentTrades[j], analysis.RecentTrades[i]
}
analysis.RecentTrades = analysis.RecentTrades[:10]
} else if len(analysis.RecentTrades) > 0 {
// 反转数组
for i, j := 0, len(analysis.RecentTrades)-1; i < j; i, j = i+1, j-1 {
analysis.RecentTrades[i], analysis.RecentTrades[j] = analysis.RecentTrades[j], analysis.RecentTrades[i]
}
}
// 计算夏普比率需要至少2个数据点
analysis.SharpeRatio = l.calculateSharpeRatio(records)
return analysis, nil
}
// calculateSharpeRatio 计算夏普比率
// 基于账户净值的变化计算风险调整后收益
func (l *DecisionLogger) calculateSharpeRatio(records []*DecisionRecord) float64 {
if len(records) < 2 {
return 0.0
}
// 提取每个周期的账户净值
var equities []float64
for _, record := range records {
equity := record.AccountState.TotalBalance + record.AccountState.TotalUnrealizedProfit
if equity > 0 {
equities = append(equities, equity)
}
}
if len(equities) < 2 {
return 0.0
}
// 计算周期收益率period returns
var returns []float64
for i := 1; i < len(equities); i++ {
if equities[i-1] > 0 {
periodReturn := (equities[i] - equities[i-1]) / equities[i-1]
returns = append(returns, periodReturn)
}
}
if len(returns) == 0 {
return 0.0
}
// 计算平均收益率
sumReturns := 0.0
for _, r := range returns {
sumReturns += r
}
meanReturn := sumReturns / float64(len(returns))
// 计算收益率标准差
sumSquaredDiff := 0.0
for _, r := range returns {
diff := r - meanReturn
sumSquaredDiff += diff * diff
}
variance := sumSquaredDiff / float64(len(returns))
stdDev := 0.0
if variance > 0 {
stdDev = 1.0
// 简单的平方根计算(牛顿迭代法)
for i := 0; i < 10; i++ {
stdDev = (stdDev + variance/stdDev) / 2
}
}
// 避免除以零
if stdDev == 0 {
if meanReturn > 0 {
return 999.0 // 无波动的正收益
} else if meanReturn < 0 {
return -999.0 // 无波动的负收益
}
return 0.0
}
// 计算夏普比率假设无风险利率为0
// 注:直接返回周期级别的夏普比率(非年化),正常范围 -2 到 +2
sharpeRatio := meanReturn / stdDev
return sharpeRatio
}