Backlight · user guide

Build a strategy, test it on history, then run it on paper.

Backlight wires small, parameterized nodes into a pipeline that picks securities, decides what to do with a decision tree, and sends orders to a connector. The same pipeline runs in backtest, paper and live mode, and every fill can be traced back to the rule that caused it.

01What Backlight does

Every tick (a bar close, a time of day, or a weekly schedule), the pipeline runs once from left to right. Each stage narrows or reshapes what the previous one produced:

Trigger when to run Universe candidates Selector filter / rank Strategy signals Refiners veto / reshape Sizer how much Execution order type Final caps, submit bar close SPY QQQ AAPL… top 5 by vol buy AAPL + stop 3% 2% of equity limit @ mid → connector one tick, left to right — every hop is recorded
The tick. Node colors in the Pipelines workbench match these stages. A signal carries the decision-tree path that produced it, so the Run report can attribute P&L to a tag, a tree leaf or a single branch.
ModeClockMarket dataOrders go to
Backtestreplays history bar by bara historical connector (cached to disk)the built-in simulator
Paperreal timea market-data connectorthe simulator, or a broker's paper account
Livereal timea market-data connectoryour broker — real money

02The window

Backlight › Backtest Lab ◌ 2/3 41% 📖 Guide 1 2 3 4 5 6
1 Navigation rail — hover an icon for its name. 2 Where you are. 3 Running jobs (backtests, paper/live runs); click one to jump to it. 4 This guide. 5 Connection to the hub: green is connected, red is reconnecting. 6 The current view.
ViewUse it to
DashboardSee active jobs, running paper/live runs and recent results at a glance.
PipelinesBuild and edit strategies as node graphs.
Backtest LabRun one or many pipelines over a date range and compare them.
RunsBrowse every run, open its report, compare, delete or purge.
TradeTrade by hand: in real time on your broker account or a local paper account (no run, no pipeline), or practice on historical data a checkpoint at a time.
LiveStart, watch, stop or kill paper and live (real-time) runs.
AdminConnectors (data and brokers), symbol sets, the data cache, theme, hub port, database size.

Hover almost anything — node ports, chips, truncated cells — for a tooltip with the full detail.

03Your first backtest

A fresh install already has five preset pipelines and the offline Synthetic data connector, so this works without any accounts or network:

  1. Open Backtest Lab in the rail.
  2. Leave Stock bars on Synthetic, keep the default symbols (or try XL? for the sector ETFs) and the last two years.
  3. Under Strategies, tick SMA crossover and Buy and hold (baseline).
  4. Press ▶ Run 2 backtests. Each pipeline gets a progress bar with equity and trade count.
  5. When the batch finishes, the Results table appears. Click a row for its full report, or Compare to overlay them.
Tip

Always include Buy and hold in a batch. A strategy that loses to doing nothing is not a strategy. It splits the account evenly across the run's symbols (the Equal weight sizer), so one lucky symbol cannot carry the benchmark.

04Connectors

A connector links Backlight to a data source or a broker. Each one plays one or more roles:

historicalPast bars for backtests and the data cache.
marketLive quotes and bars for paper and live runs.
executionAccounts, positions and order placement.

Connectors live under Admin → Connectors. Only the connectors you have added appear there and in the Backtest Lab, Live and Data pickers — so the lists stay short. A new install starts with the offline ones: Synthetic, Synthetic market, Sim (the fill simulator) and CSV.

Admin General Connectors + Add connector Synthetic synthetic historical readyTest⌄ Sim sim execution readyTest⌄ Alpaca historical market execution no API keyTest⌃ API key •••• API secret •••• Paper ☑ Save 🗑 Remove Add connector SchwabBrokerage: data, quotes, orders (OAuth) › MassiveHistorical and real-time market data › Kalshiplan only › pick one → fill in its settings → Add
Admin → Connectors. Each added connector is one row: a status dot, its roles, a one-line status and a Test button (fetches a quote or lists accounts). Click a row to expand its settings, Save and Remove. Add connector lists the ones you have not added yet.

Adding one

  1. Open Admin → Connectors and press + Add connector.
  2. Pick the connector. Its settings form appears — API keys, paper/live switch, data feed and so on.
  3. Fill in what you have and press Add. You can finish the rest later from its row.
  4. Press Test on the new row. A green check means it can reach the service; red shows the error.
ConnectorRolesNeeds
Synthetic / Synthetic markethistorical marketNothing — random-walk bars and option chains, offline. Great for trying things.
SimexecutionNothing — the fill simulator for backtests and paper runs (slippage, commissions, brackets, expiry).
CSVhistoricalA folder of SYMBOL.csv bar files.
Alpacahistorical market executionAPI key and secret; tick Paper for a paper account.
Schwabhistorical market executionApp key/secret, then sign in with OAuth start → paste the redirect URL → OAuth finish.
Massivehistorical marketAPI key.
Kalshi—Plan only; not usable for runs yet.
Secrets

API keys and tokens are stored in a separate, owner-only secrets folder — never in the config file. A saved secret shows as abc…xyz; leave the field blank to keep it. Remove deletes the connector's settings and secrets.

05Pipelines

The Pipelines workbench has three columns: the node palette on the left, the canvas in the middle, and the selected node's parameters on the right. New starts an empty pipeline; Open lists your saved pipelines, most recently edited first, with a filter box — type part of a name and press Enter to open the top match. Node positions are part of the pipeline: moving a node in a saved pipeline keeps the new layout straight away, without a Save and without a new revision.

