Hosted runner

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

This page

Hosted runner

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, VPS, and Cloudflare 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_DB/data/runner.db if /data existsSQLite file
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.

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)

Deploy body

{
  "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):

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

Same HTTP without the CLI:

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:

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

VPS: set the same env on pynescript-api.service (or the process environment scripts/deploy_vps.sh restarts). 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
fetch_failedyahoo/ccxt errorUse mock or fix data_options
Double ticksSeveral gunicorn workersGUNICORN_WORKERS=1 or rely on flock

See also