Demo chain snapshot refresh
Script: scripts/demo/refresh_snapshots.py
Epic: #482 — demo.raxx.app conversion flow
Card: #484 — Static chain snapshot operator script
What this is
The demo proposal endpoint (POST /api/demo/propose) serves proposals from a
static historical snapshot — it never calls a live options API at request time.
This script refreshes those snapshots by fetching recent price bars from Alpaca
and synthesising put-credit-spread candidates anchored to the most recent
closing price.
Run it before any marketing campaign that drives traffic to demo.raxx.app, and on the normal weekly cadence.
Prerequisites
Python packages
The script requires only requests (already in requirements.txt). pyyaml
is used to load scripts/demo/demo_ticker_config.yaml if present; the script
falls back to built-in defaults if PyYAML is not installed.
pip install requests pyyaml
Alpaca credentials
The script reads credentials from environment variables. Set one of the following pairs before running:
export ALPACA_PAPER_API_KEY=<key>
export ALPACA_PAPER_API_SECRET=<secret>
or
export ALPACA_API_KEY=<key>
export ALPACA_API_SECRET=<secret>
Credentials live in the Infisical vault at /demo under the project
raxx-api-prod. To pull them:
infisical secrets get ALPACA_PAPER_API_KEY --env=prod --path=/demo
infisical secrets get ALPACA_PAPER_API_SECRET --env=prod --path=/demo
If the vault folder does not yet exist, create it via the Infisical UI or
POST /api/v1/folders before writing secrets (see feedback_vault_folder_must_exist.md).
Output directory
By default, snapshots are written to data/demo/ in the repo root. Override
with:
export DEMO_SNAPSHOT_DIR=/path/to/output
The DEMO_SNAPSHOT_PATH env var consumed by the proposal endpoint should point
at the latest symlink for a given ticker, e.g.:
export DEMO_SNAPSHOT_PATH=/path/to/data/demo/spy-chain-snapshot-latest.json
The snapshot file shape is a JSON object keyed by ticker symbol; the script
writes one file per ticker. If you need a combined file, run the script and
then concatenate with jq.
Running the script
Standard weekly refresh
python scripts/demo/refresh_snapshots.py
This fetches Alpaca bars for all tickers in
scripts/demo/demo_ticker_config.yaml (default: SPX, SPY, QQQ) and writes
versioned snapshot files to data/demo/. A latest symlink is updated for
each ticker.
Before a marketing campaign
Run the standard refresh the day before the campaign starts to ensure the snapshot dates are recent. If you need to force-refresh even though today's snapshot already exists:
python scripts/demo/refresh_snapshots.py --force
Dry run (validate without writing)
python scripts/demo/refresh_snapshots.py --dry-run
The script fetches data and validates the output but does not write any files.
Single ticker
python scripts/demo/refresh_snapshots.py --ticker SPY
Custom output directory
python scripts/demo/refresh_snapshots.py --output-dir /tmp/demo-snapshots
Output file format
Each versioned file is named:
<ticker_lower>-chain-snapshot-<YYYY-MM-DD>.json
Example: spy-chain-snapshot-2026-05-12.json
A symlink spy-chain-snapshot-latest.json is kept up to date in the same
directory.
The file content is a JSON array of put-credit-spread candidates. Each entry includes:
| Field | Type | Description |
|---|---|---|
structure_type |
string | Always "put_credit_spread" |
strikes |
[short, long] | Strike prices as integers or floats |
width |
number | Spread width in underlying points |
delta |
number | Approximate short-put delta (negative) |
theta |
number | Approximate daily theta (positive) |
entry_price |
number | Net credit per share |
premium_received |
number | entry_price * 100 |
max_loss |
number | (width - entry_price) * 100 |
breakeven |
number | short_strike - entry_price |
historical_win_rate |
number | Historical win rate (0–1) |
snapshot_date |
string | YYYY-MM-DD of the snapshot |
underlying_close |
number | Closing price used to anchor strikes |
data_fetched_at |
string | UTC ISO-8601 timestamp of the fetch |
File size
Each snapshot file is typically 2–5 KB per ticker (3 spread candidates × ~500 bytes each). Well within git's comfort range. If you expand the ticker set significantly, confirm the aggregate size stays under 1 MB per refresh cycle before committing to the repo.
Scheduled cadence
- Default: weekly. Add a cron entry or GitHub Actions workflow after GA.
- Ad-hoc: before any marketing campaign that drives traffic to demo.raxx.app.
- A GitHub Actions workflow for the automated weekly run is tracked as a follow-on ops card (Phase 2 of #484).
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
ERROR: Alpaca credentials not found |
Env vars not set | Export credentials (see Prerequisites) |
HTTP 403 from Alpaca |
Invalid key/secret | Verify credentials in vault; regenerate if needed |
HTTP 422 from Alpaca |
Invalid ticker or date range | Check demo_ticker_config.yaml; ensure data_feed is valid for your plan |
HTTP 429 |
Rate limit hit | Script retries once automatically; if still failing, wait a minute and re-run |
Validation failed |
Alpaca returned unexpected data shape | Check the bars response manually; open an issue if the Alpaca API shape changed |
| Zero bars returned | No trading days in window | Widen lookback_days in config; do not run on market holidays |
| Symlink errors on Windows | NTFS symlink restrictions | Script falls back to a file copy automatically |
Connecting the snapshot to the proposal endpoint
Set the DEMO_SNAPSHOT_PATH environment variable on the Raptor Heroku app to
the path of the combined snapshot JSON, or update the service to read
per-ticker files. The current demo_proposal_service.load_snapshot_for_ticker()
reads from a single combined file keyed by ticker symbol.
To produce a combined file from the per-ticker outputs:
python scripts/demo/refresh_snapshots.py
# Then combine with jq:
jq -s '{"SPX": .[0], "SPY": .[1], "QQQ": .[2]}' \
data/demo/spx-chain-snapshot-latest.json \
data/demo/spy-chain-snapshot-latest.json \
data/demo/qqq-chain-snapshot-latest.json \
> data/demo/combined-chain-snapshot-latest.json
# Then set on Heroku:
heroku config:set DEMO_SNAPSHOT_PATH=/app/data/demo/combined-chain-snapshot-latest.json \
--app raxx-api-prod >/dev/null
Related
- Epic: #482
- Card: #484
- Demo proposal service:
backend_v2/api/services/demo_proposal_service.py - Ticker config:
scripts/demo/demo_ticker_config.yaml - Tests:
backend_v2/tests/test_demo_snapshot_refresh_484.py