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:
2026-10-10 00:21:49 -07:00
parent a9683b6d1f
commit cd0721eada
12 changed files with 279 additions and 41 deletions

View File

@@ -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)