Files
quant-trading/tests/test_live_pipeline.py
Gahow Wang 0d3642435b fix(data): judge late newest rows as sparse; require SPY print only in the ETF panel
Tickers count as alive for 10 rows after their last bar, so a newest row
printed by a few early tickers is dropped until the rest arrive. The
benchmark-print session rule now applies only to the mixed US/HK ETF panel:
Yahoo's 510300.SS lags CN stocks by days and would stall the CN book.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 01:51:02 +08:00

129 lines
5.9 KiB
Python

"""Contracts of the live pipeline: price panel refresh, session gating,
order sizing, and how results are reported."""
import unittest
from datetime import datetime, timezone
import numpy as np
import pandas as pd
import bridge
import data_manager
import trader
from main import backtest
def _panel(rows: dict[str, list], start: str = "2026-09-14") -> pd.DataFrame:
idx = pd.bdate_range(start, periods=len(next(iter(rows.values()))))
return pd.DataFrame(rows, index=idx, dtype=float)
class PanelRefreshTests(unittest.TestCase):
def test_refresh_overwrites_stale_window_and_keeps_missing_bars_raw(self):
stored = _panel({"AAA": [10, 11, 11, 11, 12], "BBB": [5, 5, 5, 5, 5]})
window_start = stored.index[2]
fresh = pd.DataFrame(
{"AAA": [11.5, np.nan, 12.2]}, index=stored.index[2:],
)
merged = data_manager._merge_refresh(stored, fresh, window_start)
self.assertEqual(merged.loc[stored.index[2], "AAA"], 11.5)
self.assertTrue(np.isnan(merged.loc[stored.index[3], "AAA"]))
self.assertEqual(merged.loc[stored.index[4], "AAA"], 12.2)
# Tickers not in the refresh keep their stored values.
self.assertEqual(merged.loc[stored.index[3], "BBB"], 5)
def test_refresh_rescales_history_after_split_adjustment(self):
stored = _panel({"AAA": [100, 102, 104, 106, 108, 110, 112]})
window_start = stored.index[2]
# Yahoo now reports the whole window divided by 2 (2:1 split adjusted).
fresh = stored.loc[window_start:, ["AAA"]] / 2
merged = data_manager._merge_refresh(stored, fresh, window_start)
returns = merged["AAA"].pct_change().dropna()
self.assertTrue((returns > 0).all(), returns)
self.assertAlmostEqual(merged["AAA"].iloc[0], 50.0)
def test_newest_row_printed_by_few_tickers_is_dropped(self):
panel = _panel({
"A": [1, 1, 1, np.nan], "B": [1, 1, 1, np.nan],
"C": [1, 1, 1, np.nan], "D": [1, 1, 1, 2],
})
self.assertNotIn(panel.index[-1], data_manager._drop_sparse_rows(panel).index)
def test_rows_printed_by_a_minority_of_tickers_are_dropped(self):
panel = _panel({
"SPY": [1, 1, np.nan, 1, 1],
"UPRO": [1, 1, np.nan, 1, 1],
"GLD": [1, 1, np.nan, 1, 1],
"7200.HK": [1, 1, 1, 1, 1],
})
cleaned = data_manager._drop_sparse_rows(panel)
self.assertNotIn(panel.index[2], cleaned.index)
self.assertEqual(len(cleaned), 4)
class SessionAndOrderTests(unittest.TestCase):
def test_todays_bar_is_not_a_close_until_the_session_settles(self):
raw = _panel({"SPY": [1, 1, 1]}, start="2026-09-22")
today = raw.index[-1]
intraday = datetime(2026, 9, 24, 17, 0, tzinfo=timezone.utc) # 13:00 New York
settled = datetime(2026, 9, 24, 20, 30, tzinfo=timezone.utc) # 16:30 New York
self.assertNotIn(today, trader.completed_sessions(raw, "us", "SPY", now=intraday))
self.assertIn(today, trader.completed_sessions(raw, "us", "SPY", now=settled))
def test_days_without_a_benchmark_print_are_not_sessions(self):
raw = _panel({"SPY": [1, np.nan, 1], "7200.HK": [1, 1, 1]}, start="2026-09-02")
later = datetime(2026, 10, 1, tzinfo=timezone.utc)
sessions = trader.completed_sessions(raw, "etfs", "SPY", now=later)
self.assertNotIn(raw.index[1], sessions)
def test_a_share_orders_use_board_lots_and_exit_odd_lots(self):
trades = trader.compute_trades(
holdings={"600000.SS": 150}, cash=100_000.0,
target_weights={"600519.SS": 0.5}, prices={"600519.SS": 170.0, "600000.SS": 10.0},
min_trade_value=50, integer_shares=True, lot_size=100,
)
by_ticker = {t["ticker"]: t["shares_delta"] for t in trades}
self.assertEqual(by_ticker["600519.SS"] % 100, 0)
self.assertEqual(by_ticker["600000.SS"], -150)
def test_tickers_without_a_fresh_price_are_not_traded(self):
trades = trader.compute_trades(
holdings={"UPRO": 10}, cash=5_000.0, target_weights={"TQQQ": 1.0},
prices={"UPRO": 150.0, "TQQQ": 80.0}, frozen={"UPRO"},
)
self.assertEqual([t["ticker"] for t in trades], ["TQQQ"])
class ReportingTests(unittest.TestCase):
def test_periodic_returns_include_the_first_day_of_each_period(self):
idx = pd.to_datetime(["2026-01-30", "2026-02-02", "2026-02-27", "2026-03-02"])
equity = pd.Series([100.0, 110.0, 110.0, 121.0], index=idx)
monthly, yearly = bridge.compute_periodic_returns(equity)
self.assertAlmostEqual(monthly["2026-02"], 0.10)
self.assertAlmostEqual(monthly["2026-03"], 0.10)
self.assertAlmostEqual(yearly["2026"], 0.21)
def test_injected_cash_is_not_reported_as_performance(self):
idx = pd.bdate_range("2024-01-02", periods=400)
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)
def test_ruined_portfolio_stays_at_zero(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)
if __name__ == "__main__":
unittest.main()