Download tests/test_v2_strategies.py from ParallelLLC/algorithmic_trading: direct link, hf CLI and curl.
- Browser
- Download file 10.3 kB
-
https://huggingface.co/ParallelLLC/algorithmic_trading/resolve/main/tests/test_v2_strategies.py
- Command line
-
hf download hf://ParallelLLC/algorithmic_trading/tests/test_v2_strategies.py
-
curl -L -o test_v2_strategies.py https://huggingface.co/ParallelLLC/algorithmic_trading/resolve/main/tests/test_v2_strategies.py
10.3 kB
| """Strategy zoo, data loading, and the end-to-end Lab pipeline.""" | |
| from __future__ import annotations | |
| import numpy as np | |
| import pandas as pd | |
| import pytest | |
| from algotrader import LabConfig, run_lab | |
| from algotrader.data import _normalise, load_ohlcv, simulate_ohlcv | |
| from algotrader.indicators import atr, bollinger, donchian, ema, macd, rsi, sma | |
| from algotrader.lab import run_arena | |
| from algotrader.strategies import REGISTRY, get_strategy, list_strategies | |
| from algotrader.types import MarketData | |
| from algotrader.verdict import reality_score | |
| ALL_KEYS = sorted(REGISTRY) | |
| def bars() -> pd.DataFrame: | |
| return simulate_ohlcv("AAPL", "2016-01-01", "2023-01-01") | |
| class TestIndicatorsAreCausal: | |
| """An indicator at bar t must not move when bars after t arrive.""" | |
| def test_prefix_is_stable(self, bars, fn): | |
| cut = 800 | |
| full = fn(bars).iloc[:cut] | |
| partial = fn(bars.iloc[:cut]) | |
| pd.testing.assert_series_equal(full, partial, check_names=False, rtol=1e-9) | |
| def test_rsi_stays_in_range(self, bars): | |
| values = rsi(bars["close"], 14).dropna() | |
| assert values.between(0, 100).all() | |
| def test_donchian_excludes_the_current_bar(self, bars): | |
| _, upper = donchian(bars, 20) | |
| # A breakout must be possible: the channel cannot already contain today. | |
| assert (bars["high"] > upper).any() | |
| class TestStrategies: | |
| def test_output_is_well_formed(self, bars, key): | |
| target = get_strategy(key).generate(bars) | |
| assert target.index.equals(bars.index) | |
| assert target.notna().all() | |
| assert target.between(-1.0, 1.0).all() | |
| def test_signals_are_causal(self, bars, key): | |
| cut = 900 | |
| full = get_strategy(key).generate(bars).iloc[:cut] | |
| partial = get_strategy(key).generate(bars.iloc[:cut]) | |
| pd.testing.assert_series_equal(full, partial, check_names=False, rtol=1e-9) | |
| def test_grid_is_non_empty_and_valid(self, key): | |
| strategy = get_strategy(key) | |
| grid = strategy.grid(limit=40) | |
| assert 1 <= len(grid) <= 40 | |
| for combo in grid: | |
| assert set(combo) <= {p.name for p in strategy.params} | |
| if "fast" in combo and "slow" in combo: | |
| assert combo["fast"] < combo["slow"] | |
| def test_unknown_strategy_names_the_alternatives(self): | |
| with pytest.raises(KeyError, match="Available"): | |
| get_strategy("does_not_exist") | |
| def test_clean_ignores_unknown_params_and_casts_types(self): | |
| strategy = get_strategy("sma_cross") | |
| cleaned = strategy.clean({"fast": 15.7, "nonsense": 1}) | |
| assert cleaned == {"fast": 15, "slow": 100} | |
| def test_buy_and_hold_is_always_fully_invested(self, bars): | |
| assert (get_strategy("buy_and_hold").generate(bars) == 1.0).all() | |
| def test_list_strategies_honours_exclusions(self): | |
| keys = {s.key for s in list_strategies(exclude=["coin_flip"])} | |
| assert "coin_flip" not in keys and "sma_cross" in keys | |
| class TestData: | |
| def test_simulation_is_deterministic_per_symbol(self): | |
| a = simulate_ohlcv("NVDA", "2018-01-01", "2022-01-01") | |
| b = simulate_ohlcv("NVDA", "2018-01-01", "2022-01-01") | |
| pd.testing.assert_frame_equal(a, b) | |
| def test_different_symbols_simulate_differently(self): | |
| a = simulate_ohlcv("NVDA", "2018-01-01", "2022-01-01") | |
| b = simulate_ohlcv("TSLA", "2018-01-01", "2022-01-01") | |
| assert not np.allclose(a["close"].to_numpy(), b["close"].to_numpy()) | |
| def test_simulated_bars_are_internally_consistent(self): | |
| df = simulate_ohlcv("SPY", "2015-01-01", "2023-01-01") | |
| assert (df["high"] >= df[["open", "close"]].max(axis=1) - 1e-9).all() | |
| assert (df["low"] <= df[["open", "close"]].min(axis=1) + 1e-9).all() | |
| assert (df["close"] > 0).all() | |
| assert df.index.is_monotonic_increasing | |
| def test_simulation_has_fat_tails_and_vol_clustering(self): | |
| """Naive GBM flatters strategies; the simulator must be harder than that.""" | |
| returns = simulate_ohlcv("SPY", "2005-01-01", "2023-01-01")["close"].pct_change().dropna() | |
| assert returns.kurtosis() > 1.0 | |
| assert returns.abs().autocorr(1) > 0.05 | |
| def test_yahoo_source_does_not_silently_simulate(self, monkeypatch): | |
| monkeypatch.setattr("algotrader.data._download", lambda *a, **k: None) | |
| with pytest.raises(RuntimeError, match="Yahoo returned no usable bars"): | |
| load_ohlcv("SPY", "2018-01-01", "2022-01-01", source="yahoo") | |
| def test_offline_load_falls_back_and_says_so(self, monkeypatch): | |
| monkeypatch.setattr("algotrader.data._download", lambda *a, **k: None) | |
| monkeypatch.setattr("algotrader.data._read_cache", lambda *a, **k: None) | |
| market = load_ohlcv("SPY", "2018-01-01", "2022-01-01", source="auto") | |
| assert market.source == "synthetic" | |
| assert not market.is_real | |
| assert "unavailable" in market.note | |
| def test_normalise_handles_yahoo_style_frames(self): | |
| index = pd.date_range("2020-01-01", periods=5, tz="UTC") | |
| raw = pd.DataFrame( | |
| {"Open": 1.0, "High": 2.0, "Low": 0.5, "Adj Close": 1.5, "Volume": 10}, | |
| index=index, | |
| ) | |
| out = _normalise(raw) | |
| assert list(out.columns) == ["open", "high", "low", "close", "volume"] | |
| assert out.index.tz is None | |
| def test_normalise_rejects_frames_with_no_price(self): | |
| with pytest.raises(ValueError, match="missing required column"): | |
| _normalise(pd.DataFrame({"volume": [1, 2, 3]})) | |
| def test_too_short_a_range_is_rejected(self): | |
| with pytest.raises(ValueError, match="too short"): | |
| simulate_ohlcv("SPY", "2020-01-01", "2020-01-10") | |
| class TestVerdict: | |
| def test_strong_evidence_outranks_weak_evidence(self): | |
| metrics = {"n_trades": 300, "sharpe": 1.2, "total_return": 0.8, "max_drawdown": -0.2} | |
| benchmark = {"sharpe": 0.4} | |
| strong = reality_score(metrics, benchmark, p_value=0.001, dsr=0.99, pbo=0.02, | |
| wf_efficiency=0.9, wf_win_rate=1.0, cost_stress_ratio=0.9) | |
| weak = reality_score(metrics, benchmark, p_value=0.45, dsr=0.10, pbo=0.55, | |
| wf_efficiency=-0.2, wf_win_rate=0.2, cost_stress_ratio=0.1) | |
| assert strong["score"] > 85 > weak["score"] | |
| assert strong["grade"] == "A" and weak["grade"] == "F" | |
| def test_score_is_always_inside_the_scale(self): | |
| for p in (0.0, 0.5, 1.0): | |
| for dsr in (0.0, 1.0): | |
| out = reality_score( | |
| {"n_trades": 100, "sharpe": 0.5, "total_return": 0.2, "max_drawdown": -0.1}, | |
| {"sharpe": 0.1}, p_value=p, dsr=dsr, pbo=0.2, | |
| wf_efficiency=0.5, cost_stress_ratio=0.5, | |
| ) | |
| assert 0.0 <= out["score"] <= 100.0 | |
| def test_losing_money_caps_the_score(self): | |
| out = reality_score( | |
| {"n_trades": 200, "sharpe": 0.3, "total_return": -0.4, "max_drawdown": -0.6}, | |
| {"sharpe": 0.5}, p_value=0.001, dsr=0.99, pbo=0.01, | |
| wf_efficiency=1.0, cost_stress_ratio=1.0, | |
| ) | |
| assert out["score"] <= 50 | |
| assert any("lost money" in f for f in out["flags"]) | |
| def test_too_few_trades_is_flagged_and_capped(self): | |
| out = reality_score( | |
| {"n_trades": 3, "sharpe": 2.5, "total_return": 1.0, "max_drawdown": -0.1}, | |
| {"sharpe": 0.3}, p_value=0.001, dsr=0.99, pbo=0.01, | |
| wf_efficiency=1.0, cost_stress_ratio=1.0, | |
| ) | |
| assert out["score"] <= 55 | |
| assert any("coin flips" in f for f in out["flags"]) | |
| def test_closet_indexing_is_called_out(self): | |
| out = reality_score( | |
| {"n_trades": 50, "sharpe": 0.6, "total_return": 0.5, "max_drawdown": -0.2}, | |
| {"sharpe": 0.6}, benchmark_correlation=0.99, | |
| ) | |
| assert any("repackaged long position" in f for f in out["flags"]) | |
| class TestLabEndToEnd: | |
| def test_full_pipeline_produces_a_complete_report(self, monkeypatch): | |
| monkeypatch.setattr("algotrader.lab.load_ohlcv", lambda *a, **k: MarketData( | |
| "SIM", simulate_ohlcv("SPY", "2016-01-01", "2023-01-01"), "synthetic", "1d", "test" | |
| )) | |
| report = run_lab(LabConfig(strategy="sma_cross", n_permutations=25, wf_folds=3, grid_limit=12)) | |
| assert 0.0 <= report.verdict["score"] <= 100.0 | |
| assert report.verdict["grade"] in {"A", "B", "C", "D", "F"} | |
| assert 0 < report.permutation.p_value <= 1 | |
| assert 0.0 <= report.dsr["dsr"] <= 1.0 | |
| assert report.trials["n"] > 1 | |
| assert len(report.backtest.equity) == len(report.market.df) | |
| assert report.cost_stress["sharpe_3x"] <= report.cost_stress["sharpe_1x"] + 1e-9 | |
| def test_too_little_history_gives_a_readable_error(self, monkeypatch): | |
| short = simulate_ohlcv("SPY", "2020-01-01", "2020-06-01") | |
| monkeypatch.setattr( | |
| "algotrader.lab.load_ohlcv", | |
| lambda *a, **k: MarketData("SIM", short, "synthetic", "1d", "test"), | |
| ) | |
| with pytest.raises(ValueError, match="Widen the date range"): | |
| run_lab(LabConfig(n_permutations=0, wf_folds=2)) | |
| def test_arena_ranks_every_strategy_and_keeps_the_controls(self, monkeypatch): | |
| monkeypatch.setattr("algotrader.lab.load_ohlcv", lambda *a, **k: MarketData( | |
| "SIM", simulate_ohlcv("SPY", "2017-01-01", "2022-01-01"), "synthetic", "1d", "test" | |
| )) | |
| table, market, curves = run_arena(LabConfig(), n_permutations=0) | |
| assert len(table) == len(REGISTRY) | |
| assert {"buy_and_hold", "coin_flip"} <= set(table["key"]) | |
| assert table["Evidence"].is_monotonic_decreasing | |
| assert set(curves) == set(REGISTRY) | |