Files
nofx/logger/decision_logger.go
T
2025-11-28 21:34:27 +08:00

769 lines
25 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"
"math"
"os"
"path/filepath"
"time"
)
// DecisionRecord 决策记录
type DecisionRecord struct {
Timestamp time.Time `json:"timestamp"` // 决策时间
CycleNumber int `json:"cycle_number"` // 周期编号
SystemPrompt string `json:"system_prompt"` // 系统提示词(发送给AI的系统prompt)
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"` // 错误信息(如果有)
// AIRequestDurationMs 记录 AI API 调用耗时(毫秒),方便评估调用性能
AIRequestDurationMs int64 `json:"ai_request_duration_ms,omitempty"`
}
// 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"`
InitialBalance float64 `json:"initial_balance"` // 记录当时的初始余额基准
}
// 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, update_stop_loss, update_take_profit, partial_close
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"` // 错误信息
}
// IDecisionLogger 决策日志记录器接口
type IDecisionLogger interface {
// LogDecision 记录决策
LogDecision(record *DecisionRecord) error
// GetLatestRecords 获取最近N条记录(按时间正序:从旧到新)
GetLatestRecords(n int) ([]*DecisionRecord, error)
// GetRecordByDate 获取指定日期的所有记录
GetRecordByDate(date time.Time) ([]*DecisionRecord, error)
// CleanOldRecords 清理N天前的旧记录
CleanOldRecords(days int) error
// GetStatistics 获取统计信息
GetStatistics() (*Statistics, error)
// AnalyzePerformance 分析最近N个周期的交易表现
AnalyzePerformance(lookbackCycles int) (*PerformanceAnalysis, error)
// SetCycleNumber 允许恢复内部计数(用于回测恢复)
SetCycleNumber(n int)
}
// DecisionLogger 决策日志记录器
type DecisionLogger struct {
logDir string
cycleNumber int
}
// NewDecisionLogger 创建决策日志记录器
func NewDecisionLogger(logDir string) IDecisionLogger {
if logDir == "" {
logDir = "decision_logs"
}
// 确保日志目录存在(使用安全权限:只有所有者可访问)
if err := os.MkdirAll(logDir, 0700); err != nil {
fmt.Printf("⚠ 创建日志目录失败: %v\n", err)
}
// 强制设置目录权限(即使目录已存在)- 确保安全
if err := os.Chmod(logDir, 0700); err != nil {
fmt.Printf("⚠ 设置日志目录权限失败: %v\n", err)
}
return &DecisionLogger{
logDir: logDir,
cycleNumber: 0,
}
}
// SetCycleNumber 允许外部恢复内部的周期计数(用于回测恢复)。
func (l *DecisionLogger) SetCycleNumber(n int) {
if n > 0 {
l.cycleNumber = n
}
}
// LogDecision 记录决策
func (l *DecisionLogger) LogDecision(record *DecisionRecord) error {
l.cycleNumber++
record.CycleNumber = l.cycleNumber
if record.Timestamp.IsZero() {
record.Timestamp = time.Now().UTC()
} else {
record.Timestamp = record.Timestamp.UTC()
}
// 生成文件名: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, 0600); 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", "auto_close_long", "auto_close_short":
stats.TotalClosePositions++
// 🔧 BUG FIXpartial_close 不計入 TotalClosePositions,避免重複計數
// case "partial_close": // 不計數,因為只有完全平倉才算一次
// update_stop_loss 和 update_take_profit 不計入統計
}
}
}
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
Quantity float64 `json:"quantity"` // 仓位数量
Leverage int `json:"leverage"` // 杠杆倍数
OpenPrice float64 `json:"open_price"` // 开仓价
ClosePrice float64 `json:"close_price"` // 平仓价
PositionValue float64 `json:"position_value"` // 仓位价值(quantity × openPrice
MarginUsed float64 `json:"margin_used"` // 保证金使用(positionValue / leverage
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 -> {side, openPrice, openTime, quantity, leverage}
openPositions := make(map[string]map[string]interface{})
// 为了避免开仓记录在窗口外导致匹配失败,需要先从所有历史记录中找出未平仓的持仓
// 获取更多历史记录来构建完整的持仓状态(使用更大的窗口)
allRecords, err := l.GetLatestRecords(lookbackCycles * 3) // 扩大3倍窗口
if err == nil && len(allRecords) > len(records) {
// 先从扩大的窗口中收集所有开仓记录
for _, record := range allRecords {
for _, action := range record.Decisions {
if !action.Success {
continue
}
symbol := action.Symbol
side := ""
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" || action.Action == "auto_close_short" {
side = "short"
}
// partial_close 需要根據持倉判斷方向
if action.Action == "partial_close" && side == "" {
for key, pos := range openPositions {
if posSymbol, _ := pos["side"].(string); key == symbol+"_"+posSymbol {
side = posSymbol
break
}
}
}
posKey := symbol + "_" + side
switch action.Action {
case "open_long", "open_short":
// 记录开仓
openPositions[posKey] = map[string]interface{}{
"side": side,
"openPrice": action.Price,
"openTime": action.Timestamp,
"quantity": action.Quantity,
"leverage": action.Leverage,
}
case "close_long", "close_short", "auto_close_long", "auto_close_short":
// 移除已平仓记录
delete(openPositions, posKey)
// partial_close 不處理,保留持倉記錄
}
}
}
}
// 遍历分析窗口内的记录,生成交易结果
for _, record := range records {
for _, action := range record.Decisions {
if !action.Success {
continue
}
symbol := action.Symbol
side := ""
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" || action.Action == "auto_close_short" {
side = "short"
}
// partial_close 需要根據持倉判斷方向
if action.Action == "partial_close" {
// 從 openPositions 中查找持倉方向
for key, pos := range openPositions {
if posSymbol, _ := pos["side"].(string); key == symbol+"_"+posSymbol {
side = posSymbol
break
}
}
}
posKey := symbol + "_" + side // 使用symbol_side作为key,区分多空持仓
switch action.Action {
case "open_long", "open_short":
// 更新开仓记录(可能已经在预填充时记录过了)
openPositions[posKey] = map[string]interface{}{
"side": side,
"openPrice": action.Price,
"openTime": action.Timestamp,
"quantity": action.Quantity,
"leverage": action.Leverage,
"remainingQuantity": action.Quantity, // 🔧 BUG FIX:追蹤剩餘數量
"accumulatedPnL": 0.0, // 🔧 BUG FIX:累積部分平倉盈虧
"partialCloseCount": 0, // 🔧 BUG FIX:部分平倉次數
"partialCloseVolume": 0.0, // 🔧 BUG FIX:部分平倉總量
}
case "close_long", "close_short", "partial_close", "auto_close_long", "auto_close_short":
// 查找对应的开仓记录(可能来自预填充或当前窗口)
if openPos, exists := openPositions[posKey]; exists {
openPrice := openPos["openPrice"].(float64)
openTime := openPos["openTime"].(time.Time)
side := openPos["side"].(string)
quantity := openPos["quantity"].(float64)
leverage := openPos["leverage"].(int)
// 🔧 BUG FIX:取得追蹤字段(若不存在則初始化)
remainingQty, _ := openPos["remainingQuantity"].(float64)
if remainingQty == 0 {
remainingQty = quantity // 兼容舊數據(沒有 remainingQuantity 字段)
}
accumulatedPnL, _ := openPos["accumulatedPnL"].(float64)
partialCloseCount, _ := openPos["partialCloseCount"].(int)
partialCloseVolume, _ := openPos["partialCloseVolume"].(float64)
// 对于 partial_close,使用实际平仓数量;否则使用剩余仓位数量
actualQuantity := remainingQty
if action.Action == "partial_close" {
actualQuantity = action.Quantity
}
// 计算本次平仓的盈亏(USDT
var pnl float64
if side == "long" {
pnl = actualQuantity * (action.Price - openPrice)
} else {
pnl = actualQuantity * (openPrice - action.Price)
}
// 🔧 BUG FIX:處理 partial_close 聚合邏輯
if action.Action == "partial_close" {
// 累積盈虧和數量
accumulatedPnL += pnl
remainingQty -= actualQuantity
partialCloseCount++
partialCloseVolume += actualQuantity
// 更新 openPositions(保留持倉記錄,但更新追蹤數據)
openPos["remainingQuantity"] = remainingQty
openPos["accumulatedPnL"] = accumulatedPnL
openPos["partialCloseCount"] = partialCloseCount
openPos["partialCloseVolume"] = partialCloseVolume
// 判斷是否已完全平倉
if remainingQty <= 0.0001 { // 使用小閾值避免浮點誤差
// ✅ 完全平倉:記錄為一筆完整交易
positionValue := quantity * openPrice
marginUsed := positionValue / float64(leverage)
pnlPct := 0.0
if marginUsed > 0 {
pnlPct = (accumulatedPnL / marginUsed) * 100
}
outcome := TradeOutcome{
Symbol: symbol,
Side: side,
Quantity: quantity, // 使用原始總量
Leverage: leverage,
OpenPrice: openPrice,
ClosePrice: action.Price, // 最後一次平倉價格
PositionValue: positionValue,
MarginUsed: marginUsed,
PnL: accumulatedPnL, // 🔧 使用累積盈虧
PnLPct: pnlPct,
Duration: action.Timestamp.Sub(openTime).String(),
OpenTime: openTime,
CloseTime: action.Timestamp,
}
analysis.RecentTrades = append(analysis.RecentTrades, outcome)
analysis.TotalTrades++ // 🔧 只在完全平倉時計數
// 分类交易
if accumulatedPnL > 0 {
analysis.WinningTrades++
analysis.AvgWin += accumulatedPnL
} else if accumulatedPnL < 0 {
analysis.LosingTrades++
analysis.AvgLoss += accumulatedPnL
}
// 更新币种统计
if _, exists := analysis.SymbolStats[symbol]; !exists {
analysis.SymbolStats[symbol] = &SymbolPerformance{
Symbol: symbol,
}
}
stats := analysis.SymbolStats[symbol]
stats.TotalTrades++
stats.TotalPnL += accumulatedPnL
if accumulatedPnL > 0 {
stats.WinningTrades++
} else if accumulatedPnL < 0 {
stats.LosingTrades++
}
// 刪除持倉記錄
delete(openPositions, posKey)
}
// ⚠️ 否則不做任何操作(等待後續 partial_close 或 full close
} else {
// 🔧 完全平倉(close_long/close_short/auto_close
// 如果之前有部分平倉,需要加上累積的 PnL
totalPnL := accumulatedPnL + pnl
positionValue := quantity * openPrice
marginUsed := positionValue / float64(leverage)
pnlPct := 0.0
if marginUsed > 0 {
pnlPct = (totalPnL / marginUsed) * 100
}
outcome := TradeOutcome{
Symbol: symbol,
Side: side,
Quantity: quantity, // 使用原始總量
Leverage: leverage,
OpenPrice: openPrice,
ClosePrice: action.Price,
PositionValue: positionValue,
MarginUsed: marginUsed,
PnL: totalPnL, // 🔧 包含之前部分平倉的 PnL
PnLPct: pnlPct,
Duration: action.Timestamp.Sub(openTime).String(),
OpenTime: openTime,
CloseTime: action.Timestamp,
}
analysis.RecentTrades = append(analysis.RecentTrades, outcome)
analysis.TotalTrades++
// 分类交易
if totalPnL > 0 {
analysis.WinningTrades++
analysis.AvgWin += totalPnL
} else if totalPnL < 0 {
analysis.LosingTrades++
analysis.AvgLoss += totalPnL
}
// 更新币种统计
if _, exists := analysis.SymbolStats[symbol]; !exists {
analysis.SymbolStats[symbol] = &SymbolPerformance{
Symbol: symbol,
}
}
stats := analysis.SymbolStats[symbol]
stats.TotalTrades++
stats.TotalPnL += totalPnL
if totalPnL > 0 {
stats.WinningTrades++
} else if totalPnL < 0 {
stats.LosingTrades++
}
// 刪除持倉記錄
delete(openPositions, posKey)
}
}
}
}
}
// 计算统计指标
if analysis.TotalTrades > 0 {
analysis.WinRate = (float64(analysis.WinningTrades) / float64(analysis.TotalTrades)) * 100
// 计算总盈利和总亏损
totalWinAmount := analysis.AvgWin // 当前是累加的总和
totalLossAmount := analysis.AvgLoss // 当前是累加的总和(负数)
if analysis.WinningTrades > 0 {
analysis.AvgWin /= float64(analysis.WinningTrades)
}
if analysis.LosingTrades > 0 {
analysis.AvgLoss /= float64(analysis.LosingTrades)
}
// Profit Factor = 总盈利 / 总亏损(绝对值)
// 注意:totalLossAmount 是负数,所以取负号得到绝对值
if totalLossAmount != 0 {
analysis.ProfitFactor = totalWinAmount / (-totalLossAmount)
} else if totalWinAmount > 0 {
// 只有盈利没有亏损的情况,设置为一个很大的值表示完美策略
analysis.ProfitFactor = 999.0
}
}
// 计算各币种胜率和平均盈亏
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
}
// 提取每个周期的账户净值
// 注意:TotalBalance字段实际存储的是TotalEquity(账户总净值)
// TotalUnrealizedProfit字段实际存储的是TotalPnL(相对初始余额的盈亏)
var equities []float64
for _, record := range records {
// 直接使用TotalBalance,因为它已经是完整的账户净值
equity := record.AccountState.TotalBalance
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 := math.Sqrt(variance)
// 避免除以零
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
}