Files
quant-trading/signals.py
Gahow Wang 722cade974 fix: unify backtest execution and add forward validation candidates
Share point-in-time signals and a cash-funded order ledger across backtests and trading. Correct eligibility, missing-price fills, quality factors, regime inputs, warmup windows, and cash-injection accounting. Add reproducible registered-strategy comparisons and paired forward candidates with a 20% resize band.

Validation: 125 Python tests, 23 web tests, TypeScript/Vite build, 87 strategy causality checks, and real-data target/order/equity parity for 10 strategies.
2026-10-01 02:24:01 -07:00

45 lines
2.3 KiB
Python

"""One timing and point-in-time eligibility contract for every execution path."""
import numpy as np
import pandas as pd
from strategies.base import BarStrategy
def session_targets(strategy, close: pd.DataFrame, open_data: pd.DataFrame | None = None,
bars: dict[str, pd.DataFrame] | None = None,
eligible: pd.DataFrame | None = None) -> pd.DataFrame:
if close.empty:
return pd.DataFrame(0.0, index=close.index, columns=close.columns)
member = (eligible.reindex_like(close).fillna(False).astype(bool) if eligible is not None
else pd.DataFrame(True, index=close.index, columns=close.columns))
if isinstance(strategy, BarStrategy):
inputs = dict(bars if bars is not None else {"close": close})
inputs["eligible"] = member & inputs["close"].notna()
weights = strategy.generate_signals(inputs)
known = member.shift(1, fill_value=False)
else:
# Price history remains observable before index admission. Membership
# restricts cross-sectional selection and the eventual execution targets.
inputs = close if open_data is None else open_data.reindex_like(close)
native = getattr(strategy, "accepts_eligibility", False)
kwargs = {"eligible": member} if native else {}
inputs = inputs if native else inputs.where(member)
if open_data is not None:
nxt = inputs.index[-1] + pd.Timedelta(days=1)
padded = pd.concat([inputs, inputs.iloc[[-1]].set_axis([nxt], axis=0)])
if native:
kwargs["eligible"] = pd.concat([member, member.iloc[[-1]].set_axis([nxt], axis=0)])
weights = strategy.generate_signals(padded, **kwargs).shift(-1).reindex(close.index)
known = member
else:
weights = strategy.generate_signals(inputs, **kwargs)
known = member.shift(1, fill_value=False)
weights = weights.reindex(index=close.index, columns=close.columns).fillna(0.0)
weights = weights.where(known & member, 0.0)
arr = weights.to_numpy()
if (not np.isfinite(arr).all() or (arr < -1e-10).any()
or (weights.sum(axis=1) > 1 + 1e-8).any()):
raise ValueError("Strategy produced non-finite, short or unfunded targets")
return weights.clip(lower=0.0)