AXIS β installable charting PWA for Pine Scriptβ’.
Website: hoox.sh/axis Β· Docs: hoox.sh/axis/docs Β· Repo: hoox-sh/axis
Installable PWA, fully pluggable, runs against the local pyne Pro API, a Cloudflare Worker, or fully offline with the in-browser Pyodide engine.
Part of the HOOX open trading stack:
| Product | Role | Repo | Website |
|---|---|---|---|
| HOOX | Edge trading framework (Cloudflare Workers) | hoox-sh/hoox | hoox.sh Β· docs |
| PYNE | Pine Scriptβ’ toolchain + Pro API (engine) | hoox-sh/pyne | hoox.sh/pyne Β· docs |
| pyne-agent-worker | NL β PYNE scripts (Workers AIβ’ + optional RAG) | hoox-sh/pyne-agent-worker | AXIS plugin docs |
| AXIS | Charting PWA (this repo) | hoox-sh/axis | hoox.sh/axis Β· docs |
PYNE Agent plugin (optional): Manager β Plugins β Install β PYNE Agent, or URL
https://pyne-agent-worker.cryptolinx.workers.dev/plugin/axis-pine-agent.js
(standalone Workers AI β no HOOX mesh required). Docs: PYNE Agent.
# Typical local trio
make -C ../pynescript run # pyne Pro API :5002
bun run dev # this repo :3000
# Optional desktop shell (Tauri 2 β needs Rust + system webview libs)
bun run desktop:dev # native window + Vite HMR
bun run desktop:build # package installers under src-tauri/target/release/bundle/Operator entry for install, diagnostics, Worker bootstrap (D1 / OAuth), secrets, deploy, and health.
cd packages/cli && bun install && cd ../..
bun run axis --help
bun run axis:install # app + worker + CLI deps
bun run axis:doctor # toolchain + wrangler.toml
bun run axis setup -- --github-client-id Ov23liβ¦
bun run axis setup -- d1 --remote # apply D1 schema on CF
bun run axis -- secret put ADMIN_TOKEN
bun run axis:deploy # Worker pynescript-axis
bun run axis:health -- --oauth # live /health + device OAuth
# Make wrappers: make axis-doctor make axis-deploy make axis ARGS="β¦"Docs: AXIS CLI Β· packages/cli/README.md Β· live demo axis.hoox.sh Β· Worker https://pynescript-axis.cryptolinx.workers.dev
See docs/devops/desktop.mdx for platform prerequisites.
Icons: Lucide via lucide-solid (tree-shakable stroke
icons, ISC). Wrapper: src/ui/icons.tsx.
ββββββββββββββββββββββββββββ Browser (PWA) ββββββββββββββββββββββββββββββ
β Service Worker Β· manifest.webmanifest Β· offline cache β
β UI: lightweight-charts + CodeMirror 6 + tabs β
β β
β ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββββββββββββ β
β β Sources β β Streams β β Engines β β Storage (localStorageβ β
β β (history)β β (live) β β (calc) β β + IDB for offline) β β
β ββββββ¬ββββββ ββββββ¬ββββββ ββββββ¬ββββββ ββββββββββββββββββββββ β
β β β β β
β βΌ βΌ βΌ β
β registry-driven, all plugins share the same contract β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
βΌ (only if engine=server OR stream=remote) βΌ
ββββββββββββββββββββββββββββ Backend (pluggable) ββββββββββββββββββββββββ
β Local Flask `make run` OR Cloudflare Pages + Worker β
β (dev only) β’ Pages: static PWA β
β β’ Worker: /api/run, /api/streamβ
β β’ Durable Object: live session β
β β’ KV: API keys, usage meter β
β β’ D1: persistent runs/scripts β
β β’ R2: indicator bundle cache β
β β’ WebSocket Hibernation (DO) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Solid path uses a unified TypeScript registry (src/plugins/registry.ts).
Every plugin is an object with { id, name, kind, description, configSchema, ... }.
// kind: 'source' β historical OHLCV
// kind: 'stream' β live tick / bar push
// kind: 'engine' β calculate Pine
// kind: 'storage' β user script library (local | git | cloud)
// kind: 'dataset' β on-chain / alternate series (TVL, β¦)
// kind: 'component' β UI slots (phase 2)
// See src/plugins/types.ts for full contracts.
// Active selection: store.activePlugins { source, stream, engine, storage }
// Built-ins register via ensureBuiltins() / registerBuiltins().
// Dynamic plugins: loadPluginFromUrl() in src/plugins/loader.ts
// Script library: Manager β Script Library tab
// storage-local β IndexedDB (default, offline)
// storage-cloud β Worker /api/scripts + Pro API keys
// storage-git β GitHub/GitLab Contents API (commit on Save)
//
// Manager tabs: Catalog (Use + capability badges) Β· Install (URL) Β· Script Library
// Settings: engine list from registry, storage picker, endpoint when needed
// Status bar: active engine id + storage backendLegacy vanilla shell still uses src/registry.js (see LEGACY.md).
| Kind | id | Source / role |
|---|---|---|
| Source | binance-rest |
https://api.binance.com/... |
| Source | okx-rest |
OKX public candles |
| Source | bybit-rest |
Bybit v5 spot klines |
| Source | coinbase-rest |
Coinbase Exchange candles |
| Source | geckoterminal-ohlcv |
DEX pool OHLCV (GeckoTerminal) |
| Source | mock-walk |
pure-synthetic random walk |
| Source | csv-upload |
user-uploaded file |
| Dataset | defillama-tvl |
Protocol TVL history (DefiLlama) |
| Stream | binance-ws |
wss://stream.binance.com/... |
| Stream | mock-poll |
synthetic poll (offline) |
| Stream | none |
paused |
| Engine | server |
POST {endpoint}/run |
| Engine | pyodide |
in-browser Python (Pyodide) |
Add a new plugin via the Plugin Manager Install tab (URL module), or
register in TypeScript under src/sources|streams|engines and the unified
src/plugins/registry.ts. Dynamic load:
import { loadPluginFromUrl } from './src/plugins/loader';
await loadPluginFromUrl('https://example.com/my-plugin.js');| Surface | What it does |
|---|---|
| Topbar Load | One-shot historical fetch (historyBars / venue page cap) into the chart |
| Data Sources panel | Background multi-page backfill to a past date, validate density, fill gaps, durable IDB cache, optional Load to chart |
| On-Chain panel | DefiLlama TVL lines, GeckoTerminal DEX pool candles, TVL spike events, CSV export, refresh jobs |
| Scripts panel | Applied indicators/strategies (list, visibility, colors) β renamed from βIndicatorsβ |
| Chart themes | Ten curated high-end presets (void, classic, mono, obsidian, graphite, pacific, dusk, porcelain, parchment, β¦) β no neon high-contrast default |
| Price scale | Auto or 0β8 decimals from symbol + bars; Pine plot.style_* parity on overlays |
| Run | Accent color only while a run is executing (ghost when idle) |
| Library / Plugins / Settings | Script storage (import v6 starter pack), plugin catalog (incl. component URL), engine endpoint |
| Workers Manager | Health cards + install helpers for Flask / Worker / Pyodide / PWA / PYNE Agent |
| AXIS CLI | packages/cli β install, doctor, setup, secrets, deploy, health |
| Desktop | Optional Tauri 2 shell (bun run desktop:dev) |
Docs: On-Chain data Β· Data Source Manager Β· AXIS CLI Β· UI shell
Primary path is Vite + Solid (this is what ships on the VPS demo):
bun install && bun run dev # Vite on :3000 (proxies /run β :5002)
# production: bun run build && python3 axis_pwa_server.py # serves dist/ on :8081# Terminal 1 β backend (pyne Pro API)
# from the pynescript / pyne checkout:
make run # Flask on :5002
# Terminal 2 β AXIS PWA
bun run devstyle.css, main.js, server.ts, and root-level index.html without Vite
are the pre-Solid shell. Prefer bun run dev or dist/ from bun run build.
See LEGACY.md.
For an offline-first demo: set Source = Mock Walk, Stream = Mock Poll,
Engine = Client-Side (Pyodide). Disable network in DevTools β Run still works.
axis/ (this repo root)
index.html Vite entry
public/ PWA icons, manifest, example plugins, vendor wheels
src/
app.tsx Solid shell: docks, panels, chart workspace
data/
load-symbol.ts Topbar one-shot history β chart
data-source-manager.ts Background backfill + validate + gap-fill jobs
bars-cache.ts IDB OHLCV cache for manager
bars-gaps.ts Series completeness / gap detection
onchain/ Dataset plane: DefiLlama TVL, GeckoTerminal, events, jobs
theme/ Chart theme catalog + 10 presets
sources/catalog.ts binance / okx / bybit / coinbase / gecko / mock / csv
streams/ Live multiplex + venue WS
engines/ server + pyodide
indicators/ runAndApply + Scripts panel UI
editor/ CM6 Pine editor, diagnostics, profiler, pins
chart/ lightweight-charts host, drawings, on-chain overlays
ui/ Topbar, panels, On-Chain, command palette, settings
store/ Solid app state + persistence
plugins/ Unified registry contracts + loader (incl. dataset)
worker/ Cloudflare Worker (API / onchain proxy / DO / D1)
packages/cli/ AXIS CLI (@hoox-sh/axis-cli) β setup / deploy / doctor
docs/ Product docs (MDX; mirrored to hoox.sh/axis/docs)
tests/ Bun unit + integration tests
e2e/ Playwright
- Local Flask: existing
make runon:5002. PWA talks to it directly (CORS handled by the backend). Default endpoint ishttp://localhost:5002. - VPS demo: PWA + Pro API same-origin at
https://axis.hoox.sh(Cloudflare β nginx TLS β staticaxis-pwa+ reverse-proxy/run//ws//healthtopynescript-apion:5002). Default store endpoint ishttps://axis.hoox.sh. - Cloudflare Worker:
bun run axis:deploy(ormake worker-deploy). The Worker exposes/api/run,/api/stream,/api/keys,/api/onchain/*(DefiLlama + GeckoTerminal allowlisted proxy), Git OAuth device flow, etc. Seeworker/README.md, Worker docs, and AXIS CLI.
CORS is enforced by the API you call (pyne Pro API or the AXIS Worker), not by the static AXIS PWA.
backend/app.py uses flask-cors with ALLOWED_ORIGINS (comma-separated). Defaults include
https://pynescript.ai, https://app.pynescript.ai, and a localhost/127.0.0.1 any-port regex.
| Origin style | Safe? | Notes |
|---|---|---|
localhost / 127.0.0.1 (+ port) |
Yes | Local AXIS / Vite; only your machine presents these Origins |
Public demo host (http://VPS:8081) |
Yes if listed | Needed when UI is on VPS and API is elsewhere (or same box) |
* |
Demo-only | Reflects any Origin β fine for a public demo, not for production secrets |
0.0.0.0 |
Skip | Not a normal browser Origin; listening on 0.0.0.0 β CORS |
Same-origin proxy (recommended): browser calls https://axis.hoox.sh/run β no CORS.
Direct :5002 is still fine for demos if ALLOWED_ORIGINS includes the page origin.
# /etc/systemd/system/pynescript-api.service
Environment=ALLOWED_ORIGINS=*
# or tighter:
# Environment=ALLOWED_ORIGINS=https://axis.hoox.sh,^https?://(localhost|127\.0\.0\.1)(:\d+)?$worker/src/index.ts pickOrigin echoes localhost / 127.0.0.1, known product hosts (*.hoox.sh, *.pynescript.ai, β¦), and *.pynescript-axis.pages.dev only (not open *.pages.dev); otherwise comma-separated ALLOWED_ORIGIN (first entry is fallback). See CORS docs.
Smoke:
curl -sS -D- -o /dev/null -X OPTIONS https://axis.hoox.sh/run \
-H "Origin: https://axis.hoox.sh" \
-H "Access-Control-Request-Method: POST"
# expect Access-Control-Allow-Origin echoing the Origin
curl -sS -X POST https://axis.hoox.sh/run \
-H "Content-Type: application/json" \
-H "Origin: https://axis.hoox.sh" \
-d '{"script":"//@version=6\nindicator(\"t\")\nplot(close)","data":[{"time":1,"open":1,"high":1,"low":1,"close":1,"volume":1}]}'- Manifest at
manifest.webmanifest(void theme#0a0b10, icons 192/512). - Service Worker at
sw.jsregistered on first load. Cache-first for the app shell, network-first for/api/*, fallback to a 503 JSON for offline API calls (thepyodideengine keeps the app fully usable offline). - Install prompt: in Chrome/Edge, look for the install icon in the URL bar.
pynescript.axis.v1 holds:
| Field | Purpose |
|---|---|
script |
Last Pine source |
symbol/interval |
Last market selection |
engine |
server or pyodide |
source |
binance-rest/mock-walk/csv-upload |
stream |
binance-ws/mock-poll/none |
endpoint |
Backend URL |
mode |
local or cloud |
apiKey |
stored only in this browser |
pluginsConfig |
per-plugin configuration |
make run-frontendthen openhttp://localhost:8081.- App loads, chart shows BTC/USDT, top bar exposes Engine / Source / Stream pickers. DevTools β Application β Manifest + Service Workers confirms PWA.
- Engine =
pyodide+ Source =mock-walk: go offline (DevTools β Network β Offline). Click Run. Pine still executes.