How a live AI trade-signal system actually works.
Drishti is a public, always-on system that asks a frontier AI model for a trade idea on ten crypto and metal perpetual futures — every 15 minutes, around the clock. This page opens the box: the loop, the prompt, the experiments, and the anatomy of a single signal.
Illustrative only — not financial advice. Drishti generates and tracks paper signals to study AI decision-making in markets. Nothing here is a recommendation to trade.
One cycle, every fifteen minutes
Drishti never closes. A single systemd loop on a small VM runs three phases in order, then sleeps to the next quarter-hour and does it again — about a hundred times a day.
tracker.pyWalk the tape on every open signal; close the ones that resolved.
generator.pyFor every asset without an open signal, build features → ask the model → gate → save.
publisher.pyWrite the JSON the website reads, then git-push so Vercel redeploys.
What actually happens each cycle
Close what resolved, decide on what's open, publish the result. The middle phase — the decision — is where the AI lives.
Track & Close
tracker.pyFor each open signal, the tracker replays 1-minute candles from when it was created. If price touched take-profit first it closes as a win; if it hit the stop first, a loss; if neither within 24 hours, it expires (counted as neither). On a same-bar wick the stop wins — a deliberately pessimistic tie-break.
Decide
generator.pyThe heart of the system. For each of the ten assets that has no open signal, it assembles ~30 engineered features, hands them to Claude Opus 4.8 with a forced-tool-call schema at temperature 0, runs the answer through a risk gate, and persists it. A directional call posts to Slack immediately.
Fetch + compute ~30 signals: multi-timeframe technicals, funding, open interest, options skew, liquidations, macro overlay, cross-asset correlation, and per-asset daily-move stats. A failed feed renders n/a rather than crashing the cycle.
Claude Opus 4.8 receives the system prompt + the per-asset feature block and must answer through a JSON tool schema (DrishtiDecision). Temperature 0 for determinism; one open idea per asset is enforced before the call.
A thin, mechanical check — not a strategy. It rejects anything whose levels are on the wrong side of entry, and enforces a reward-to-risk floor of ≥ 1. Everything else the model decided stands.
Valid signals are written to DuckDB with their full feature hash and cost, then posted to Slack. The whole per-asset path is fail-soft: one bad asset never sinks the cycle.
Publish
publisher.pyThe publisher serializes the live state to JSON (signals, stats, the prompt itself, cost, health), rebases on the latest main, commits, and pushes. Vercel sees the push and redeploys the public site — about 96 times a day, just under the platform's daily cap.
Anatomy of a single call
This is a real closed signal from the snapshot. Every field is something the model chose — or something the system measured afterward.
Every signal is a single structured object the model fills in. Tap any part to see what it means.
Long, short, or no_trade. The model is told to pick the side the evidence supports — never to favour a direction a priori.
Before the model is asked anything, the generator assembles ~36 features per asset across 11 categories. Tap a category to see what's inside.
A missing feature isn't always a problem. Structural absence is by design — gold tokens have no options chain — and the model is told not to dock confidence for it. Operational absence is a feed that should be there but failed; that one is a quality signal, and the model leans toward caution.
Three ways a signal closes
The tracker walks 1-minute candles until price touches a level — or 24 hours pass.
How the instructions evolved
The signal quality lives almost entirely in the prompt. It has been through three deliberate generations — each one measurable against the last.
Hand the model the one fact it lacked — how far each asset typically travels in a day — as data, not instruction.
Live data showed ~25% of signals expiring and oversized (>3%) targets winning only ~14%. Giving the model a direct read on reachable spans should shrink both problems — and being data-only keeps the change measurable against v22.
- Per-asset <daily_move> data block: 7d/30d avg range, p90 busy-day, net move
- DATA ONLY — no new instruction, no new risk-gate cap
- RR floor unchanged; a fresh v23 cohort opens here
Treating it like an experiment
A live AI system is only as trustworthy as the way you measure it. Drishti keeps a clean north star, runs controlled A/Bs, grades its own confidence, and alerts on silent failure.
North star: resolved win rate
win = TP1 hit first · loss = SL hit first · expired (neither in 24h) is excluded. Win rate = won ÷ (won + lost). A clean, honest denominator — expiries don't pad the number either way.
no per-asset track record, no past-signal attribution
the previous prompt, run in parallel for comparison
Production currently runs history-free — the model sees no record of its own past signals — to test whether that record was making it gun-shy. A parallel shadow arm runs the history-on prompt on the same features, labelled v21-histon, so the two can be compared under identical market conditions.
Score the model's calibration
The model reports its own outcome odds with every signal. After it closes, those odds are graded by a multiclass Brier score — squared error against what actually happened. Lower is better. Drag the odds, pick the real outcome.
What can go wrong (and how you'd know)
A migration once silently deleted every new-prompt and shadow row on each cycle, so the A/B never accumulated. It looked like an empty table, not an error. The fix removed the destructive delete; the lesson became three data-integrity Slack alerts — count-drop, stale, and budget — that now catch silent pipeline failure the way a missing chart never could.
The plumbing that keeps it honest
Twelve data feeds, a fleet of scheduled jobs, cost tracking down to the cent, and a publisher that keeps the public site — and its own documentation — in sync with reality.
drishti.serviceevery 15 minThe main loop — track → decide → publish.autodeployevery 1 minPulls code-only pushes to main and restarts the service.monitorevery 5 minProbes cycle health; alerts if the loop goes stale.etf-flowsdailyIngests BTC/ETH spot-ETF net flows.Born from a bug that silently deleted data — silent failure should never look like “everything’s fine.”
signal_count_dropA cycle emits far fewer signals than its recent baseline — the canary for a broken generator.
signals_staleThe published signals.json stops refreshing while the model isn't quota-blocked.
budget_warningCumulative spend nears the daily model-gateway limit.
Publish self-healA stuck git repo auto-recovers — rebase-abort, reset-hard, retry — instead of going dark.
Watchdog + crash-loop guardA systemd watchdog restarts a hung process; a start-limit stops restart thrash if the process keeps crashing.
Stall auto-restartAn on-VM monitor restarts the service when a cycle or publish stall is detected.
Off-VM dead-man's switchA heartbeat pages even if the whole VM or network dies — silence itself triggers the alert.
Always-on ops pagingOutage alerts bypass the trade-only Slack filter so they always deliver, regardless of verbosity settings.
Bump the prompt version or swap the model and the publisher regenerates the machine-readable surfaces automatically — the llms.txt family, site metadata, and an auto changelog entry — all gated on actual content change so nothing churns. The public 'System' and 'Changelog' tabs read those files directly, so the site explains its current self without a human editing copy.