Backlight
Automated stock and options trading platform: pipelines of parameterized nodes pick securities, decide how to trade them through tagged decision trees, refine those decisions, and hand orders to a connector. One runner drives the same pipeline in backtest, paper, or live mode. Built on the same stack as proxer: pywebview + FastAPI + Vue 3 (Vuetify).
Quick start
pip install py-backlight # or, from a checkout: make install (pip install -e .[dev] + build the UI)
backlight hub # opens the desktop window (or: backlight hub --headless)
The PyPI distribution is py-backlight (backlight there is an old, empty
project); the package, the import and the command are all backlight. Extras:
py-backlight[agent] (the pipeline agent) and py-backlight[databento].
make check builds the wheel with the UI bundled and runs twine check;
make deploy uploads it.
Upgrading from trader, the old name: the first run moves ~/.config/trader,
~/.trader (and trader.db in it) and ~/.cache/trader to their backlight
names, TRADER_* environment variables still work where no BACKLIGHT_*
one is set, and agent nodes are rewritten to import backlight.
Without the window, the API is at http://127.0.0.1:8770/docs.
Deployed on a cluster, headless, behind a Cloudflare tunnel and with sign-in
on: see docs/deploy.md (Dockerfile, Helm chart in
charts/backlight). Sign-in is optional — off on the desktop unless you add a
user with backlight auth add-user NAME.
From the command line:
backlight catalog # node types and connectors
backlight backtest preset-sma-cross --symbols SPY,QQQ --start 2022-01-01 --end 2023-12-31
backlight runs --mode backtest
backlight report <run_id> # markdown run report
backlight report <run_id_a> <run_id_b> # compare
backlight purge --older-than-days 30 --vacuum # delete old backtest runs
backlight fetch --provider massive --symbols SPY --start 2020-01-01 --end 2024-12-31
backlight flatfiles --start 2024-01-01 --end 2024-12-31 # Massive option minute files, once
backlight backtest preset-opt-0dte-condor --symbols SPY --provider alpaca --option-provider massive_files
The first launch seeds fifteen preset pipelines (stock, options, and two branching
examples) and four decision trees into
~/.config/backlight/, and the synthetic data connector needs no network, so
the Backtest Lab works out of the box.
Layout
| Path | What |
|---|---|
src/backlight/types.py |
The objects that cross node ports: SecurityId, Bar, Quote, OptionContract, Universe, Signal, Intent, OrderRequest, OrderEvent, Position. |
src/backlight/pipeline/ |
params.py (parameter schemas, from proxer), nodes/ (trigger, universe, selector, strategy, refiner, sizer, execution, output, flow), tree/ (decision tree model, safe expression language, action templates), graph/ (pipeline docs, validation, compile, catalog, plugins, stores, presets), engine.py (the tick). |
src/backlight/connectors/ |
base.py (the three interfaces), sim (fill model, brackets, option expiry), synthetic (offline random-walk bars, Black-Scholes chains, paper-mode market data), csv, schwab, alpaca, massive, massive_files (option flat files over S3), kalshi (plan only). |
src/backlight/runner/ |
portfolio.py (ledger), feed.py (bar views for nodes and the sim), backtest.py, live.py (paper and live), recorder.py. |
src/backlight/db/ |
SQLite schema (every run-scoped table cascades from runs, which carries a mode column), migrations, run repo with purge, batched record writer, report queries and metrics. |
src/backlight/hub/ |
FastAPI routers (api/), background jobs, WebSocket event bus, hub config, secrets, pywebview shell. |
gui/ |
Vue 3 + Vuetify + VueFlow UI: Dashboard, Pipelines workbench, Decision Trees, Backtest Lab, Runs, Live, Data, Settings (with the Connectors tab), and an in-app illustrated guide (gui/public/guide.html). |
charts/backlight/, Dockerfile |
Container image and Helm chart: one hub pod on a PVC, optional sign-in, optional cloudflared tunnel. |
docs/ |
The proposal, architecture notes, connector setup, Kalshi plan. |
How a tick works
trigger -> universe -> selector -> strategy (decision tree) -> refiners -> sizer -> execution -> final
Every node has PARAMETERS rendered by the GUI, typed ports checked at compile
time, and tags. A strategy emits Signals carrying the DecisionPath that
produced them; refiners veto or reshape and leave a note; the sizer turns the
signal into an Intent; execution builds OrderRequests (with bracket
children); output.final applies buying power and per-underlying caps. Every
hop is recorded, so a fill walks back to the tree leaf and selector rule that
caused it, and reports attribute P&L per tag, per leaf, and per tree edge.
Modes
| Mode | Clock | Data | Execution |
|---|---|---|---|
| backtest | bar iterator | historical connector + Parquet cache | sim |
| paper | wall clock | market connector | sim or a venue's paper account |
| live | wall clock | market connector | venue (Schwab) |
Backtest runs can be purged from the Runs view or with backlight purge; paper
and live runs are kept unless purged by mode explicitly.
Writing a node
from backlight.pipeline.nodes.base import RefinerNode, register_node
from backlight.pipeline.params import Parameter
@register_node
class NoFridays(RefinerNode):
"""Veto new entries on Fridays."""
kind = "refiner.no-fridays"
label = "No Fridays"
PARAMETERS = [Parameter("enabled_days", "number", "Days", default=4, min=0, max=6, step=1)]
def applies(self, signal, ctx):
return signal.action.value.startswith("open")
def apply(self, signal, ctx):
return self.veto("friday") if ctx.now.weekday() == 4 else signal
The docstring is the node's documentation in the workbench: its first
paragraph is the palette description, the whole of it shows under More.
PORT_DOCS = {"port": "..."} says what a port means on this node. Text is
plain: blank lines split paragraphs, - starts a list item, backticks mark
code.
Port types (run, universe, signal, intent, order) live in a registry
in backlight.pipeline.ports; clicking a port in the workbench opens its
documentation. A plugin registers its own from the same module:
from backlight.pipeline.ports import PortType, register_port_type
register_port_type(PortType(
"alert", "Alert", "A message for the operator.",
doc="Longer explanation for the port-type dialog.",
carries=Alert, # a dataclass: its fields are listed
field_docs={"text": "What to say."}, color="#e11d48"))
The engine only moves values along the ports each node category handles, so a plugin type is documented and wired in the editor, but nothing produces or consumes it yet.
Drop the file in ~/.config/backlight/plugins/nodes/ or register it under the
backlight.nodes entry-point group. Connectors work the same way under
plugins/connectors/ and the backlight.connectors group.
Pipeline agent
In the pipeline editor, Agent opens a chat that builds and edits the open pipeline: it reads the node catalog and other pipelines, adds and wires nodes, sets parameters and validates, and the canvas follows each edit (nothing is saved until you press Save). Chats are per user and kept per pipeline.
When no node fits, the agent can write one. A written node loads only after:
a static check (allowed imports and calls, no private attributes or routes to
connectors, never an execution or output node), an import test in a separate
process with an empty environment, and approval by a reviewer agent. Approved
nodes live in ~/.config/backlight/plugins/agent-nodes/ and are listed (with
their code and review) under Admin → Settings → Agent, where an admin can
remove them.
Setup: pip install -e ".[agent]" (the Docker image includes it), then Admin →
Settings → Agent: provider (OpenAI or Anthropic), its API key, model, Enable.
OpenAI also covers any OpenAI-compatible endpoint through the base URL.
Anthropic is spoken natively (Claude's thinking is kept across turns, an
effort setting, prompt caching, and on Anthropic's own API a declined request
is retried on Anthropic's recommended fallback model). One hub-wide key per
provider; every signed-in user's chats use the active one.
The tools (backlight.agent.tools) are plain name + JSON schema + handler over a
server-side working pipeline, so another front end (an MCP server) can expose
the same set.
Paths
| What | Where | Override |
|---|---|---|
| pipelines, trees, hub config, secrets, plugins | ~/.config/backlight/ |
BACKLIGHT_CONFIG_DIR |
| SQLite database | ~/.backlight/backlight.db |
BACKLIGHT_DATA_DIR, BACKLIGHT_DB |
| Parquet bar cache, logs | ~/.cache/backlight/ |
BACKLIGHT_CACHE_DIR |
Moving a cache to another instance
A provider's cached bars, option blocks and downloaded store (Massive flat files, Databento's dataset) export to one tar and import anywhere, from the Data tab (Export / import cache) or the CLI:
backlight cache list # what each provider holds
backlight cache export --provider massive --provider massive_files -o massive.tar
backlight cache export --provider massive_files --timeframe 1d -o day.tar # only some timeframes
backlight cache import massive.tar # on the other instance
ssh a backlight cache export --provider massive -o - | backlight cache import - # or piped
An import never deletes. Files already there are kept unless the archive's
copy is newer (--mode newer, the default; bar files merge instead), or
always replaced (--mode replace), or always kept (--mode skip). Store
files land in the importing instance's own data_dir for that connector.
The Data tab uploads in 32 MB pieces, so a proxy's body limit (Cloudflare:
100 MB) doesn't get in the way.
Development
make test # pytest
make smoke # every preset through a two-year synthetic backtest
make gui # rebuild the UI after editing gui/src
BACKLIGHT_HUB_DEBUG=1 backlight hub # WebKit inspector in the window
Metadata
Release files for py-backlight 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| py_backlight-0.1.0.tar.gz | 3.3 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| py_backlight-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 6.7 MB
Release files / py_backlight-0.1.0.tar.gz
| Download URL | py_backlight-0.1.0.tar.gz |
|---|---|
| Size | 3.3 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ea02a63e9a8d945f244e7051c88673ab17a4bd976a9e24516f1bfc512867938f
|
|
BLAKE2b-256 checksum How to use checksums |
0e5af1ae3ef18734f4f547171d9d005d571dd896cca5fec51f37405eb3122454
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Release files / py_backlight-0.1.0-py3-none-any.whl
| Download URL | py_backlight-0.1.0-py3-none-any.whl |
|---|---|
| Size | 3.4 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0447ebf9db54e4324004425a0a65a97fedcaf6f365e2a16ff5ff70f3be213344
|
|
BLAKE2b-256 checksum How to use checksums |
35abdc33870ae1b3b32611a5dad9c430d9503ae9f0ff85db81dcda42d843a00f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|