+ New ▭ Open SMA crossover (daily) 1d · 300 ✓ ⧉ 🗑 Run in Backtest Lab NODES ⌕ search… Bar close Top N Decision tree Attach stop Risk per trade drag Bar close Run symbols SMA cross Max positions % of equity Final SMA CROSS · PARAMETERS Fast period 20 Slow period 50 Allow short ☐ Tags: trend, daily
Workbench. Drag nodes from the palette (search it, or collapse the categories you don't use), connect an output port to an input port of the same type (port colors match; hover a port to see its type), then select a node to edit its parameters. In the node panel, More shows the node's full description, a port chip opens that port type's documentation (what the value is, its fields, which nodes make and take it), and View code shows the node's Python, read-only. The problems bar under the toolbar lists anything that won't compile.
StrategiesStock: SMA cross, RSI reversion, breakout, buy & hold, exit rules. Options: credit spread, iron condor, cash-secured put (wheel), covered call, poor man's covered call (LEAPS with rolled short calls), long call/put, debit spread, long straddle, option exit rules. Or Decision tree for your own logic.
PickersHow an option strategy's contracts are chosen: by delta, % out of the money, premium, expiry rules (monthlies, Fridays, a DTE window), liquidity guard. See Picking contracts.
RefinersAttach stop, block when…, daily loss limit, max positions, min confidence, cooldown, trading window.
SizersFixed dollar, percent of equity, risk per trade (stop distance), volatility target, option max-loss budget.
FlowSwitch sends each symbol down a branch by its own indicators; Regime switch moves the whole universe on one reference symbol.

Branching

A pipeline is a graph, not a line. A Switch node holds a list of cases, each a name and a condition; every case becomes an output port labeled with its name, plus a final else. Each symbol goes down the first case (top to bottom) whose condition holds for it. Wire each case to its own strategy, then join them again at any shared node — signals from several strategies simply add up at a refiner or sizer.

Option metrics Switch per symbol Strong trend Range + rich IV Uptrend + IV else: sits out Breakout (stock) Iron condor Credit spread % equity Max loss Exec
The “Branching: per-symbol router” preset. Stock and option branches get their own sizers and rejoin at execution. A branch that receives nobody this tick still runs with an empty universe, so its strategy keeps managing (and exiting) the positions it opened earlier. Set the switch's Reference symbol (e.g. SPY) to route the whole universe together — see the “Branching: market regime switch” preset.

Options

Option strategy nodes pick contracts from the chain by delta and days to expiry, enter only when IV rank is in range, and manage the whole position (all legs) together: take profit at a share of the premium, stop at a loss multiple, trail the best P&L seen, close at N days to expiry, close when a short strike is tested, close after N minutes, or close N minutes before the session ends. Entries can skip contracts whose bid/ask spread is too wide. Widths are a percent of the stock price so one pipeline fits a $20 and a $500 name (a spread's wing is always at least one listed strike away). Put Option metrics before a strategy to drop names without a chain and to record IV, IV rank and friends on every signal.

Three strategies are built for intraday bars (5- or 15-minute timeframes):

NodeWhat it does
Option scalp (intraday)Buys a short-dated call when price crosses up through VWAP (or breaks the opening range, or a fast EMA crosses a slow one), a put in the mirror case. Exits on a premium target, stop or trail, after N minutes, when price crosses back, and before the close.
Opening-range breakout credit spread (0DTE)When price closes outside the opening range (60 minutes by default) in the morning, sells a same-day spread on the other side: a put spread with its short strike at the range low after an upside break, a call spread at the range high after a downside one. Once a day per underlying, skips narrow-range days and spreads whose credit is too thin for their width, stops when the short strike is tested and is flat a few minutes before the close. Optionally only takes breakouts with a daily trend.
0DTE iron condor / flySells a same-day condor (or ATM fly) once the open has settled, at most once a day per underlying, and is flat before the close. Needs an underlying with daily expiries: SPY, QQQ, IWM and the cash indices.
Gamma scalp (hedged straddle)Buys an ATM straddle when IV is at or below realized vol, then trades shares back to delta-neutral whenever net delta passes a band. The shares close with the straddle.
NameMeaning
iv, hv30-day at-the-money implied vol; 20-day realized vol (0.25 = 25%)
iv_rank, iv_percentile0–100: where IV sits in its past year. In backtests it ranks the IV model's history; live, without IV history, it ranks realized vol (iv_rank_source = 0)
expected_move, skew, term_slope1-sigma move to 30 days; 25-delta put minus call IV; 60-day minus 30-day IV
expected_move_close, expected_move_close_pct1-sigma move from now to today's close, in price and as a fraction of it
dte, hours_to_expiryfor the position being managed: calendar days to the nearest expiry (0 = today); hours until it stops trading
delta, net_gamma, net_theta, short_deltanet delta in shares, delta gained per $1 up, $/day decay, the most-tested short leg
premium, premium_pnl_pct, premium_pnl_peaknet premium at entry (negative = credit); P&L over that premium — 0.5 = half the credit kept; the best such P&L since entry
spread_pct, minutes_in_tradethe legs' bid/ask spread as a fraction of their value; minutes since the position opened

They work anywhere an expression does: tree conditions, Filter by expression, Block when… and Switch branches. Unavailable values are blank, and a condition on a blank value is false. When the backtest's data source has option history (Alpaca from late February 2024, Massive within your plan's history), chains, fills, marks and stops come from the real option bars: a contract that traded in the bar uses that price, one that traded earlier keeps its last implied vol and is repriced at the current stock price, and the fill model's spread goes around both (neither venue's history has bid/ask). Where the history stops, and with Option prices → synthetic in the Lab, options are priced off an IV that follows the stock's recent realized volatility (and, on intraday bars, today's realized vol so far). The run page says which (options: real history). The first run over a period downloads the option bars (seconds per day on Alpaca, ~30 s per day for SPY on Massive); later runs read them from the cache. Options expire at the expiry day's close, so a same-day option decays through the session. The chain is rebuilt every bar, and synthetic listings include same-day expiries (Fridays for everything, every weekday for SPY, QQQ, IWM and the cash indices).

