""" Tests for VBT validator, visualizer, report generator, and CLI integration. """ import numpy as np import pandas as pd class TestValidationReport: def test_empty_report_passes(self): from backtests.vbt_validator import ValidationReport r = ValidationReport(strategy="test", interval="1h") assert r.passes assert r.all_checks_pass def test_errors_cause_failure(self): from backtests.vbt_validator import ValidationReport r = ValidationReport(strategy="test", interval="1h") r.errors.append("lookahead detected") assert not r.passes def test_warnings_dont_cause_failure(self): from backtests.vbt_validator import ValidationReport r = ValidationReport(strategy="test", interval="1h") r.warnings.append("low trade count") assert r.passes def test_checks_tracking(self): from backtests.vbt_validator import ValidationReport r = ValidationReport(strategy="test", interval="1h") r.checks["no_lookahead"] = True r.checks["min_trades"] = False assert not r.all_checks_pass class TestVBTValidator: def make_data(self, n=500): dates = pd.date_range("2026-01-01", periods=n, freq="1h") close = pd.Series(100 + np.cumsum(np.random.RandomState(42).randn(n) * 0.1), index=dates) entries = pd.Series(False, index=dates) exits = pd.Series(False, index=dates) entries.iloc[[50, 100, 150, 200, 250, 300, 350, 400, 420, 440, 460]] = True exits.iloc[[60, 110, 160, 210, 260, 310, 360, 410, 430, 450, 470]] = True return entries, exits, close def test_clean_data_passes(self): from backtests.vbt_validator import VBTValidator entries, exits, close = self.make_data() v = VBTValidator() report = v.validate(entries=entries, exits=exits, close=close, strategy="test", interval="1h") assert report.passes def test_lookahead_detected(self): from backtests.vbt_validator import VBTValidator entries, exits, close = self.make_data() entries.iloc[0] = True v = VBTValidator() report = v.validate(entries=entries, exits=exits, close=close, strategy="test", interval="1h") assert not report.checks["no_lookahead"] def test_coincident_signals_detected(self): from backtests.vbt_validator import VBTValidator entries, exits, close = self.make_data() entries.iloc[100] = True exits.iloc[100] = True v = VBTValidator() report = v.validate(entries=entries, exits=exits, close=close, strategy="test", interval="1h") assert not report.checks["no_coincident_entry_exit"] def test_nan_detected(self): from backtests.vbt_validator import VBTValidator entries, exits, close = self.make_data() close.iloc[50] = np.nan v = VBTValidator() report = v.validate(entries=entries, exits=exits, close=close, strategy="test", interval="1h") assert not report.checks["no_nan_close"] def test_duplicate_timestamps_detected(self): from backtests.vbt_validator import VBTValidator dates = pd.date_range("2026-01-01", periods=500, freq="1h") dates = dates.insert(3, dates[2]) close = pd.Series(100 + np.random.randn(501) * 0.1, index=dates) entries = pd.Series(False, index=dates) exits = pd.Series(False, index=dates) entries.iloc[[50, 100]] = True exits.iloc[[60, 110]] = True v = VBTValidator() report = v.validate(entries=entries, exits=exits, close=close, strategy="test", interval="1h") assert not report.checks["no_duplicate_timestamps"] def test_low_trade_count_warns(self): from backtests.vbt_validator import VBTValidator dates = pd.date_range("2026-01-01", periods=100, freq="1h") close = pd.Series(100 + np.random.randn(100) * 0.1, index=dates) entries = pd.Series(False, index=dates) exits = pd.Series(False, index=dates) entries.iloc[20] = True exits.iloc[30] = True entries.iloc[40] = True exits.iloc[50] = True v = VBTValidator(min_trades=10) report = v.validate(entries=entries, exits=exits, close=close, strategy="test", interval="1h") assert not report.checks["min_trade_count"] def test_benchmark_accepts_series(self): from backtests.vbt_validator import VBTValidator entries, exits, close = self.make_data() bm = close * 1.01 v = VBTValidator() report = v.validate(entries=entries, exits=exits, close=close, benchmark_close=bm, strategy="test", interval="1h") assert report.checks["benchmark_available"] assert "benchmark_return_pct" in report.details class TestVBTVisualizer: def test_visualizer_creates_output_dir(self): from backtests.vbt_viz import VBTVisualizer viz = VBTVisualizer(output_dir="/tmp/vbt_test_viz") assert viz._output_dir.exists() def test_equity_curve_with_data(self): import vectorbt as vbt from backtests.vbt_viz import VBTVisualizer dates = pd.date_range("2026-01-01", periods=100, freq="1h") close = pd.Series(100 + np.cumsum(np.random.RandomState(42).randn(100) * 0.1), index=dates) entries = pd.Series(False, index=dates) exits = pd.Series(False, index=dates) entries.iloc[[10, 30, 50, 70]] = True exits.iloc[[20, 40, 60, 80]] = True pf = vbt.Portfolio.from_signals(close=close, entries=entries, exits=exits, fees=0.001, init_cash=10000.0) viz = VBTVisualizer() fig = viz.equity_curve(pf) assert fig is not None def test_drawdown_returns_figure(self): import vectorbt as vbt from backtests.vbt_viz import VBTVisualizer dates = pd.date_range("2026-01-01", periods=100, freq="1h") close = pd.Series(100 + np.cumsum(np.random.RandomState(42).randn(100) * 0.2), index=dates) entries = pd.Series(False, index=dates) exits = pd.Series(False, index=dates) entries.iloc[[10, 50]] = True exits.iloc[[30, 70]] = True pf = vbt.Portfolio.from_signals(close=close, entries=entries, exits=exits, fees=0.001, init_cash=10000.0) viz = VBTVisualizer() fig = viz.drawdown(pf) assert fig is not None def test_returns_distribution_empty_safe(self): from backtests.vbt_viz import VBTVisualizer import plotly.graph_objects as go viz = VBTVisualizer() fig = viz.returns_distribution(None) assert isinstance(fig, go.Figure) def test_monthly_heatmap_empty_safe(self): from backtests.vbt_viz import VBTVisualizer import plotly.graph_objects as go viz = VBTVisualizer() fig = viz.monthly_heatmap(None) assert isinstance(fig, go.Figure) def test_param_heatmap_empty_safe(self): from backtests.vbt_viz import VBTVisualizer import plotly.graph_objects as go viz = VBTVisualizer() fig = viz.param_heatmap(None, "w", "t", "sharpe") assert isinstance(fig, go.Figure) def test_dashboard_returns_list(self): import vectorbt as vbt from backtests.vbt_viz import VBTVisualizer dates = pd.date_range("2026-01-01", periods=200, freq="1h") close = pd.Series(100 + np.cumsum(np.random.RandomState(42).randn(200) * 0.1), index=dates) entries = pd.Series(False, index=dates) exits = pd.Series(False, index=dates) entries.iloc[[20, 50, 80, 110, 140]] = True exits.iloc[[35, 65, 95, 125, 155]] = True pf = vbt.Portfolio.from_signals(close=close, entries=entries, exits=exits, fees=0.001, init_cash=10000.0) viz = VBTVisualizer() figs = viz.dashboard(pf, close, entries, exits, strategy="test", interval="1h") assert len(figs) >= 5 def test_holding_periods_empty_safe(self): from backtests.vbt_viz import VBTVisualizer import plotly.graph_objects as go viz = VBTVisualizer() fig = viz.holding_periods(None) assert isinstance(fig, go.Figure) def test_gross_vs_net_empty_safe(self): from backtests.vbt_viz import VBTVisualizer import plotly.graph_objects as go viz = VBTVisualizer() fig = viz.gross_vs_net(None) assert isinstance(fig, go.Figure) class TestVBTReport: def test_generates_markdown(self): from backtests.vbt_report import generate_markdown_report result = { "strategy": "pairs", "interval": "1h", "pnl": 10.0, "total_return_pct": 0.1, "sharpe": 0.5, "sortino": 0.6, "max_drawdown_pct": 2.0, "win_rate": 0.55, "profit_factor": 1.2, "expectancy": 0.1, "n_bars": 1000, "trades": [], "total_trades": 20, "params": {"z_entry": 1.5}, "fee_info": {"effective_rate_pct": 0.045, "tier_name": "Tier 0"}, } md = generate_markdown_report(result) assert "pairs" in md assert "1h" in md assert "Sharpe" in md assert "Limitations" in md def test_generates_html(self): from backtests.vbt_report import generate_html_report result = { "strategy": "test", "interval": "1h", "pnl": 0.0, "total_return_pct": 0.0, "sharpe": 0.0, "sortino": 0.0, "max_drawdown_pct": 0.0, "win_rate": 0.0, "profit_factor": 0.0, "expectancy": 0.0, "n_bars": 0, "trades": [], "total_trades": 0, "params": {}, "fee_info": {}, } html = generate_html_report(result) assert "