fix(backtest): settle delisted US holdings (engine v3), PIT membership before 2011, registry causality test
- execution/trader: a held US stock with no print for 5 sessions that has left the index is sold at its last print (execution.SETTLE_AFTER_SESSIONS), in both the batch replay and the live session ledger. Acquired names used to stay at their last mark forever: lowrisk_top20 ended 2017-12..2026-10 with 11 of 26 dead positions, and CAGR rises 1.7-5.5pp once they settle. ENGINE_VERSION cash-ledger-pit-v3; the API cache key follows. - universe_history: current members whose pre-2011 addition the Wikipedia change log missed now start at the list page's "Date added" (80 of 94 current members added 2001-2010 were open-start, e.g. ISRG, MA, BKNG). Membership from 2016 on is unchanged; the research panel cache now keys on the membership intervals. - research/lab/registered: pure-ETF strategies replay on the ETF panel (they ran as all-cash books or crashed); prepare_strategy refuses an ETF strategy that lacks its ETF universe. - research/lab/factors: trend_hzz uses a month's regression betas only from the session its forward returns end (short months read 1-2 sessions ahead). - tests: every registered strategy (US and CN) is truncation-invariant on the production signal path; batch and live ledgers settle a delisting identically. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,31 @@ class ExecutionContracts(unittest.TestCase):
|
||||
self.assertAlmostEqual(state["daily_equity"][str(day.date())], batch["equity"].loc[day], places=2)
|
||||
self.assertEqual(state["trade_log"], batch["trade_log"])
|
||||
|
||||
def test_a_delisted_stock_is_settled_at_its_last_print_in_both_ledgers(self):
|
||||
dates = pd.bdate_range("2026-01-05", periods=9)
|
||||
# A is acquired: last print on day 2, out of the index from day 4.
|
||||
# H halts on the same day but stays a member, so it stays frozen.
|
||||
prices = pd.DataFrame({"A": [100, 101, 102] + [np.nan] * 6,
|
||||
"H": [50, 50, 51] + [np.nan] * 6,
|
||||
"B": [20.] * 9}, index=dates)
|
||||
member = pd.DataFrame(True, index=dates, columns=prices.columns)
|
||||
member.loc[dates[4]:, "A"] = False
|
||||
targets = pd.DataFrame([[.4, .3, .3]] * 9, 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, market="us", member=member, **options)
|
||||
settled = [(t["ticker"], t["date"], t["price"]) for t in batch["trade_log"] if t.get("settled")]
|
||||
self.assertEqual(settled, [("A", str(dates[7].date()), 102.0)])
|
||||
self.assertIn("H", batch["holdings"])
|
||||
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],
|
||||
member_row=member.loc[day])
|
||||
self.assertAlmostEqual(state["daily_equity"][str(day.date())], batch["equity"].loc[day], places=2)
|
||||
self.assertEqual(state["trade_log"], batch["trade_log"])
|
||||
china = simulate_book(targets, prices, 10000, market="cn", member=member, **options)
|
||||
self.assertFalse(any(t.get("settled") for t in china["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)
|
||||
|
||||
Reference in New Issue
Block a user