research: lagged futures signals for stock exposure vs matched ETF rules (none passes, retain baseline)
128 fixed runs (32 profiles x daily ledger / overnight-only x 2 costs) plus 12 post-hoc availability diagnostics on overnight_recovery_top10, 2006-10 to 2026-09. Futures trend/shock/rates rules do not beat the same rules on SPY/QQQ/IEF or a static 75% exposure; all six default schemes fail the gates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
64
tests/test_futures_stock_signals.py
Normal file
64
tests/test_futures_stock_signals.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from research.lab.futures_stock_signals import configurations, overnight_exposure, risk_exposure
|
||||
|
||||
|
||||
class FuturesTimingTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
dates = pd.bdate_range("2020-01-01", periods=180)
|
||||
t = np.arange(len(dates))
|
||||
equity = 100 * np.exp(.001 * t + .002 * np.sin(t))
|
||||
rates = 100 * np.exp(.0001 * t + .0002 * np.sin(t))
|
||||
self.prices = pd.DataFrame({"ES=F": equity, "NQ=F": equity, "ZN=F": rates,
|
||||
"SPY": equity, "QQQ": equity, "IEF": rates}, index=dates)
|
||||
|
||||
def test_future_prices_cannot_change_earlier_decisions(self):
|
||||
changed = self.prices.copy()
|
||||
changed.iloc[110:, :3] *= .7
|
||||
for config in configurations():
|
||||
original, known = risk_exposure(self.prices, config)
|
||||
modified, modified_known = risk_exposure(changed, config)
|
||||
last = 110 + config.get("lag", 1)
|
||||
with self.subTest(config=config["name"]):
|
||||
pd.testing.assert_series_equal(original.iloc[:last], modified.iloc[:last])
|
||||
pd.testing.assert_series_equal(known.iloc[:last], modified_known.iloc[:last])
|
||||
|
||||
def test_same_day_futures_crash_is_not_usable_for_entry_or_preceding_night(self):
|
||||
config = next(c for c in configurations() if c["name"] == "futures_equity_shock_default")
|
||||
changed = self.prices.copy()
|
||||
changed.iloc[110:, :2] *= .7
|
||||
entry, _ = risk_exposure(changed, config)
|
||||
self.assertEqual(entry.iloc[110], 1.0)
|
||||
self.assertEqual(entry.iloc[111], .5)
|
||||
night = overnight_exposure(entry)
|
||||
self.assertEqual(night.iloc[111], 1.0)
|
||||
self.assertEqual(night.iloc[112], .5)
|
||||
|
||||
def test_identical_etf_and_futures_inputs_have_identical_rules(self):
|
||||
configs = {c["name"]: c for c in configurations()}
|
||||
for name, config in configs.items():
|
||||
if not name.startswith("futures_"):
|
||||
continue
|
||||
with self.subTest(config=name):
|
||||
a, av = risk_exposure(self.prices, config)
|
||||
b, bv = risk_exposure(self.prices, configs[name.replace("futures_", "etf_", 1)])
|
||||
pd.testing.assert_series_equal(a, b, check_names=False)
|
||||
pd.testing.assert_series_equal(av, bv, check_names=False)
|
||||
|
||||
def test_unavailable_prices_are_not_backward_filled_from_future(self):
|
||||
config = next(c for c in configurations() if c["name"] == "futures_equity_shock_default")
|
||||
missing = self.prices.copy()
|
||||
missing.iloc[110, :2] = np.nan
|
||||
exposure, known = risk_exposure(missing, config)
|
||||
self.assertFalse(known.iloc[111])
|
||||
self.assertEqual(exposure.iloc[111], 1.0)
|
||||
prefix, prefix_known = risk_exposure(missing.iloc[:112], config)
|
||||
pd.testing.assert_series_equal(exposure.iloc[:112], prefix)
|
||||
pd.testing.assert_series_equal(known.iloc[:112], prefix_known)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user