Wave
An autonomous day-trading system that reads the whole market every minute — and makes its own AI earn the right to act.
A native macOS app that re-ranks every tradable US stock each minute, runs rule-based strategies through judges, a cost gate and a hard risk engine, and manages every position on its own asyncio task with a stop resting at the broker. A gradient-boosted model and a budget-capped Claude layer watch every decision — in shadow, until they earn more. Paper trading on Alpaca.
- stocks re-ranked every minute
- 13,509
the full tradable US universe, 4:00–16:00 ET
- lines of Python
- 63.8K
73 modules · 202 async functions
- test functions
- 1,035
the build refuses to ship if they fail
- paper trades
- 310
Alpaca paper · Aug 19 – Sep 24, 2026
The idea
Every minute, Wave reads the entire US stock market. Almost nothing it sees becomes a trade — and that is the point.
01Overview
Paper trading on Alpaca · live trading is locked in code · v1.0.0
A trading desk in one native app.
Wave scans every tradable US stock, ranks the ones that are moving for a reason, and lets rule-based strategies propose trades. Each proposal then has to survive a gauntlet: day-type and efficacy judges, a Claude veto on chased entries, a cost gate that demands the expected move beat fees and slippage three times over, and a hard risk engine.
What survives becomes a bracket order with a stop resting at the broker from the moment of fill, managed by its own asyncio task. Every decision — taken or refused — is journaled to SQLite, and that journal is what the machine-learning layer learns from overnight.
02Architecture
Thousands of signals in. A few protected orders out.
Every refusal is logged with its reason, so the system can be audited — and so the model has something honest to learn from.
Market feeds
SIP · news · EDGAR
Scanner 2.0
13,509 · every 60 s
The menu
top 20 names
Strategies
ORB · Gap · VWAP · FPB
Judges & veto
day · efficacy · Claude
TradeGate & risk
costs × 3 · 1% risk
Order
broker-side stop
03The scanner
Wave’s mind: the whole market, every minute.
Scanner 2.0 re-ranks every tradable symbol on time-anchored relative volume — today’s volume against the stock’s own 20-day average at the same minute — boosted by fresh news, earnings and event leaderboards, and capped so leveraged ETFs can’t crowd the menu. In the app, each star is one scanned symbol.
04Decision engine
Rules decide direction. Everything else decides whether.
- 01
Strategies
Opening-range breakout, Gap-and-Go, VWAP trend pullbacks, First Pullback and opening-auction setups emit typed signals.
- 02
Day judge
Market breadth classifies the session as trend-up, trend-down or chop — with hysteresis, and never before 10:00 ET.
- 03
Efficacy guard
Scores each fill against what happened next and flips the whole book when signals keep failing.
- 04
TradeGate
Refuses any trade whose expected move doesn’t clear spread + slippage + FINRA and SEC fees × 3. Fees are data, not constants.
- 05
Risk engine
1% risk per trade, ≤25% notional, ≤0.5% of daily volume, 3% day and 6% week loss halts, SSR and LULD handling, kill switch.
- 06
Position actors
One asyncio task per position: bracket entry, an entry ladder, idempotent order IDs and a stop check on every exit path.
05Machine learning
A model that has to earn its way in.
A scikit-learn gradient-boosting ensemble scores every candidate and writes its verdict to the journal — in shadow mode, with zero influence on trading. Each night it relabels the day’s scanner snapshots and retrains. The switch to ACTIVE exists in the app, and stays disabled until the model has proven itself.
Brain
SHADOW — the Brain watches every candidate and writes its verdict in the journal. Zero influence on trading.
LESSONS LEARNED
WINNING LESSONS
LOSING LESSONS
STATE
06ML engineering
Built not to fool itself.
Triple-barrier labels
+1 ATR before −1.5 ATR within the horizon, entering at the next bar’s open — no look-ahead, and ties count as losses.
Purged, embargoed CV
Day-grouped 5-fold cross-validation with an embargo around every test fold, so neighbouring days can’t leak.
Noise probes
Two random features ride every retrain; any real feature that can’t beat them is flagged.
Calibrated ensemble
Ten seeds of HistGradientBoosting averaged, then Platt-calibrated on out-of-fold scores into a real probability.
Checksum armour
Every model ships with a SHA-256 and golden vectors; if the loaded model scores them differently, it is refused and Wave runs rules-only.
One feature builder
The exact same function builds features for training and for live scoring — no train/serve skew.
07LLM layer
Claude in the loop — on a budget.
Claude Haiku 4.5, called over plain HTTPS with strict JSON outputs, hard spending caps and a cache — never allowed to invent a trade.
- News brain
Headline classification
First-print headlines on watched names become structured JSON — event type, direction, magnitude, sympathy tickers — after a free keyword filter has already triaged them.
- Money armour
$0.25 a day, by design
At most 400 calls and $0.25 per day, measured from the API’s own token counts, with a 24-hour cache. Any error or timeout simply leaves the keyword tags in place.
- The advisor
Trained on my own rules
A second prompt encodes my trading rules and three of my real trades as examples, judges live decision moments, and is graded against what the market did next.
- The veto
One narrow permission
Its only power: refusing entries more than 4% above the day’s open when it is ≥ 80% confident the move is finished. It can tighten, never loosen.
08Exits
Every position gets its own brain — and a stop at the broker.
A per-second exit state machine measures how stretched price is from VWAP in units of the stock’s own volatility, switches between three “gears”, caps giveback and banks volume-burst climaxes. Behind it sits a classic 7-layer ATR exit engine — and the broker-side stop that never stands down. Stops only ever ratchet tighter.
09Results
Paper-traded, journaled, rebuilt from fills.
Wave trades an Alpaca paper account, and its P&L is rebuilt trade by trade from the broker’s own fill records. Going live is gated by a written checklist — at least 30 sessions, 200 trades, a profit factor of 1.3 and zero unreconciled incidents.
+1,010.92 $ (+1.02%)
all time · Alpaca paper account
Recreated from Wave’s Performance tab. Paper trading — past results say nothing about future ones.
10Interface
A native cockpit — and a Telegram bridge.
A PyQt6 app with Apple-style liquid glass and seven tabs: live position cards, a stepped equity curve, the scanner galaxy, the ML brain, system health with a market-clock ring, a searchable log and Touch-ID-protected settings. From the phone, an owner-only Telegram bridge reports and pauses — dangerous commands need a one-time code.
CONNECTIONS
- Alpaca connected (paper)
- Data feed — SIP stream live
- Telegram bridge polling
- DB wave_paper.db — schema v14
MARKET CLOCK
--:--:--
market opens in
09:30 ET · NYSE
RISK & LIMITS
no halts · entries flowing
1% / trade · 3% day · 6% week · ≤25% notional · ≤0.5% of daily volume
Kill switchDATA FEED HEARTBEATS
trades 0s ago
quotes 0s ago
bars 0s ago
NEWS BRAIN (LLM)
Claude Haiku 4.5 · first-print headlines only
cap $0.25 / day · 400 calls
FEE SCHEDULE IN FORCE
finra_taf $0.000195 / share sold
sec_section31 $20.6 per $1M sold
11In numbers
- Python modules
- 73
181 classes, 1,338 functions
- SQLite tables
- 25
14 numbered migrations, WAL mode
- functions type-annotated
- 97%
ruff with security rules on
- pipeline stages
- 11
feeds to dashboard, all journaled
12From the source
Fail closed, by construction.
golden_x = np.asarray(bundle["golden_x"], dtype=np.float64)
golden_p = np.asarray(bundle["golden_p"], dtype=np.float64)
got = brain.score_raw_matrix(golden_x)
if got.shape != golden_p.shape or np.max(np.abs(got - golden_p)) > GOLDEN_TOLERANCE:
logger.error(
"BRAIN REFUSED: golden-vector self-test failed (max diff %.3g) — rules-only",
float(np.max(np.abs(got - golden_p))) if got.shape == golden_p.shape else -1.0,
)
return Nonefor fold_days in folds:
if len(fold_days) == 0:
continue
fold_set = set(fold_days.tolist())
lo_i = days.index(fold_days[0])
hi_i = days.index(fold_days[-1])
embargo = set(days[max(0, lo_i - EMBARGO_DAYS) : lo_i]) | set(
days[hi_i + 1 : hi_i + 1 + EMBARGO_DAYS]
)
train_mask = np.array([d not in fold_set | embargo for d in day_arr])
test_mask = np.array([d in fold_set for d in day_arr])Wave is a personal project that trades a paper (simulated) brokerage account; live trading is locked in code. It is not a product, a service or financial advice, and past results do not predict future ones.
Next project
MiniGPT Studio