fix: unify backtest execution and add forward validation candidates

Share point-in-time signals and a cash-funded order ledger across backtests and trading. Correct eligibility, missing-price fills, quality factors, regime inputs, warmup windows, and cash-injection accounting. Add reproducible registered-strategy comparisons and paired forward candidates with a 20% resize band.

Validation: 125 Python tests, 23 web tests, TypeScript/Vite build, 87 strategy causality checks, and real-data target/order/equity parity for 10 strategies.
This commit is contained in:
2026-10-01 02:24:01 -07:00
parent 761725d2c8
commit 722cade974
45 changed files with 1561 additions and 1021 deletions

View File

@@ -0,0 +1,139 @@
import unittest
import json
import tempfile
from pathlib import Path
from unittest import mock
import numpy as np
import pandas as pd
import data_manager
import trader
from execution import simulate_book, rebalance, portfolio_value
from execution import compute_trades, market_permissions
from signals import session_targets
from strategies.momentum import MomentumStrategy
from strategies.momentum_quality import rolling_max_drawdown
from strategies.composite_alpha import CompositeAlphaStrategy
from strategies.robust_momentum import RobustMomentumStrategy
class ExecutionContracts(unittest.TestCase):
def test_a_halt_cannot_be_sold_at_the_carried_mark(self):
dates = pd.bdate_range("2026-01-05", periods=5)
raw = pd.DataFrame({"A": [100, np.nan, np.nan, 10, 10]}, index=dates)
targets = pd.DataFrame({"A": [1, 0, 0, 0, 0]}, index=dates)
result = simulate_book(targets, raw, 10000, transaction_cost=0, min_trade_value=0)
self.assertAlmostEqual(result["equity"].iloc[-1], 1000)
self.assertEqual([t["date"] for t in result["trade_log"]],
[str(dates[0].date()), str(dates[3].date())])
def test_fractional_and_integer_orders_are_funded_after_all_fees(self):
prices = {"A": 73., "B": 29.}
for integer in [False, True]:
state = {"holdings": {}, "cash": 1000., "trade_log": []}
rebalance(state, {"A": .5, "B": .5}, prices, set(prices),
fixed_fee=2, fee_base=1.88, fee_per_share=.009,
integer_shares=integer, min_trade_value=0)
self.assertGreaterEqual(state["cash"], 0)
equity = portfolio_value(state["holdings"], prices, state["cash"])
self.assertAlmostEqual(equity, 1000 - sum(t["commission"] for t in state["trade_log"]), places=2)
def test_batch_replay_matches_session_ledger_with_a_halt_and_drift(self):
dates = pd.bdate_range("2026-01-05", periods=5)
prices = pd.DataFrame({"A": [100, 110, np.nan, 70, 90], "B": [20, 21, 22, 22, 23]}, index=dates)
targets = pd.DataFrame([[.5, .5], [.5, .5], [0, 1], [0, 1], [.5, .5]], index=dates, columns=prices.columns)
options = dict(transaction_cost=.001, fixed_fee=2., integer_shares=True, resize_band=.1)
batch = simulate_book(targets, prices, 10000, **options)
state = trader.init_state("us", "test", 10000)
fees = {"tx_cost": .001, "fixed_fee": 2.}
for day in dates:
trader._run_session(state, "us", day, "B", prices.ffill(), prices, fees, True, targets.loc[day])
self.assertAlmostEqual(state["daily_equity"][str(day.date())], batch["equity"].loc[day], places=2)
self.assertEqual(state["trade_log"], batch["trade_log"])
def test_contributions_are_executed_and_fees_affect_unit_returns(self):
dates = pd.bdate_range("2026-01-05", periods=3)
prices = pd.DataFrame({"A": [100., 100., 100.]}, index=dates)
weights = pd.DataFrame(1., index=dates, columns=["A"])
base = simulate_book(weights, prices, 1000, transaction_cost=0, fixed_fee=2)
injected = simulate_book(weights, prices, 1000, transaction_cost=0, fixed_fee=2,
contributions=pd.Series([0, 1000, 0], index=dates))
self.assertEqual(len(base["trade_log"]), 1)
self.assertEqual(len(injected["trade_log"]), 2)
self.assertAlmostEqual(injected["equity"].iloc[-1], 1996)
self.assertLess(injected["unit_equity"].iloc[-1], base["unit_equity"].iloc[-1])
def test_missing_open_never_sees_same_day_close(self):
close = pd.DataFrame({"A": [100., 200.]})
raw_open = pd.DataFrame({"A": [np.nan, np.nan]})
self.assertTrue(data_manager._fill_open(close, raw_open).isna().all().all())
def test_china_limit_permissions_tax_and_star_minimum(self):
raw = pd.Series({"600000.SS": 11., "000001.SZ": 9., "688001.SS": 12.})
buy, sell, tax = market_permissions("cn", pd.Timestamp("2026-09-30"), raw, raw * 0 + 10)
self.assertNotIn("600000.SS", buy)
self.assertNotIn("000001.SZ", sell)
self.assertNotIn("688001.SS", buy)
self.assertEqual(tax, .0005)
self.assertEqual(market_permissions("cn", pd.Timestamp("2023-08-25"), raw, raw * 0 + 10)[2], .001)
orders = compute_trades({}, 10000, {"688001.SS": .7}, {"688001.SS": 23.},
integer_shares=True, lot_size=100)
self.assertEqual(orders[0]["shares_delta"], 304)
class SignalContracts(unittest.TestCase):
def test_live_refresh_updates_the_membership_snapshot_before_selection(self):
with tempfile.TemporaryDirectory() as tmp:
cache = Path(tmp) / "sp500_history.json"
cache.write_text(json.dumps({"date": "2000-01-01", "intervals": {"OLD": [["2020-01-01", None]]}}))
history = {"OLD": [["2020-01-01", "2026-09-30"]], "NEW": [["2026-09-30", None]]}
with mock.patch.object(trader.universe_history, "CACHE_DIR", tmp), \
mock.patch.object(trader.universe_history, "build_sp500_history", return_value=history), \
mock.patch.object(trader, "strategy_universe", return_value=(["NEW"], "SPY")), \
mock.patch.object(data_manager, "update"):
trader.refresh_panels("us", ["momentum"], "test")
member = trader.index_members("us", pd.DatetimeIndex(["2026-10-01"]))
self.assertTrue(member.iloc[0]["NEW"])
self.assertFalse(member.iloc[0]["OLD"])
def test_close_signal_is_unaffected_by_its_execution_days_close(self):
dates = pd.bdate_range("2024-01-01", periods=30)
c = pd.DataFrame({"A": np.arange(100., 130.), "B": np.arange(130., 100., -1)}, index=dates)
s = MomentumStrategy(lookback=5, skip=1, top_n=1)
base = session_targets(s, c)
shocked = c.copy(); shocked.iloc[20:] = shocked.iloc[20:] * [0.1, 10]
actual = session_targets(s, shocked)
pd.testing.assert_frame_equal(base.iloc[:21], actual.iloc[:21])
def test_trader_and_backtest_use_identical_membership_histories(self):
rng = np.random.default_rng(42)
dates = pd.bdate_range("2024-01-01", periods=370)
close = pd.DataFrame(100 * np.exp(rng.normal(.0005, .02, (370, 25)).cumsum(0)),
index=dates, columns=[f"S{i}" for i in range(25)])
opens = close.shift(1).fillna(100)
member = pd.DataFrame(True, index=dates, columns=close.columns)
member.iloc[:320, 0] = False
member.iloc[330:, 1] = False
bars = {"close": close, "open": opens}
strategy = RobustMomentumStrategy("overnight_recovery", top_n=5)
expected = session_targets(strategy, close, opens, bars, member)
with mock.patch.object(trader, "index_members", lambda m, d: member.reindex(d)):
actual = trader.session_signals("us", "overnight_recovery_top10", list(close), dates[-20:],
close, opens, bars=bars, strategy=strategy)
pd.testing.assert_frame_equal(actual, expected.reindex(index=actual.index, columns=actual.columns))
self.assertTrue((actual["S1"] == 0).all())
def test_quality_prefers_a_smaller_drawdown_and_needs_only_one_window(self):
p = pd.DataFrame({"small": [100, 110, 99, 108], "large": [100, 110, 55, 108]})
dd = rolling_max_drawdown(p, 4)
self.assertGreater(dd.iloc[-1]["small"], dd.iloc[-1]["large"])
self.assertAlmostEqual(dd.iloc[-1]["small"], -.1)
def test_missing_regime_benchmark_is_an_error(self):
with self.assertRaisesRegex(ValueError, "benchmark"):
CompositeAlphaStrategy().generate_signals(pd.DataFrame({"A": [1, 2]}))
if __name__ == "__main__":
unittest.main()

