fix: backtest module PostgreSQL compatibility and bug fixes

- Fix PostgreSQL placeholder conversion (? to $1, $2...) in all SQL queries
- Fix int4 overflow for timestamp columns (ALTER to BIGINT)
- Fix notional calculation bug in position Close() using proportional entry
- Fix potential panic in DecisionTimestamp with bounds check
- Fix nil pointer dereference in sliceUpTo with defensive checks
- Fix race condition in releaseLock using sync.Once
- Fix UnrealizedPnLPct always 0 in convertPositions
- Improve Sharpe ratio calculation with proper negative return handling
This commit is contained in:
tinkle-community
2026-01-09 01:48:02 +08:00
parent 2f88205231
commit 705aa641b0
9 changed files with 186 additions and 83 deletions

View File

@@ -91,8 +91,13 @@ func maxDrawdown(points []EquityPoint, state *BacktestState) float64 {
return maxDD
}
// sharpeRatio calculates the Sharpe ratio from equity points.
// Uses sample standard deviation (n-1) and annualizes assuming ~252 trading days.
// Returns math.NaN() for edge cases (insufficient data, zero variance).
func sharpeRatio(points []EquityPoint) float64 {
if len(points) < 2 {
// Need at least 10 data points for meaningful Sharpe calculation
const minDataPoints = 10
if len(points) < minDataPoints {
return 0
}
@@ -108,34 +113,42 @@ func sharpeRatio(points []EquityPoint) float64 {
returns = append(returns, ret)
prev = curr
}
if len(returns) == 0 {
if len(returns) < minDataPoints-1 {
return 0
}
// Calculate mean return
mean := 0.0
for _, r := range returns {
mean += r
}
mean /= float64(len(returns))
// Calculate sample variance (using n-1 for unbiased estimator)
variance := 0.0
for _, r := range returns {
diff := r - mean
variance += diff * diff
}
variance /= float64(len(returns))
if len(returns) > 1 {
variance /= float64(len(returns) - 1)
}
std := math.Sqrt(variance)
if std == 0 {
if mean > 0 {
return 999
}
if mean < 0 {
return -999
}
if std < 1e-10 {
// Zero or near-zero volatility - return 0 instead of infinity/NaN
return 0
}
return mean / std
// Calculate Sharpe ratio (assuming risk-free rate = 0 for crypto)
// Annualize by multiplying by sqrt(periods per year)
// Assuming each equity point represents ~1 hour, we have ~8760 periods/year
// For conservative estimate, use sqrt(252) as if daily returns
periodsPerYear := 252.0
annualizationFactor := math.Sqrt(periodsPerYear)
sharpe := (mean / std) * annualizationFactor
return sharpe
}
func fillTradeMetrics(metrics *Metrics, events []TradeEvent) {
@@ -189,7 +202,8 @@ func fillTradeMetrics(metrics *Metrics, events []TradeEvent) {
if totalLossAmount > 0 {
metrics.ProfitFactor = totalWinAmount / totalLossAmount
} else if totalWinAmount > 0 {
metrics.ProfitFactor = 999
// No losses but have wins - use a high but reasonable cap
metrics.ProfitFactor = 100.0
}
bestSymbol := ""