Architecture
Velox is a four-component pipeline with Redis as the shared bus. Every component can run independently; none is responsible for more than one concern.
High-level diagram
Fig. 01 · Browser calls AI APIs directly; all persistence flows through Redis; four crons coordinate asynchronously
The cron pipeline
Four independent cron jobs form a pipeline. Each reads state written by the previous.
| Cron | Path | Schedule | Timeout | Writes |
|---|---|---|---|---|
| Scanner | /api/scan | */10 * * * * | 300s | dual_portfolio_data, ai_signal_log |
| Monitor | /api/monitor-positions | */2 * * * * | 300s | dual_portfolio_data, trade lessons |
| Evaluator | /api/evaluate-positions | */30 * * * * | 300s | dual_portfolio_data |
| Optimizer | /api/optimize | 0 */6 * * * | 300s | optimization_config |
All four functions are env-gated at runtime by CRONS_ENABLED. When unset, scheduled invocations return a 200 skip response immediately; manual HTTP calls still run. See the API reference for details.
Two parallel execution environments
The same trading logic runs in two independent environments:
- Browser (Svelte app) — runs when the tab is open. Pulls market data, calls three AI APIs directly from the browser, manages trades in
localStorage. - Vercel cron — runs headlessly 24/7. Calls the same AIs server-side, manages trades in Redis.
Both write to the same Redis keys. There is no conflict resolution — last write wins. This is intentional: the browser and the cron should converge because they're running the same logic against the same data. Any divergence is a bug we want to be loud.
Redis as the shared bus
All inter-component state lives in Redis:
dual_portfolio_data— authoritative portfolio state (trades, balances, equity)optimization_config— live optimizer parameters, written by optimizer every 6hai_signal_log— per-model signal history with shadow P&Lsignal:{SYMBOL}— per-coin cooldown tracking (24–48h TTL)
Risk sizing chain
Position sizing applies four multipliers in sequence, hard-capped at 40% of portfolio:
final_risk_pct = base_risk × Kelly × Anti-Martingale × Regime × Confidence
= min(final_risk_pct, 0.40)
- Kelly — classic Kelly criterion from win-rate + payoff ratio
- Anti-Martingale — scales up after wins, down after losses
- Regime — risk-on vs. risk-off detection; blocks
CHOPPYregimes - Confidence — per-model confidence weight from the optimizer
On top of sizing, seven circuit breakers can block trade opening entirely:
- Daily loss limit
- Weekly loss limit
- Drawdown kill switch
- Max exposure ratio
- Extreme funding rate filter
- BTC dominance filter
- Optimizer pause (after N consecutive losses)
AI prompt architecture
All three models receive an identical prompt built by the scanner. The prompt includes:
- Each model's rolling track record (self-learning feedback loop)
- Discord community-call context (opt-in channel ingest)
- Social sentiment data
- Full indicator pack for all 80 coins (1H / 4H / Daily timeframes)
The consensus mechanism averages entry / SL / TP across agreeing models and weights confidence by the optimizer-tuned per-model weights.
The optimizer feedback loop
Every 6 hours the optimizer:
- Reads closed trades since the last cycle
- Computes per-model win rate, average R, and contribution-to-portfolio-return
- Adjusts per-model weights, risk caps, and blocked-regime set — capped at ±20% change per cycle
- Writes the new config to Redis
- The scanner picks up the new config on the next run
This closes the loop: bad decisions a month ago shrink that model's weight today.
Why this shape?
Every design choice above is load-bearing.
- Consensus over ensemble averaging — averaging probabilities produces smooth noise. Requiring unanimous-ish agreement produces discrete signals where disagreement is the valuable output, not a bug.
- Redis as the bus, not a queue — we don't need delivery guarantees; we need last-write-wins convergence. Redis fits.
- Dual virtual portfolios — one aggressive, one conservative. Compares outcomes under identical signals to discover which risk posture actually produces Sharpe.
- Cron, not event loop — scheduled scans are idempotent and trivially restartable. A crashed cron just waits 10 minutes and runs again.