THE DESKRESEARCH
Internals · the whole machine

Every directory, every subsystem, every strategy.

The architecture page is a one-screen mental model. This is the long form: the actual repository, traversed from the data layer up through the strategy contract, the discipline overlays, the backtest harness, the promotion gate, and the paper-trading orchestrator. Every active and shelved strategy is named and described. Nothing is omitted because it sounds boring.

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.

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:

FunctionWhat 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.

FileRole
daily_bars.pyTop-level loader. load_daily(symbol, start, end) resolves cache → Massive → Polygon fallback.
massive.pyMassive.com REST adapter. Pulls all ~5,300 US common stocks with sector + shares-outstanding metadata.
polygon.py · polygon_daily.pyPolygon endpoints — minute and daily — with on-disk caching.
universe_scanner.pyThe leader-universe scanner. See below — this is the single biggest piece of plumbing in the bot.
daily_watchlist.pyLoads the universe for a given date.
earnings.pyLocal earnings cache. has_earnings_within(symbol, days) drives the catalyst gate.
sectors.pySector ETF mapping. is_biotech() is the biotech-blow-up filter.
session.py · market.pyMarket 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), setup name, trader attribution, rationale, timestamp. The dataclass exposes a notional_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 of Signals (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 either None (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.

Long-only Short-only Bidirectional Active (paper-trading set) Candidate (paper validation pending) Research shelf

Kullamägi Breakout Swinglib/setups_swing/kullamaggie_breakout_swing.py

LongActiveDaily bars

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

LongActiveDaily barsCatalyst-driven

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

ShortActiveDaily barsBorrow cost charged

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

LongActiveDaily barsNovel

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
"The signal: stock's 20-day cumulative absorbed flow is in the top decile of own historical distribution while the same stock's 20-day return is only median (30–55th percentile). This is the silent-accumulation footprint — massive directional flow with no markup yet, because a large buyer hasn't finished and supply is being absorbed faster than price can adjust."

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

LongActiveDaily barsNovel

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.

"The textbook reading of rising short-term vol is risk-off, sell. We do the opposite: when backwardation appears at the same time as a fresh 60-day high on volume, we treat it as the early signature of a new participant arriving who hasn't finished accumulating yet."

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

LongActiveDaily barsNovel

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

BidirectionalActiveDaily barsNovel

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.

"Most volatility-contraction breakouts (NR7, Bollinger squeeze) use range compression alone, which captures both healthy coils and dying stocks. Volume-up-while-range-down isolates coils where institutional flow is building, not evaporating."

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.

ModuleConcept
failure_to_fail_swingA failed test of prior support becomes the entry — the move higher confirms by virtue of not breaking down.
hidden_strength_outside_swingOutside-day reversal where intraday close pattern reveals strength masked by the wide range.
open_drive_failure_swingOpen drive into a key level fails, reverses; entry is the reversal.
regime_flip_gap_swingA single-day regime-shift gap; treats the gap day as the start of a new regime, not noise.
volatility_squeeze_release_swingClassic VCP-style squeeze followed by directional release. Predates the volume-range coil version above.
volume_range_cascade_swingMulti-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

LongCandidate1-min bars

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:

  1. 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.
  2. 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

BidirectionalCandidate1-min bars

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
"The edge is detecting an information event mid-session, before the chart pattern matures. Most pattern-based intraday signals trigger late — by the time a flag, ORB, or pullback prints, the move is half done. A volatility-regime shift fires on the first decisive bar of the new regime, capturing the asymmetric tail."

Ensemble correlation with RSAlpha: −0.08. The portfolio_rsalpha_volregshift.yaml config pairs them deliberately.

Research-only intraday modules

ModuleSideConcept
diagonal_squeeze.pyBidirectionalEMA9/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.pyBidirectionalCounter-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.pyDirectionalHierarchical 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.pyLongFat-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.pyShortOvernight 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.pyLong14: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.pyLongSession'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.pyBidirectionalContraction 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.pyLongRate-of-change of realized volatility (vol "velocity") as a long-side trigger.
kullamaggie_continuation_long_intraday.pyLongIntraday 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.pyShortMinute-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.pyRotating 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:

  1. 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.
  2. Prior-bar confirmation (opt-in). Long entries require close > prior bar high.
  3. 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.
  4. Multi-timeframe size-up. 25% larger position when intraday and daily trends align.
  5. 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:

  1. Cool-down after losses — after N consecutive losing trades on the same day (default 3), no new entries.
  2. Hard daily loss cap — at −1.5% of equity, all new entries halt for the day.
  3. Friday afternoon block — no new entries after 14:30 ET on Friday (no-Friday rule from the corpus, with an afternoon cushion).
  4. 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

RunnerWhat it does
swing_runner.pyDaily-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.pyMinute-bar walker, research-only.
daily_rotating_intraday_runner.pyDaily-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.pyMulti-strategy intraday harness. Currently used for the RSAlpha + VolatilityRegimeShift pair (ensemble PF 1.72, Sharpe 3.63 on the 3-month sample).
portfolio_runner.pyPortfolio-level backtest aggregating multiple strategies into a single equity curve.
pattern_miner.pyRule extraction from profitable backtests — generates new strategy candidates from observed trade clusters.
sweep.pyExhaustive 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

  1. Combined-metrics pass. Profit factor, Sharpe, max drawdown, CAGR — all must clear thresholds set per-strategy in the YAML.
  2. Out-of-sample sample size. Configurable, default ≥ 30 trades. The OOS window is everything after oos_start in the config.
  3. Out-of-sample profitability. OOS profit factor > 1.0 and OOS total P&L > 0.
  4. 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.
  5. Data quality. No missing-bar sequences over the test window.

Three sequential phases

  1. Backtest vetting — automated, the gates above.
  2. Paper trading — passed strategies run on Alpaca or IBKR paper for ≥ 90 days without intervention. Performance must persist.
  3. 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

PageReadsPurpose
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.htmldata/dashboard.json
data/promotion_report.json
Per-strategy candlestick charts, equity curves, metrics table, gate results.
trades.htmldata/all_trades.json
data/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.

  1. 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.
  2. Sector confirmation gate. A sector-trend alignment filter was added to swing entries to avoid taking entries into sector-wide weakness.
  3. Confirmation candle reverted. An experiment requiring a confirmation candle on breakouts was tested and rolled back — it didn't improve profit factor.
  4. Intraday parabolic short. An intraday version of the Kullamägi parabolic short was added with a daily-context filter and timezone fix.
  5. Daily-rotating intraday runner. The intraday execution framework was completed; the 2-strategy ensemble (RSAlpha + VolatilityRegimeShift) was live tested.
  6. 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.
  7. Status page replaced the dashboard. The dashboard panel was refactored to a command-bridge Status page tied to paper.py logs.
  8. paper.py added. The brief / scan / manage orchestrator landed — the foundation for live trading and paper validation.
  9. 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.
  10. 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) − 1 over 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_start in 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() in indicators.py.
EP
Episodic Pivot — the Kullamägi catalyst-gap setup.
ORB
Opening Range Breakout. opening_range_fractal.py uses a hierarchical OR_5 / OR_15 / OR_60 alignment.