py-backlight 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py_backlight-0.1.0/.gitignore +10 -0
- py_backlight-0.1.0/PKG-INFO +243 -0
- py_backlight-0.1.0/README.md +203 -0
- py_backlight-0.1.0/pyproject.toml +92 -0
- py_backlight-0.1.0/src/backlight/__init__.py +14 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/AccountView-CbigG0KS.js +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/AccountView-DnAwP8fS.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/BacktestLabView-DRPXydD8.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/BacktestLabView-vvjxJ5m8.js +2 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/CampaignView-CIlc_wqc.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/CampaignView-Cdfvmpsd.js +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/CompareView-6H2jxNiO.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/CompareView-CrJ8oxXP.js +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/DashboardView-C0Fxmzc8.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/DashboardView-DgqPCo7B.js +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/EquityChart-BpYxPxyQ.js +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/EquityChart-JLe9hJb5.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/JobProgress-C8SnTQlc.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/JobProgress-CB1VLleo.js +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/LiveView-BVCPEc4g.js +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/LiveView-DvTIB5_8.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/ModeledQuotesNotice-D0qjkt2g.js +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/ModeledQuotesNotice-DCxPJFwb.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/ParamFields-Bv3NSsrU.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/ParamFields-DuoR6hfm.js +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/PendingWork-CeFaVq-R.js +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/PendingWork-DvsBtxZp.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/PipelinesView-DfsIL-LQ.js +16 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/PipelinesView-uM0b2NEl.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/RunDetailView-Bo1vVl8s.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/RunDetailView-CHmNsBqj.js +3 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/RunsView-CyjlERtU.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/RunsView-DF-h2_0z.js +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/SettingsView-86qI0Qk1.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/SettingsView-DpvjM-g5.js +4 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/SymbolsInput-B0Xwgzk1.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/SymbolsInput-BZQQUVay.js +2 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/TagsEditor-BbnU4zcL.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/TagsEditor-aMliL_fI.js +26 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/TradeAccountsView-BDXBsKhm.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/TradeAccountsView-C6Dq9TAx.js +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/TreesView-B1yCydQC.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/TreesView-JCR9VFL1.js +6 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/WebKeysPanel-BYAPo1o9.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/WebKeysPanel-C63Vkpqw.js +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/WorldPicker-CL4AVVEG.js +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/WorldPicker-CmwvzOtp.css +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/formMemory-Dnvfj_p0.js +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/graph-BOsYCB6S.js +1 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/highlight-HvVvo4wj.js +4 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/index-BfAt6891.js +44 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/index-Cs4gchcM.css +5 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
- py_backlight-0.1.0/src/backlight/_gui/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
- py_backlight-0.1.0/src/backlight/_gui/favicon.svg +1 -0
- py_backlight-0.1.0/src/backlight/_gui/guide.html +1433 -0
- py_backlight-0.1.0/src/backlight/_gui/index.html +14 -0
- py_backlight-0.1.0/src/backlight/agent/__init__.py +21 -0
- py_backlight-0.1.0/src/backlight/agent/draft.py +319 -0
- py_backlight-0.1.0/src/backlight/agent/llm.py +403 -0
- py_backlight-0.1.0/src/backlight/agent/loop.py +159 -0
- py_backlight-0.1.0/src/backlight/agent/nodegen.py +571 -0
- py_backlight-0.1.0/src/backlight/agent/prompts.py +99 -0
- py_backlight-0.1.0/src/backlight/agent/sessions.py +141 -0
- py_backlight-0.1.0/src/backlight/agent/settings.py +114 -0
- py_backlight-0.1.0/src/backlight/agent/tools.py +305 -0
- py_backlight-0.1.0/src/backlight/cli.py +622 -0
- py_backlight-0.1.0/src/backlight/connectors/__init__.py +8 -0
- py_backlight-0.1.0/src/backlight/connectors/_http.py +305 -0
- py_backlight-0.1.0/src/backlight/connectors/alpaca/__init__.py +640 -0
- py_backlight-0.1.0/src/backlight/connectors/alpaca/orders.py +197 -0
- py_backlight-0.1.0/src/backlight/connectors/alpaca/stream.py +64 -0
- py_backlight-0.1.0/src/backlight/connectors/base.py +385 -0
- py_backlight-0.1.0/src/backlight/connectors/csv/__init__.py +122 -0
- py_backlight-0.1.0/src/backlight/connectors/databento/__init__.py +676 -0
- py_backlight-0.1.0/src/backlight/connectors/databento/batch.py +181 -0
- py_backlight-0.1.0/src/backlight/connectors/kalshi/__init__.py +95 -0
- py_backlight-0.1.0/src/backlight/connectors/massive/__init__.py +576 -0
- py_backlight-0.1.0/src/backlight/connectors/massive_files/__init__.py +674 -0
- py_backlight-0.1.0/src/backlight/connectors/schwab/__init__.py +658 -0
- py_backlight-0.1.0/src/backlight/connectors/schwab/auth.py +193 -0
- py_backlight-0.1.0/src/backlight/connectors/schwab/orders.py +261 -0
- py_backlight-0.1.0/src/backlight/connectors/sim/__init__.py +9 -0
- py_backlight-0.1.0/src/backlight/connectors/sim/engine.py +513 -0
- py_backlight-0.1.0/src/backlight/connectors/synthetic/__init__.py +203 -0
- py_backlight-0.1.0/src/backlight/connectors/synthetic/market.py +75 -0
- py_backlight-0.1.0/src/backlight/connectors/world.py +285 -0
- py_backlight-0.1.0/src/backlight/data/__init__.py +1 -0
- py_backlight-0.1.0/src/backlight/data/cache.py +210 -0
- py_backlight-0.1.0/src/backlight/data/cache_transfer.py +400 -0
- py_backlight-0.1.0/src/backlight/data/coverage.py +109 -0
- py_backlight-0.1.0/src/backlight/data/option_cache.py +239 -0
- py_backlight-0.1.0/src/backlight/data/option_history.py +874 -0
- py_backlight-0.1.0/src/backlight/data/options.py +437 -0
- py_backlight-0.1.0/src/backlight/data/session.py +187 -0
- py_backlight-0.1.0/src/backlight/data/symbol_sets.py +218 -0
- py_backlight-0.1.0/src/backlight/data/symbols.py +226 -0
- py_backlight-0.1.0/src/backlight/db/__init__.py +42 -0
- py_backlight-0.1.0/src/backlight/db/migrations.py +77 -0
- py_backlight-0.1.0/src/backlight/db/names.py +83 -0
- py_backlight-0.1.0/src/backlight/db/records.py +159 -0
- py_backlight-0.1.0/src/backlight/db/reports/__init__.py +1 -0
- py_backlight-0.1.0/src/backlight/db/reports/blotter.py +245 -0
- py_backlight-0.1.0/src/backlight/db/reports/markdown.py +66 -0
- py_backlight-0.1.0/src/backlight/db/reports/metrics.py +213 -0
- py_backlight-0.1.0/src/backlight/db/reports/run_report.py +275 -0
- py_backlight-0.1.0/src/backlight/db/runs.py +274 -0
- py_backlight-0.1.0/src/backlight/db/schema.sql +270 -0
- py_backlight-0.1.0/src/backlight/db/scratch.py +65 -0
- py_backlight-0.1.0/src/backlight/hub/__init__.py +1 -0
- py_backlight-0.1.0/src/backlight/hub/access.py +116 -0
- py_backlight-0.1.0/src/backlight/hub/api/__init__.py +129 -0
- py_backlight-0.1.0/src/backlight/hub/api/_deps.py +10 -0
- py_backlight-0.1.0/src/backlight/hub/api/agent.py +176 -0
- py_backlight-0.1.0/src/backlight/hub/api/backtests.py +459 -0
- py_backlight-0.1.0/src/backlight/hub/api/campaigns.py +807 -0
- py_backlight-0.1.0/src/backlight/hub/api/catalog.py +42 -0
- py_backlight-0.1.0/src/backlight/hub/api/connectors.py +149 -0
- py_backlight-0.1.0/src/backlight/hub/api/data.py +565 -0
- py_backlight-0.1.0/src/backlight/hub/api/desk.py +337 -0
- py_backlight-0.1.0/src/backlight/hub/api/live.py +229 -0
- py_backlight-0.1.0/src/backlight/hub/api/pipelines.py +155 -0
- py_backlight-0.1.0/src/backlight/hub/api/reports.py +33 -0
- py_backlight-0.1.0/src/backlight/hub/api/runs.py +272 -0
- py_backlight-0.1.0/src/backlight/hub/api/settings.py +88 -0
- py_backlight-0.1.0/src/backlight/hub/api/symbol_sets.py +99 -0
- py_backlight-0.1.0/src/backlight/hub/api/trees.py +141 -0
- py_backlight-0.1.0/src/backlight/hub/api/users.py +301 -0
- py_backlight-0.1.0/src/backlight/hub/api/worlds.py +542 -0
- py_backlight-0.1.0/src/backlight/hub/app.py +106 -0
- py_backlight-0.1.0/src/backlight/hub/auth.py +686 -0
- py_backlight-0.1.0/src/backlight/hub/config.py +111 -0
- py_backlight-0.1.0/src/backlight/hub/events.py +60 -0
- py_backlight-0.1.0/src/backlight/hub/jobs.py +308 -0
- py_backlight-0.1.0/src/backlight/hub/login.html +93 -0
- py_backlight-0.1.0/src/backlight/hub/reconcile.py +190 -0
- py_backlight-0.1.0/src/backlight/hub/secrets.py +84 -0
- py_backlight-0.1.0/src/backlight/hub/services.py +125 -0
- py_backlight-0.1.0/src/backlight/hub/users.py +281 -0
- py_backlight-0.1.0/src/backlight/hub/webkeys.py +88 -0
- py_backlight-0.1.0/src/backlight/indicators.py +212 -0
- py_backlight-0.1.0/src/backlight/logsetup.py +31 -0
- py_backlight-0.1.0/src/backlight/paths.py +127 -0
- py_backlight-0.1.0/src/backlight/pipeline/__init__.py +1 -0
- py_backlight-0.1.0/src/backlight/pipeline/allocation.py +160 -0
- py_backlight-0.1.0/src/backlight/pipeline/context.py +307 -0
- py_backlight-0.1.0/src/backlight/pipeline/engine.py +169 -0
- py_backlight-0.1.0/src/backlight/pipeline/graph/__init__.py +1 -0
- py_backlight-0.1.0/src/backlight/pipeline/graph/catalog.py +71 -0
- py_backlight-0.1.0/src/backlight/pipeline/graph/plugins.py +64 -0
- py_backlight-0.1.0/src/backlight/pipeline/graph/presets.py +599 -0
- py_backlight-0.1.0/src/backlight/pipeline/graph/schema.py +277 -0
- py_backlight-0.1.0/src/backlight/pipeline/graph/store.py +118 -0
- py_backlight-0.1.0/src/backlight/pipeline/graph/trees.py +116 -0
- py_backlight-0.1.0/src/backlight/pipeline/memory.py +231 -0
- py_backlight-0.1.0/src/backlight/pipeline/nodes/__init__.py +7 -0
- py_backlight-0.1.0/src/backlight/pipeline/nodes/base.py +344 -0
- py_backlight-0.1.0/src/backlight/pipeline/nodes/execution.py +112 -0
- py_backlight-0.1.0/src/backlight/pipeline/nodes/flow.py +128 -0
- py_backlight-0.1.0/src/backlight/pipeline/nodes/option_strategies.py +1112 -0
- py_backlight-0.1.0/src/backlight/pipeline/nodes/output.py +119 -0
- py_backlight-0.1.0/src/backlight/pipeline/nodes/pickers.py +177 -0
- py_backlight-0.1.0/src/backlight/pipeline/nodes/refiners.py +210 -0
- py_backlight-0.1.0/src/backlight/pipeline/nodes/selectors.py +150 -0
- py_backlight-0.1.0/src/backlight/pipeline/nodes/sizers.py +504 -0
- py_backlight-0.1.0/src/backlight/pipeline/nodes/strategies.py +334 -0
- py_backlight-0.1.0/src/backlight/pipeline/nodes/triggers.py +84 -0
- py_backlight-0.1.0/src/backlight/pipeline/nodes/universe.py +70 -0
- py_backlight-0.1.0/src/backlight/pipeline/options.py +495 -0
- py_backlight-0.1.0/src/backlight/pipeline/params.py +259 -0
- py_backlight-0.1.0/src/backlight/pipeline/picking.py +321 -0
- py_backlight-0.1.0/src/backlight/pipeline/ports.py +216 -0
- py_backlight-0.1.0/src/backlight/pipeline/tree/__init__.py +7 -0
- py_backlight-0.1.0/src/backlight/pipeline/tree/expr.py +323 -0
- py_backlight-0.1.0/src/backlight/pipeline/tree/model.py +259 -0
- py_backlight-0.1.0/src/backlight/pipeline/tree/templates.py +479 -0
- py_backlight-0.1.0/src/backlight/pipeline/warmup.py +145 -0
- py_backlight-0.1.0/src/backlight/pipeline/web.py +311 -0
- py_backlight-0.1.0/src/backlight/runner/__init__.py +1 -0
- py_backlight-0.1.0/src/backlight/runner/account.py +226 -0
- py_backlight-0.1.0/src/backlight/runner/advise.py +388 -0
- py_backlight-0.1.0/src/backlight/runner/backtest.py +600 -0
- py_backlight-0.1.0/src/backlight/runner/campaign.py +1818 -0
- py_backlight-0.1.0/src/backlight/runner/desk.py +867 -0
- py_backlight-0.1.0/src/backlight/runner/feed.py +516 -0
- py_backlight-0.1.0/src/backlight/runner/live.py +884 -0
- py_backlight-0.1.0/src/backlight/runner/portfolio.py +314 -0
- py_backlight-0.1.0/src/backlight/runner/prefetch.py +105 -0
- py_backlight-0.1.0/src/backlight/runner/recorder.py +64 -0
- py_backlight-0.1.0/src/backlight/runner/restore.py +183 -0
- py_backlight-0.1.0/src/backlight/runner/step.py +102 -0
- py_backlight-0.1.0/src/backlight/runner/walk.py +154 -0
- py_backlight-0.1.0/src/backlight/types.py +522 -0
- py_backlight-0.1.0/src/backlight/version.py +1 -0
- py_backlight-0.1.0/src/backlight/worlds/__init__.py +17 -0
- py_backlight-0.1.0/src/backlight/worlds/analytics.py +459 -0
- py_backlight-0.1.0/src/backlight/worlds/debrief.py +178 -0
- py_backlight-0.1.0/src/backlight/worlds/gen_v1.py +424 -0
- py_backlight-0.1.0/src/backlight/worlds/personalities.py +270 -0
- py_backlight-0.1.0/src/backlight/worlds/rng.py +69 -0
- py_backlight-0.1.0/src/backlight/worlds/scenarios.py +106 -0
- py_backlight-0.1.0/src/backlight/worlds/sets.py +198 -0
- py_backlight-0.1.0/src/backlight/worlds/store.py +683 -0
- py_backlight-0.1.0/src/backlight/worlds/surface.py +167 -0
- py_backlight-0.1.0/src/backlight/worlds/tags.py +339 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: py-backlight
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Backtest, paper trade and live trade stock and option strategies built from pipelines of nodes
|
|
5
|
+
Project-URL: Homepage, https://gitlab.com/JasonBerger/backlight
|
|
6
|
+
Project-URL: Documentation, https://gitlab.com/JasonBerger/backlight/-/blob/main/README.md
|
|
7
|
+
Project-URL: Repository, https://gitlab.com/JasonBerger/backlight
|
|
8
|
+
Project-URL: Issues, https://gitlab.com/JasonBerger/backlight/-/issues
|
|
9
|
+
Project-URL: Source, https://gitlab.com/JasonBerger/backlight
|
|
10
|
+
Author-email: Jason Berger <berge472@gmail.com>
|
|
11
|
+
License: MIT
|
|
12
|
+
Keywords: algorithmic-trading,backtesting,options,paper-trading,trading
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: fastapi>=0.110
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: numpy>=1.26
|
|
24
|
+
Requires-Dist: pyarrow>=15
|
|
25
|
+
Requires-Dist: pydantic>=2.5
|
|
26
|
+
Requires-Dist: pywebview>=5.0
|
|
27
|
+
Requires-Dist: pyyaml>=6.0
|
|
28
|
+
Requires-Dist: uvicorn>=0.27
|
|
29
|
+
Requires-Dist: websockets>=12
|
|
30
|
+
Provides-Extra: agent
|
|
31
|
+
Requires-Dist: anthropic>=1.0; extra == 'agent'
|
|
32
|
+
Requires-Dist: openai>=1.40; extra == 'agent'
|
|
33
|
+
Provides-Extra: databento
|
|
34
|
+
Requires-Dist: databento-dbn>=0.20; extra == 'databento'
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: pytest-timeout>=2; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
38
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# Backlight
|
|
42
|
+
|
|
43
|
+
Automated stock and options trading platform: pipelines of parameterized nodes
|
|
44
|
+
pick securities, decide how to trade them through tagged decision trees, refine
|
|
45
|
+
those decisions, and hand orders to a connector. One runner drives the same
|
|
46
|
+
pipeline in backtest, paper, or live mode. Built on the same stack as proxer:
|
|
47
|
+
pywebview + FastAPI + Vue 3 (Vuetify).
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
pip install py-backlight # or, from a checkout: make install (pip install -e .[dev] + build the UI)
|
|
53
|
+
backlight hub # opens the desktop window (or: backlight hub --headless)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The PyPI distribution is `py-backlight` (`backlight` there is an old, empty
|
|
57
|
+
project); the package, the import and the command are all `backlight`. Extras:
|
|
58
|
+
`py-backlight[agent]` (the pipeline agent) and `py-backlight[databento]`.
|
|
59
|
+
`make check` builds the wheel with the UI bundled and runs `twine check`;
|
|
60
|
+
`make deploy` uploads it.
|
|
61
|
+
|
|
62
|
+
Upgrading from `trader`, the old name: the first run moves `~/.config/trader`,
|
|
63
|
+
`~/.trader` (and `trader.db` in it) and `~/.cache/trader` to their `backlight`
|
|
64
|
+
names, `TRADER_*` environment variables still work where no `BACKLIGHT_*`
|
|
65
|
+
one is set, and agent nodes are rewritten to import `backlight`.
|
|
66
|
+
|
|
67
|
+
Without the window, the API is at `http://127.0.0.1:8770/docs`.
|
|
68
|
+
|
|
69
|
+
Deployed on a cluster, headless, behind a Cloudflare tunnel and with sign-in
|
|
70
|
+
on: see [docs/deploy.md](docs/deploy.md) (Dockerfile, Helm chart in
|
|
71
|
+
`charts/backlight`). Sign-in is optional — off on the desktop unless you add a
|
|
72
|
+
user with `backlight auth add-user NAME`.
|
|
73
|
+
|
|
74
|
+
From the command line:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
backlight catalog # node types and connectors
|
|
78
|
+
backlight backtest preset-sma-cross --symbols SPY,QQQ --start 2022-01-01 --end 2023-12-31
|
|
79
|
+
backlight runs --mode backtest
|
|
80
|
+
backlight report <run_id> # markdown run report
|
|
81
|
+
backlight report <run_id_a> <run_id_b> # compare
|
|
82
|
+
backlight purge --older-than-days 30 --vacuum # delete old backtest runs
|
|
83
|
+
backlight fetch --provider massive --symbols SPY --start 2020-01-01 --end 2024-12-31
|
|
84
|
+
backlight flatfiles --start 2024-01-01 --end 2024-12-31 # Massive option minute files, once
|
|
85
|
+
backlight backtest preset-opt-0dte-condor --symbols SPY --provider alpaca --option-provider massive_files
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The first launch seeds fifteen preset pipelines (stock, options, and two branching
|
|
89
|
+
examples) and four decision trees into
|
|
90
|
+
`~/.config/backlight/`, and the `synthetic` data connector needs no network, so
|
|
91
|
+
the Backtest Lab works out of the box.
|
|
92
|
+
|
|
93
|
+
## Layout
|
|
94
|
+
|
|
95
|
+
| Path | What |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| `src/backlight/types.py` | The objects that cross node ports: `SecurityId`, `Bar`, `Quote`, `OptionContract`, `Universe`, `Signal`, `Intent`, `OrderRequest`, `OrderEvent`, `Position`. |
|
|
98
|
+
| `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). |
|
|
99
|
+
| `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). |
|
|
100
|
+
| `src/backlight/runner/` | `portfolio.py` (ledger), `feed.py` (bar views for nodes and the sim), `backtest.py`, `live.py` (paper and live), `recorder.py`. |
|
|
101
|
+
| `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. |
|
|
102
|
+
| `src/backlight/hub/` | FastAPI routers (`api/`), background jobs, WebSocket event bus, hub config, secrets, pywebview shell. |
|
|
103
|
+
| `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`). |
|
|
104
|
+
| `charts/backlight/`, `Dockerfile` | Container image and Helm chart: one hub pod on a PVC, optional sign-in, optional cloudflared tunnel. |
|
|
105
|
+
| `docs/` | The proposal, architecture notes, connector setup, Kalshi plan. |
|
|
106
|
+
|
|
107
|
+
## How a tick works
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
trigger -> universe -> selector -> strategy (decision tree) -> refiners -> sizer -> execution -> final
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Every node has `PARAMETERS` rendered by the GUI, typed ports checked at compile
|
|
114
|
+
time, and tags. A strategy emits `Signal`s carrying the `DecisionPath` that
|
|
115
|
+
produced them; refiners veto or reshape and leave a note; the sizer turns the
|
|
116
|
+
signal into an `Intent`; execution builds `OrderRequest`s (with bracket
|
|
117
|
+
children); `output.final` applies buying power and per-underlying caps. Every
|
|
118
|
+
hop is recorded, so a fill walks back to the tree leaf and selector rule that
|
|
119
|
+
caused it, and reports attribute P&L per tag, per leaf, and per tree edge.
|
|
120
|
+
|
|
121
|
+
## Modes
|
|
122
|
+
|
|
123
|
+
| Mode | Clock | Data | Execution |
|
|
124
|
+
| --- | --- | --- | --- |
|
|
125
|
+
| backtest | bar iterator | historical connector + Parquet cache | `sim` |
|
|
126
|
+
| paper | wall clock | market connector | `sim` or a venue's paper account |
|
|
127
|
+
| live | wall clock | market connector | venue (Schwab) |
|
|
128
|
+
|
|
129
|
+
Backtest runs can be purged from the Runs view or with `backlight purge`; paper
|
|
130
|
+
and live runs are kept unless purged by mode explicitly.
|
|
131
|
+
|
|
132
|
+
## Writing a node
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from backlight.pipeline.nodes.base import RefinerNode, register_node
|
|
136
|
+
from backlight.pipeline.params import Parameter
|
|
137
|
+
|
|
138
|
+
@register_node
|
|
139
|
+
class NoFridays(RefinerNode):
|
|
140
|
+
"""Veto new entries on Fridays."""
|
|
141
|
+
kind = "refiner.no-fridays"
|
|
142
|
+
label = "No Fridays"
|
|
143
|
+
PARAMETERS = [Parameter("enabled_days", "number", "Days", default=4, min=0, max=6, step=1)]
|
|
144
|
+
|
|
145
|
+
def applies(self, signal, ctx):
|
|
146
|
+
return signal.action.value.startswith("open")
|
|
147
|
+
|
|
148
|
+
def apply(self, signal, ctx):
|
|
149
|
+
return self.veto("friday") if ctx.now.weekday() == 4 else signal
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The docstring is the node's documentation in the workbench: its first
|
|
153
|
+
paragraph is the palette description, the whole of it shows under **More**.
|
|
154
|
+
`PORT_DOCS = {"port": "..."}` says what a port means on this node. Text is
|
|
155
|
+
plain: blank lines split paragraphs, `- ` starts a list item, backticks mark
|
|
156
|
+
code.
|
|
157
|
+
|
|
158
|
+
Port types (`run`, `universe`, `signal`, `intent`, `order`) live in a registry
|
|
159
|
+
in `backlight.pipeline.ports`; clicking a port in the workbench opens its
|
|
160
|
+
documentation. A plugin registers its own from the same module:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from backlight.pipeline.ports import PortType, register_port_type
|
|
164
|
+
|
|
165
|
+
register_port_type(PortType(
|
|
166
|
+
"alert", "Alert", "A message for the operator.",
|
|
167
|
+
doc="Longer explanation for the port-type dialog.",
|
|
168
|
+
carries=Alert, # a dataclass: its fields are listed
|
|
169
|
+
field_docs={"text": "What to say."}, color="#e11d48"))
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The engine only moves values along the ports each node category handles, so
|
|
173
|
+
a plugin type is documented and wired in the editor, but nothing produces or
|
|
174
|
+
consumes it yet.
|
|
175
|
+
|
|
176
|
+
Drop the file in `~/.config/backlight/plugins/nodes/` or register it under the
|
|
177
|
+
`backlight.nodes` entry-point group. Connectors work the same way under
|
|
178
|
+
`plugins/connectors/` and the `backlight.connectors` group.
|
|
179
|
+
|
|
180
|
+
## Pipeline agent
|
|
181
|
+
|
|
182
|
+
In the pipeline editor, **Agent** opens a chat that builds and edits the open
|
|
183
|
+
pipeline: it reads the node catalog and other pipelines, adds and wires nodes,
|
|
184
|
+
sets parameters and validates, and the canvas follows each edit (nothing is
|
|
185
|
+
saved until you press Save). Chats are per user and kept per pipeline.
|
|
186
|
+
|
|
187
|
+
When no node fits, the agent can write one. A written node loads only after:
|
|
188
|
+
a static check (allowed imports and calls, no private attributes or routes to
|
|
189
|
+
connectors, never an execution or output node), an import test in a separate
|
|
190
|
+
process with an empty environment, and approval by a reviewer agent. Approved
|
|
191
|
+
nodes live in `~/.config/backlight/plugins/agent-nodes/` and are listed (with
|
|
192
|
+
their code and review) under Admin → Settings → Agent, where an admin can
|
|
193
|
+
remove them.
|
|
194
|
+
|
|
195
|
+
Setup: `pip install -e ".[agent]"` (the Docker image includes it), then Admin →
|
|
196
|
+
Settings → Agent: provider (OpenAI or Anthropic), its API key, model, Enable.
|
|
197
|
+
OpenAI also covers any OpenAI-compatible endpoint through the base URL.
|
|
198
|
+
Anthropic is spoken natively (Claude's thinking is kept across turns, an
|
|
199
|
+
effort setting, prompt caching, and on Anthropic's own API a declined request
|
|
200
|
+
is retried on Anthropic's recommended fallback model). One hub-wide key per
|
|
201
|
+
provider; every signed-in user's chats use the active one.
|
|
202
|
+
|
|
203
|
+
The tools (`backlight.agent.tools`) are plain name + JSON schema + handler over a
|
|
204
|
+
server-side working pipeline, so another front end (an MCP server) can expose
|
|
205
|
+
the same set.
|
|
206
|
+
|
|
207
|
+
## Paths
|
|
208
|
+
|
|
209
|
+
| What | Where | Override |
|
|
210
|
+
| --- | --- | --- |
|
|
211
|
+
| pipelines, trees, hub config, secrets, plugins | `~/.config/backlight/` | `BACKLIGHT_CONFIG_DIR` |
|
|
212
|
+
| SQLite database | `~/.backlight/backlight.db` | `BACKLIGHT_DATA_DIR`, `BACKLIGHT_DB` |
|
|
213
|
+
| Parquet bar cache, logs | `~/.cache/backlight/` | `BACKLIGHT_CACHE_DIR` |
|
|
214
|
+
|
|
215
|
+
### Moving a cache to another instance
|
|
216
|
+
|
|
217
|
+
A provider's cached bars, option blocks and downloaded store (Massive flat
|
|
218
|
+
files, Databento's dataset) export to one tar and import anywhere, from the
|
|
219
|
+
Data tab (Export / import cache) or the CLI:
|
|
220
|
+
|
|
221
|
+
```sh
|
|
222
|
+
backlight cache list # what each provider holds
|
|
223
|
+
backlight cache export --provider massive --provider massive_files -o massive.tar
|
|
224
|
+
backlight cache export --provider massive_files --timeframe 1d -o day.tar # only some timeframes
|
|
225
|
+
backlight cache import massive.tar # on the other instance
|
|
226
|
+
ssh a backlight cache export --provider massive -o - | backlight cache import - # or piped
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
An import never deletes. Files already there are kept unless the archive's
|
|
230
|
+
copy is newer (`--mode newer`, the default; bar files merge instead), or
|
|
231
|
+
always replaced (`--mode replace`), or always kept (`--mode skip`). Store
|
|
232
|
+
files land in the importing instance's own `data_dir` for that connector.
|
|
233
|
+
The Data tab uploads in 32 MB pieces, so a proxy's body limit (Cloudflare:
|
|
234
|
+
100 MB) doesn't get in the way.
|
|
235
|
+
|
|
236
|
+
## Development
|
|
237
|
+
|
|
238
|
+
```sh
|
|
239
|
+
make test # pytest
|
|
240
|
+
make smoke # every preset through a two-year synthetic backtest
|
|
241
|
+
make gui # rebuild the UI after editing gui/src
|
|
242
|
+
BACKLIGHT_HUB_DEBUG=1 backlight hub # WebKit inspector in the window
|
|
243
|
+
```
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Backlight
|
|
2
|
+
|
|
3
|
+
Automated stock and options trading platform: pipelines of parameterized nodes
|
|
4
|
+
pick securities, decide how to trade them through tagged decision trees, refine
|
|
5
|
+
those decisions, and hand orders to a connector. One runner drives the same
|
|
6
|
+
pipeline in backtest, paper, or live mode. Built on the same stack as proxer:
|
|
7
|
+
pywebview + FastAPI + Vue 3 (Vuetify).
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pip install py-backlight # or, from a checkout: make install (pip install -e .[dev] + build the UI)
|
|
13
|
+
backlight hub # opens the desktop window (or: backlight hub --headless)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The PyPI distribution is `py-backlight` (`backlight` there is an old, empty
|
|
17
|
+
project); the package, the import and the command are all `backlight`. Extras:
|
|
18
|
+
`py-backlight[agent]` (the pipeline agent) and `py-backlight[databento]`.
|
|
19
|
+
`make check` builds the wheel with the UI bundled and runs `twine check`;
|
|
20
|
+
`make deploy` uploads it.
|
|
21
|
+
|
|
22
|
+
Upgrading from `trader`, the old name: the first run moves `~/.config/trader`,
|
|
23
|
+
`~/.trader` (and `trader.db` in it) and `~/.cache/trader` to their `backlight`
|
|
24
|
+
names, `TRADER_*` environment variables still work where no `BACKLIGHT_*`
|
|
25
|
+
one is set, and agent nodes are rewritten to import `backlight`.
|
|
26
|
+
|
|
27
|
+
Without the window, the API is at `http://127.0.0.1:8770/docs`.
|
|
28
|
+
|
|
29
|
+
Deployed on a cluster, headless, behind a Cloudflare tunnel and with sign-in
|
|
30
|
+
on: see [docs/deploy.md](docs/deploy.md) (Dockerfile, Helm chart in
|
|
31
|
+
`charts/backlight`). Sign-in is optional — off on the desktop unless you add a
|
|
32
|
+
user with `backlight auth add-user NAME`.
|
|
33
|
+
|
|
34
|
+
From the command line:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
backlight catalog # node types and connectors
|
|
38
|
+
backlight backtest preset-sma-cross --symbols SPY,QQQ --start 2022-01-01 --end 2023-12-31
|
|
39
|
+
backlight runs --mode backtest
|
|
40
|
+
backlight report <run_id> # markdown run report
|
|
41
|
+
backlight report <run_id_a> <run_id_b> # compare
|
|
42
|
+
backlight purge --older-than-days 30 --vacuum # delete old backtest runs
|
|
43
|
+
backlight fetch --provider massive --symbols SPY --start 2020-01-01 --end 2024-12-31
|
|
44
|
+
backlight flatfiles --start 2024-01-01 --end 2024-12-31 # Massive option minute files, once
|
|
45
|
+
backlight backtest preset-opt-0dte-condor --symbols SPY --provider alpaca --option-provider massive_files
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The first launch seeds fifteen preset pipelines (stock, options, and two branching
|
|
49
|
+
examples) and four decision trees into
|
|
50
|
+
`~/.config/backlight/`, and the `synthetic` data connector needs no network, so
|
|
51
|
+
the Backtest Lab works out of the box.
|
|
52
|
+
|
|
53
|
+
## Layout
|
|
54
|
+
|
|
55
|
+
| Path | What |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `src/backlight/types.py` | The objects that cross node ports: `SecurityId`, `Bar`, `Quote`, `OptionContract`, `Universe`, `Signal`, `Intent`, `OrderRequest`, `OrderEvent`, `Position`. |
|
|
58
|
+
| `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). |
|
|
59
|
+
| `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). |
|
|
60
|
+
| `src/backlight/runner/` | `portfolio.py` (ledger), `feed.py` (bar views for nodes and the sim), `backtest.py`, `live.py` (paper and live), `recorder.py`. |
|
|
61
|
+
| `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. |
|
|
62
|
+
| `src/backlight/hub/` | FastAPI routers (`api/`), background jobs, WebSocket event bus, hub config, secrets, pywebview shell. |
|
|
63
|
+
| `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`). |
|
|
64
|
+
| `charts/backlight/`, `Dockerfile` | Container image and Helm chart: one hub pod on a PVC, optional sign-in, optional cloudflared tunnel. |
|
|
65
|
+
| `docs/` | The proposal, architecture notes, connector setup, Kalshi plan. |
|
|
66
|
+
|
|
67
|
+
## How a tick works
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
trigger -> universe -> selector -> strategy (decision tree) -> refiners -> sizer -> execution -> final
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Every node has `PARAMETERS` rendered by the GUI, typed ports checked at compile
|
|
74
|
+
time, and tags. A strategy emits `Signal`s carrying the `DecisionPath` that
|
|
75
|
+
produced them; refiners veto or reshape and leave a note; the sizer turns the
|
|
76
|
+
signal into an `Intent`; execution builds `OrderRequest`s (with bracket
|
|
77
|
+
children); `output.final` applies buying power and per-underlying caps. Every
|
|
78
|
+
hop is recorded, so a fill walks back to the tree leaf and selector rule that
|
|
79
|
+
caused it, and reports attribute P&L per tag, per leaf, and per tree edge.
|
|
80
|
+
|
|
81
|
+
## Modes
|
|
82
|
+
|
|
83
|
+
| Mode | Clock | Data | Execution |
|
|
84
|
+
| --- | --- | --- | --- |
|
|
85
|
+
| backtest | bar iterator | historical connector + Parquet cache | `sim` |
|
|
86
|
+
| paper | wall clock | market connector | `sim` or a venue's paper account |
|
|
87
|
+
| live | wall clock | market connector | venue (Schwab) |
|
|
88
|
+
|
|
89
|
+
Backtest runs can be purged from the Runs view or with `backlight purge`; paper
|
|
90
|
+
and live runs are kept unless purged by mode explicitly.
|
|
91
|
+
|
|
92
|
+
## Writing a node
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from backlight.pipeline.nodes.base import RefinerNode, register_node
|
|
96
|
+
from backlight.pipeline.params import Parameter
|
|
97
|
+
|
|
98
|
+
@register_node
|
|
99
|
+
class NoFridays(RefinerNode):
|
|
100
|
+
"""Veto new entries on Fridays."""
|
|
101
|
+
kind = "refiner.no-fridays"
|
|
102
|
+
label = "No Fridays"
|
|
103
|
+
PARAMETERS = [Parameter("enabled_days", "number", "Days", default=4, min=0, max=6, step=1)]
|
|
104
|
+
|
|
105
|
+
def applies(self, signal, ctx):
|
|
106
|
+
return signal.action.value.startswith("open")
|
|
107
|
+
|
|
108
|
+
def apply(self, signal, ctx):
|
|
109
|
+
return self.veto("friday") if ctx.now.weekday() == 4 else signal
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The docstring is the node's documentation in the workbench: its first
|
|
113
|
+
paragraph is the palette description, the whole of it shows under **More**.
|
|
114
|
+
`PORT_DOCS = {"port": "..."}` says what a port means on this node. Text is
|
|
115
|
+
plain: blank lines split paragraphs, `- ` starts a list item, backticks mark
|
|
116
|
+
code.
|
|
117
|
+
|
|
118
|
+
Port types (`run`, `universe`, `signal`, `intent`, `order`) live in a registry
|
|
119
|
+
in `backlight.pipeline.ports`; clicking a port in the workbench opens its
|
|
120
|
+
documentation. A plugin registers its own from the same module:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from backlight.pipeline.ports import PortType, register_port_type
|
|
124
|
+
|
|
125
|
+
register_port_type(PortType(
|
|
126
|
+
"alert", "Alert", "A message for the operator.",
|
|
127
|
+
doc="Longer explanation for the port-type dialog.",
|
|
128
|
+
carries=Alert, # a dataclass: its fields are listed
|
|
129
|
+
field_docs={"text": "What to say."}, color="#e11d48"))
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The engine only moves values along the ports each node category handles, so
|
|
133
|
+
a plugin type is documented and wired in the editor, but nothing produces or
|
|
134
|
+
consumes it yet.
|
|
135
|
+
|
|
136
|
+
Drop the file in `~/.config/backlight/plugins/nodes/` or register it under the
|
|
137
|
+
`backlight.nodes` entry-point group. Connectors work the same way under
|
|
138
|
+
`plugins/connectors/` and the `backlight.connectors` group.
|
|
139
|
+
|
|
140
|
+
## Pipeline agent
|
|
141
|
+
|
|
142
|
+
In the pipeline editor, **Agent** opens a chat that builds and edits the open
|
|
143
|
+
pipeline: it reads the node catalog and other pipelines, adds and wires nodes,
|
|
144
|
+
sets parameters and validates, and the canvas follows each edit (nothing is
|
|
145
|
+
saved until you press Save). Chats are per user and kept per pipeline.
|
|
146
|
+
|
|
147
|
+
When no node fits, the agent can write one. A written node loads only after:
|
|
148
|
+
a static check (allowed imports and calls, no private attributes or routes to
|
|
149
|
+
connectors, never an execution or output node), an import test in a separate
|
|
150
|
+
process with an empty environment, and approval by a reviewer agent. Approved
|
|
151
|
+
nodes live in `~/.config/backlight/plugins/agent-nodes/` and are listed (with
|
|
152
|
+
their code and review) under Admin → Settings → Agent, where an admin can
|
|
153
|
+
remove them.
|
|
154
|
+
|
|
155
|
+
Setup: `pip install -e ".[agent]"` (the Docker image includes it), then Admin →
|
|
156
|
+
Settings → Agent: provider (OpenAI or Anthropic), its API key, model, Enable.
|
|
157
|
+
OpenAI also covers any OpenAI-compatible endpoint through the base URL.
|
|
158
|
+
Anthropic is spoken natively (Claude's thinking is kept across turns, an
|
|
159
|
+
effort setting, prompt caching, and on Anthropic's own API a declined request
|
|
160
|
+
is retried on Anthropic's recommended fallback model). One hub-wide key per
|
|
161
|
+
provider; every signed-in user's chats use the active one.
|
|
162
|
+
|
|
163
|
+
The tools (`backlight.agent.tools`) are plain name + JSON schema + handler over a
|
|
164
|
+
server-side working pipeline, so another front end (an MCP server) can expose
|
|
165
|
+
the same set.
|
|
166
|
+
|
|
167
|
+
## Paths
|
|
168
|
+
|
|
169
|
+
| What | Where | Override |
|
|
170
|
+
| --- | --- | --- |
|
|
171
|
+
| pipelines, trees, hub config, secrets, plugins | `~/.config/backlight/` | `BACKLIGHT_CONFIG_DIR` |
|
|
172
|
+
| SQLite database | `~/.backlight/backlight.db` | `BACKLIGHT_DATA_DIR`, `BACKLIGHT_DB` |
|
|
173
|
+
| Parquet bar cache, logs | `~/.cache/backlight/` | `BACKLIGHT_CACHE_DIR` |
|
|
174
|
+
|
|
175
|
+
### Moving a cache to another instance
|
|
176
|
+
|
|
177
|
+
A provider's cached bars, option blocks and downloaded store (Massive flat
|
|
178
|
+
files, Databento's dataset) export to one tar and import anywhere, from the
|
|
179
|
+
Data tab (Export / import cache) or the CLI:
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
backlight cache list # what each provider holds
|
|
183
|
+
backlight cache export --provider massive --provider massive_files -o massive.tar
|
|
184
|
+
backlight cache export --provider massive_files --timeframe 1d -o day.tar # only some timeframes
|
|
185
|
+
backlight cache import massive.tar # on the other instance
|
|
186
|
+
ssh a backlight cache export --provider massive -o - | backlight cache import - # or piped
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
An import never deletes. Files already there are kept unless the archive's
|
|
190
|
+
copy is newer (`--mode newer`, the default; bar files merge instead), or
|
|
191
|
+
always replaced (`--mode replace`), or always kept (`--mode skip`). Store
|
|
192
|
+
files land in the importing instance's own `data_dir` for that connector.
|
|
193
|
+
The Data tab uploads in 32 MB pieces, so a proxy's body limit (Cloudflare:
|
|
194
|
+
100 MB) doesn't get in the way.
|
|
195
|
+
|
|
196
|
+
## Development
|
|
197
|
+
|
|
198
|
+
```sh
|
|
199
|
+
make test # pytest
|
|
200
|
+
make smoke # every preset through a two-year synthetic backtest
|
|
201
|
+
make gui # rebuild the UI after editing gui/src
|
|
202
|
+
BACKLIGHT_HUB_DEBUG=1 backlight hub # WebKit inspector in the window
|
|
203
|
+
```
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
# `backlight` on PyPI is an empty 2012 project, so the distribution is
|
|
7
|
+
# py-backlight; the package, the CLI and the import are all `backlight`.
|
|
8
|
+
name = "py-backlight"
|
|
9
|
+
dynamic = ["version"]
|
|
10
|
+
description = "Backtest, paper trade and live trade stock and option strategies built from pipelines of nodes"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
authors = [{ name = "Jason Berger", email = "berge472@gmail.com" }]
|
|
15
|
+
keywords = ["trading", "backtesting", "options", "paper-trading", "algorithmic-trading"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: POSIX :: Linux",
|
|
22
|
+
"Operating System :: MacOS",
|
|
23
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"fastapi>=0.110",
|
|
27
|
+
"uvicorn>=0.27",
|
|
28
|
+
"pydantic>=2.5",
|
|
29
|
+
"pywebview>=5.0",
|
|
30
|
+
"pyyaml>=6.0",
|
|
31
|
+
"httpx>=0.27",
|
|
32
|
+
"websockets>=12",
|
|
33
|
+
"numpy>=1.26",
|
|
34
|
+
"pyarrow>=15",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
# `backlight databento-import`: reads Databento batch downloads (DBN metadata)
|
|
39
|
+
databento = [
|
|
40
|
+
"databento-dbn>=0.20",
|
|
41
|
+
]
|
|
42
|
+
# the pipeline agent (Pipelines → chat): OpenAI (or an OpenAI-compatible API) or Anthropic
|
|
43
|
+
agent = [
|
|
44
|
+
"openai>=1.40",
|
|
45
|
+
"anthropic>=1.0",
|
|
46
|
+
]
|
|
47
|
+
dev = [
|
|
48
|
+
"pytest>=7",
|
|
49
|
+
"pytest-timeout>=2",
|
|
50
|
+
"ruff>=0.4",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[project.scripts]
|
|
54
|
+
backlight = "backlight.cli:main"
|
|
55
|
+
|
|
56
|
+
[project.urls]
|
|
57
|
+
Homepage = "https://gitlab.com/JasonBerger/backlight"
|
|
58
|
+
Documentation = "https://gitlab.com/JasonBerger/backlight/-/blob/main/README.md"
|
|
59
|
+
Repository = "https://gitlab.com/JasonBerger/backlight"
|
|
60
|
+
Issues = "https://gitlab.com/JasonBerger/backlight/-/issues"
|
|
61
|
+
Source = "https://gitlab.com/JasonBerger/backlight"
|
|
62
|
+
|
|
63
|
+
[project.entry-points."backlight.nodes"]
|
|
64
|
+
# Third-party node plugins register here: name = "package.module".
|
|
65
|
+
|
|
66
|
+
[project.entry-points."backlight.connectors"]
|
|
67
|
+
# Third-party connector plugins register here: name = "package.module".
|
|
68
|
+
|
|
69
|
+
[tool.hatch.version]
|
|
70
|
+
path = "src/backlight/version.py"
|
|
71
|
+
|
|
72
|
+
[tool.hatch.build]
|
|
73
|
+
# The built UI, copied in by `make dist` (git ignores it, hence an artifact).
|
|
74
|
+
# Absent, the wheel still builds and the hub serves gui/dist from a checkout.
|
|
75
|
+
artifacts = ["src/backlight/_gui/**"]
|
|
76
|
+
|
|
77
|
+
[tool.hatch.build.targets.wheel]
|
|
78
|
+
packages = ["src/backlight"]
|
|
79
|
+
|
|
80
|
+
[tool.hatch.build.targets.sdist]
|
|
81
|
+
include = ["src/backlight", "README.md", "pyproject.toml"]
|
|
82
|
+
|
|
83
|
+
[tool.ruff]
|
|
84
|
+
line-length = 120
|
|
85
|
+
target-version = "py311"
|
|
86
|
+
|
|
87
|
+
[tool.ruff.lint]
|
|
88
|
+
select = ["E", "F", "W"]
|
|
89
|
+
per-file-ignores = { "tests/*" = ["E501"], "src/backlight/db/records.py" = ["E501"] }
|
|
90
|
+
|
|
91
|
+
[tool.pytest.ini_options]
|
|
92
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Backlight: automated stock and options trading platform.
|
|
2
|
+
|
|
3
|
+
Pipelines of parameterized nodes pick securities, decide how to trade them
|
|
4
|
+
through tagged decision trees, refine those decisions, and hand orders to a
|
|
5
|
+
connector. One runner drives the same pipeline in backtest, paper, or live
|
|
6
|
+
mode. See ``docs/`` for the proposal this implements.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from backlight.paths import adopt_legacy_env
|
|
10
|
+
from backlight.version import __version__
|
|
11
|
+
|
|
12
|
+
adopt_legacy_env()
|
|
13
|
+
|
|
14
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{_ as N,aR as T,c as m,d as s,s as k,u as v,F as U,e as t,w as o,h as x,f as n,l as V,k as u,j as q,a3 as L,m as R,n as r,a4 as g,t as h,i as C,H as W}from"./index-BfAt6891.js";import{C as D,S as F,W as j}from"./WebKeysPanel-C63Vkpqw.js";import"./SymbolsInput-BZQQUVay.js";const E={class:"page"},H={class:"page-head"},I={class:"muted"},K={key:0,class:"card muted"},M={key:0,class:"apanel"},Y={key:3,class:"agrid"},G={class:"card"},J={class:"card-title"},O={class:"card"},Q={class:"card-title"},X={__name:"AccountView",setup(Z){const w=T(),c=L(),P=R(),d=V({get:()=>["brokers","symbols","webkeys"].includes(c.query.tab)?c.query.tab:"profile",set:l=>P.replace({query:{...c.query,tab:l==="profile"?void 0:l}})});async function S(l){w.global.name.value=l,await h(()=>C.putSettings({ui:{theme:l}}))}const a=q({current:"",next:"",again:"",busy:!1}),b=V(()=>a.value.next?a.value.next.length<8?"at least 8 characters":a.value.again&&a.value.again!==a.value.next?"the two new passwords differ":"":"");async function z(){a.value.busy=!0;const l=await h(()=>C.changePassword(a.value.current,a.value.next),null);a.value.busy=!1,l&&(W("password changed: sign in again with the new one","success"),setTimeout(()=>location.assign("/login"),1200))}return(l,e)=>{const p=u("v-tab"),A=u("v-tabs"),_=u("v-icon"),y=u("v-btn"),B=u("v-btn-toggle"),f=u("v-text-field");return r(),m("div",E,[s("div",H,[e[4]||(e[4]=s("h1",null,"Account",-1)),s("span",I,k(v(g).name)+" · "+k(v(g).role),1)]),v(g).auth?(r(),m(U,{key:1},[t(A,{modelValue:d.value,"onUpdate:modelValue":e[0]||(e[0]=i=>d.value=i),density:"compact",color:"primary",class:"mb-4 atabs"},{default:o(()=>[t(p,{value:"profile","prepend-icon":"mdi-account-outline"},{default:o(()=>[...e[5]||(e[5]=[n("Profile",-1)])]),_:1}),t(p,{value:"brokers","prepend-icon":"mdi-bank-outline"},{default:o(()=>[...e[6]||(e[6]=[n("My brokers",-1)])]),_:1}),t(p,{value:"symbols","prepend-icon":"mdi-format-list-group"},{default:o(()=>[...e[7]||(e[7]=[n("Symbol sets",-1)])]),_:1}),t(p,{value:"webkeys","prepend-icon":"mdi-key-chain-variant"},{default:o(()=>[...e[8]||(e[8]=[n("Web keys",-1)])]),_:1})]),_:1},8,["modelValue"]),d.value==="brokers"?(r(),m("div",M,[t(D,{mine:""})])):d.value==="symbols"?(r(),x(F,{key:1})):d.value==="webkeys"?(r(),x(j,{key:2})):(r(),m("div",Y,[s("div",G,[s("div",J,[t(_,{icon:"mdi-theme-light-dark",size:"14"}),e[9]||(e[9]=n("Theme",-1))]),t(B,{"model-value":v(w).global.name.value,mandatory:"",density:"compact",variant:"outlined",divided:"","onUpdate:modelValue":S},{default:o(()=>[t(y,{value:"dark",size:"small","prepend-icon":"mdi-weather-night"},{default:o(()=>[...e[10]||(e[10]=[n("Dark",-1)])]),_:1}),t(y,{value:"light",size:"small","prepend-icon":"mdi-white-balance-sunny"},{default:o(()=>[...e[11]||(e[11]=[n("Light",-1)])]),_:1})]),_:1},8,["model-value"]),e[12]||(e[12]=s("div",{class:"muted mt-3",style:{"font-size":"11.5px"}}," Your own: the theme, Backtest Lab and Live form values, and campaign defaults are kept for you alone. ",-1))]),s("div",O,[s("div",Q,[t(_,{icon:"mdi-form-textbox-password",size:"14"}),e[13]||(e[13]=n("Password",-1))]),t(f,{modelValue:a.value.current,"onUpdate:modelValue":e[1]||(e[1]=i=>a.value.current=i),type:"password",label:"Current password",autocomplete:"current-password",class:"mb-2"},null,8,["modelValue"]),t(f,{modelValue:a.value.next,"onUpdate:modelValue":e[2]||(e[2]=i=>a.value.next=i),type:"password",label:"New password",autocomplete:"new-password",class:"mb-2"},null,8,["modelValue"]),t(f,{modelValue:a.value.again,"onUpdate:modelValue":e[3]||(e[3]=i=>a.value.again=i),type:"password",label:"New password again",autocomplete:"new-password","error-messages":b.value?[b.value]:[]},null,8,["modelValue","error-messages"]),t(y,{color:"primary",variant:"flat",class:"mt-2",loading:a.value.busy,disabled:!a.value.current||!a.value.next||a.value.next!==a.value.again||!!b.value,onClick:z},{default:o(()=>[...e[14]||(e[14]=[n("Change password",-1)])]),_:1},8,["loading","disabled"]),e[15]||(e[15]=s("div",{class:"muted mt-2",style:{"font-size":"11.5px"}},"Changing it signs you out everywhere.",-1))])]))],64)):(r(),m("div",K,"Sign-in is off on this hub: there is one local admin, and everything is in Admin."))])}}},te=N(X,[["__scopeId","data-v-e180d83b"]]);export{te as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.agrid[data-v-e180d83b]{display:grid;grid-template-columns:1fr 1fr;gap:12px;align-items:start;max-width:900px}.apanel[data-v-e180d83b]{max-width:1100px}.atabs[data-v-e180d83b]{border-bottom:1px solid var(--line)}@media(max-width:800px){.agrid[data-v-e180d83b]{grid-template-columns:1fr}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.g2[data-v-de158446]{display:grid;grid-template-columns:1fr 1fr;gap:16px 12px;padding-top:6px}.g4[data-v-de158446]{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px 10px;padding-top:6px}.g6[data-v-de158446]{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:18px 10px;padding-top:6px;margin-bottom:12px}.g4+.muted[data-v-de158446]{margin-top:16px}.sets[data-v-de158446]{display:flex;flex-direction:column;gap:6px}.set[data-v-de158446]{border:1px solid var(--line);border-radius:8px;padding:8px 10px;cursor:pointer;font-size:12.5px}.set[data-v-de158446]:hover{background:var(--panel-2)}.set.on[data-v-de158446]{border-color:var(--accent);background:#4f8cff14}.chips[data-v-de158446]{display:flex;flex-wrap:wrap;gap:4px;align-items:center;margin-top:4px}.small[data-v-de158446]{font-size:11.5px}.newset[data-v-de158446]{border:1px dashed var(--line);border-radius:8px;padding:10px}.small[data-v-f109decf]{font-size:11.5px}.plist[data-v-f109decf]{display:flex;flex-direction:column;gap:3px}.pitem[data-v-f109decf]{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;border:1px solid transparent;font-size:12.5px}.pitem[data-v-f109decf]:hover{background:var(--panel-2)}.pitem.on[data-v-f109decf]{border-color:#4f8cff80;background:#4f8cff14}.bandchart[data-v-ccd75fcf]{position:relative}svg[data-v-ccd75fcf]{width:100%;display:block}.grid line[data-v-ccd75fcf]{stroke:var(--line);stroke-width:1;vector-effect:non-scaling-stroke}.one[data-v-ccd75fcf]{stroke:var(--muted);stroke-dasharray:3 3;vector-effect:non-scaling-stroke;opacity:.6}.cross[data-v-ccd75fcf]{stroke:var(--muted);vector-effect:non-scaling-stroke}.axis text[data-v-ccd75fcf]{fill:var(--muted);font-size:11px}.readout[data-v-ccd75fcf]{position:absolute;top:8px;right:24px;background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:6px 8px;font-size:11.5px;pointer-events:none;width:430px;opacity:.96}.readout[data-v-ccd75fcf]:not(.right){right:auto;left:64px}.rr[data-v-ccd75fcf]{display:flex;gap:6px;align-items:center}.rr .nm[data-v-ccd75fcf]{flex:1}.sw[data-v-ccd75fcf]{width:10px;height:10px;border-radius:2px;display:inline-block;flex:0 0 auto}.legend[data-v-ccd75fcf]{display:flex;flex-wrap:wrap;gap:4px 14px;font-size:11.5px;margin-top:6px}.lg[data-v-ccd75fcf]{display:inline-flex;align-items:center;gap:6px;cursor:default;padding:1px 4px;border-radius:4px}.lg.on[data-v-ccd75fcf]{background:var(--panel-2)}.lg.dim[data-v-ccd75fcf]{opacity:.45}.hm-tools[data-v-5cc040ef]{display:flex;align-items:center;flex-wrap:wrap;gap:4px;margin-bottom:8px;font-size:12px}.hm-wrap[data-v-5cc040ef]{overflow-x:auto}.hm[data-v-5cc040ef]{border-collapse:separate;border-spacing:2px;font-size:11.5px}.hm th.fam[data-v-5cc040ef]{font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);font-weight:500;border-bottom:1px solid var(--line);padding:2px 4px}.hm th.colh[data-v-5cc040ef]{font-weight:500;color:var(--muted);padding:2px 4px;max-width:90px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.hm th.rowh[data-v-5cc040ef]{text-align:left;font-weight:400;min-width:180px;max-width:220px;padding-right:8px}.hm td[data-v-5cc040ef]{min-width:64px;height:34px;text-align:center;border-radius:4px;cursor:pointer;font-variant-numeric:tabular-nums}.hm td.empty[data-v-5cc040ef]{cursor:default;background:var(--panel-2)!important}.hm td.sel[data-v-5cc040ef]{outline:2px solid var(--accent)}.hm-note[data-v-5cc040ef]{font-size:11px;margin-top:6px}.ev-grid[data-v-c525c5bd]{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px}.ev[data-v-c525c5bd]{border:1px solid var(--line);border-radius:8px;padding:8px}.ev-h[data-v-c525c5bd]{font-size:12px;margin-bottom:4px}svg[data-v-c525c5bd]{width:100%;display:block}.zero[data-v-c525c5bd]{stroke:var(--line)}.day0[data-v-c525c5bd]{stroke:var(--muted);stroke-dasharray:2 3}.bench[data-v-c525c5bd]{stroke:var(--muted);stroke-dasharray:5 4;fill:none;stroke-width:1.2}.ax[data-v-c525c5bd]{fill:var(--muted);font-size:10px}.ev-row[data-v-c525c5bd]{display:flex;gap:6px;align-items:center;font-size:11.5px}.ev-row .clip[data-v-c525c5bd]{flex:1}.sw[data-v-c525c5bd]{width:10px;height:10px;border-radius:2px;flex:0 0 auto}.ev-note[data-v-c525c5bd]{font-size:11px;margin-top:6px}.br[data-v-861b6cb9]{fill:#f59e0b2e}.one[data-v-861b6cb9]{stroke:var(--muted);stroke-dasharray:3 3;vector-effect:non-scaling-stroke;opacity:.6}.ax[data-v-861b6cb9]{fill:var(--muted);font-size:11px}.runs-n[data-v-e1cd547d]{font-size:11px;opacity:.8}.vis-pick[data-v-e1cd547d]{flex:0 0 160px}.lab-grid[data-v-e1cd547d]{display:grid;grid-template-columns:300px minmax(0,1fr);gap:12px;align-items:start}.rail[data-v-e1cd547d]{display:flex;flex-direction:column;gap:10px}.rail .card[data-v-e1cd547d]{margin:0}.results[data-v-e1cd547d]{min-height:520px}.rhead[data-v-e1cd547d]{display:flex;align-items:center;border-bottom:1px solid var(--line);margin:-4px 0 8px}.tabbody[data-v-e1cd547d]{padding:4px 2px}.manage[data-v-e1cd547d]{font-size:11px;color:var(--accent);text-decoration:none;cursor:pointer}.manage[data-v-e1cd547d]:hover{text-decoration:underline}.src[data-v-e1cd547d]{display:flex;align-items:center;gap:6px;font-size:12.5px;flex-wrap:wrap}.dot[data-v-e1cd547d]{display:inline-block;width:8px;height:8px;border-radius:50%;background:#ef4444}.dot.ok[data-v-e1cd547d]{background:#22c55e}.g2[data-v-e1cd547d]{display:grid;grid-template-columns:1fr 1fr;gap:8px}.small[data-v-e1cd547d]{font-size:11.5px}.note[data-v-e1cd547d]{font-size:11.5px;color:var(--muted);display:flex;gap:4px;align-items:flex-start;flex-wrap:wrap}.chips[data-v-e1cd547d]{display:flex;flex-wrap:wrap;gap:4px;align-items:center}.tray[data-v-e1cd547d]{display:flex;flex-direction:column;gap:4px}.tchip[data-v-e1cd547d]{display:flex;align-items:center;gap:6px;padding:4px 8px;border:1px solid var(--line);border-radius:999px;font-size:12.5px}.tchip .clip[data-v-e1cd547d]{flex:1}.tchip.missing[data-v-e1cd547d]{border-style:dashed;opacity:.7}.tchip .mx[data-v-e1cd547d]{cursor:pointer;opacity:.5}.tchip .mx[data-v-e1cd547d]:hover{opacity:1}.sympv[data-v-e1cd547d]{font-size:12px;display:flex;flex-direction:column;gap:3px}.symchips[data-v-e1cd547d]{display:flex;flex-wrap:wrap;gap:3px}.symchip[data-v-e1cd547d]{font-size:11px;padding:1px 6px;border-radius:4px;background:var(--panel-2);border:1px solid var(--line)}.prog[data-v-e1cd547d]{padding:6px 2px 10px;border-bottom:1px solid var(--line);margin-bottom:10px}.prow[data-v-e1cd547d]{display:grid;grid-template-columns:minmax(0,1.6fr) 74px minmax(0,1fr) 100px 100px;gap:10px;align-items:center;font-size:12px;padding:3px 0}.prow-data[data-v-e1cd547d]{grid-column:1 / -1;display:flex;align-items:center;gap:6px;font-size:11px;color:var(--muted)}.empty[data-v-e1cd547d]{text-align:center;padding:70px 40px;display:flex;flex-direction:column;gap:8px;align-items:center;max-width:620px;margin:0 auto}.sum[data-v-e1cd547d]{width:100%;border-collapse:collapse;font-size:12.5px}.sum th[data-v-e1cd547d]{text-align:left;font-weight:500;color:var(--muted);font-size:11.5px;padding:6px 8px;border-bottom:1px solid var(--line)}.sum td[data-v-e1cd547d]{padding:6px 8px;border-bottom:1px solid var(--line);vertical-align:top}.sum .r[data-v-e1cd547d]{text-align:right;font-variant-numeric:tabular-nums}.sum tr.clickable[data-v-e1cd547d]{cursor:pointer}.sum tr.clickable:hover td[data-v-e1cd547d]{background:var(--panel-2)}.range[data-v-e1cd547d]{font-size:10.5px}.live i[data-v-e1cd547d]{font-size:10.5px;opacity:.7}.sw[data-v-e1cd547d]{width:10px;height:10px;border-radius:2px;display:inline-block;margin-right:6px}.cell-detail[data-v-e1cd547d]{border:1px solid var(--line);border-radius:8px;padding:8px 10px;font-size:12.5px}.sec-h[data-v-e1cd547d]{font-size:11px;text-transform:uppercase;letter-spacing:.04em;margin:12px 0 6px}.fgrid[data-v-e1cd547d]{display:grid;gap:16px 12px}.fgrid+.fgrid[data-v-e1cd547d]{margin-top:16px}.adv-dlg .sec-h[data-v-e1cd547d]{margin:20px 0 10px}.adv-dlg .sec-h[data-v-e1cd547d]:first-child{margin-top:6px}.adv-dlg .checks[data-v-e1cd547d]{margin-top:8px}.adv-dlg[data-v-e1cd547d] .v-card-text{padding-top:8px!important}.adv-row[data-v-e1cd547d]{display:flex;align-items:center;gap:8px}.adv-row .clip[data-v-e1cd547d]{flex:1;min-width:0}.fgrid.g3[data-v-e1cd547d]{grid-template-columns:repeat(3,minmax(0,1fr))}.fgrid.g4[data-v-e1cd547d]{grid-template-columns:repeat(4,minmax(0,1fr))}.checks[data-v-e1cd547d]{display:flex;flex-wrap:wrap;gap:0 14px}.rcol[data-v-e1cd547d]{min-width:0}.setworlds[data-v-e1cd547d]{display:flex;flex-wrap:wrap;gap:2px 8px;font-size:11.5px}.setworlds .ctl[data-v-e1cd547d]{color:var(--muted)}.batches td.pl[data-v-e1cd547d]{max-width:420px}.batches tr.active td[data-v-e1cd547d]{background:#4f8cff1a}.batches .del[data-v-e1cd547d]{opacity:.45}.batches tr:hover .del[data-v-e1cd547d]{opacity:1}@media(max-width:1100px){.lab-grid[data-v-e1cd547d]{grid-template-columns:1fr}}
|