View File

@@ -77,6 +77,7 @@ class LiveBookTests(unittest.TestCase):
mock.patch.object(trader, "strategy_universe", lambda m, n: (["AAA", "BBB"], "SPY")), \
mock.patch.object(trader, "pit_candidates", lambda m, n, s: []), \
mock.patch.object(trader, "load_strategy_prices", lambda *a, **k: (close, close, close)), \
mock.patch.object(data_manager, "load_raw", lambda *a, **k: None), \
mock.patch.object(trader, "session_signals", lambda m, n, t, days, c, o: equal.loc[days]):
return trader._process_sessions(args, state, refresh=False)
@@ -151,20 +152,17 @@ class ReportingTests(unittest.TestCase):
equity = pd.Series(10_000 * 1.0003 ** np.arange(400), index=idx)
schedule = {"amount": 5_000, "frequency": "yearly_months", "months": [2, 8], "startYear": 1}
plain = bridge._build_strategy_result("x", equity, [], None, 10_000)
injected = bridge._build_strategy_result("x", equity, [], schedule, 10_000)
self.assertAlmostEqual(injected["metrics"]["totalReturn"], plain["metrics"]["totalReturn"])
flows = injected["capitalFlows"]
self.assertEqual(flows["invested"], 10_000 + 5_000 * len(injected["injections"]))
self.assertAlmostEqual(flows["profit"], flows["finalValue"] - flows["invested"], places=2)
with self.assertRaisesRegex(ValueError, "order replay"):
bridge._build_strategy_result("x", equity, [], schedule, 10_000)
def test_ruined_portfolio_stays_at_zero(self):
def test_unfunded_leverage_is_rejected(self):
class TwiceLevered:
def generate_signals(self, data):
return pd.DataFrame(2.0, index=data.index, columns=data.columns)
prices = pd.DataFrame({"X": [10.0, 10.0, 4.0, 5.0]}, index=pd.bdate_range("2026-01-01", periods=4))
equity = backtest(TwiceLevered(), prices, initial_capital=100)
self.assertTrue((equity.iloc[2:] == 0).all(), equity)
with self.assertRaisesRegex(ValueError, "unfunded"):
backtest(TwiceLevered(), prices, initial_capital=100)
if __name__ == "__main__":

