Contents
The repository has three trees that matter: bot/ (the engine), site/ (this dossier you're reading), and research/ (a paper trail of source-of-truth notes). Below is a fully traversed map. Skip with the table of contents.
bot/, site/, research/.
02Data layerBars, caches, earnings, sectors, the universe scanner.
03Regime gateWhen the bot is allowed to be long.
04Strategy contractSignal + SetupModule ABC.
05Active swing setupsThe seven daily-bar strategies wired into paper.py.
06Swing research shelfSix dormant swing modules kept for future work.
07Intraday setupsTwo paper candidates plus ten research modules.
08Discipline overlaysBreitstein harness, mistake filters, theme caps.
09Risk & sizingRisk-percent vs notional, the asymmetric R-ratchet.
10Backtest pipelineRunners, configs, results, sweeps.
11Promotion gatesWhat it takes to leave the research shelf.
12Paper orchestratorpaper.py: brief, scan, manage.
13Site & data flowHow dashboard.json and all_trades.json are built.
14Recent trajectoryWhat the last ten commits tell you.
15GlossaryADR, R, MFE, OOS, PF, CAGR, mDD, CIR.
01 — Repository map
The repo separates concerns hard: the engine never reads from site/, and the site never imports Python. The only contract between the two is a small set of JSON files written by build scripts in bot/scripts/ and read by static pages in site/.
trader/
├── bot/ # Python engine
│ ├── lib/ # Reusable subsystems
│ │ ├── indicators.py # SMA, EMA, ATR, ADR%, ADX, Donchian, anchored VWAP, VCP score
│ │ ├── data/ # Bars, caches, earnings, sectors, the universe scanner
│ │ ├── universe/ # Relative-strength tables and trend templates
│ │ ├── regime/ # SPY bull-gate (longs blocked when off)
│ │ ├── setups/ # Strategy contract — Signal dataclass + SetupModule ABC
│ │ ├── setups_swing/ # 15 daily-bar strategies (3 active Kullamägi + 4 novel + 8 shelf)
│ │ ├── setups_intraday/ # 14 minute-bar strategies (2 candidates + 12 research)
│ │ ├── discipline/ # Breitstein harness, mistake filters
│ │ ├── risk/ # Position sizing helpers
│ │ ├── execution/ # (reserved — routing lives in runners)
│ │ ├── broker/ # base, alpaca, ibkr adapters
│ │ └── journal/ # Trade reporting
│ ├── backtests/ # Walk-forward harnesses + YAML configs + per-run results
│ │ ├── swing_runner.py # Daily-bar walker (production)
│ │ ├── intraday_runner.py # Minute-bar walker (research)
│ │ ├── daily_rotating_intraday_runner.py # Daily rebalance of an intraday strategy
│ │ ├── ensemble_intraday.py # Multi-strategy intraday harness
│ │ ├── portfolio_runner.py # Portfolio-level backtest
│ │ ├── pattern_miner.py # Rule extraction from profitable backtests
│ │ ├── sweep.py # Parameter grid search
│ │ ├── configs_swing/ # 16 YAML configs
│ │ ├── configs_intraday/ # 15 YAML configs
│ │ ├── results_swing/ # 150+ run dirs (metrics.json, equity_curve.csv, trades.csv)
│ │ ├── results_intraday_rotating/
│ │ ├── results_portfolio/
│ │ ├── sweep_results/
│ │ └── pattern_results/
│ ├── scripts/ # Operational tooling
│ │ ├── paper.py # Live paper-trading orchestrator (brief / scan / manage)
│ │ ├── validate_promotions.py # Promotion-gate checker → site/data/promotion_report.json
│ │ ├── build_dashboard.py # Backtest results → site/data/dashboard.json
│ │ ├── compile_trades.py # Trades → site/data/all_trades.json
│ │ ├── fetch_full_universe.py # Polygon 1-min cache populator
│ │ └── refresh_polygon_meta.py # Refresh ticker metadata
│ ├── paper/ # Paper-trade ledger (jsonl)
│ ├── data_cache/ # Daily bars + universe snapshots + earnings + sectors
│ ├── data_cache_intraday/ # Polygon 1-min bars
│ ├── data_cache_polymarket/ # (parked — predictions cache)
│ ├── algos/ # LEAN entry points (kept for parity, currently minimal)
│ ├── ops/ # kill_switch.py, daily_reconcile.py
│ └── tests/ # Smoke tests
│
├── site/ # Static dossier — deployed to Vercel, root = site/
│ ├── index.html # Methodology, ~1,000-name funnel, charlatan filter
│ ├── ten.html # The ranked ten traders
│ ├── kullamaggie.html # Kullamägi corpus deep-dive
│ ├── architecture.html # One-screen architecture overview
│ ├── internals.html # ← you are here
│ ├── dashboard.html # Status + per-strategy charts (reads dashboard.json)
│ ├── trades.html # Interactive trade browser (reads all_trades.json)
│ ├── styles.css # Shared stylesheet
│ └── data/ # Generated JSON: dashboard.json, all_trades.json,
│ # promotion_report.json, trade_metadata.json
│
└── research/ # Source-of-truth notes (not imported by code)
├── top-10-traders-2026.md # Methodology corpus behind the ranked ten
├── BOT_OUTLINE.md # Architecture roadmap
└── dashboard-plan.md # Future cockpit notes
Two design choices to call out. First, nothing in site/ imports anything from bot/ — the bridge is a small set of JSON files written into site/data/ by scripts. That makes the site a deployable static artifact without a Python runtime on the edge. Second, research/ is read-only narrative — methodology notes the bot is built from, not configuration the bot consumes. Whenever you see a number in the bot whose source isn't obvious, the chain leads back to a markdown file in research/.
02 — Data layer
Everything starts here. The bot operates point-in-time: at any historical bar, the only data visible is what was knowable on that day. The data layer is the contract that enforces it.
Indicators — lib/indicators.py
A small library of pure functions over a bars frame. None of them store state, all of them are written so the result at index i depends only on rows ≤ i. The set:
| Function | What it computes |
|---|---|
sma(s, n) | Simple moving average |
ema(s, n) | Exponential moving average |
atr(df, n=14) | EMA-based Average True Range |
adr_pct(df, n=20) | Kullamägi's mean(high/low) − 1 as a percent — the volatility filter that decides whether a name even qualifies |
adx(df, n=14) | Trend-strength index |
donchian(df, n=20) | Highest high / lowest low over n bars |
anchored_vwap(df, anchor) | VWAP from a chosen anchor index forward |
rolling_percentile_rank(s, w) | Cross-time percentile rank — used for momentum and volatility-regime detection |
volatility_contraction(df, n=60) | 0–1 VCP score for tight-base detection |
Bars & caches — lib/data/
Eleven files. Daily bars come from Massive.com (Polygon-compatible, ~5,300 US common stocks cached since 2010). Intraday 1-minute bars come from Polygon (Stocks Starter tier — 5y of history, no per-minute cap, $29/mo). Both are cached locally so backtests don't pay network round-trips on every reload.
| File | Role |
|---|---|
daily_bars.py | Top-level loader. load_daily(symbol, start, end) resolves cache → Massive → Polygon fallback. |
massive.py | Massive.com REST adapter. Pulls all ~5,300 US common stocks with sector + shares-outstanding metadata. |
polygon.py · polygon_daily.py | Polygon endpoints — minute and daily — with on-disk caching. |
universe_scanner.py | The leader-universe scanner. See below — this is the single biggest piece of plumbing in the bot. |
daily_watchlist.py | Loads the universe for a given date. |
earnings.py | Local earnings cache. has_earnings_within(symbol, days) drives the catalyst gate. |
sectors.py | Sector ETF mapping. is_biotech() is the biotech-blow-up filter. |
session.py · market.py | Market session helpers — open/close, trading days, regime helpers. |
The universe scanner
The corpus rule (Kullamägi, episode 212): trade only stocks that are in the top 1–2% of returns simultaneously across multiple lookback horizons. The old version of this bot hardcoded 25–40 megacap tickers per strategy — which is the dominant reason the original backtests lost money, because megacaps don't have ADR ≥ 4 and they aren't where breakouts live. The current implementation:
~5,300 US common stocks (cached daily bars)
│
▼
universe_scanner.leader_universe(as_of, filters)
─ cross-sectional percentile rank of ROC over 1m / 3m / 6m
─ keep names in top 15% on at least 2 of 3 timeframes
─ require ADR ≥ 4, $5M dollar volume, close above 50-SMA
─ exclude biotech (sector blow-up risk)
─ sort by composite momentum score; take top N
│
▼
Cached as data_cache/universes/{key}.json (cold ~30s, warm instant)
│
▼
Runners rebuild every Monday — strategies scan only this pool
The UniverseFilters dataclass exposes top_n (200 for breakout, 300 for episodic-pivot and shorts), min_dollar_volume, min_adr_pct, momentum_periods, and momentum_pctile_min. Configs in bot/backtests/configs_swing/ set the "dynamic_leader" universe and override these values per strategy. A 3-year backtest performs roughly 150 weekly rebuilds; the cache means a re-run is effectively free.
03 — Regime gate
A single-file subsystem with one job: decide whether the broad tape is hospitable to long entries. lib/regime/spy_regime.py implements the corpus rule — SPY's 10-day SMA above the 20-day SMA, and both rising over the past five bars. When the gate is closed, swing-long signals don't generate. Shorts are not gated; they want a hostile tape.
The regime gate is intentionally simple. The corpus is unanimous on this point: in every drawdown the bot has tested through, the regime gate would have prevented the worst trades. Cleverer regime detection adds parameters, parameters get over-fit, over-fit regime models miss the next regime change. Two moving averages on SPY is the lowest-degree-of-freedom rule that actually works.
04 — Strategy contract
Every strategy in the repo derives from one tiny abstract base class. The contract is what makes it possible to wire dozens of setups into a single runner without bespoke glue.
lib/setups/base.py
Two pieces:
Signal— a dataclass capturing one trade idea:symbol,side(long/short),entry,stop,target,risk_pct(fraction of equity risked if stop is hit),setupname,traderattribution,rationale,timestamp. The dataclass exposes anotional_pct()method that converts risk-to-stop into the leverage-capped notional allocation a runner should use.SetupModule— abstract base class with three methods every strategy must implement:precompute(bars)— derive any indicators that need to exist before scanning. Pure function over the bars frame, must be point-in-time-safe.scan(bars, context)— return a list ofSignals (often zero, occasionally one, rarely many) given the precomputed bars and runner context (date, regime, open positions, universe).exit_rules(position, bars)— given an open position and the latest bars, return eitherNone(hold) or an exit instruction (price, reason).
The contract is deliberately narrow. The runner owns dates, position-sizing, slippage, fees, journaling, regime gates, theme caps, and the discipline overlays. The strategy module owns one thing: what is the setup, and where does the stop go. When a new strategy is added, the diff is one file in setups_swing/ or setups_intraday/, plus one YAML in configs_*/. No runner changes.
05 — Active swing setups
Seven daily-bar strategies are currently wired into bot/scripts/paper.py as ACTIVE_STRATEGIES. Three are the Kullamägi corpus implementations carried since the rebuild; four are novel mechanics added in the last commit cycle.
Kullamägi Breakout Swinglib/setups_swing/kullamaggie_breakout_swing.py
Stock is in the top 1–2% of return leaders, has built a 2–8 week tight base with rising 10/20/50 MAs and tightening range, and finally closes above the base high on volume ≥ 1.5× the 20-day average. Entry is the breakout close. The stop is the breakout bar's low, capped at one ADR. Exit is a 10-EMA trail by default; calendar partials are opt-in (and turned off — three independent corpus studies showed them hurting returns).
This is the canonical Kullamägi setup — the one most retail traders recognize from his Twitter posts. The implementation precomputes ema10, sma50, prior 1-/3-month moves, volume ratios, and ADR. Base detection is a rolling-window check over a configurable window for higher lows + tightening range.
Kullamägi Episodic Pivot Swinglib/setups_swing/kullamaggie_episodic_pivot_swing.py
A stock that has been dormant for 3–6 months gaps up at least 10% on a real catalyst (typically earnings), trades on volume ≥ 5× the 20-day average, closes in the upper 60% of the day's range, and hadn't already extended (prior 3-month return ≤ 40%, gap ≤ 50%). Entry is the close above the gap. The stop is the entry day's low, capped at 1.5× ADR. A strict mode requires a confirmed earnings event within two trading days.
Sourced from HackerTrader's 64% CAGR study — only four features were load-bearing in the original analysis, and the implementation honours that minimalism.
Kullamägi Parabolic Short Swinglib/setups_swing/kullamaggie_parabolic_short_swing.py
Two flavours, gated by market cap. For large caps (>$5B) a 50–100% run; for small caps a 300–1000% run. Either way the stock is in day 3 or later of a parabolic rally — three to five consecutive up days, extension at least 30% above the 10-SMA. The trigger is a red close that breaks the prior day's low; entry only fires day 3+ (never on a secondary push). Stop is the entry day's high. Cover at the 10-SMA, with a hard 10-day max-hold cap.
The corpus is explicit that this is "the riskiest if done wrong." Borrow cost (~3.6%/yr) is subtracted from short-side P&L in the runner so backtest results don't flatter live performance. Strict-cap mode treats unknown-cap names as small (requiring the 300% run threshold) rather than guessing.
Accrual-Volume Divergence (AVD)lib/setups_swing/accrual_divergence_swing.py
Builds a signed-dollar-volume accumulator using close-location-value (CLV) as the sign weight, then ranks the 20-day cumulative cross-sectionally against the same stock's 20-day return.
clv = ((close - low) - (high - close)) / (high - low) in [-1, +1] signed_dv = clv * close * volume 20-day sum of signed_dv = "absorbed dollar flow" with directional bias
Trigger: first close above the prior bar's high with strong CLV (≥ 0.5) — the absorption phase resolves into upward markup. Stop is tight (low of the trigger bar); the asymmetric R-ratchet handles exits.
Realized Vol Term-Structure Backwardation (TSC)lib/setups_swing/vol_term_backwardation_swing.py
Reconstructs an implicit "realized vol term structure" from nested-window realized vol on the underlying alone — no options data. A monotonically backwardated curve (RV5 > RV20 > RV60) means short-term vol is rising faster than long-term, the signature of variance preceding drift when fresh capital arrives.
Only fires when the stock isn't already extended — explicitly avoids buying blow-off tops. Stop is in dollar units of one daily-vol unit (auto-scales). The asymmetric R-ratchet handles exits.
ROC Skew Convergence (ROCS)lib/setups_swing/roc_acceleration_swing.py
Three-timeframe ROC alignment with annualisation-adjusted acceleration. Most multi-timeframe momentum filters require all timeframes positive (1m/3m/6m up). ROCS additionally requires each shorter horizon to be growing faster than the longer one when annualised:
annualised_5d = (1 + roc_5d)^(252/5) − 1 annualised_21d = (1 + roc_21d)^(252/21) − 1 annualised_63d = (1 + roc_63d)^(252/63) − 1 required: annualised_5d > annualised_21d > annualised_63d > 0
This catches the early part of a parabolic phase, before peak. Standard all-positive momentum filters trigger throughout the move — including the top — whereas ROCS only fires while acceleration is still increasing. An anti-overshoot guard requires the ATR-percentile to sit in the middle 50% of its own annual distribution, which skips already-blown-off names. Stop is one ADR; the asymmetric R-ratchet handles exit.
Volume-Range Divergence Coil (VRDC)lib/setups_swing/coil_release_swing.py
Detects 5-day windows where dollar volume is above its 20-day mean while range is below its 20-day mean. The decoupling is the fingerprint of an "auction balance" being defended — more shares crossing inside a tightening band.
The 5-day average close-in-range bias (CIR) votes direction: cir_avg_5 ≥ 0.55 long, ≤ 0.45 short. Trigger is a close breaking coil_high (long) or coil_low (short) on conviction volume and today's CIR confirms direction. Stop is the opposite end of the coil — intentionally wide R, which the asymmetric R-ratchet then locks in.
06 — Swing research shelf
Six additional swing modules sit in lib/setups_swing/ with YAML configs in configs_swing/, but are not wired into paper.py. They exist for backtesting, comparison, and possible future promotion. They're listed here so the inventory is complete.
| Module | Concept |
|---|---|
failure_to_fail_swing | A failed test of prior support becomes the entry — the move higher confirms by virtue of not breaking down. |
hidden_strength_outside_swing | Outside-day reversal where intraday close pattern reveals strength masked by the wide range. |
open_drive_failure_swing | Open drive into a key level fails, reverses; entry is the reversal. |
regime_flip_gap_swing | A single-day regime-shift gap; treats the gap day as the start of a new regime, not noise. |
volatility_squeeze_release_swing | Classic VCP-style squeeze followed by directional release. Predates the volume-range coil version above. |
volume_range_cascade_swing | Multi-bar volume-cascade pattern (sustained volume increase across a contracting range). |
07 — Intraday setups
Fourteen minute-bar strategies live in lib/setups_intraday/. Two are paper candidates with configs in configs_intraday/ wired into ACTIVE_INTRADAY_CANDIDATES in paper.py. The other twelve are research-only — promising on early samples, awaiting either parameter tuning or a longer window before they earn a place in the candidate set.
Paper candidates
RSAlpha Longlib/setups_intraday/rs_alpha_long.py
Classical "relative strength" trades enter when a stock is up while the index is down on a daily timeframe. RSAlpha applies the concept to minute bars and adds two never-published constraints:
- Continuous-divergence requirement. The stock must hold a cumulative-return advantage over SPY for at least 20 consecutive minutes. One-bar divergence is noise; sustained divergence is institutional accumulation.
- Polarity-flip trigger. Enter long only when SPY is making a fresh intraday low while the stock is not. This is the precise micro-moment where the divergence hits its widest before resolution.
Risk geometry: stop = entry × (1 − 0.5 × stock_atr_pct), capped at 1.5%. Target is 4R (asymmetric — divergence-resolution moves are tail-fat). Trail tightens after +2R.
3-month sample: PF 1.72, 23% CAGR, −0.6% mDD. 6-month sample degraded to PF 0.70 — small-sample noise the live paper-trade will resolve.
Volatility Regime Shiftlib/setups_intraday/volatility_regime_shift.py
Detects a structural regime change in the realized 1-minute volatility series rather than in price. When a stock spends a sustained period in a low-realized-vol regime and suddenly transitions into a high-realized-vol regime, the transition implies an informational shift — news leak, large institutional order surfacing, sector rotation accelerating. The first such shift of the session is a high-conviction directional signal in the direction the shift bar resolved.
rvol = rolling 30-bar stdev of close-to-close 1-min returns
low = rvol < 20th pct of last 60 bars
high = rvol > 80th pct of last 60 bars
trigger = 5+ consecutive low bars, current bar high,
AND |close − open| / (high − low) > 0.7 (decisive bar)
direction = sign(close − open)
stop = prior 5-bar swing low / high
target = 5R, trail-after-2.5R buffer 1.0R
guards = skip first 60 minutes; one trade per session per symbol
Ensemble correlation with RSAlpha: −0.08. The portfolio_rsalpha_volregshift.yaml config pairs them deliberately.
Research-only intraday modules
| Module | Side | Concept |
|---|---|---|
diagonal_squeeze.py | Bidirectional | EMA9/EMA21 convergence persisting ≥ 30 min (novel persistence gate); release fires on the first bar closing outside the squeeze with EMA separation ≥ 1.5× the 60-bar baseline. |
quiet_coil.py | Bidirectional | Counter-intuitive: quiet open (volume < 0.7× 5-day avg, range < 1× ATR_5min × 12 over 60 min) → first 1-min break with volume > 3× fires. Inverts the conventional "high-vol opens are the move." |
opening_range_fractal.py | Directional | Hierarchical OR alignment: OR_5 < OR_15 < OR_60. Entry when the OR_60 breaks with volume > 2× its average — the slowest timeframe confirms the others. |
tail_hunter.py | Long | Fat-tail Kelly bet: tight 0.4R stop with 6R target, achieved via stepped profit-lock (lock +0.5R after +2R, lock +2R after +4R MFE). Bets on asymmetry, not pattern. |
overnight_gap_fade.py | Short | Overnight gap up to 25%, intraday lower-high pattern, vol burst > 1.4× the 30-min average, close below VWAP, real retracement (≥ 30% of gap already back). |
power_hour_reversal.py | Long | 14:30–15:30 ET window. Stock down ≥ 2% (or > 1.5× ATR below VWAP), first higher-low pattern post-60-min, vol > 1.5× the 30-min average. Time-of-day plus mean-reversion within intraday weakness. |
liquidity_sweep_reversal.py | Long | Session's first defended level gets swept (bar low < early_low but ≤ 0.3× ATR_5), then rejected (close back above), high vol > 1.8× the 20-bar average. Algo-executed sweep + institutional rejection. |
microstructure_breath.py | Bidirectional | Contraction sequence (4+ bars with decreasing bar_size_norm < 0.7× median) → expansion bar (bar_size_norm > 2×, decisive ratio > 0.6). Keys on volatility structure, not price. |
vol_velocity_long.py | Long | Rate-of-change of realized volatility (vol "velocity") as a long-side trigger. |
kullamaggie_continuation_long_intraday.py | Long | Intraday VWAP retest within an established daily uptrend; brief close below VWAP triggers entry on bounce. Symmetric stop, exit on VWAP reclaim or time stop. |
kullamaggie_parabolic_short_intraday.py | Short | Minute-bar version of the swing parabolic short, gated by a daily-context filter so it only fires inside an extended daily structure. |
rs_alpha_long_rotating.py · volatility_regime_shift_rotating.py | — | Rotating wrappers around the two paper candidates — daily rebalance across the universe rather than a fixed symbol set. |
08 — Discipline overlays
A signal is necessary but not sufficient. Two cross-cutting overlays sit between strategy modules and the broker, and they have authority to veto entries the strategies would otherwise take. Both are research-active for swing and live for intraday paper.
Breitstein harness — lib/discipline/breitstein.py
Lance Breitstein's teaching survives in this bot as a discipline harness rather than a signal generator. The harness exposes:
- VWAP regime gate. Blocks fade-side entries if the stock has been on the wrong side of VWAP for 2+ of the last 10 bars.
- Prior-bar confirmation (opt-in). Long entries require
close > prior bar high. - Prior-bar trailing stop. Once a trade is in profit, the stop ratchets to the prior bar's low (long) or high (short) on each new bar.
- Multi-timeframe size-up. 25% larger position when intraday and daily trends align.
- Quality tiers (A/B/C) — risk multipliers attached to setup quality classification.
What is deliberately not mechanized: Breitstein's "A+ capitulation fade." It's pure tape-reading and attempts to code it produce the canonical "bot shorts the bottom tick" disaster. The decision to leave that behaviour out is a stronger expression of his teaching than coding a poor proxy would be.
Mistake filters — lib/discipline/mistake_filters.py
Stateful guardrails that reset daily and update on each closed trade:
- Cool-down after losses — after N consecutive losing trades on the same day (default 3), no new entries.
- Hard daily loss cap — at −1.5% of equity, all new entries halt for the day.
- Friday afternoon block — no new entries after 14:30 ET on Friday (no-Friday rule from the corpus, with an afternoon cushion).
- Session-edge guard — skip the first 15 minutes (open auction noise) and the last 15 minutes (close imbalance) of the session.
The MistakeFilters object wraps three calls: reset_day() at the open, on_trade_close(pnl_pct) from the runner, and approve_entry(ts) before any new order. The runner refuses to place an entry if the filter says no.
Theme cap (in the runner)
The corpus pattern: when one biotech blows up, ten do. The swing runner caps concurrent positions per theme bucket at 2–3 (configurable). Sector classification comes from lib/data/sectors.py; the cap is applied at signal acceptance, not at exit.
09 — Risk & sizing
The accounting unit is risk-percent, not notional-percent. This is the single biggest difference between this bot and most retail bot frameworks — and the source of the most common confusion when reading the trades CSV.
The two numbers
Signal.risk_pct is the fraction of equity at risk if the stop is hit. A signal with risk_pct = 0.5 means: if this trade goes from entry to stop, the account drops 0.5%. This is independent of how big the position is in dollars.
The runner converts that to notional via:
notional_pct = min( risk_pct × entry / |entry − stop| , 1.0 )
No leverage. A tight stop + small risk-percent ⇒ small risk in dollars but possibly large notional (the cap is what prevents over-allocation). A wide stop + same risk-percent ⇒ small notional. Both risk_pct and notional_pct are written to trades.csv for every closed trade so the post-hoc accounting matches.
The asymmetric R-ratchet
R is the per-trade risk unit — (entry − stop) for longs, (stop − entry) for shorts. The four novel swing setups (AVD, TSC, ROCS, VRDC) and several intraday setups (TailHunter, RSAlpha, VolRegimeShift) lean hard on a stepped exit policy:
at +1R do nothing at +2R lock stop at break-even (or +0.5R for tail-friendly setups) at +3R trail to prior swing structure at +4R+ target / partial / trail tighter — strategy-specific
The asymmetry is the point: stops are tight, ratchets are slow. Most exits land between +2R and +5R; the rare +8–12R outliers do the heavy lifting. Calendar partials at days 3–5 were tested and removed across the swing trio after three independent corpus studies (HackerTrader 64% CAGR, Whos_Agent E0, mxteo) showed them hurting returns.
10 — Backtest pipeline
Every strategy is run through walk-forward backtests before any other consideration. The pipeline has four layers: runners, configs, results, sweeps. Each is a directory in bot/backtests/.
Runners
| Runner | What it does |
|---|---|
swing_runner.py | Daily-bar walker. Date-aligned across symbols on the SPY calendar; weekly Monday rebalance of the dynamic-leader universe; per-bar strategy.scan(); pre-trade discipline checks (Breitstein VWAP gate, mistake filters, Friday afternoon block); multi-day position management (default 10-EMA trail, hard day cap); earnings blackout (configurable, opt-out for episodic-pivot); theme cap; borrow-cost subtraction for shorts. |
intraday_runner.py | Minute-bar walker, research-only. |
daily_rotating_intraday_runner.py | Daily-rebalance variant of intraday strategies — load minute bars for all symbols in the universe each session, scan for entries, hold and manage to same-session exit. |
ensemble_intraday.py | Multi-strategy intraday harness. Currently used for the RSAlpha + VolatilityRegimeShift pair (ensemble PF 1.72, Sharpe 3.63 on the 3-month sample). |
portfolio_runner.py | Portfolio-level backtest aggregating multiple strategies into a single equity curve. |
pattern_miner.py | Rule extraction from profitable backtests — generates new strategy candidates from observed trade clusters. |
sweep.py | Exhaustive parameter grid search; outputs to sweep_results/<strategy>__<variant>/. |
Config schema
Every strategy is paired with a YAML in configs_swing/ or configs_intraday/. The config is the single source of truth for what's being tested:
name: kullamaggie_breakout_swing
strategy: lib.setups_swing.kullamaggie_breakout_swing:KullamaggieBreakoutSwing
universe: dynamic_leader
universe_params:
top_n: 200
min_adr_pct: 4.0
start: 2021-01-01
end: 2026-04-27
oos_start: 2024-01-01 # in-sample / out-of-sample split
params: { ... strategy-specific knobs ... }
fees: { slippage_pct: 0.001, commission_pct: 0.001 }
risk: { max_concurrent_positions: 3, daily_loss_cap_pct: -0.015 }
vetting_thresholds:
min_oos_profit_factor: 1.5
min_sharpe: 1.0
max_drawdown_pct: 0.20
min_trades: 30
max_top5_concentration: 0.60
Results layout
Each run writes a directory under results_swing/ or results_intraday_rotating/:
metrics.json— combined / in-sample / out-of-sample metrics: Sharpe, profit factor, win rate, CAGR, max drawdown, top-5 concentration.equity_curve.csv— daily equity over time, used to render dashboard charts.trades.csv— every closed trade with entry, exit, P&L,risk_pct,notional_pct, exit reason.
There are 150+ result subdirectories under results_swing/ across the main strategies plus parameter sweep variants. The newest runs as of this writing — accrual_divergence_swing__20260428_032748 and vol_term_backwardation_swing__20260428_032619 — are the freshly tuned AVD and TSC candidates from commit dce3399.
11 — Promotion gates
A strategy doesn't reach paper trading by decree. bot/scripts/validate_promotions.py reads each strategy's metrics.json + trades.csv, checks them against the vetting_thresholds in the YAML, and writes site/data/promotion_report.json. The dashboard reads that file. Failure on any gate is a public failure.
The gates
- Combined-metrics pass. Profit factor, Sharpe, max drawdown, CAGR — all must clear thresholds set per-strategy in the YAML.
- Out-of-sample sample size. Configurable, default ≥ 30 trades. The OOS window is everything after
oos_startin the config. - Out-of-sample profitability. OOS profit factor > 1.0 and OOS total P&L > 0.
- Top-winner concentration. The top-5 trades must not exceed 60% of total profit (configurable). This catches strategies whose edge is one or two outliers.
- Data quality. No missing-bar sequences over the test window.
Three sequential phases
- Backtest vetting — automated, the gates above.
- Paper trading — passed strategies run on Alpaca or IBKR paper for ≥ 90 days without intervention. Performance must persist.
- Live capital — starts at $10k per strategy. Expands only on evidence.
Current state
The latest promotion_report.json shows none of the swing strategies promoted to paper yet — the OOS numbers are interesting (Episodic Pivot OOS PF 2.41, Parabolic Short OOS PF 1.76) but configured vetting still fails on return quality, sample size, or data completeness. The two intraday candidates (RSAlpha + VolatilityRegimeShift) are the closest — wired into paper.py as candidates, with the explicit caveat that a 6-month sample degraded versus the 3-month sample and live paper validation is required before any further promotion.
12 — Paper orchestrator
bot/scripts/paper.py is the command bridge between the bot and a paper-broker connection. Three subcommands.
paper.py brief
Pre-market, read-only. Prints today's watchlist (the universe for the week), open positions, cash balance. No execution. The morning briefing.
paper.py scan [--execute]
End-of-day. Loads today's bars for every strategy's universe, calls strategy.scan(), prints proposed signals with rationale. The --execute flag is the safety: without it, signals are inspected; with it, they're submitted via lib/broker/alpaca.AlpacaBroker.
paper.py manage [--execute]
Daily position management. Loads open positions, computes the Breitstein ratchet trail + MA trail + hard stops, prints the order adjustments needed. --execute modifies orders.
Active set as of dce3399
ACTIVE_STRATEGIES = [
"kullamaggie_breakout_swing",
"kullamaggie_episodic_pivot_swing",
"kullamaggie_parabolic_short_swing",
"vol_term_backwardation_swing",
"accrual_divergence_swing",
"roc_acceleration_swing",
"coil_release_swing",
]
ACTIVE_INTRADAY_CANDIDATES = [
"rs_alpha_long_rotating",
"volatility_regime_shift_rotating",
]
Every action is appended to bot/paper/ledger.jsonl as a JSON line — strategy, symbol, side, price, size, rationale, broker response. The ledger is the long-term audit trail; the site/data/all_trades.json compilation pulls from it (for paper) and from backtest trades.csv (for backtests).
13 — Site & data flow
The site is a static deployment. Each panel reads a JSON file generated by a script in bot/scripts/. The flow:
bot/backtests/results_swing/<run>/metrics.json ──┐
bot/backtests/results_swing/<run>/equity_curve.csv ──┤ build_dashboard.py
bot/backtests/results_swing/<run>/trades.csv ──┴───► site/data/dashboard.json ──► dashboard.html
site/data/all_trades.json ──► trades.html
bot/backtests/configs_swing/*.yaml ──┐
bot/backtests/results_swing/<run>/metrics.json ──┤ validate_promotions.py
bot/backtests/results_swing/<run>/trades.csv ──┴───► site/data/promotion_report.json ──► dashboard.html
bot/paper/ledger.jsonl ──┐ compile_trades.py (mode=paper)
└───► site/data/all_trades.json ──► trades.html
The site panels
| Page | Reads | Purpose |
|---|---|---|
index.html | (static) | Methodology, ~1,000-name funnel, charlatan filter — the research dossier. |
ten.html | (static) | The ranked ten traders with returns, methodology, sources. |
kullamaggie.html | (static) | Corpus deep-dive on Kullamägi's actual rules versus folklore. |
architecture.html | (static) | One-screen architecture overview. |
internals.html | (static) | This page — the long-form repository walkthrough. |
dashboard.html | data/dashboard.jsondata/promotion_report.json | Per-strategy candlestick charts, equity curves, metrics table, gate results. |
trades.html | data/all_trades.jsondata/trade_metadata.json | Interactive trade browser — filter by symbol, setup, date range; drill into each trade. |
14 — Recent trajectory
The shape of the last ten commits, in order from oldest to newest, tells you where the work has been moving.
- Portfolio harness complete. A 3-year corpus test landed (Sharpe 1.14, 1015 trades) — the first end-to-end multi-strategy backtest the repo could run.
- Sector confirmation gate. A sector-trend alignment filter was added to swing entries to avoid taking entries into sector-wide weakness.
- Confirmation candle reverted. An experiment requiring a confirmation candle on breakouts was tested and rolled back — it didn't improve profit factor.
- Intraday parabolic short. An intraday version of the Kullamägi parabolic short was added with a daily-context filter and timezone fix.
- Daily-rotating intraday runner. The intraday execution framework was completed; the 2-strategy ensemble (RSAlpha + VolatilityRegimeShift) was live tested.
- Swing trio tuned. Parameter sweeps on the three Kullamägi setups produced "3-of-3 momentum, looser shorts" — the parabolic-short filter relaxed for better OOS behaviour.
- Status page replaced the dashboard. The dashboard panel was refactored to a command-bridge Status page tied to
paper.pylogs. paper.pyadded. The brief / scan / manage orchestrator landed — the foundation for live trading and paper validation.- Four novel strategies + algorithmic R-ratchet. AVD, TSC, ROCS, VRDC were added; the asymmetric R-ratchet exit became load-bearing across the new modules. 5 of 7 strategies showed OOS profitability.
- Sweep-tune AVD + TSC, wire all 7 into
paper.py. The current state — seven swing strategies in the active set, two intraday candidates pending paper validation.
The arc: a single Kullamägi-only swing bot in mid-April; four novel mechanics plus a paper-trading orchestrator in late April; intraday ensemble candidates queued for live paper validation by early May. Tomorrow's commits will probably be parameter sweeps on the four novel modules to find their stable operating points.
15 — Glossary
Acronyms and unit conventions used throughout the codebase and this page.
- ADR
- Average Daily Range, expressed as a percent. Computed as
mean(high/low) − 1over a window (default 20 bars). Kullamägi's volatility filter — the bot rejects names with ADR < 4 because breakouts on quiet names don't have the gas to justify the strategy. - R
- The per-trade risk unit. For a long,
R = entry − stop; for a short,R = stop − entry. Targets and trails are expressed in multiples of R (2R, 4R, 6R) so they're scale-invariant across symbols and prices. - MFE
- Maximum Favourable Excursion — the best unrealised profit a trade reaches before exit. Used by stepped profit-locks (e.g., "lock +2R after MFE +4R").
- OOS
- Out-of-sample. The period after
oos_startin a backtest config. The bot's promotion gates require strategies to be profitable OOS, not just on the full sample. - PF
- Profit factor.
gross profit / gross loss. Above 1.0 is profitable; the promotion gate requires > 1.5 OOS for swing. - CAGR
- Compound annual growth rate of the equity curve.
- mDD / max DD
- Maximum drawdown — the largest peak-to-trough decline in equity over the test window.
- CIR
- Close-in-range.
(close − low) / (high − low)in [0, 1]. Used by the VRDC coil setup as a directional vote. - CLV
- Close-location-value.
((close − low) − (high − close)) / (high − low)in [−1, +1]. Used by the AVD setup as a sign weight on dollar volume. - RVn
- Realized volatility over n trailing bars — used by the TSC term-structure backwardation setup.
- VWAP
- Volume-weighted average price. Anchored VWAP (from a chosen anchor index forward) is used by the Breitstein harness as a regime gate.
- VCP
- Volatility Contraction Pattern — Minervini's tightening-base structure, scored by
volatility_contraction()inindicators.py. - EP
- Episodic Pivot — the Kullamägi catalyst-gap setup.
- ORB
- Opening Range Breakout.
opening_range_fractal.pyuses a hierarchical OR_5 / OR_15 / OR_60 alignment.