Skip to content

boom90lb/prism

Repository files navigation

Prism

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) and MARKETS.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 named prism (formerly trading-ensemble; old GitHub URLs redirect). Read SPEC.md first. 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 §10 kill-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 at mechanics_clean under 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-action predict); 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. See research/scripts/rl_seed_eval.py for the multi-seed overfitting study.


Methodology

Every choice below exists because the naive alternative makes a backtest look better than the strategy is. Read these before interpreting any number.

Purged, embargoed walk-forward CV (López de Prado, AFML §7.4)

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.

Execution model: target on close, fill at next open

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.

Baselines and legacy comparisons

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.

Overfitting-adjusted metrics

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.py over a real hyperparameter grid; the default target-weight backtest records it in the root claim packet when you pass --trial_sharpes_json.

Research claim packets

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.

Conformal prediction bands (EnbPI + ACI)

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.

Leakage audit

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.


How to read the backtest output

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/a when 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_json from 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.


Usage

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 tools

The 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=...

1. Train (produces a run directory the backtest replays)

# 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 200000

2. Backtest (requires a training run; never a bare symbol list)

python -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_orders

3. Hyperparameter sweep → honest DSR (forecast-only)

python -m research.scripts.sweep --symbols AAPL,MSFT
# writes selected_config.json + trial_sharpes.json (feed the latter to backtest)

4. Multi-seed RL overfitting study

# 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

5. Statistical arbitrage pair research

# 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).


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}.log that 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).


Data quality

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_dividendstarget_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 1d1day 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.


Out of scope

  • Point-in-time universe via a delisting database (CRSP/Norgate).
  • Sentiment distillation. The pipeline ships keyword SentimentAnalyzer and an interim FinBERT TransformerSentimentAnalyzer; the white-box distillation plan is documented in docs/sentiment_roadmap.md but not implemented.
  • A market simulator for multi-step synthetic OHLCV; positions are held flat across the prediction horizon rather than iteratively re-forecast.

Project layout

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

Configuration is split at the production/research boundary (SPEC §9):

  • src/prism/config.py — production: project directories, the Twelve Data key, and the ExecutionConfig/TradingConfig cost 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.


Dependencies

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.

License

MIT License, Copyright (c) 2026 Brendon Reperttang

About

Trading bot and research stack for directional ensembles and statistical arbitrage, focused on trustworthy out-of-sample evaluation.

Topics

Resources

License

Stars

5 stars

Watchers

1 watching

Forks

Packages

 
 
 

Contributors