View File

@@ -155,12 +155,11 @@ class BacktestUniverseTests(unittest.TestCase):
panel = pd.DataFrame(1.0, index=idx, columns=["AAA", "BBB", "SPY", "TQQQ"])
fake_universe = {"us": {"fetch": lambda: ["AAA"], "benchmark": "SPY",
"benchmark_label": "SPY"}}
members = pd.DataFrame(True, index=idx, columns=["AAA", "BBB"])
with mock.patch.object(backtest_runner, "UNIVERSES", fake_universe), \
mock.patch.object(backtest_runner.uh, "pit_tickers_ever", lambda m, since=None: ["AAA", "BBB"]), \
mock.patch.object(backtest_runner.uh, "mask_prices_pit", lambda p, m: p), \
mock.patch.object(backtest_runner.data_manager, "update",
lambda m, t, fields=None: {f: panel for f in fields}):
data = backtest_runner.load_stock_panels("us", ["momentum"], None)
mock.patch.object(backtest_runner.uh, "pit_membership", lambda m, days: members.reindex(days).fillna(False)), \
mock.patch.object(backtest_runner, "_panels", lambda *a: {"close": panel, "open": panel}):
data = backtest_runner.load_stock_panels("us", ["momentum"], None, refresh=False)
self.assertEqual(data["stocks"], ["AAA", "BBB"])
@@ -180,15 +179,13 @@ class MembershipTests(unittest.TestCase):
bars["open"]["S00"] = bars["close"]["S00"]
days = bars["close"].index[-5:]
others = [c for c in bars["close"].columns if c != "S00"]
members = pd.DataFrame(True, index=days[:-1], columns=bars["close"].columns)
members["S00"] = False # joins the index on the last day
with mock.patch.object(trader, "index_members", lambda m, d: members.loc[d]):
runs = trader.session_universes("us", "momentum", others + ["S00"], days)
members = pd.DataFrame(True, index=bars["close"].index, columns=bars["close"].columns)
members.loc[members.index < days[-1], "S00"] = False
with mock.patch.object(trader, "index_members", lambda m, d: members.reindex(d)):
weights = trader.session_signals("us", "momentum", others + ["S00"], days,
bars["close"], bars["open"])
self.assertEqual([len(r) for _, r in runs], [4, 1])
self.assertNotIn("S00", runs[0][0])
self.assertTrue((weights.loc[days[:-1], "S00"] == 0).all())
# The historical observations were public before index admission.
self.assertGreater(weights.loc[days[-1], "S00"], 0)
def test_changes_table_with_two_level_headers_is_found(self):

View File

@@ -3,6 +3,7 @@ kept to their own columns, shares that drift like separate accounts, and the
live trader's session timing."""
import unittest
from unittest import mock
import numpy as np
import pandas as pd
@@ -82,6 +83,17 @@ class SleevePortfolioTests(unittest.TestCase):
got = trader.bar_session_weights(strat, bars, days)
pd.testing.assert_frame_equal(got, full)
def test_trader_truncates_all_bar_fields_before_padding_sleeves(self):
bars = _bars()
days = bars["close"].index[-4:-2]
stocks = [c for c in bars["close"] if c not in ETF_UNIVERSE]
member = pd.DataFrame(True, index=bars["close"].index, columns=stocks)
expected = us_three_sleeve().generate_signals({f: b.loc[:days[-1]] for f, b in bars.items()}).loc[days]
with mock.patch.object(trader, "index_members", lambda m, d: member.reindex(d)):
actual = trader.session_signals("us", "us_three_sleeve", list(bars["close"]), days,
bars["close"], bars["open"])
pd.testing.assert_frame_equal(actual, expected.reindex(columns=actual.columns))
def test_registered_as_a_us_mixed_strategy(self):
self.assertIsInstance(trader.STRATEGY_REGISTRY["us_three_sleeve"](), SleevePortfolioStrategy)
self.assertEqual(set(trader.strategy_extra_tickers("us_three_sleeve")), set(ETF_UNIVERSE))