Heuristic:HKUDS AI Trader Sortino Ratio Capping
| Knowledge Sources | |
|---|---|
| Domains | Metrics, Visualization |
| Last Updated | 2026-02-09 14:00 GMT |
Overview
Cap Sortino Ratio values to the range [-20, 20] and apply a minimum downside standard deviation threshold of 0.0001 to prevent extreme spikes in performance visualization charts.
Description
The Sortino Ratio measures risk-adjusted return using only downside deviation. In early trading periods or when an agent has few/no negative returns, the ratio can spike to extreme values (infinity or very large numbers), making charts unreadable. The AI-Trader system applies two stabilization techniques: (1) clipping the Sortino to [-20, 20] using np.clip(), and (2) enforcing a minimum downside standard deviation of 0.0001 to prevent division-by-near-zero. When there are no negative returns at all, a positive mean return is capped at 20.
Usage
Apply this heuristic in performance visualization and metrics reporting code. Any expanding-window Sortino calculation should use these caps. Without them, early-period charts will show vertical spikes that obscure meaningful trends.
The Insight (Rule of Thumb)
- Action: Apply
np.clip(sortino, -20, 20)after calculation. Setmin_downside_std = 0.0001as a floor. - Value: Cap = 20 (positive and negative). Minimum denominator = 0.0001.
- Trade-off: Capping masks genuinely extreme performance in rare cases. A Sortino of 25 would display as 20. This is acceptable for visualization but raw metrics should preserve the true value.
Reasoning
In expanding-window calculations, early periods have very few data points. If the first few returns are all positive, downside deviation is zero, producing an infinite Sortino. Even with 1-2 negative returns, a tiny downside std (e.g., 0.00001) produces ratios in the thousands. The cap of 20 was chosen as a practical upper bound: real-world annual Sortino ratios above 5 are exceptional, so 20 provides ample headroom while preventing chart distortion.
Code Evidence
Sortino capping from tools/plot_metrics.py:86-98:
if len(negative_returns) > 0:
downside_std = negative_returns.std()
# Use a minimum threshold for downside std to avoid extreme spikes
min_downside_std = 0.0001
downside_std = max(downside_std, min_downside_std)
sortino = (returns_so_far.mean() / downside_std) * np.sqrt(periods_per_year)
# Cap to reasonable range
sortino = np.clip(sortino, -20, 20)
else:
# No negative returns yet
mean_return = returns_so_far.mean()
if mean_return > 0:
sortino = 20 # Cap at upper limit
else:
sortino = 0
Sortino in final metrics from tools/calculate_metrics.py:227-232:
negative_returns = returns[returns < 0]
if len(negative_returns) > 0:
downside_std = np.std(negative_returns)
sortino = excess_return / downside_std * np.sqrt(periods_per_year) if downside_std > 0 else 0
else:
sortino = float('inf') if np.mean(returns) > 0 else 0