Metadata-Version: 2.5
Name: py-backlight
Version: 0.1.0
Summary: Backtest, paper trade and live trade stock and option strategies built from pipelines of nodes
Project-URL: Homepage, https://gitlab.com/JasonBerger/backlight
Project-URL: Documentation, https://gitlab.com/JasonBerger/backlight/-/blob/main/README.md
Project-URL: Repository, https://gitlab.com/JasonBerger/backlight
Project-URL: Issues, https://gitlab.com/JasonBerger/backlight/-/issues
Project-URL: Source, https://gitlab.com/JasonBerger/backlight
Author-email: Jason Berger <berge472@gmail.com>
License: MIT
Keywords: algorithmic-trading,backtesting,options,paper-trading,trading
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Office/Business :: Financial :: Investment
Requires-Python: >=3.11
Requires-Dist: fastapi>=0.110
Requires-Dist: httpx>=0.27
Requires-Dist: numpy>=1.26
Requires-Dist: pyarrow>=15
Requires-Dist: pydantic>=2.5
Requires-Dist: pywebview>=5.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: uvicorn>=0.27
Requires-Dist: websockets>=12
Provides-Extra: agent
Requires-Dist: anthropic>=1.0; extra == 'agent'
Requires-Dist: openai>=1.40; extra == 'agent'
Provides-Extra: databento
Requires-Dist: databento-dbn>=0.20; extra == 'databento'
Provides-Extra: dev
Requires-Dist: pytest-timeout>=2; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# 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

```sh
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](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:

```sh
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 `Signal`s carrying the `DecisionPath` that
produced them; refiners veto or reshape and leave a note; the sizer turns the
signal into an `Intent`; execution builds `OrderRequest`s (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

```python
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:

```python
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:

```sh
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

```sh
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
```
