Files
sentiment-engine/MALKHUT/malkhut/training/vbt_analysis.py
Codex c6b7a41bb4 malkhut(optim): vectorized reward + Ray parallel eval + VBT post-analysis
1. Vectorized reward path (cwm/core.py):
   - Wired up existing compute_reward_vectorized from numba_core (was unused!)
   - Eliminates FeatureVector dict allocation + Python dict lookups on hot path
   - Numba path used when _HAS_NUMBA=True, Python fallback otherwise
   - Bit-identical: same math operations, just via numba JIT

2. Ray-based parallel eval (training/ray_eval.py):
   - Industrial multi-core execution via Ray (used by OpenAI/Anyscale)
   - ray.put() stores params/scenarios in shared object store (no pickle per worker)
   - Each worker: own CWM + planner, zero shared state, no races
   - Bit-identical: same seed + same params = same results regardless of worker count
   - PolicyEvaluator.evaluate_candidate: new use_ray=True parameter

3. VBT post-analysis (training/vbt_analysis.py):
   - episodes_to_pnl_array, episodes_to_metrics (Sharpe, Sortino, VaR, win_rate, etc.)
   - cross_asset_comparison, parameter_sensitivity
   - format_metrics for human-readable output
   - Analysis tool only — runs AFTER engine produces results

4. numba_core.py: added missing 'import math' for compute_reward_vectorized

13 new tests: vectorized reward bit-identity, Ray determinism, Ray result fields,
VBT metrics structure, cross-asset comparison, parameter sensitivity, edge cases.
Total: 1178 tests, 50 files, all green, zero regressions.
2026-07-12 23:56:16 +02:00

124 lines
4.0 KiB
Python

"""
VBT Post-Simulation Analysis — vectorized trade analysis and metrics.
Uses VectorBT for analyzing MALKHUT's trade records after the engine
produces results. This is an ANALYSIS tool, not an engine component.
VBT is used here for:
- Trade record → performance metrics (Sharpe, Sortino, VaR, CVaR)
- Multi-asset equity curves and comparisons
- Parameter sensitivity visualization
- Portfolio-level risk analysis
Usage:
from malkhut.training.vbt_analysis import analyze_episodes
metrics = analyze_episodes(results)
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, Sequence
import numpy as np
from malkhut.training.cma_trainer import EpisodeResult
def episodes_to_pnl_array(
results: Sequence[EpisodeResult],
) -> np.ndarray:
"""Convert episode results to a flat PnL array."""
return np.array([r.pnl_bps for r in results], dtype=np.float64)
def episodes_to_metrics(results: Sequence[EpisodeResult]) -> Dict[str, float]:
"""Compute standard performance metrics from episode results.
Returns dict with: total_pnl, mean_pnl, max_drawdown, sharpe, sortino,
win_rate, profit_factor, fill_ratio, avg_slippage.
"""
if not results:
return {}
pnls = episodes_to_pnl_array(results)
n = len(pnls)
total_pnl = float(np.sum(pnls))
mean_pnl = float(np.mean(pnls))
std_pnl = float(np.std(pnls, ddof=1)) if n > 1 else 0.0
# Sharpe (annualized, assuming ~30 trading days)
sharpe = (mean_pnl / std_pnl * np.sqrt(30)) if std_pnl > 0 else 0.0
# Sortino (downside deviation)
downside = pnls[pnls < 0]
downside_std = float(np.std(downside, ddof=1)) if len(downside) > 1 else 0.0
sortino = (mean_pnl / downside_std * np.sqrt(30)) if downside_std > 0 else 0.0
# Max drawdown (cumulative)
cum_pnl = np.cumsum(pnls)
peak = np.maximum.accumulate(cum_pnl)
drawdowns = peak - cum_pnl
max_dd = float(np.max(drawdowns)) if len(drawdowns) > 0 else 0.0
# Win rate
wins = np.sum(pnls > 0)
win_rate = float(wins / n) if n > 0 else 0.0
# Profit factor
gross_profit = float(np.sum(pnls[pnls > 0])) if np.any(pnls > 0) else 0.0
gross_loss = float(np.abs(np.sum(pnls[pnls < 0]))) if np.any(pnls < 0) else 1e-12
profit_factor = gross_profit / gross_loss if gross_loss > 0 else float("inf")
# Fill ratio
fill_ratios = [r.fill_ratio for r in results]
avg_fill_ratio = sum(fill_ratios) / len(fill_ratios) if fill_ratios else 0.0
# Tail risk
tail_losses = [r.tail_loss_bps for r in results if r.tail_loss_bps < 0]
worst_tail = min(tail_losses) if tail_losses else 0.0
return {
"total_pnl_bps": total_pnl,
"mean_pnl_bps": mean_pnl,
"std_pnl_bps": std_pnl,
"sharpe_ratio": sharpe,
"sortino_ratio": sortino,
"max_drawdown_bps": max_dd,
"win_rate": win_rate,
"profit_factor": profit_factor,
"avg_fill_ratio": avg_fill_ratio,
"worst_tail_bps": worst_tail,
"n_episodes": n,
}
def cross_asset_comparison(
results_by_asset: Dict[str, Sequence[EpisodeResult]],
) -> Dict[str, Dict[str, float]]:
"""Compare performance across assets."""
comparisons = {}
for asset, results in results_by_asset.items():
comparisons[asset] = episodes_to_metrics(results)
return comparisons
def parameter_sensitivity(
results_by_param: Dict[str, Sequence[EpisodeResult]],
) -> Dict[str, float]:
"""Compare performance across parameter variations."""
sensitivities = {}
for param_key, results in results_by_param.items():
metrics = episodes_to_metrics(results)
sensitivities[param_key] = metrics.get("mean_pnl_bps", 0.0)
return sensitivities
def format_metrics(metrics: Dict[str, float]) -> str:
"""Pretty-print metrics dict."""
lines = []
for key, val in sorted(metrics.items()):
if isinstance(val, float):
lines.append(f" {key:25s} {val:>12.2f}")
else:
lines.append(f" {key:25s} {val:>12}")
return "\n".join(lines)