Skip to main content

mts1b-research

Strategy discovery + ladder-sweep orchestration + factor library + dual-paper executors + drift signal store + factor-discovery factory.

Repo: github.com/MTS1B/mts1b-research Layer: 5 Wave: 2 (months 4-7) Depends on: foundation, platform, quantkit, portfolio, GPUbacktester, datalake, llm Audience: quant researchers + the live strategy executor in mts1b-oms

What it is

The strategy-discovery + live-execution glue. Wraps mts1b-GPUbacktester (compute engine) with the workflows MTS1B uses to find, validate, and deploy alpha:

  • Ladder sweep — L1→L5 staged elimination across millions of param combinations
  • Factor library — reusable factors registered via @register
  • Strategy registry — versioned strategy configs that live trade
  • Dual-paper executors — same strategy code on paper and live brokers
  • Factor-discovery factory — automated factor mining + ensemble composition
  • Drift monitor — watches live IC vs backtest IC, auto-throttles decayed factors

What it's NOT

  • ❌ Not the backtest engine itself — that's mts1b-GPUbacktester (pure compute)
  • ❌ Not the OMS / risk gates — those are mts1b-oms and mts1b-riskengine
  • ❌ Not the sizer — that's mts1b-portfolio

Module layout

mts1b_research/
├── ladder/
│ ├── flow.py # L1→L5 staged elimination
│ ├── stage.py # each ladder stage config
│ └── survivor_bridge.py # promote survivors to next stage
├── factory/
│ ├── canonical_alpha_factory.py
│ ├── canonical_signal_executor.py
│ ├── factory_ensemble.py
│ ├── paper_factory.py
│ └── multi_asset_pipeline.py
├── strategies/
│ ├── registry.py
│ ├── live_executor.py # listens to NATS, emits orders
│ └── dual_paper.py # paper + live in lockstep
├── factors/
│ ├── library/ # built-in factors (momentum, value, quality, ...)
│ └── registry.py
├── ops/
│ ├── drift_monitor.py
│ └── decay_throttle.py
└── api/
├── rest.py # FastAPI (register strategy, get drift status)
└── nats.py # publish signals, subscribe ladder events

Ladder sweep workflow

L1 (broad): 100k param combos
↓ keep top 1% by walk-forward Sharpe
L2: 1k param combos
↓ keep top 10% with stability_score > 0.5
L3: 100 combos
↓ multi-regime test (bull / chop / crash)
L4: 20 combos
↓ cost stress: must survive 2x backtested cost
L5: 5 final candidates → manual review → paper trade for 90d → live

Each stage writes to data/sweeps/ladder/<run_id>/{per_stage, disparities, survivors}.parquet. Survivors pass to the next stage.

from mts1b_research.ladder import run_ladder

await run_ladder(
factor_class="momentum",
param_grid={
"h_long": list(range(60, 365, 5)),
"h_skip": [0, 5, 21, 42],
"winsorize": [True, False],
},
universe="us-large-cap",
start="2010-01-01", end="2024-01-01",
cost_bps=5,
)

Factor registration

from mts1b_research.factors import register
from mts1b_quantkit.factors import zscore_cross_sectional
from mts1b_foundation.market_data import UniversePanel


@register("f_momentum_12_1")
def f_momentum_12_1(panel: UniversePanel, /, h_long: int = 252, h_skip: int = 21):
"""Classic 12-1 momentum."""
close = panel.close
ret = close[-h_skip-1] / close[-h_long-h_skip-1] - 1
return zscore_cross_sectional(ret)

The registry is queryable; backtest CLIs look up factors by name.

Strategy registry

from mts1b_research.strategies import StrategyRegistry, StrategySpec

await StrategyRegistry.register(
StrategySpec(
strategy_id="momentum_v3",
factor="f_momentum_12_1",
params={"h_long": 252, "h_skip": 21},
universe="us-large-cap",
rebal="monthly",
sizing="kelly_voltarget_12",
cost_bps=5,
enabled=True,
)
)

# List active strategies
for spec in await StrategyRegistry.active():
print(spec.strategy_id, spec.factor, spec.last_run)

Live executor

A long-running worker that:

  1. At each rebalance window, looks up active strategies.
  2. For each, computes the factor on the latest universe panel.
  3. Applies the sizer in mts1b-portfolio.
  4. Diffs against current mts1b-oms positions.
  5. Emits child orders via mts1b-oms.
mts1b-research live-executor run --fund-id paper-momentum --strategies-from registry

Dual-paper executor

For each strategy: run exactly the same code against a paper book AND a live book, log both. Used during the 90-day "promote to live" gate:

from mts1b_research.strategies import DualPaperExecutor

executor = DualPaperExecutor(
strategy_id="momentum_v3",
paper_fund="paper-momentum-demo",
live_fund="live-momentum",
diff_threshold_bps=10.0, # alert if paper and live diverge > 10bps
)
await executor.run()

Divergences between paper and live trigger Telegram alerts via mts1b-platform/messaging.

Drift monitor

# Runs every hour
for strategy in await StrategyRegistry.active():
drift = await measure_drift(strategy)
# Live IC vs backtest IC, last 30d

if drift.zscore < -2.0:
await StrategyRegistry.shadow(strategy.strategy_id, reason="decay")
await alert(f"strategy {strategy.strategy_id} shadowed: drift_zscore={drift.zscore:.2f}")
elif drift.zscore < -1.0:
await StrategyRegistry.halve_allocation(strategy.strategy_id, reason="early decay")

Factory ensembles

canonical_alpha_factory + factory_ensemble automate the "many weak factors → one strong portfolio" workflow:

  1. Discover N candidate factors via ladder sweeps.
  2. Filter by walk-forward IC + stability.
  3. Compose into an ensemble using HRP (or BL) on factor returns.
  4. The composite is a meta-strategy registered in the registry.
mts1b-research factory run \
--asset-class equities \
--candidates-per-class 50 \
--ensemble-method hrp \
--ensemble-name equities_meta_v1

NATS subjects

SubjectDirectionPayload
mts.v1.research.signals.publishedpublishSignal
mts.v1.research.ladder.stage_completepublishstage summary
mts.v1.research.drift.measuredpublishdrift snapshot
mts.v1.marketdata.bars.*subscribelatest bars for rebalancing

Build + test

pip install -e ".[dev]"
pytest -m unit
docker compose up -d postgres nats redis
pytest -m integration

Roadmap

VersionItems
0.1 (Wave 2)Ladder, factor library, registry, live executor, dual-paper
0.2 (Wave 2)Drift monitor, factory ensemble
0.3 (Wave 3)RL-based factor discovery (research)
1.0 (LTS)Stable strategy spec

See also