Hosted runner

Optional Flask/container script registry + bar-close scheduler (PYNE_RUNNER). Off by default.

This page

Abstract

The Pro API is request/response: AXIS (or curl) POSTs bars, gets alerts[] in that reply, and nothing runs after the client disconnects. PYNE_RUNNER is an optional Flask-side loop that matches pyne-worker’s “upload then keep going” model on local, Docker, and containers.

Off unless PYNE_RUNNER=1 (or true / yes / on). Routes then 404 with RUNNER_DISABLED. Health reports features.script_runner.

This is not HOOX order routing. Alerts leave via the same L2 webhooks as POST /run. Strategy events stay on the tick JSON unless you forward them yourself.

Conceptual model

Diagram

Rendering…

Each tick fetches OHLCV for the script’s symbol / timeframe / data_source. If last bar time did not advance, the job is skipped. Otherwise a full Runtime.run (same evaluate contract as /run) and last-bar alert webhooks.

Interface surface

Environment

VariableDefaultRole
PYNE_RUNNERoffMaster switch — HTTP registry + /cron/run
PYNE_RUNNER_SCHEDULERoffIn-process poll loop (needs PYNE_RUNNER)
PYNE_RUNNER_POLL_SECONDS60Poll interval (floor 5s)
PYNE_RUNNER_WORKERS2Job threads per tick (floor 1, cap 16)
PYNE_RUNNER_DB/data/runner.db if /data existsSQLite file (sqlite backend)
PYNE_RUNNER_STORE_BACKENDsqlitesqlite (single host) or redis (multi-replica, needs REDIS_URL)
ADMIN_TOKENunsetWhen set, writes require X-Admin-Token

Gunicorn multi-worker: every worker may start a scheduler thread; ticks take a flock on PYNE_RUNNER_DB.lock. Prefer GUNICORN_WORKERS=1 when the scheduler is on.

Store backends

sqlite (default) keeps scripts, cron state, and run history in one local SQLite file and serializes ticks with a filesystem flock — single host only. PYNE_RUNNER_STORE_BACKEND=redis shares the same registry across API replicas through REDIS_URL and serializes ticks with a SET NX token (TTL = 2× the poll interval, floor 120s), so two replicas never double-tick. /health reports the active backend as features.script_runner_store. Run history keeps the last 100 runs per script (1000 globally); the script record itself always stores source and webhook_url in plaintext (reads redact the webhook), so keep Redis private — the compose service never publishes its ports.

Each script is only fetched once its next bar can have closed (last_bar_time + the timeframe interval), so a 1w job is not hammered every poll. Failed fetches or runs back off exponentially (60s doubling, capped at 30 min); force bypasses both gates. Every ok / error run is appended to the audit log — routine skips (no_new_bar, not_due) are not recorded.

Routes (PYNE_RUNNER=1)

MethodPathAuthRole
POST/scriptsadmin if ADMIN_TOKENDeploy / replace
GET/scriptsnoneList (no source)
GET/scripts/:idnoneSource + cron state
DELETE/scripts/:idadmin if ADMIN_TOKENRemove
GET/cron/jobsnoneScripts + last tick
PUT/cron/jobsadmin if ADMIN_TOKEN{jobs:[{script_id, enabled}]}
POST/cron/runadmin if ADMIN_TOKENTick now (force, optional script_id)
GET/cron/historynoneRun audit log (script_id, limit ≤ 500)

Deploy body

json
{
  "id": "btc-sma",
  "script": "//@version=6\nindicator(\"t\")\nplot(close)\nalertcondition(close>open,\"up\",\"green\")",
  "symbol": "BTCUSDT",
  "timeframe": "1d",
  "mode": "interpret",
  "data_source": "mock",
  "period": "6mo",
  "max_bars": 500,
  "enabled": true,
  "webhook_url": "https://hooks.example.com/pine",
  "forward_alerts": true
}

data_source: mock (deterministic, no network), yahoo, ccxt (data_options.exchange), alphavantage. Same provider factory as CLI pyne data.

Worked examples

Local (no Docker):

bash
export PYNE_RUNNER=1 PYNE_RUNNER_SCHEDULER=1
python -m backend.app   # :5002
pyne runner deploy script.pine --id demo --symbol BTCUSDT --source mock
pyne runner tick --id demo --force
pyne runner list
pyne runner history --id demo

Same HTTP without the CLI:

bash
curl -sS -X POST http://127.0.0.1:5002/scripts -H 'Content-Type: application/json' \
  -d '{"id":"demo","script":"//@version=6\nindicator(\"t\")\nplot(close)","data_source":"mock","timeframe":"1d","max_bars":80}'
curl -sS -X POST http://127.0.0.1:5002/cron/run -d '{"force":true}' -H 'Content-Type: application/json'

Docker:

bash
PYNE_RUNNER=1 PYNE_RUNNER_SCHEDULER=1 docker compose up --build api

Self-host: set the same env on the API process. SQLite path should sit on a persistent disk (PYNE_RUNNER_DB=/data/runner.db).

Cloudflare® Container: keep PYNE_RUNNER=0 in cf/src/index.ts unless you want the runner inside the isolate; one gunicorn worker is already the default.

Failure modes

SymptomCauseFix
RUNNER_DISABLED 404Switch offPYNE_RUNNER=1
FORBIDDEN 403ADMIN_TOKEN set, header missingX-Admin-Token
skipped / no_new_barLast close unchangedWait, or force: true
skipped / not_dueTimeframe gate: next bar not closed yetWait, or force: true
skipped / backoffRecent fetch/run failure; retry delayedWait, or force: true
fetch_failedyahoo/ccxt errorUse mock or fix data_options
Double ticksSeveral gunicorn workersGUNICORN_WORKERS=1 or rely on flock

See also