Admin Reference

Crypto Trading Workflow

End-to-end pipeline for the crypto paper account: 24/7 detection every 15 minutes → 5-condition signal match → automated entry/exit → trade log → email alert → Trade Crypto & Crypto Account pages. Built and validated 2026-10-03, live since the same day.

⏱
Timer
systemd · every 15 min
24/7, no market hours
1
Scan
25 live + 30 signal-only
→ full condition state
1b
Signal Check
5-condition rule
→ match / no match
2
Entry / Exit
Buy $400 notional
Sell at 15m RSI≥80
3
Trade Log
crypto_trades.json
+ equity snapshot
4
Email Alert
Every buy/sell
fired this cycle
5
Admin Pages
Trade Crypto +
Crypto Account
1
Detection Cycle — crypto_autotrade.py
Runs automatically every 15 minutes, every day, all day (systemd timer dip-swing-crypto.timer) — no market-hours gating like the equities detector, since crypto trades 24/7. Output goes to the service journal (journalctl -u dip-swing-crypto).
Live-Traded Universe (25)
BTC, ETH, XRP, SOL, HYPE, DOGE, LINK, ADA, BCH, UNI, LTC, AVAX, SHIB, AAVE, ONDO, DOT, PAXG, PEPE, RENDER, CRV, LDO, XTZ, BONK, WIF, BAT (all vs USD) — every pair that's simultaneously Robinhood-tradable, market cap >= $100M, and actually tradable on Alpaca. See Crypto Backtest for the full methodology and results.
Signal-Only Universe (30)
Same $100M+ Robinhood-tradable bar, but Alpaca doesn't carry these: BNB, ZEC, XLM, NEAR, SUI, HBAR, GRAM, QNT, ENA, WLD, MORPHO, ETC, ALGO, ATOM, INJ, FLR, PYTH, RAY, SEI, FET, VIRTUAL, XPL, OP, JTO, SYRUP, FLOKI, EIGEN, AXS, ORCA, ZRX. Shown on Trade Crypto for manual reference only — this account never buys or sells them.
No Persistent Archive
Unlike the equities pipeline (which maintains a growing candle archive in data/archive/bars), crypto re-downloads fresh from yfinance every cycle — under 60 pairs total, so the cost is manageable. The slow-moving Daily/4H trend legs are cached (data/archive/crypto_ema_state.json, refreshed at most every ~3 hours) since recomputing a 2-year daily EMA/SMA200 for every symbol every 15 minutes would be wasteful. The time-sensitive legs (1H RSI, 15m breakout) are fetched fresh every single cycle.
1b
Signal Evaluation — the 5-condition rule
The same BO15 rule validated on equities, adapted for a market with no session to anchor a 4H candle to.
# all 5 must be True simultaneously, checked on the latest completed candle 1. Daily EMA(20) > Daily EMA(50) 2. Price > Daily SMA(200) 3. 4H EMA(20) > 4H EMA(50) # plain UTC-floor 4h blocks, NOT the equities 9:30/13:30 ET split 4. 1H RSI(14) < 45 5. 15m close > entire previous 15m candle's high # clean breakout, wick included # exit (held positions only) 15m RSI(14) >= 80 # same RSI-80 exit as BO15, no stop-loss, no time limit
Why 4H Needed Adapting
Equities' 4H candles split each trading day at 9:30 / 13:30 ET — there's no equivalent "session" in a market that never closes. Crypto's 4H candles here are plain UTC-aligned blocks (00:00, 04:00, 08:00…), built by flooring 1H timestamps. Conditions 1, 2, 4, and 5 are otherwise identical math to the equities rule, just evaluated continuously instead of session-bound.
No Supertrend, Same As BO15
Matches the equities rule exactly in spirit: no Supertrend condition anywhere, a plain breakout trigger, and an RSI-only pullback gate on entry. The live equities rule arrived at this shape after several iterations (see Workflow's 1c–1e sections) — crypto started from that already-settled shape rather than re-running the same iteration from scratch.
1c
Backtest Validation — checked 2026-10-03
Full methodology, every pair tested, and the complete results table live on Crypto Backtest — summary only here.
Progression
Started with 10 major pairs (BTC, ETH, SOL, BNB, XRP, ADA, DOGE, AVAX, LINK, DOT) as a first signal check, then expanded to a self-compiled 93-pair list once the first pass looked promising. Two real bugs surfaced and were fixed along the way: a non-deterministic rolling download window (fixed with fixed start/end dates) and thin-liquidity bad ticks inflating returns (fixed by excluding any single trade with |return| > 30%). Later switched the universe itself to Robinhood's actual tradable-crypto list (pulled live from its public API) filtered to market cap >= $100M, then cross-checked against Alpaca's real asset list — see Crypto Backtest for the full writeup.
Final Results (clean, deterministic)
All 55 Robinhood pairs with market cap >= $100M: 216 trades, 75.0% win rate, +3.48% avg return, profit factor 4.45.

The 25 live-traded on Alpaca: 107 trades, 69.2% win rate, +2.87% avg return, profit factor 3.73.

The 30 signal-only (Robinhood, not Alpaca): 109 trades, 80.7% win rate, +4.08% avg return, profit factor 5.20 — actually stronger than the live-traded set, reported plainly rather than glossed over.
  • ! Only a 60-day window — yfinance's hard limit for 15-minute data. Treat this as a first signal, not a long-run edge; revisit once real paper-trade history accumulates.
  • ✓ Fully reproducible — same fixed-date cache gives identical results on repeated runs (confirmed across 3 independent re-runs before trusting any number).
  • 2
    Trade Execution
    Market orders through a dedicated $10K Alpaca paper account (CRYPTO_ALPACA_KEY/SECRET), via src/alpaca_crypto_api.py.
    Entries
    $400 notional market buy the moment one of the 25 live pairs matches — notional (dollar amount), not a share count, so there's no fractional-quantity rounding to handle. Up to 25 simultaneous positions (one slot per tracked pair, $400 x 25 = $10,000); when full, a new match is skipped rather than rotating out an existing position (no rotation logic yet, unlike BO15's equities rule). A repeat detection within 3% of a symbol's own last entry price is ignored — same whipsaw suppression as BO15. The 30 signal-only pairs never reach this step regardless of whether they match.
    Exits
    Full-position market sell the moment 15m RSI(14) reaches 80. Fill price is confirmed via order-polling (up to ~5s) before logging — a market order's own POST response often comes back unfilled, so trusting it directly risks logging the wrong exit price. No manual buys exist; only the automated cycle and the page's own manual "Close" override ever place an order.
    15m BO + 4 other conditions match → Held already? skip if yes
    Within 3% of last entry? → skip (whipsaw)
    25 slots full? → skip (no rotation)
    BUY $400 notional (market) → log to crypto_trades.json → email alert
    3
    Admin Pages
    Two pages, read mostly from data this cycle already computed — no extra live Alpaca calls from the page itself beyond account balance and live position prices.
    Live condition breakdown for all 55 pairs (25 live + 30 signal-only), every cycle — Daily EMA, Daily SMA200, 4H EMA, 1H RSI, 15m BO, a buy-signal match flag, and a sell-signal flag (15m RSI≥80) so a signal-only pair still shows an actionable exit hint for manual trading. Rows are highlighted when matching, and signal-only rows are visually distinguished from live-traded ones. Also shows the trade log and account equity/cash. Refreshes client-side every 60 seconds.
    Modeled on AutoTrade's own dashboard — equity curve, Wins/Losses/Win Rate/Realized/Unrealized stats, full trade log with CSV export, position-slots card, Overrule/Close controls, and a real-return table benchmarked against Bitcoin, Ethereum, and Solana (reusing the same generic /api/benchmark_history endpoint the equities pages use, just with crypto symbols).
    4
    Email Alerts
    One email per cycle that actually traded something — never a no-op email, and never a per-trade spam burst.
    Fires only when the cycle bought and/or sold at least one pair — a quiet cycle (the common case) sends nothing. The email lists every buy (symbol, fill price, dollar amount) and every sell (symbol, fill price, realized P&L%) from that cycle in one message, mirroring BO15's own alert pattern (_send_bo15_alert) but for actual fills rather than just new signal matches. Sent via the same Gmail SMTP relay (GMAIL_USER/GMAIL_APP_PASSWORD) every other alert in this codebase uses. Fire-and-forget — a mailer hiccup is logged and swallowed, never breaks the trading cycle itself.
    5
    API Endpoints
    All admin-gated (same _require_admin() session check as every other admin endpoint on the site).
    MethodPathUsed byReturns
    GET/api/admin/crypto_signalsTrade CryptoAll 55 pairs' current condition state (25 live + 30 signal-only), trade log, account snapshot
    GET/api/crypto/accountCrypto AccountEquity, cash, buying power, day P&L, open positions, max slots
    GET/api/crypto/equity_historyCrypto AccountDaily equity snapshots for the chart
    GET/api/crypto/real_returnCrypto AccountToday/Yesterday/Week/Month real % return vs. $10K starting capital
    GET/api/crypto/refreshCrypto Account "Sync" buttonDrift-check: broker positions vs. local trade log, fixes mismatches
    POST/api/crypto/closeCrypto Account "Close" buttonManual market sell of one open position
    POST/api/crypto/overruleCrypto Account "Overrule" toggleHolds a position back from the automated RSI exit
    6
    Credentials & Safety
    Dedicated $10K Alpaca paper account — not real money, not connected to any customer account.
  • ✓ CRYPTO_ALPACA_KEY / CRYPTO_ALPACA_SECRET in .env — the module is a complete no-op (logs once, does nothing) until both are set.
  • ✓ Credential values are never read, printed, or logged beyond a presence check — same policy as every broker credential in this codebase.
  • ✓ This account only ever trades through crypto_autotrade.py's own cycle (plus the page's manual Close override) — no other code path can place an order on it.
  • ! Live since 2026-10-03, enabled explicitly after confirming a clean dry-run against the real account — the 24/7 timer does not start trading on its own without that explicit step.
  • ! The 30 signal-only pairs are a different kind of thing entirely — this module never places an order for them, period. Their buy/sell flags on Trade Crypto exist for manually trading them on Robinhood with real money, if you choose to act on one — that decision and any resulting trade are entirely outside this system.