Picking contracts

An option strategy decides what to trade — a put credit spread on SPY now — and picks its contracts its own way: the strike nearest its delta, the expiry nearest the middle of its days-to-expiry window. Put a picker node between the strategy and the sizer to choose them by other rules; the strategy's entries are rebuilt under the picker's rules, its exits are untouched:

PickerChooses
Pick by deltaOther target deltas for the sold and bought legs, and optionally another DTE window (0 keeps the strategy's).
Pick by % out of the moneySold (or bought) legs that far out of the money: puts below the price, calls above it; negative is in the money.
Pick by premiumThe strike whose premium at mid is nearest a target, in dollars or % of the stock price: "sell the put that pays about $1.50".
Expiry rulesMonthly (third Friday) or Friday expiries only, another DTE window, and which expiry in it: the nearest, the middle or the farthest.
Liquidity guardOnly contracts with a bid, a bid/ask spread under a percent of the mid and some size; the next-best contracts are used instead.

Pickers chain: Expiry rules → Pick by premium → Liquidity guard applies all three. The structure keeps its shape — a spread's wing stays its width beyond the short strike, a condor's legs share one expiry, a PMCC's short call stays above the LEAPS breakeven and expires first — and when no contract fits the rules the signal is dropped (the run's Signals tab says why). Closes, stock trades and re-hedges pass through untouched. The same pickers suggest contracts on the Practice and Trade chains (Practice & Trade). Three presets come in a (monthlies, liquid) variant — the wheel, covered calls, and PMCC into the wheel — with Expiry rules (standard monthlies, short legs 25–55 days out) and a Liquidity guard (a bid, spread at most 20% of the mid) after the strategy: open the pair in the Backtest Lab to compare them.

History & memory

Expressions can also see what a pipeline has done, not just the current bar. Each symbol's closed positions are grouped into exits (legs closed together count as one), and nodes keep their own notes in a scratchpad. Paper and live runs load both from earlier runs of the same pipeline, so a restart picks up where it left off; a backtest starts fresh.

NameMeaning
last_had("structure:pmcc")true when the symbol's last close carried that tag (a structure, exit:… rule or node:…)
last_structure, last_exitwhat it last closed ("pmcc", "short_put") and why ("pmcc-profit", "assigned"); "" if never
days_since_exit, closed_countdays since that close (blank if never); positions closed on the symbol so far
last_pnl, last_proceedsthe close's net P&L; the cash it brought in
scratch("node_id", "key")a value a node remembered for this symbol, e.g. scratch("pmcc", "rolls")

The Options: PMCC into the wheel preset uses this to alternate strategies: a Switch case last_had("structure:pmcc") and days_since_exit <= 30 sends a symbol whose poor man's covered call just closed to the cash-secured put (wheel) branch, and everything else to the PMCC branch. Its LEAPS runs to 120 DTE with 15-delta calls sold against it; when buying a call back would cost more than the cash on hand the whole PMCC closes instead, and wheel calls are never written below what the assigned shares cost. Its LEAPS are sized by Smart weights (below) with 60% of equity in play, so the rest stays free for buying calls back.

Smart weights

Other sizers look at one signal at a time, so whichever symbols fire first take the money. Smart weights plans for all of the run's symbols at once, each week by default (Refresh targets: weekly, monthly or on every signal):

  1. Eligible: enough history, a chain with a put near the probe delta (30-delta, 30 days), and a probe spread under the limit.
  2. Score: each symbol is ranked on IV/HV, the probe put's annualized yield, IV rank, how tight its spread is and (off by default) trend. How much each score counts mixes the ranks. An entry that buys premium (a long option, a debit spread) flips IV/HV and IV rank, so cheap volatility scores well.
  3. Weigh: the top Most names funded symbols get score ÷ volatility. A symbol keeps its place until it falls two places past the cut. Each one is capped at Cap per name, symbols whose returns move together are capped as a group, and leveraged ETFs are haircut. Score tilt 0 ignores scores and weighs by volatility alone.
  4. Round: a symbol whose single contract (or LEAPS, or 100 shares) is larger than its cap gets nothing (lot too large). Dollars freed by rounding go one unit at a time to the best-scored symbols that still have room.

An entry is then sized to its symbol's dollar target, less what is already open or working on it. A symbol over its target is never trimmed; it just gets no new contracts. Entries whose strategy sets the size itself (calls on assigned shares, a put funded by last_proceeds) pass through unchanged. Each refresh writes a sizing_plan entry to the run's log with every symbol's scores, weight, target and the reason any symbol got nothing; skipped entries log sizing_skip.

06Decision trees

A decision tree is the logic inside a Decision tree strategy node. It is evaluated once per symbol per tick, from the root, until it reaches an action.

ROOT · CONDITION close > sma(200) CONDITION rsi(14) < param("oversold") ACTION hold() ACTION · leaf “pullback-buy” buy(stop_pct=3) SUBTREE → preset-exit-basic yesno yesno 412 visits · +$3,180
Nodes. A condition is an expression; boolean ones branch yes/no, others can branch on any value with else as the fallback. An action resolves a template (buy, sell, close, spreads…). A subtree jumps into another tree. With Overlay run stats on, each node shows visit counts and each leaf its P&L from a run.

Expressions read like a formula. Press Help in the tree editor for the full, searchable list of indicators, names, functions and action templates — click one to insert it.

sma(20) > sma(50) and rsi(14) < 30
close > sma(200, ago=1) * 1.02
close > sma(50, tf="1d")
param("threshold") <= feature("score")
iv_rank >= 50 and close > sma(50)
premium_pnl_pct >= 0.5 or dte <= 21
close > vwap() and close(ago=1) <= vwap(ago=1)
close > or_high(30) and minutes_since_open >= 30
minutes_to_close <= 15 or premium_pnl_peak - premium_pnl_pct >= 0.2
ref("SPY", "rsi", 14) > 50
vertical_put_spread(dte=30..45, short_delta=0.30, width=5)
iron_condor(dte=30..45, short_delta=0.16, width=5)

