v0.3.0 re-founds this project. The authoritative documents are now
SPEC.md(the constitution — the cross-sectional score → residualize → construct → execute engine, the invariants, the claim tiers, and the kill-criterion) andMARKETS.md(the zero-budget market-structure analysis: which markets are core execution vs regime signal). The "ensemble of models" is no longer the organizing abstraction — it is demoted to one plug-in signal node. Package, distribution, and repository are all namedprism(formerlytrading-ensemble; old GitHub URLs redirect). ReadSPEC.mdfirst. The sections below describe the honest evaluation harness, which v0.3.0 keeps almost intact and builds on.Status of the alpha, stated plainly: the
SPEC.md §10kill-criterion fired on 2026-07-06 — across a pre-registered, fully counted 17-trial budget, no residual-reversion configuration achieved a deflated net Sharpe above zero under calibrated per-bucket costs, and the sleeve is archived. The negative result is the harness's first certification:docs/certifications/001-residual-reversion-daily-negative.md. The next candidate (monthly cross-sectional momentum, the demotion budget's side discovery) enters atmechanics_cleanunder its own pre-registered budget (docs/momentum_design.md). No configuration has ever cleared the deflated evidence bar; nothing is deployable today.
Prism is a cross-sectional systematic trading engine — score → residualize
→ construct → execute, conditioned by a regime layer — gated by an
evaluation harness built to produce honest out-of-sample numbers: purged
walk-forward CV, next-open fills with realistic costs, shared-capital
target-weight accounting, breadth / capacity / cost-toll diagnostics, and
overfitting-adjusted metrics (DSR, PBO). The production spine is the
survivorship-counted S&P residual path (src/prism/residual/) plus the
construction, execution, and regime machinery shipped in v0.3.0
(validation/{metrics,capacity}, execution/participation,
portfolio.step_no_trade_band, regime/). The classical forecaster ensemble
survives as one plug-in signal node (prism.signal.EnsembleSignalNode, an
XGBoost + ARIMA blend under the same harness); its heavier legacy members —
Prophet and the three reinforcement-learning policies (LSTM-PPO, xLSTM-PPO,
xLSTM-GRPO) — are quarantined research members under research/, off the
production import path (N8).
It also includes a separate statistical-arbitrage path for market-neutral pair
research: train-only cointegration discovery, residual stationarity and
multiple-testing filters, causal spread targets, capped portfolio weights, and
next-open costed accounting. See docs/stat_arb.md.
The mandate (SPEC.md §1) is a production-grade, zero-data-budget systematic
trading bot. Deployment is the goal and it is gated hard: capital is risked
only on edges that clear the claim-tier evidence bar (SPEC.md §10). The
harness is the bar — and it has produced its first verdict: the residual
sleeve was certified uneconomic at retail cost and archived after its
pre-registered trial budget was exhausted
(certification 001).
The current candidate under the bar is the momentum program
(docs/momentum_design.md), held to exactly the
tier its evidence supports.
See ARCHITECTURE.md for the end-to-end data flow (what
calls what), docs/operations.md for operational
gotchas (vendor tier, interval mapping, member contribution, per-bar cost),
docs/handoff.md for the long-horizon doctrine
behind the roadmap, and formal/ for the Lean 4
machine-checked kernel invariants.
Note on the RL members (research-side only). The three RL policies are quarantined under
research/and are not production signals (SPEC.md §8). They were stub implementations in the original codebase (hardcoded losses, random-actionpredict); they now have real gradient updates and end-to-end fit/predict. If you extend them, check that the policy actually takes gradient steps and that a windowed member produces non-flat positions before trusting any RL-attributed Sharpe — under a small training budget an undertrained policy legitimately produces ~zero positions and a NaN Sharpe. Seeresearch/scripts/rl_seed_eval.pyfor the multi-seed overfitting study.
Every choice below exists because the naive alternative makes a backtest look better than the strategy is. Read these before interpreting any number.
src/prism/validation/walk_forward.py:PurgedWalkForward drives both training and
backtest. Two distinct leakage controls:
- Purge — training rows whose forward-label window overlaps the test slice
are dropped (
purge_horizon, defaulting to the prediction horizon, since the label is a horizon-bar forward return). - Embargo — a buffer after each test slice is excluded from subsequent
folds (
embargo_pct).
Both research/scripts/training.py and research/scripts/backtest.py iterate the same fold
structure: training writes split_idx.npz per fold; the backtest replays the
identical test-date ranges. There is no 80/20 split anywhere.
The default research/scripts/backtest.py path uses
src/prism/execution/target_weights.py: each model emits a continuous target weight
at bar t's close, the target fills at the next bar's open, and PnL accrues
only after that fill. Fold-last targets are dropped so a pending order cannot
leak across folds; already-filled weights continue into the next fold. Small
same-side changes can be suppressed with --rebalance_band_weight, and rows are
scaled to --max_gross_exposure.
The legacy order path remains available with --legacy_orders. It uses
src/prism/execution/execution_model.py: a signal computed on bar t's close is
translated to LONG/SHORT/FLAT orders and filled at bar t+1 (market-on-open by
default, --order_type MOC for market-on-close). Nothing fills same-bar.
Costs applied in both paths:
- half-spread (
--spread_bps) + linear notional impact (--slippage_coeff), - optional ADV participation impact (
--adv_impact_coeff,--adv_floor_dollars) when dollar-volume panels are available, - commission in bps (
--commission_bps), - daily borrow on open short notional (
--borrow_bps_annual) — shorts are not free.
Reported PnL is net of all of the above on a fold-aligned equity curve.
research/baselines/: Buy-and-Hold, MA-crossover (--ma_fast/--ma_slow), and
time-series momentum (--tsmom_lookback). In --legacy_orders mode they
traverse the same fold structure with the same costs, so the comparison is
fair and the cross-strategy PBO is well-defined. The default target-weight mode
emits one shared portfolio packet rather than per-symbol baseline tables.
src/prism/validation/metrics.py:
- PSR (Probabilistic Sharpe Ratio) — skew/kurtosis-adjusted probability the true Sharpe exceeds 0.
- PBO (Probability of Backtest Overfitting) via CSCV — across the {ensemble + baselines} strategy set, the fraction of IS/OOS splits where the IS-best strategy underperforms OOS. High PBO ⇒ the selection is overfit.
- DSR (Deflated Sharpe Ratio) — PSR with the benchmark set to the expected
maximum Sharpe under the False Strategy Theorem given the number of trials.
Computed by
research/scripts/sweep.pyover a real hyperparameter grid; the default target-weight backtest records it in the root claim packet when you pass--trial_sharpes_json.
src/prism/validation/trials.py defines the canonical research-trial packet used to
turn script outputs into a publishable claim surface. A packet records the
strategy, config hash, code commit, data convention, artifact manifest,
gross/net/cost metrics, trial count/DSR when available, and a claim tier:
mechanics_clean, gross_edge, net_edge, or robust_edge.
research/scripts/backtest.py, research/scripts/sweep.py, research/scripts/rl_seed_eval.py, and the
stat-arb CLIs write claim_packet.json; new strategy surfaces should do the
same before any result is described as more than a mechanics smoke.
src/prism/conformal/. The ensemble emits a position band, not just a point. EnbPI
reuses the meta-learner's out-of-fold residuals for finite-sample-valid
intervals (block-cross-conformal, not split conformal — the latter assumes
exchangeability that time series violate). ACI (Gibbs & Candès) adapts the
miscoverage level α online as outcomes realize. Wide (uncertain) bands dampen
position size in trading.calculate_signal.
Done before relying on any WFO number (a WFO over leaky features is a
well-organized lie). Closed leaks: point-in-time UTC sentiment bucketing
(searchsorted against bar-close times, no across-bar ffill), train-only
feature clipping bounds, per-fold scaler refits. See research/sentiment_analysis.py
and research/features.py plus tests/test_sentiment_leakage.py /
tests/test_feature_engineer_leakage.py.
By default, research/scripts/backtest.py writes one shared-capital portfolio under
results/wfo_backtest_*:
target_weights.csv— close-time portfolio targets.fill_weights.csv— weights actually filled at next opens.costs.csv— turnover, gross/net exposure, borrow, execution costs, and dividend return contribution.equity_curve.csv— portfolio value and net returns.claim_packet.json— canonical result packet with config/data/artifact identity and claim tier.
With --legacy_orders, the script preserves the older per-symbol report and
prints a per-strategy comparison block:
Strategy TotRet AnnRet Sharpe PSR(>0) MaxDD Calmar
- TotRet / AnnRet — total / annualized return, net of costs.
- Sharpe — annualized; a headline number, not the one to trust alone.
- PSR(>0) — probability the true Sharpe is positive after skew/kurtosis adjustment. Treat a Sharpe with low PSR as noise.
- MaxDD — maximum drawdown (positive-magnitude convention).
- Calmar — annualized return / max drawdown;
n/awhen undefined.
Legacy footers (one per backtest, not per strategy):
- PBO — across all strategies in the table. The single most important line: a strong ensemble Sharpe with PBO near or above 0.5 means the result is likely selection-overfit.
- Deflated Sharpe Ratio — shown when
--trial_sharpes_jsonfrom a sweep is supplied; the ensemble Sharpe deflated by the trial count. Read DSR, not raw Sharpe, when a grid was searched.
Per-strategy returns are inner-joined by date before PBO so the matrix is
fold-aligned. In default target-weight mode, read the root claim_packet.json
and metrics.json first.
Environment (Python 3.12+, uv):
git clone https://github.com/boom90lb/prism.git
cd prism
uv sync --extra research # full research env: core + jax/torch/mlflow + dev toolsThe core prism package itself installs slim (uv pip install -e . — no
JAX/torch/mlflow, SPEC N8). Every research/scripts CLI below needs the
[research] extra, so use the uv sync --extra research form to run them.
API keys in a .env (Twelvedata for bars/dividends; Polygon for sentiment):
TWELVEDATA_API_KEY=...
POLYGON_API_KEY=...
# Per-symbol purged WFO; writes runs/{run_name}/{symbol}/fold_*/.
python -m research.scripts.training --symbols AAPL,MSFT,GOOG --start_date 2018-01-01 \
--horizon 5 --n_splits 5
# Universe file + as-of point-in-time inclusion guard.
python -m research.scripts.training --universe data/universe/2026-05-30.txt \
--universe_asof 2018-01-01
# RL members at a real training budget (the default 100k is heavy).
python -m research.scripts.training --symbols AAPL --models xgboost,lstm_ppo \
--rl_timesteps 200000python -m research.scripts.backtest --training_run runs/{run_name}
# With a sweep's trial Sharpes so the report shows DSR:
python -m research.scripts.backtest --training_run runs/{run_name} \
--trial_sharpes_json results/{sweep}/trial_sharpes.json
# Preserve the old per-symbol order/backtest table path:
python -m research.scripts.backtest --training_run runs/{run_name} --legacy_orderspython -m research.scripts.sweep --symbols AAPL,MSFT
# writes selected_config.json + trial_sharpes.json (feed the latter to backtest)# EXPENSIVE: |members| x |symbols| x |folds| x |seeds| bare RL fits.
python -m research.scripts.rl_seed_eval --training_run runs/{run_name} \
--members lstm_ppo --seeds 0,1,2 --rl_timesteps 50000# Rolling formation/test walk-forward. This is the credible research path.
python -m research.scripts.stat_arb_wfo --symbols AAPL,MSFT,GOOG,AMZN,META,NVDA \
--start_date 2020-01-01 --formation_bars 504 --test_bars 63 --max_pairs 5
This is the only path in the repo that should currently be called
"arbitrage-like": it forms cross-asset hedge-ratio books rather than independent
single-symbol bets. The WFO command writes fold-level pair selection ledgers,
target weights, returns, costs, pair-trial Sharpes, and a pair_set_dsr field
so pair-search bias is visible rather than hidden behind a raw Sharpe.
Add --verbose to any script for console DEBUG output (the per-run log file is
always DEBUG regardless — see Logging).
src/prism/logging_utils.configure_logging (called by every script's main)
installs:
- a console handler at INFO (DEBUG with
--verbose), and - a rotating file handler at
logs/run_{ts}.logthat always captures DEBUG (50 MB cap, 5 backups) — the file is the complete record even when the console is quiet.
Per-symbol log records carry extra={"symbol": ...} (via get_symbol_logger),
so the file is greppable by ticker and aligns with the per-symbol MLflow runs.
logs/ is gitignored.
MLflow tracks a parent run per invocation, nested per symbol, with per-fold
metrics at step=fold_idx. MLFLOW_TRACKING_URI defaults to a local
file://.../mlruns store; sqlite:///mlflow.db is the documented migration
target (the file backend is deprecated).
Corporate actions. Prices are fetched adjust=splits — split-adjusted but
not dividend-adjusted, so the close is a faithful tradeable price.
Dividends are credited explicitly in the backtest (DataLoader.fetch_dividends
→ target_weights.py dividend-return contribution, or
TradingStrategy.apply_dividends in legacy order mode): a long held over an
ex-date takes a mark-to-market markdown that the dividend credit offsets (a
short is debited), making the position total-return correct without
back-adjusting prices. Trade-off: ex-dividend gaps remain in the return/volatility
features. --no_dividends disables the credit.
Cache integrity. Cached bars are keyed by requested range
({symbol}_{interval}_{start}_{end}.parquet) and reused only when the cached
range contains the request, so a narrow cache can't silently satisfy a wider
query with a too-short slice.
Operational gotchas (vendor tier limits, the 1d→1day interval mapping,
which ensemble members actually feed the backtest, and the per-bar JAX cost)
are documented in docs/operations.md.
Universe & survivorship. --universe <file> (one symbol per line, #
comments) and --universe_asof YYYY-MM-DD (drop names with no data
at/before the date) give a best-effort point-in-time universe on the
included names. It does not recover delisted/acquired tickers — true
survivorship-bias-free construction needs a delisting database (CRSP, Norgate),
which is out of scope. Read results with that residual survivorship bias in
mind.
- Point-in-time universe via a delisting database (CRSP/Norgate).
- Sentiment distillation. The pipeline ships keyword
SentimentAnalyzerand an interim FinBERTTransformerSentimentAnalyzer; the white-box distillation plan is documented indocs/sentiment_roadmap.mdbut not implemented. - A market simulator for multi-step synthetic OHLCV; positions are held flat across the prediction horizon rather than iteratively re-forecast.
src/prism/ (the distribution — production import path)
config.py production config: directories, spine API key,
ExecutionConfig / TradingConfig cost dataclasses
io/ loader (Twelvedata bars + dividends, range-keyed cache +
incremental store), PIT universe, token-bucket rate limit
signal/ the Signal contract + nodes: EnsembleSignalNode
(JAX-free XGBoost/ARIMA blend), ResidualSignalNode
residual/ factor model + causal OU s-scores + hedged book builder
portfolio/ book construction: caps, no-trade bands (batch + online step)
execution/ target-weight accounting, ExecutionModel + cost functions,
participation gate, per-bucket spread estimator
regime/ curve / vol / inflation / net-liquidity regime state
($0 sources) + FRED/DefiLlama fetch adapters
live/ durable order state, write-ahead daily loop,
Alpaca paper/live broker adapter
validation/ PurgedWalkForward, metrics (PSR/PBO/DSR/Calmar + FLAM breadth),
capacity / cost-toll, research claim packets
conformal/ EnbPI + ACI
logging_utils.py configure_logging + per-symbol adapter
scripts/ build_sp500_universe (periodic PIT universe build),
paper_loop (nightly Alpaca paper cycle)
research/ (quarantined per SPEC §9 — not in the wheel; may import
prism, never the reverse)
config.py ensemble-side config: member/ensemble/training dataclasses
trading.py the legacy v0.2 per-symbol engine (TradingStrategy)
features.py legacy technical indicators, train-only clip bounds
sentiment_analysis.py keyword + FinBERT analyzers, PIT bucketing
models/ legacy forecast members (arima, prophet, xgboost) +
EnsembleModel, registry, vol-sizing mapping;
RL policy members: lstm_ppo, xlstm_ppo, xlstm_grpo (JAX)
baselines/ buy-and-hold, MA-crossover, TSMOM
arbitrage/ cointegration pair scan + stat-arb WFO fold ledgers
tracking/ MLflow wrappers
scripts/ batch CLIs, run as `python -m research.scripts.<name>`:
training.py per-symbol purged-WFO training → runs/{run_name}/
backtest.py target-weight WFO, legacy order WFO + baselines/PBO
sweep.py ensemble-layer grid → DSR
rl_seed_eval.py multi-seed RL overfitting study
stat_arb_wfo.py rolling formation/test pairs stat-arb WFO
formal/ Lean 4 machine-checked kernel invariants (N4 ledger
conservation, no-trade-band hysteresis + batch-replay
divergence, purge/embargo geometry, participation gate)
tests/ 636+ offline tests (validation, leakage, execution,
conformal, live loop, logging); the slim subset runs
without the [research] extra in CI
Configuration is split at the production/research boundary (SPEC §9):
src/prism/config.py— production: project directories, the Twelve Data key, and theExecutionConfig/TradingConfigcost dataclasses consumed by the execution/accounting path.research/config.py— the legacy ensemble side:ModelConfig/EnsembleConfig/TrainingConfig,DEFAULT_MODEL_WEIGHTS(the default forecast members; the quarantined RL members are opt-in via research CLIs and fall back to weight 1.0), the MLflow tracking URI, and the Polygon news key.
Both halves validate fields in __post_init__ (e.g. n_splits >= 2,
0 <= embargo_pct < 1, 0 < position_size <= 1,
borrow_rate_bps_annual >= 0); invalid CLI arguments fail fast at config
construction.
Core (the production import path, SPEC N8): Python 3.12+ · numpy, pandas,
scikit-learn · statsmodels, XGBoost · pyarrow · the Twelvedata API.
[research] extra: Prophet, Flax/JAX, PyTorch, transformers (FinBERT),
Gymnasium, MLflow, matplotlib, and the Polygon.io news API.
MIT License, Copyright (c) 2026 Brendon Reperttang