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:
| Mode | Clock | Market data | Orders go to |
|---|---|---|---|
| Backtest | replays history bar by bar | a historical connector (cached to disk) | the built-in simulator |
| Paper | real time | a market-data connector | the simulator, or a broker's paper account |
| Live | real time | a market-data connector | your broker — real money |
02The window
| View | Use it to |
|---|---|
| Dashboard | See active jobs, running paper/live runs and recent results at a glance. |
| Pipelines | Build and edit strategies as node graphs. |
| Backtest Lab | Run one or many pipelines over a date range and compare them. |
| Runs | Browse every run, open its report, compare, delete or purge. |
| Trade | Trade 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. |
| Live | Start, watch, stop or kill paper and live (real-time) runs. |
| Admin | Connectors (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:
- Open Backtest Lab in the rail.
- Leave Stock bars on Synthetic, keep the default symbols (or try
XL?for the sector ETFs) and the last two years. - Under Strategies, tick SMA crossover and Buy and hold (baseline).
- Press ▶ Run 2 backtests. Each pipeline gets a progress bar with equity and trade count.
- When the batch finishes, the Results table appears. Click a row for its full report, or Compare to overlay them.
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:
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.
Adding one
- Open Admin → Connectors and press + Add connector.
- Pick the connector. Its settings form appears — API keys, paper/live switch, data feed and so on.
- Fill in what you have and press Add. You can finish the rest later from its row.
- Press Test on the new row. A green check means it can reach the service; red shows the error.
| Connector | Roles | Needs |
|---|---|---|
| Synthetic / Synthetic market | historical market | Nothing — random-walk bars and option chains, offline. Great for trying things. |
| Sim | execution | Nothing — the fill simulator for backtests and paper runs (slippage, commissions, brackets, expiry). |
| CSV | historical | A folder of SYMBOL.csv bar files. |
| Alpaca | historical market execution | API key and secret; tick Paper for a paper account. |
| Schwab | historical market execution | App key/secret, then sign in with OAuth start → paste the redirect URL → OAuth finish. |
| Massive | historical market | API key. |
| Kalshi | — | Plan only; not usable for runs yet. |
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.
- Save bumps the pipeline's revision (
r3). Each run keeps a snapshot, so later edits never change old results. - Presets (marked preset) can be edited, but Save as copy keeps the original for reference. Admin → Reset presets restores them.
- The palette's search matches names, descriptions and tags; Enter adds the first match, Esc clears it. Categories start collapsed: click a header to open one (the ⇕ buttons expand or collapse all).
- Each node's LED toggles it on and off without deleting it — handy for A/B-ing a refiner.
- Agent opens a chat that builds or changes the open pipeline: describe the strategy, and the agent picks nodes, wires them, sets parameters and validates while you watch the canvas. Nothing is saved until you press Save. If no node fits, it can write a new one, which loads only after automatic checks and a reviewer agent approve it. An admin picks the provider (OpenAI or Anthropic), model and key under Admin → Settings → Agent.
- Timeframe sets the bar size; Lookback bars is how much history indicators get each tick. How much a backtest loads before Start is worked out from the nodes (see Warm-up).
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.
- Add case appends a case; the arrows reorder (order matters — first match wins); ✕ removes one, along with any wire from its port.
- Rename a case freely: wires attach to its port id (shown under the case, e.g.
port: case_4), which never changes. - A case with a blank condition never matches — handy while you are still wiring.
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):
| Node | What 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 / fly | Sells 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. |
| Name | Meaning |
|---|---|
iv, hv | 30-day at-the-money implied vol; 20-day realized vol (0.25 = 25%) |
iv_rank, iv_percentile | 0–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_slope | 1-sigma move to 30 days; 25-delta put minus call IV; 60-day minus 30-day IV |
expected_move_close, expected_move_close_pct | 1-sigma move from now to today's close, in price and as a fraction of it |
dte, hours_to_expiry | for the position being managed: calendar days to the nearest expiry (0 = today); hours until it stops trading |
delta, net_gamma, net_theta, short_delta | net delta in shares, delta gained per $1 up, $/day decay, the most-tested short leg |
premium, premium_pnl_pct, premium_pnl_peak | net 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_trade | the 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:
| Picker | Chooses |
|---|---|
| Pick by delta | Other target deltas for the sold and bought legs, and optionally another DTE window (0 keeps the strategy's). |
| Pick by % out of the money | Sold (or bought) legs that far out of the money: puts below the price, calls above it; negative is in the money. |
| Pick by premium | The 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 rules | Monthly (third Friday) or Friday expiries only, another DTE window, and which expiry in it: the nearest, the middle or the farthest. |
| Liquidity guard | Only 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.
| Name | Meaning |
|---|---|
last_had("structure:pmcc") | true when the symbol's last close carried that tag (a structure, exit:… rule or node:…) |
last_structure, last_exit | what it last closed ("pmcc", "short_put") and why ("pmcc-profit", "assigned"); "" if never |
days_since_exit, closed_count | days since that close (blank if never); positions closed on the symbol so far |
last_pnl, last_proceeds | the 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):
- Eligible: enough history, a chain with a put near the probe delta (30-delta, 30 days), and a probe spread under the limit.
- 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.
- 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.
- 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.
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)
- New starts an empty tree; Open lists your saved trees, most recently edited first, with a filter box (name, id, description or tag) — Enter opens the top match.
- Node positions are saved with the tree; moving a node in a saved tree keeps the layout straight away, without a Save or a new revision.
- Session names restart at each regular-hours open (09:30 ET):
vwap(),day_open(),day_high(),day_low(),or_high(minutes)/or_low(minutes)(the opening range),prev_close(),prev_high(),prev_low()andgap_pct(). The clock namesminutes_since_openandminutes_to_closeuse the regular session (the venue's real close in paper and live).ref("SYMBOL", "indicator", …)reads any indicator on another symbol. - Other timeframes. Any indicator takes
tf=to read longer bars than the pipeline's:close > sma(50, tf="1d")in an hourly pipeline compares the latest hourly close with the 50-day average, wheresma(50)would be 50 hours. Only bars that have closed count (a daily bar at 16:00 ET), andagocounts bars of that timeframe. A shorter timeframe than the pipeline's is ignored. - Parameters (the table beside the canvas) are named numbers a tree reads with
param("name"). Pipelines — and individual backtest runs — can override them without editing the tree. - Validate checks every expression and that each branch ends in an action.
- Select a node and use Set as root, Delete node or Delete branch.
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.
| Card | Settings |
|---|---|
| Market | Change 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. |
| Symbols | Tickers, @sets and wildcards (below), and the timeframe (pipeline uses each pipeline's own). On a world,
blank means all of its tickers. |
| Account | Starting 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. |
| Pipelines | The 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):
- Summary: a row per pipeline. On a world set: the median return with its 5–95% range, the worst drawdown, the share of worlds that beat the world's index, and the control worlds' median (a pipeline that only wins where the controls win too is riding drift).
- Equity: each pipeline's return over time; on a world set, the median with bands for the middle half and 90% of worlds. Hover a legend entry to show that one alone.
- Conditions and Events: how each pipeline did in bull and bear markets, high volatility, crash days, around earnings (see Conditions & tags).
- Worlds: one row per world of a set.
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:
| Pattern | Matches |
|---|---|
| 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. |
| @mag7 | A named symbol set, inlined in place — here AAPL, MSFT, GOOGL, AMZN, META, NVDA, TSLA. |
| -TSLA, -XL?, -@semis | A 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.
- Record full detail (Advanced) saves every signal and tree evaluation — needed for the Signals tab and tree overlay. Turn it off for big sweeps.
- Use bar cache reuses previously downloaded bars; untick to force a fresh fetch. Liquidate at end closes everything on the last bar so the final P&L is realized.
- Robustness · simulated hub outages (Advanced) takes the hub down at random times, up to Longest (days) each.
While it is down the pipeline does not tick. When it comes back, the run is restored the way a paper or live run is after a restart:
the book is replayed from its fills, the pipeline starts fresh, and working orders are either re-placed (lost with the hub,
as with the paper simulator) or were left working at the broker the whole time (live). Options that expired meanwhile settle on resume.
Run the same pipeline with and without outages and compare the two. Strategies that act on an event (a crossover, a breakout) can miss it
entirely. The run header lists the outages, and warns if a restored book ever differed from the one the run held.
On the command line:
backlight backtest … --random-outages 5 --outage-max-days 4or--outage 2024-03-01/2024-03-08. - The Lab remembers your last settings. A batch keeps running if you leave the view — the top bar shows its progress.
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 uses | Warm-up (daily bars) |
|---|---|
| Windows on its nodes — SMA lengths, channel bars, volatility / correlation windows, Min history bars | the 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 expression | 272: 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.
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):
| Setting | What it does |
|---|---|
| Seed | Blank for a random one, or the dice. Same seed and settings → the same world. |
| Universe | Index + 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. |
| Scenario | Natural (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 / Bars | The 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 edges | The 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:
| Set | Worlds |
|---|---|
| @natural-20 | 20 natural worlds. |
| @stress-50 | 15 natural, 10 bear, 10 flash crash, 10 vol spike, plus 5 control. |
| @stocks-100 | 100 shares-only worlds (index + 20 stocks): cheap, for stock and ETF strategies. |
| @holdout-50 | A 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
- Leave the Lab's symbol box blank to run on all of the world's tickers.
@world.index,@world.stocks,@world.sectors,@world.optionableand@world.tech(or any sector) pick some. SPY(and VOO, IVV and the other broad index funds) is the world's index under that name.- Any other real ticker you type, or that a pipeline names inside a node, gets a stand-in: a fresh made-up path that behaves like that kind of stock (AAPL → a mega-cap tech stock, TQQQ → a 3× leveraged ETF). It is not AAPL's history.
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:
- Throw something at me… (the ⋮ menu): from tomorrow the world forks and something happens — a crash, a vol spike, a bear market or a squeeze — without saying which. Everything up to today stays the same; resetting to before it undoes it.
- Debrief, once the session has ended: the market's true regime over your session, each symbol's hidden fair value under its price, the volatility options charged against what followed, what each earnings report really said, and your trades marked with the regime and how rich or cheap the stock was when you opened them.
How the data is generated
A world is generated in layers. Each layer reads only the ones above it, plus its own random numbers.
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:
| Personality | Behaviour |
|---|---|
| Index fund | The market itself, paying a 1.5% dividend. The world's benchmark. |
| Sector ETF | Market plus its sector factor. |
| 3× / −3× leveraged ETF | Three times the index's daily move (or minus three), reset daily, so it decays in choppy markets. |
| Mega-cap tech · High-beta growth | Beta 1.15 / 1.6, bigger earnings moves for growth. |
| Bank · Energy · Cyclical industrial | High beta to the cycle; banks and energy pay dividends. |
| Consumer defensive · Pharma · Utility | Low beta, steady dividends; pharma has occasional trial-result jumps. |
| Sleepy value | Cheap, slow mid-cap with no options. |
| Meme small-cap | Very 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:
| Edge | How it is planted | Who it pays |
|---|---|---|
| Equity drift (always on) | Positive drift in most regimes, plus dividends | Buy and hold, over years and not every year |
| Fundamental signal | Price is pulled toward a hidden fair value; earnings surprises read it | Patient, signal-driven stock pickers |
| Post-earnings drift | Part of an earnings move arrives over ~40 days | Swing traders acting on surprises |
| Momentum / reversal | A small tilt toward the trailing year's winners; last week's overreaction partly reverts | Trend followers and dip buyers, with discipline |
| Volatility risk premium | Implied vol above the vol that follows, with occasional painful shortfalls | Premium 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:
| Family | Kinds | How it is drawn |
|---|---|---|
| Market regime | bull · correction · bear · recovery | From the index's drawdowns: 10–20% from a peak is a correction, 20% or more a bear market, until the peak is regained |
| Volatility | calm · normal · high · extreme | 20-day realized vol against its own past 5 years |
| Trend | uptrend · range · downtrend | Price against its 200-day average and that average's slope |
| Shocks | crash day · gap · vol spike · squeeze | Index down 4%+ in a day, outsized gaps, vol doubling within weeks, +50% in 5 days |
| Events | earnings · ex-dividend | From the data source (single days) |
| Options | IV rich · IV cheap | Implied vol against the past month's realized vol |
| Named events | COVID crash, 2022 bear market, … | A built-in catalog of real episodes (dates approximate) |
| Worlds only | true regime · injected scenario | The 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
- In Runs, filter by mode (All / Backtest / Paper / Live / Campaign), click a label to rename it, and tick rows to Compare or Delete.
- With Batches on (the default) each Backtest Lab batch is one row: its market and a headline return per pipeline (a world set's median). Open it for each pipeline's numbers, and on a world set for each world's run. Ticking a batch selects all of its runs, so Delete removes the whole batch. Turn Batches off for the flat list.
- Compare overlays normalized equity curves, lines up the metrics with the best in each row highlighted, and lists every parameter that differs between the runs' pipelines.
- Purge backtests (the broom) deletes old backtest runs — optionally only world-set, single-world or real-history ones, keeping the selected ones — and can VACUUM the database to reclaim disk. World-set batches add a run per pipeline per world, so they pile up fastest. Paper and live runs are never purged by it.
10Paper & live trading
- Open Live and choose Paper.
- Pick a pipeline, symbols, a Market data connector and an Execution connector (Sim for a pure simulation, or a broker's paper account).
- 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.
- 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 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.
- 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).
- 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.
- 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).
- 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.
- 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:
- Working stops and limits are checked against each day's range. For options that uses the day's option trade range, so fills between checkpoints are approximate. DAY orders expire at the close.
- Options expiring that day are exercised or assigned when in the money, or expire worthless. Dividends, interest on cash and (margin accounts) interest on a debit balance are booked overnight.
- Cash account: stock and long options paid in full; covered calls, cash-secured puts and defined-risk spreads, including diagonals and PMCCs. A short call is covered only by shares or by a long call expiring on or after it. If an assignment leaves short stock, or a short call becomes uncovered, exercise the long call or close it before the next close, or it is closed at market.
- Margin account: Reg-T style: 50% initial and 25% maintenance on stock (75% on leveraged ETFs such as TQQQ), naked options at 20% of the underlying less the out-of-the-money amount (10% minimum), short stock. Falling below maintenance is a margin call: deposit cash or close positions; if it is still open at a later close, positions are liquidated at market.
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:
- Broker: your account at a broker (keys in Account → My brokers). Live or the broker's own paper account is the broker's setting (Alpaca's paper toggle). Its cash, buying power and positions are the broker's — the At the broker panel shows them as the broker reports; working orders sent from here are re-attached when the desk opens. A live account says so in red and asks before every order.
- Local paper: the simulator, at live quotes from the market data connector you pick, with its own book kept by the hub (orders, fills and positions survive a restart). Pick the starting cash and cash or margin rules; Deposit adds cash any time.
- Practice: a session on history, above.
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
- General: hub host/port (applies on next start), dark/light theme, reset presets, file paths, database row counts and size, raw config.
- Connectors: see section 4.
- Symbol sets: named symbol lists used as
@name; see Backtest Lab. Yours are kept in~/.config/backlight/symbol-sets.yml.
| What | Where | Override |
|---|---|---|
| Pipelines, trees, hub config, secrets, plugins | ~/.config/backlight/ | BACKLIGHT_CONFIG_DIR |
| Database (runs, trades, signals…) | ~/.backlight/backlight.db | BACKLIGHT_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:
| Admin | User | |
|---|---|---|
| Hub settings, connectors, the data cache, built-in symbol sets, users | changes them (Admin) | — |
| Pipelines, trees, backtests, campaigns, paper/live runs | makes their own; sees and changes everyone's | makes their own; sees everyone's public ones, changes only their own |
| Broker keys for paper/live | their own, or the hub's | their own (Account → My brokers) |
- Owners. Everything you make is yours, and shows your username: the Owner column in Runs, the User column in Practice, a chip next to a pipeline's or tree's name. Presets belong to nobody (shared): admins keep them, everyone runs and copies them.
- Public or private. New runs, pipelines, trees and campaigns are public: everyone on the hub sees them. Click the globe/lock beside your own to make one private (only you and admins see it) — handy for experiments nobody else needs in their lists. The Backtest Lab and Live forms choose it for new runs.
- Only mine. The Runs, Practice (on Trade) and Live lists, and the pipeline and tree pickers, have an Only mine switch; it is remembered per list in your browser. In the Backtest Lab, ticked strategies stay listed even when they aren't yours or don't match the filter.
- Someone else's pipeline opens read-only: run it, or Save as my copy to change it. Their trees work the same way (Duplicate to edit).
- Practice sessions are played only by whoever started them. Open someone's public campaign to follow along (you see their clock, positions and orders, nothing is clickable), or Duplicate to play on from the same checkpoint in a copy of your own — a good way to study a position together.
- Paper and live runs trade on their owner's own broker keys, added in Account → My brokers — any broker connector, whether or not the hub has one of its own. Nobody else's runs use them, and an admin who has none of their own trades on the hub's. Market data comes from the hub's connectors unless you add your own.
- Symbol sets (Account → Symbol sets): anyone makes them, and they are
theirs to change or delete. Everyone sees every set —
@namemeans the same list in anyone's backtest — so pick a name nobody has; someone else's set (or a built-in) can be saved under a new name. - Your settings — theme, Backtest Lab and Live form values, campaign defaults — are yours alone.
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
| Symptom | Try |
|---|---|
| A provider or broker is missing from a picker | Add it in Admin → Connectors. Pickers only list added connectors that have the right role. |
| Red dot on a connector | Expand it — the status line says what is missing (usually a key). Save, then Test. |
| Sent back to the sign-in page | The session ended: it expired, your password changed, or the hub's session secret was rotated. Sign in again. |
| Schwab says the token expired | Run OAuth start / OAuth finish again; Token status shows when it expires. |
| Pipeline won't save or run | Read the problems bar under the workbench toolbar and press Validate. Mismatched port types are the usual cause. |
| A backtest sits at pending with no progress | It 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 trades | Clear 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 bar | The window lost the hub; it reconnects automatically. If it stays red, restart backlight hub. |
| Database is large | Runs → Purge backtests with VACUUM, or turn off Record full detail for sweeps. |