07Backtest Lab

The Lab runs pipelines on one market at a time — real history, one generated world, or a world set — with the same account and fills, so the results compare directly. The setup is a narrow column on the left; the results take the rest.

CardSettings
MarketChange picks Real history, One world or World set (see Generated worlds). Real history shows the stock-bar and option sources (set in Admin → Connectors; a dot shows whether each is ready) and the start and end dates. A world or set shows its recipe and its own start and end, limited to the dates it has.
SymbolsTickers, @sets and wildcards (below), and the timeframe (pipeline uses each pipeline's own). On a world, blank means all of its tickers.
AccountStarting cash. Advanced… holds the rest: margin, interest on cash (4%/yr by default: idle cash and short-put collateral earn it, a negative balance pays it), recurring capital, a batch label, the fill profile (Retail, Zero cost, Harsh), stock and option fills (slippage, commissions, next-open or close fills; option fills that cross the spread or fill at mid, and the modeled option spread), warm-up bars (blank = auto, see Warm-up), seed, recording and simulated outages. Scalps live or die on the spread: keep option fills on cross unless you are sure of mid fills.
PipelinesThe chosen pipelines. + Add / change opens a picker with a filter (name, timeframe, owner or preset), Only mine and the pipelines you ran recently.

Results appear as soon as you press Run and fill in while the batch runs (a run still going shows its return so far):

Recent batches under the results reopens any earlier batch; Use this setup puts its pipelines and market back in the setup to run again. Compare opens the runs side by side.

Symbols, sets and wildcards

Type symbols separated by commas. Any entry with a wildcard is expanded against the provider, and a preview under the field shows exactly which symbols will run before you press Run:

PatternMatches
AA*Everything starting with AA — AAL, AAPL…
XL?XL plus exactly one character — the sector ETFs XLF, XLK…
[AM]SFT, *One of the listed characters; * alone is every symbol the provider lists.
@mag7A named symbol set, inlined in place — here AAPL, MSFT, GOOGL, AMZN, META, NVDA, TSLA.
-TSLA, -XL?, -@semisA leading minus drops a symbol, every match of a pattern, or a whole set from the list it is in: @mag7, -TSLA.

Symbol sets are the groups you keep reaching for. Type @ in any symbol box (Backtest Lab, Live, Data, and the Static list / Run symbols nodes) and the box suggests them; typing letters suggests common tickers and shows which sets each belongs to. ↑/↓ pick, Enter or Tab accepts. Manage them in Admin → Symbol sets: built-ins such as @options-17 (the 17 names with cached real option data — the Lab's default), @0dte, @sector-spdrs, @semis, @leveraged-etfs, @dow30 and @sp100 ship with the app. Saving over a built-in keeps your own version (Reset to built-in brings it back). A set can hold wildcards, exclusions and other sets. Index memberships drift, so treat the built-ins as a starting point. The @ follows the named-set references of firewall tools like nftables. It never clashes with ticker characters (BRK.B) or wildcards. Pipelines resolve sets when a run starts, and the run records the symbols it actually used.

Each provider matches its own universe: Alpaca's tradable assets, Massive's ticker reference, Schwab's instrument search, the files in a CSV folder, or a built-in list of large caps and ETFs for Synthetic. Symbols already in the bar cache for that provider always count. A pattern that matches nothing, or more than 500 symbols, is flagged in red and blocks the run, and so does an unknown @set. The Data view's Fetch bars and backlight backtest --symbols accept the same patterns and sets.

Warm-up

Before Start a backtest loads extra bars so indicators already have history on the first tick; it doesn't trade during them. Leave Warm-up bars (Advanced) blank and each pipeline gets what it actually reads:

The pipeline usesWarm-up (daily bars)
Windows on its nodes — SMA lengths, channel bars, volatility / correlation windows, Min history barsthe longest window + 1 (SMA cross 20/50 with Min history 60 → 61)
Indicators in expressions and decision trees: sma(200), macd(12, 26, 9), rsi()the bars the call needs (201, 48, 15)
Option metrics — the Option metrics selector, any option strategy, smart sizing, or iv_rank in an expression272: IV rank compares today's IV with the past 252 trading days, and the first IV value needs 20 days
Nothing with history (buy and hold)20

The field's placeholder shows the result for the ticked pipelines (auto · 61–272), and the collapsed Advanced line says auto warm-up. Weekly runs scale the same way (IV rank → 56 weekly bars). Intraday runs keep at least two whole sessions, and intraday option pipelines keep 300 bars: their IV rank ranks realized volatility over the loaded bars, since a year of 5-minute bars isn't worth loading for it.

Typing a number overrides it, longer or shorter, for every ticked pipeline. Shorter than a pipeline needs still runs, but its first ticks see partial history: an IV rank built from a few months jumps around, so IV-rank filters and smart sizing misbehave early on. The Lab flags this before you run, and the run carries a warning.

Where the bars come from. Warm-up moves the first bar needed back: a 2023-09-10 start with 272 daily bars needs bars from about mid-2022. When the stock-bar connector's cache starts later for some symbols, the Lab names them under the symbols box (bars must start around …). The run fetches the missing range from that connector, and a plan that doesn't cover it (Massive stock bars older than about two years, for example) answers 429 and the run crawls. Start later, lower the warm-up, or take stock bars from a connector whose cache reaches back (Alpaca). The run page header shows the warm-up a run used (warm-up 272 bars (auto); hover for the reasons).

A pipeline's own Lookback bars is separate: it's how much history nodes see each tick, not how much is loaded before Start.

08Generated worlds

A generated world is a made-up market: an index, a few sector ETFs and a set of stocks with invented tickers (KRVX, HAKK…), ten years of daily bars, dividends, earnings reports and an option chain on most of them. It is built from a seed, so the same seed and settings give exactly the same world on any hub. You can backtest on it in the Lab and trade it by hand in Practice, just like real history.

No hindsightNobody remembers what KRVX did in 2014. Practice decisions are real decisions.
Many pathsRun a pipeline on 50 worlds and you get a distribution of outcomes, not one lucky history.
DrillsScenarios put a bear market, a flash crash or a vol spike into a world on purpose.
The truth, afterwardsA world knows what was really going on; Practice shows it in a debrief when you end the session.
What worlds are not for

A world only rewards the edges built into it (see How the data is generated). Use worlds to test how robust a strategy is and to practise, not to discover an edge: anything that wins only on generated data won because the model let it. Every world set includes control worlds with all edges switched off for exactly this comparison.

Making a world

In the Lab, Change on the market card → One world → New world (or in Practice → Generated world):

SettingWhat it does
SeedBlank for a random one, or the dice. Same seed and settings → the same world.
UniverseIndex + 10 stocks (3 sector ETFs and a spread of stock types), Index + 20 stocks (shares only), Tech sector, Leveraged ETFs (the index with its 3× and −3× daily funds), Dividend portfolio.
ScenarioNatural (no injected shocks), 2008-style bear, Flash crash, Vol spike, Slow grind up, Meme squeeze. A scenario is written into the world when it is made, on dates drawn from the seed.
Starts / Years / BarsThe calendar (weekdays, from 2010 by default, 10 years) and daily or 5-minute bars. 5-minute worlds allow intraday pipelines and take about five times the disk.
Planted edgesThe five effects a strategy can earn from (below). Control world switches them all off.

Code copies the world's recipe as a short string (W1.…); From a code rebuilds the same world on any hub. Worlds are generated a year at a time the first time they are used, and stored under ~/.backlight/worlds.

World sets

A world set is a recipe for many worlds: a seed, how many of each scenario, a number of control worlds, the universe and the window. Choosing a set as the Lab's market runs every pipeline on every world in it, and the results come back as a distribution: the median return with its 5th–95th percentile, the share of worlds where the pipeline beat the world's index, and the control worlds' median beside it. Built-in sets:

SetWorlds
@natural-2020 natural worlds.
@stress-5015 natural, 10 bear, 10 flash crash, 10 vol spike, plus 5 control.
@stocks-100100 shares-only worlds (index + 20 stocks): cheap, for stock and ETF strategies.
@holdout-50A held-out set: for a final out-of-sample check, never for tuning.

Make your own with New world set in the market dialog. A set's worlds are generated the first time the set is run; Generate does it ahead of time. Tuning a pipeline against one set until it looks good fits those particular paths, which is what held-out sets are for: running one requires ticking This is a final check, and each use is counted.

Symbols on a world

Looking at a world

Click a world's name anywhere (the Lab, a run's page, the Runs list) to open the world viewer: its tickers on the left, grouped as index, sector ETFs and stocks; the chosen one's bars, return, volatility, drawdown, dividends and reported earnings on the right. A world set opens the same viewer with its worlds in an extra column on the far left; moving between worlds keeps the same ticker slot. The viewer shows only what the market showed — the hidden fair values and regimes stay in the debrief.

Practice on a world

Trade → New practice → Generated world. The session starts a year into the world, the watchlist defaults to all of its tickers, and dates show as Day 1, Day 2…. Everything else is the Practice screen. Two things are only on worlds:

How the data is generated

A world is generated in layers. Each layer reads only the ones above it, plus its own random numbers.

Market regime: calm bull · choppy · bear · crisis GARCH vol (rises after down days), jumps 7 sector factors · scenario overrides Each symbol beta × market + sector + its own shocks hidden fair value · earnings · dividends momentum / reversal tilts Each session overnight gap, then 78 five-minute steps open / high / low / close come from the path volume follows moves and earnings Option surface ATM vol = expected vol × premium skew · term structure · earnings bump Quotes and fills chains, bids/asks for options and stocks priced on read, never stored
The layers. A symbol never reads another symbol, so adding one never changes the rest; the option surface and quotes are recomputed from stored numbers whenever they are asked for.

The market. A hidden regime switches now and then between calm bull, choppy, bear and crisis. Calm stretches last about a year and a half on average, crises about a month; each regime sets the market's drift, its volatility target and how often it jumps (crashes are mostly downward). Day to day volatility follows a GARCH process: it clusters, decays back toward the regime's level, and rises more after down days than up days. Returns have fat tails. Seven sector factors (tech, finance, energy, health, consumer, industrial, utilities) move on top of the market.

Each symbol has a personality that fixes how it behaves:

PersonalityBehaviour
Index fundThe market itself, paying a 1.5% dividend. The world's benchmark.
Sector ETFMarket plus its sector factor.
3× / −3× leveraged ETFThree times the index's daily move (or minus three), reset daily, so it decays in choppy markets.
Mega-cap tech · High-beta growthBeta 1.15 / 1.6, bigger earnings moves for growth.
Bank · Energy · Cyclical industrialHigh beta to the cycle; banks and energy pay dividends.
Consumer defensive · Pharma · UtilityLow beta, steady dividends; pharma has occasional trial-result jumps.
Sleepy valueCheap, slow mid-cap with no options.
Meme small-capVery volatile, fat-tailed, squeeze-prone, expensive to short.

A symbol's daily return is its beta times the market, plus its sector, plus its own GARCH-driven shocks and jumps. Underneath the price sits a hidden fair value that grows at the symbol's growth rate (which changes now and then), and the price is pulled slowly toward it, closing half the gap in about a year and a half. Every quarter the company reports earnings: the surprise is a noisy reading of how far the price is from fair value, the stock gaps on the report, and part of the move seeps in over the next ~40 days. Dividends are paid quarterly and the price drops by them on the ex-day.

Each session starts with an overnight gap (earnings, dividends and jumps land there), then follows a path of 78 five-minute steps that is busier near the open and the close. The day's high and low come from that path, so they always contain the open and close. Volume rises on big moves and earnings days.

Options are priced from a daily volatility surface. At-the-money implied vol is the volatility the model expects over the next month times a risk premium of about 1.1–1.15. Puts are priced richer than calls (skew), and more so after selloffs; the term structure slopes up in calm markets and inverts when vol is high; an expiry that spans an earnings date carries the event's extra variance. Bids and asks use the fitted spread model, wider when vol is high. Before 3:30 ET each day the surface is the previous close's (adjusted for the overnight gap), so nothing reads where the day ends.

Stocks also quote a bid and ask: about 1 basis point for the index, 2 for mega-caps, 3–5 for most stocks and about 30 for the meme small-cap, wider in volatile stretches and below $10. Market orders buy at the ask and sell at the bid; slippage still applies on top.

The planted edges

These are the only things a strategy can earn beyond chance, each at roughly the size seen in real markets:

EdgeHow it is plantedWho it pays
Equity drift (always on)Positive drift in most regimes, plus dividendsBuy and hold, over years and not every year
Fundamental signalPrice is pulled toward a hidden fair value; earnings surprises read itPatient, signal-driven stock pickers
Post-earnings driftPart of an earnings move arrives over ~40 daysSwing traders acting on surprises
Momentum / reversalA small tilt toward the trailing year's winners; last week's overreaction partly revertsTrend followers and dip buyers, with discipline
Volatility risk premiumImplied vol above the vol that follows, with occasional painful shortfallsPremium sellers (covered calls, the wheel, PMCC short legs)

How close it is to a real market, averaged over many natural worlds: the index returns about 8% a year (total, log) at about 16% volatility, nine decades in ten end up, daily returns have fat tails (kurtosis around 13) and volatility clusters. An earnings surprise correlates about 0.05 with the next 40 days' return when the edges are on and about zero in a control world, and options are priced about 4 volatility points above what follows.

Same day, same data

What you see on a day of a world depends only on the world and the day — never on how you got there. Jumping a week or stepping a day seven times, a reset, a duplicate, a symbol added halfway: all see identical prices, quotes and fills. Every random number is drawn from a stream named for its purpose (the world's seed, the layer, the ticker and the year), and each year of each symbol is generated once from the end of the year before and stored with a checksum. Scenarios and shocks are part of the world, not of the play: a shock makes a new world that is identical up to the day it lands.

Conditions, tags and events

The Lab's Conditions tab cuts every result by market condition, on real history and on worlds alike. A tag marks a stretch of time (or a day) with a condition:

FamilyKindsHow it is drawn
Market regimebull · correction · bear · recoveryFrom the index's drawdowns: 10–20% from a peak is a correction, 20% or more a bear market, until the peak is regained
Volatilitycalm · normal · high · extreme20-day realized vol against its own past 5 years
Trenduptrend · range · downtrendPrice against its 200-day average and that average's slope
Shockscrash day · gap · vol spike · squeezeIndex down 4%+ in a day, outsized gaps, vol doubling within weeks, +50% in 5 days
Eventsearnings · ex-dividendFrom the data source (single days)
OptionsIV rich · IV cheapImplied vol against the past month's realized vol
Named eventsCOVID crash, 2022 bear market, …A built-in catalog of real episodes (dates approximate)
Worlds onlytrue regime · injected scenarioThe world's own truth

Each cell of the heatmap is how a pipeline did inside that condition against the index over the same days (green beats it, red trails); a world set's cell is the median across its worlds. Click a cell to see those stretches shaded on the equity curve. The Events tab averages each pipeline's path from 5 days before to 20 after every earnings report, crash day or vol spike. Tags that need hindsight to draw (where a bear market bottomed) are used here only — never fed to a pipeline.

09Runs, reports & comparing

NET P&L RETURN MAX DD SHARPE WIN RATE TRADES +$18,240+18.2% −7.9%1.34 54%86 Equity curve · drawdown shaded below Trades Tag attributionTree leaves SignalsOrdersLog
Run report. Metric tiles, equity and drawdown (the Invested switch overlays the capital tied up in open positions: stock and long options at value, cash-secured puts at their collateral, spreads at max loss), then tabs: Trades; Tag attribution (P&L by node tag); Tree leaves (P&L by the leaf that opened the trade); Signals (each signal expands to its decision path, refinements and notes); Orders; Log. report.md downloads it all as Markdown.

10Paper & live trading

  1. Open Live and choose Paper.
  2. Pick a pipeline, symbols, a Market data connector and an Execution connector (Sim for a pure simulation, or a broker's paper account).
  3. Set risk limits: Max daily loss % halts the run for the day, Max positions caps open positions, Flatten on halt closes everything when a limit trips.
  4. Start. The run ticks on the pipeline's schedule (market hours only unless Tick outside market hours is on). Its portfolio updates live on the right.
Live mode

Live places real orders with real money. It needs a real broker as the execution connector and an explicit confirmation tick. Run the same pipeline on paper first and compare it against its backtest.

Stop ends a run gracefully. KILL stops it immediately; tick Also flatten to close every position at market as well.

Semi-auto. Tick Semi-auto: approve each order and the run proposes instead of trading: every order its pipeline would send waits under the run on the Live page, with why (the strategy's note and what a picker chose), how far its price has moved since it was proposed, and when it lapses. Approve sends it; if the price has moved more than 2% it asks first (its limit price is still the proposed one). Reject drops it, and one nobody answers is dropped after the minutes you set (30 by default). While nothing is working, the strategy proposes again on its next tick, and the newer proposal replaces the waiting one.

Pipelines with option nodes (or trees that use option templates) keep each symbol's chain fresh, refetching it at most once a minute. Alpaca's option data defaults to the free indicative feed, which is delayed: set Admin → Connectors → Alpaca → Options data feed to opra (a paid plan) before scalping on it.

11Practice & Trade: trading by hand

Practice is trading by hand on history, moving a clock forward yourself (below). Trade is the same screen on a real-time account (further down). Practice can also run on a generated world: fake tickers, no hindsight, and a debrief at the end.

A campaign lets you trade stocks and options yourself on real historical prices, moving the clock forward when you choose instead of waiting in real time. It is recorded as a run (mode campaign), so its equity curve, trades and report work like any backtest's, and Compare can put it next to a pipeline over the same dates.

  1. Open Trade → New account → Practice (or New practice above the Practice list). Pick a start date, starting capital, a Cash or Margin account and a watchlist. Add capital deposits a fixed amount every N days, weeks or months; Deposit on the Account panel adds cash at any checkpoint. Deposits are capital, not profit: net P/L leaves them out and the return is time-weighted, as in backtests with contributions. Prices come from the Backtest data sources set in Admin → Connectors; option prices are real bid/ask quotes from 2022-03-08 on (modeled before that, or with a source that has no quotes).
  2. The clock stands on a checkpoint: each trading day's open (9:45 ET) or close (3:45 ET). Orders placed there fill at once when they can (market orders, marketable limits), at that checkpoint's prices. Options cross the spread: you buy at the ask and sell at the bid.
  3. Trade from the option chain: click an ask to buy or a bid to sell; shift-click adds a leg. The Strategy picker builds verticals, calendars, diagonals, straddles, strangles, butterflies, iron condors and covered calls from the strike you click (Width: strikes between legs; Exp. gap: where a calendar or diagonal's second leg goes, either a number of expiries out (1 = the next one) or a span such as 45d, 3w, 2m or 1y for the later expiry closest to that long after the one you click). A PMCC is a long LEAPS call plus a shift-clicked short nearer call. The order ticket opens right under the row you clicked (for shares, under the symbol header). Review shows the price, max profit and loss, break-evens and the buying-power effect before you Send. A limit order can Walk price: every interval it is cancelled and sent again one step closer to the market (a buy or a spread's net walks up, a sell down) until it fills or reaches Up to (empty: the market's side, the natural price). Practice moves in open / close checkpoints, so there a walk steps at most once per checkpoint; on the Trade desk it runs in real time (5 seconds at the fastest).
  4. Move the clock with To close / To next open, +1D, +1W, +1M or Until (the next expiry of an option you hold, an order filling, or a date). Keys: N, D, W, M. A new margin call always stops the clock.
  5. The Monitor tab lists positions by underlying (close, roll, exercise), working orders (cancel) and filled orders. % of max is the open P/L as a share of the most the position can make at expiry, for defined-risk structures (all options on one expiry, upside capped: verticals, condors, short options, covered calls), on the underlying's row; a short option leg that stands alone (naked, covered, a PMCC's short call) shows the share of its credit captured, a spread's short leg does not. Roll moves an option out by its last settings, set from its arrow: Expiry, counted from the Held expiry (expiries out, or a span such as 30d or 1m; default 1, the next expiry) or from Today (2 = the second listed expiry, 45d = the expiry nearest 45 days out; it must be after the held one) and either Strikes ± (strikes higher or lower; default 0, the same strike) or a Delta target (the strike in the new expiry whose delta is nearest it, e.g. 0.30). The Columns button (the table icon) on the position statement and on the option chain picks the columns shown; your choice is kept with your settings. Score compares your return with SPY bought and held from the start.

Suggest (above the option chain) proposes contracts for a structure on the symbol you are looking at: pick the structure — cash-secured put, covered call, credit or debit spread, iron condor, long call/put, straddle, strangle, a PMCC, or calls against shares you hold — and what chooses its contracts: the nearest delta, a picker with its settings, or a node of one of your pipelines. A strategy node brings its own structure and settings (and says whether its entry rules — trend, IV rank, timing — would pass right now); a picker node brings the pickers before it in its pipeline, and a strategy node the ones after it. You get each leg's delta, how far out of the money it is, days to expiry, mid and spread, the credit or debit, max loss and capital tied up. Load into ticket fills the order ticket; nothing is placed.

Advise (on the clock bar) asks one of your pipelines what it would do now: it runs once at the checkpoint on your positions and watchlist, as if its trigger had fired, and lists the orders it would place — entries, closes, rolls — each with why and what it does to the account. Place it, load it into the ticket to adjust it first, or skip it. Signals that went no further (a refiner vetoed them, the sizer had no room) are listed too. A wheel or PMCC pipeline can tell you "buy 2 LEAPS and sell 2 calls against them" and, weeks later, "roll the call".

What happens as the clock moves:

The first time a symbol and month are needed outside the cached data, the hub downloads them (option history can take a few minutes on a REST plan): a banner shows what is being downloaded and how far along it is, and the screen fills in when it is done. After that the data comes from the cache. Only about two months past the clock are downloaded ahead.

Reset… takes the campaign back to any earlier checkpoint and asks how: Duplicate, then reset copies the campaign and resets the copy (the original is kept), or Hard reset deletes everything after that checkpoint in this campaign (it cannot be undone; the campaign counts its resets). Blind mode hides the year: dates show as Day 1, Day 2… so you cannot trade on what you remember. End campaign writes the final summary; positions stay as they are.

Trade: by hand, in real time

Trade lists your real-time accounts, then your Practice sessions, one full-width tile each: net liq, cash, buying power, open and realized P/L, positions, working orders and the biggest holdings, with a red live · real money badge on a live broker account. A real-time account is the Practice screen with no clock and no run. Add one with New account:

Everything else is the Practice screen: watchlist, chart, the option chain (click a bid or ask, shift-click to add legs, the Strategy picker), the order ticket with its Review, the Monitor tab (close, roll, cancel), and Suggest and Advise at live prices — "what would my wheel pipeline do on this account now?". Orders sent from the desk are tagged manual: the broker scan under Live lists them as yours, not as unclaimed, and a pipeline's run on the same broker keeps managing only its own positions. Accounts are private: nobody else on the hub, admins included, sees or trades yours. Removing a broker account from the hub leaves the account at the broker alone; removing a local paper one deletes its book.

Market hours. The desk shows whether the market is open (and when it closes) or closed (and when it opens), from the broker's own trading calendar when it has one, else the price feed's; hover the chip to see whose. While it is closed, market orders can't be sent; limit and stop orders are accepted and wait for the open. Local paper fills nothing while the market is closed, and its DAY orders end at the close of their session.

The chart (Practice and Trade) has a range (1M–2Y), a bar size (day, week, month) and candles or a line, remembered in this browser.

Redacted view (the user menu, top right) hides real-money figures for screen recording: a live account's balances, positions, orders and activity on its tile and trading screen, and a live run's P&L, equity curve and book on the Dashboard, Runs, Live and the run report. Paper, local paper, practice and backtests stay visible. A redacted chip in the top bar shows it is on; click it to turn it off.

12Data cache

Historical bars are cached on disk the first time a backtest needs them. The Admin → Data view lets you pre-fetch a range (Fetch bars), see what is cached per provider, symbol and timeframe, delete entries, and preview any of them as candles or a line.

13Admin & files

WhatWhereOverride
Pipelines, trees, hub config, secrets, plugins~/.config/backlight/BACKLIGHT_CONFIG_DIR
Database (runs, trades, signals…)~/.backlight/backlight.dbBACKLIGHT_DATA_DIR, BACKLIGHT_DB
Bar cache, logs~/.cache/backlight/BACKLIGHT_CACHE_DIR

Custom nodes and connectors are Python files dropped into ~/.config/backlight/plugins/nodes/ or plugins/connectors/; they show up in the palette and the Add-connector list after a restart. A node's docstring is its documentation in the workbench, and a plugin can register its own port types with their documentation (see the README's Writing a node).

Everything in the app is also available from the command line:

backlight backtest preset-sma-cross --symbols SPY,QQQ --start 2022-01-01 --end 2023-12-31
backlight runs --mode backtest
backlight report <run_id> [<other_run_id>]   # markdown report / comparison
backlight fetch --provider massive --symbols SPY --start 2020-01-01 --end 2024-12-31
backlight purge --older-than-days 30 --vacuum

Signing in. Sign-in is optional and off on the desktop. When a hub has users — a deployed hub always does — every visit starts at a sign-in page, the app bar shows who is signed in, and its menu has Account, My brokers and Sign out. A session lasts a week and renews while you use it; an expired one sends you back to sign in and then to the view you were on.

Sharing a hub

A group can share one hub: everyone backtests and plays campaigns on the same data, sees each other's work, and trades paper or live on their own account. Each user is an admin or a user:

AdminUser
Hub settings, connectors, the data cache, built-in symbol sets, userschanges them (Admin)—
Pipelines, trees, backtests, campaigns, paper/live runsmakes their own; sees and changes everyone'smakes their own; sees everyone's public ones, changes only their own
Broker keys for paper/livetheir own, or the hub'stheir own (Account → My brokers)

Cache-only. An admin can switch on Admin → General → Cache-only. Nothing is downloaded for users from then on: the Backtest Lab shows what is cached (with a use link that fills in those symbols and dates) and, for the backtest being set up, which symbols' bars are missing — such a backtest cannot start until you narrow the dates or symbols. Option history that is not cached falls back to synthetic chains, and campaigns play as far as the cache goes. Admins are not held to it: they fill the cache in Admin → Data.

Sign-ups. An admin can turn on Admin → Users → Allow sign-ups: the sign-in page then offers Request access. A new account cannot sign in until an admin approves it (as a user or an admin) or turns it down; admins see waiting requests as a badge on their name in the app bar.

Users are managed in Admin → Users: add one (with a password they can change in Account), make them an admin or a user, set a new password, or remove them — handing their runs, pipelines and trees to someone else, or leaving them under their name. Users from the command line (backlight auth add-user NAME) or a cluster's users Secret (docs/deploy.md) are added here once, as admins.

14Troubleshooting

SymptomTry
A provider or broker is missing from a pickerAdd it in Admin → Connectors. Pickers only list added connectors that have the right role.
Red dot on a connectorExpand it — the status line says what is missing (usually a key). Save, then Test.
Sent back to the sign-in pageThe session ended: it expired, your password changed, or the hub's session secret was rotated. Sign in again.
Schwab says the token expiredRun OAuth start / OAuth finish again; Token status shows when it expires.
Pipeline won't save or runRead the problems bar under the workbench toolbar and press Validate. Mismatched port types are the usual cause.
A backtest sits at pending with no progressIt is still loading bars, often fetching warm-up history the cache doesn't have from a connector whose plan refuses it (HTTP 429 in the hub log). Check the Lab's bars must start around … notice; see Warm-up.
A backtest makes no tradesClear Warm-up bars (auto covers every window the pipeline declares) or raise it for a custom node's hidden window, and check the Signals tab for refiner vetoes.
Red dot in the top barThe window lost the hub; it reconnects automatically. If it stays red, restart backlight hub.
Database is largeRuns → Purge backtests with VACUUM, or turn off Record full detail for sweeps.