fix(trader): replay and catch-up trade each day's point-in-time index members

Replaying live books over past sessions used today's constituent list for
every day, so US books bought names (BE, MRVL, ILMN, FLEX, P) before they
joined the S&P 500. session_universes/session_signals now compute each past
session on that day's members (S&P change log, CSI 300 snapshots); the
latest session keeps the live list. Used by auto/monitor catch-up, replay
and simulate. _run_session takes precomputed targets only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-25 20:29:39 +08:00
parent cc1366b2dc
commit 905ca4fe05
2 changed files with 104 additions and 24 deletions

View File

@@ -174,6 +174,23 @@ class MembershipTests(unittest.TestCase):
# Never-member columns (benchmarks) are not masked.
self.assertTrue(mask["510300.SS"].all())
def test_past_sessions_never_hold_a_name_before_it_joined_the_index(self):
bars = _bars()
bars["close"]["S00"] *= np.exp(np.linspace(0, 3, len(bars["close"]))) # the best momentum
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)
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())
self.assertGreater(weights.loc[days[-1], "S00"], 0)
def test_changes_table_with_two_level_headers_is_found(self):
cols = pd.MultiIndex.from_tuples([("Effective Date", "Effective Date"), ("Added", "Ticker"),
("Removed", "Ticker"), ("Reason", "Reason")])