from __future__ import annotations
import pytest
from wbt import WeightBacktest
HISTORY_KEYS = {
"mode",
"is_good",
"reason",
"yearly_metrics",
"complete_year_count",
"alpha_degenerate",
"cond_yearly_passed",
"history_alpha_max_drawdown",
"history_alpha_sharpe",
"cond_history_dd_passed",
"cond_history_sharpe_passed",
}
RECENT_KEYS = {
"mode",
"is_good",
"reason",
"recent_start_date",
"recent_end_date",
"recent_actual_days",
"recent_abs_return",
"recent_alpha_return",
"recent_alpha_max_drawdown",
"history_alpha_max_drawdown_excl_recent",
"history_window_empty",
"alpha_degenerate",
"cond_recent_return_passed",
"cond_recent_dd_passed",
}
def test_history_mode_returns_expected_keys(wb: WeightBacktest):
r = wb.is_good_strategy(mode="history")
assert isinstance(r, dict)
assert r["mode"] == "history"
assert isinstance(r["is_good"], bool)
assert set(r.keys()) == HISTORY_KEYS, (
f"unexpected key set; missing: {HISTORY_KEYS - set(r.keys())}; extra: {set(r.keys()) - HISTORY_KEYS}"
)
assert isinstance(r["yearly_metrics"], list)
for entry in r["yearly_metrics"]:
assert {
"year",
"abs_return",
"alpha_return",
"alpha_max_drawdown",
"days",
"is_complete_year",
"year_passed",
} == set(entry.keys())
assert isinstance(entry["is_complete_year"], bool)
assert isinstance(entry["year_passed"], bool)
def test_recent_mode_returns_expected_keys(wb: WeightBacktest):
r = wb.is_good_strategy(mode="recent")
assert isinstance(r, dict)
assert r["mode"] == "recent"
assert isinstance(r["is_good"], bool)
assert isinstance(r["history_window_empty"], bool)
assert set(r.keys()) == RECENT_KEYS, (
f"unexpected key set; missing: {RECENT_KEYS - set(r.keys())}; extra: {set(r.keys()) - RECENT_KEYS}"
)
def test_history_and_recent_key_sets_are_disjoint(wb: WeightBacktest):
history = set(wb.is_good_strategy(mode="history").keys())
recent = set(wb.is_good_strategy(mode="recent").keys())
shared = {"mode", "is_good", "reason", "alpha_degenerate"}
history_only = history - shared
recent_only = recent - shared
assert history_only.isdisjoint(recent_only), (
f"history-only ∩ recent-only must be empty; got history_only={history_only}, recent_only={recent_only}"
)
def test_invalid_mode_raises(wb: WeightBacktest):
with pytest.raises(Exception) as excinfo:
wb.is_good_strategy(mode="xxx")
msg = str(excinfo.value)
assert "invalid input" in msg or "invalid mode" in msg or "xxx" in msg, msg
def test_invalid_target_vol_raises(wb: WeightBacktest):
with pytest.raises(Exception) as excinfo:
wb.is_good_strategy(target_vol=0.0)
assert "target_vol" in str(excinfo.value) or "invalid input" in str(excinfo.value)
def test_invalid_max_dd_threshold_raises(wb: WeightBacktest):
with pytest.raises(Exception) as excinfo:
wb.is_good_strategy(max_dd_threshold=-0.1)
assert "max_dd_threshold" in str(excinfo.value) or "invalid input" in str(excinfo.value)
def test_recent_zero_recent_days_raises(wb: WeightBacktest):
with pytest.raises(Exception) as excinfo:
wb.is_good_strategy(mode="recent", recent_days=0)
msg = str(excinfo.value)
assert "recent_days" in msg or "invalid input" in msg
def test_yearly_metrics_carry_per_year_drawdown(wb: WeightBacktest):
r = wb.is_good_strategy(mode="history")
ym = r["yearly_metrics"]
assert isinstance(ym, list) and ym, "fixture should produce at least one year bucket"
for entry in ym:
dd = entry["alpha_max_drawdown"]
assert isinstance(dd, (int, float)) and not isinstance(dd, bool)
assert dd >= 0.0
def test_default_parameters_work(wb: WeightBacktest):
r = wb.is_good_strategy()
assert r["mode"] == "history"
assert isinstance(r["alpha_degenerate"], bool)
def test_min_history_days_zero_is_accepted(wb: WeightBacktest):
r = wb.is_good_strategy(mode="recent", min_history_days=0)
assert r["mode"] == "recent"
def test_dict_key_order_is_deterministic(wb: WeightBacktest):
a = list(wb.is_good_strategy(mode="history").keys())
b = list(wb.is_good_strategy(mode="history").keys())
assert a == b == sorted(a), f"keys must be sorted: {a}"
def test_reason_is_string_and_empty_on_success(wb: WeightBacktest):
r = wb.is_good_strategy(mode="history", max_dd_threshold=1.0)
assert isinstance(r["reason"], str)
if r["is_good"]:
assert r["reason"] == ""
def test_history_mode_returns_full_sample_hard_gate_keys(wb: WeightBacktest):
r = wb.is_good_strategy(mode="history")
dd = r["history_alpha_max_drawdown"]
assert dd is None or (isinstance(dd, (int, float)) and not isinstance(dd, bool))
sharpe = r["history_alpha_sharpe"]
assert sharpe is None or (isinstance(sharpe, (int, float)) and not isinstance(sharpe, bool))
assert isinstance(r["cond_history_dd_passed"], bool)
assert isinstance(r["cond_history_sharpe_passed"], bool)
if dd is not None:
if dd <= 0.30:
assert r["cond_history_dd_passed"] is True
if dd > 0.30:
assert r["cond_history_dd_passed"] is False
if sharpe is not None:
if sharpe > 0.5:
assert r["cond_history_sharpe_passed"] is True
if sharpe <= 0.5:
assert r["cond_history_sharpe_passed"] is False
def test_history_mode_sharpe_threshold_is_strict_greater(wb: WeightBacktest):
r = wb.is_good_strategy(mode="history")
if r["alpha_degenerate"] or r["history_alpha_sharpe"] is None:
pytest.skip("fixture yields alpha-degenerate data; Sharpe gate is N/A")
sharpe = r["history_alpha_sharpe"]
r2 = wb.is_good_strategy(mode="history", min_full_sharpe=sharpe + 1.0)
assert r2["cond_history_sharpe_passed"] is False
r3 = wb.is_good_strategy(mode="history", min_full_sharpe=sharpe - 1.0)
assert r3["cond_history_sharpe_passed"] is True
def test_history_mode_dd_threshold_is_le(wb: WeightBacktest):
r = wb.is_good_strategy(mode="history")
if r["alpha_degenerate"] or r["history_alpha_max_drawdown"] is None:
pytest.skip("fixture yields alpha-degenerate data; DD gate is N/A")
dd = r["history_alpha_max_drawdown"]
r_eq = wb.is_good_strategy(mode="history", max_alpha_dd_threshold=dd)
assert r_eq["cond_history_dd_passed"] is True, f"dd {dd} == threshold should pass"
r_lt = wb.is_good_strategy(mode="history", max_alpha_dd_threshold=max(dd - 1e-6, 0.0))
assert r_lt["cond_history_dd_passed"] is False, f"dd {dd} > threshold-eps should fail"
def test_history_mode_default_thresholds(wb: WeightBacktest):
r = wb.is_good_strategy(mode="history")
r_loose = wb.is_good_strategy(mode="history", max_alpha_dd_threshold=1.0, min_full_sharpe=-1e9)
dd = r["history_alpha_max_drawdown"]
sharpe = r["history_alpha_sharpe"]
if dd is not None:
assert r_loose["cond_history_dd_passed"] is True
if sharpe is not None:
assert r_loose["cond_history_sharpe_passed"] is True
def test_history_mode_invalid_max_alpha_dd_threshold_raises(wb: WeightBacktest):
with pytest.raises(Exception) as excinfo:
wb.is_good_strategy(mode="history", max_alpha_dd_threshold=-0.1)
msg = str(excinfo.value)
assert "max_alpha_dd_threshold" in msg or "invalid input" in msg, msg
def test_history_mode_invalid_min_full_sharpe_raises(wb: WeightBacktest):
with pytest.raises(Exception) as excinfo:
wb.is_good_strategy(mode="history", min_full_sharpe=float("nan"))
msg = str(excinfo.value)
assert "min_full_sharpe" in msg or "invalid input" in msg, msg
def test_history_mode_alpha_degenerate_dd_and_sharpe_are_none(sample_dfw):
import datetime as _dt
n = 300
df = sample_dfw.iloc[:0].copy() rows = []
for i in range(n):
dt = (_dt.datetime(2020, 1, 1, 9, 30) + _dt.timedelta(days=i)).strftime("%Y-%m-%d %H:%M:%S")
rows.append({"dt": dt, "symbol": "DEGEN", "weight": 0.5, "price": 100.0})
import pandas as _pd
df = _pd.DataFrame(rows)
wb2 = WeightBacktest(df, digits=2, fee_rate=0.0002, n_jobs=1, weight_type="ts", yearly_days=252)
r = wb2.is_good_strategy(mode="history")
if not r["alpha_degenerate"]:
pytest.skip("fixture did not trigger alpha-degenerate; covered by Rust unit tests")
assert r["history_alpha_max_drawdown"] is None
assert r["history_alpha_sharpe"] is None
assert r["cond_history_dd_passed"] is False
assert r["cond_history_sharpe_passed"] is False
assert r["is_good"] is False