Skip to content

Repository files navigation

spike_bot

A crypto perpetual-futures trading system in Rust: one strategy wired three ways — live execution, tick-accurate backtest, and a parameter optimizer that has evaluated 17.3 billion grid points — over a shared tick/bar data layer, with a browser dashboard for the operator.

185k lines of Rust, 1,881 tests, zero clippy warnings. Built for a single operator running a single account: not a framework, a SaaS, or a signal service.

The operator dashboard

The dashboard on a fresh keys-less install — cumulative PnL, open and closed trades, and the bots driving them. Everything here runs without an exchange account; see Quick start.

The problem worth solving

The strategy is the least interesting part of this repository. The engineering problem is that a parameter search over tick data explodes: one year of BTCUSDT is about 35,000 fifteen-minute bars against over a billion trade ticks, and a grid holds tens of thousands of points. Replaying every tick for every point is not slow — it is impossible.

Three layers make it tractable. Each was measured before it was kept:

1. Bars are pre-aggregated once per instrument-month. The strategy enters on a bar close, and ticks outnumber bars 50,000:1. Re-aggregating ticks per trial pays that factor for something the strategy never looks at. Cached as uncompressed Arrow IPC, mmap'd, ~200 KB per symbol-month.

2. Ticks are read only while a position is open. The trailing stop needs ticks; a flat strategy does not. Time in market is 10–30%, so most intervals are skipped outright — roughly 5× on top of the bar cache. This is a licensed shortcut, not a general one: it holds because on_tick has no side effects for this strategy while flat, and the engine falls back to walking every tick when that is not true.

3. Exit tables are memoised across the whole grid. This is the one that matters. For a single instrument, (bar, pullback) → entry and (bar, pullback, stop, activation, distance) → exit are pure functions of the tick stream — the grid axes only index into them. Computing those tables once, before the parallel sweep, turns each trial from a tick replay into a candidate-bar walk with O(1) lookups: 10–100× faster than the reference engine, and the dominant reason the numbers below are billions rather than millions.

The fast path is proven equal to the slow one. A memoised executor that silently disagrees with the engine would invalidate every result, so tools/optimize_diag parity runs N random grid points through both and gates on tolerances — trade count must match exactly. Where the two still diverge, the residuals are catalogued with their causes in BACKTEST_VS_LIVE.md rather than rounded away.

What it ran at

Measured from the optimizer's own database:

Grid points evaluated 17,268,564,946
Studies 59,751
Largest single study 200,553,104 points

Measured hot path

cargo bench (divan), Apple M1 Max, rustc 1.96, single thread:

Operation Median
Tick iteration, mmap'd Arrow IPC 226 M ticks/s
Opening a month of ticks 16.6 µs — flat in file size
Tick binary search (per signal fire) 19.4 ns
Trailing-stop update, scaled-integer path 1.74 ns
Trailing-stop update, Decimal path 18.8 ns
Entry signal, cached SMA 72.8 ns
Entry signal, recomputed SMA 441.9 ns
Bar aggregation from ticks 64.9 M ticks/s

Two of those rows are the point of the other rows: keeping the trailing stop on scaled i64 instead of Decimal is 10.8×, and precomputing the volume SMA once per grid instead of per bar is 6.1×. Neither was guessed — both replaced a slower version that is still in the tree behind the benchmark that justified the change.

The live side

Half the repository is the executor, where the hard part is not speed but agreeing with reality after something goes wrong:

  • The same trailing-stop state machine drives live and backtest. It is pure in-memory logic over ticks, and both callers drive it identically — that is what makes parity testable at all. A native exchange TRAILING_STOP_MARKET was rejected precisely because the exchange does not publish its algorithm, so the simulator could never match it.
  • 7,300 lines of reconciliation. On startup and at runtime the bot re-derives its own state from exchange truth — open positions, resting conditional orders, fills missed while the socket was down — rather than trusting its database.
  • Several bots may trade one instrument. Binance nets positions, so each bot's stop is reduce_only for its own slice rather than closePosition, and reconciliation compares the net exchange quantity against the sum of slices.
  • Divergences from the simulator are catalogued, not hidden. BACKTEST_VS_LIVE.md is 409 lines of exactly where and why simulation and live disagree — funding at entry boundaries, mark-price resolution, 8-decimal truncation in the memo cache — each with its measured size.

The result: the strategy does not work, and that is published

Most trading repositories show a backtest that made money. This one ships the study that tried to kill its own strategy — and succeeded.

That is the second reason the infrastructure above exists: 17 billion evaluations are what it takes to answer "is this edge real?" honestly rather than to find one good-looking curve. The verdict, written up in docs/WHAT_WORKS.md, is blunt:

  • The volume-spike entry has no measurable edge on any horizon tested.
  • On a 15-minute horizon the ~11.6 bps round-trip cost eats the signal outright.
  • The stop-plus-trailing exit it uses ranked last of seven exits tested.

What did survive the same tests is a different, much simpler system — a daily-bar 20-day breakout, long and short, exiting on a 50-day moving average — which posted five profitable years and one flat across 2021–2026. That system is described in docs/WHAT_WORKS.md; it is research output, not what the live binary runs.

Reaching that verdict needed methodology, not just compute: gates declared before the run, errors bootstrapped over coins rather than trades (one coin's trades share its regime), every result checked against a control — a random entry through the same exit, a random config from the same ranges, buy-and-hold over the same window. Six such rules are listed in WHAT_WORKS.md §3 — each one paid for with a mistake the study made first, including a 1% trailing stop that a 15-minute bar overstated by 93 bps against the tick engine.

The engine, the backtester, the tick layer and the optimizer are worth reading on their own. The negative result is worth reading too — it is the part that took the most work to be sure of.

This is not investment advice. The bot places real orders with real money on a live exchange account. There is no testnet mode and no paper trading — that is a deliberate design decision, not an omission. If you run it, you can lose money. Read the code first.

Architecture

bin/live   bin/backtest   bin/optimize   bin/download
                     │
        ┌────────────┴────────────┐
     exec                     optimize
   (Binance adapter,        (grid search,
    order execution)         rayon, metrics)
        │                         │
        │                    backtest
        │                (fill / fee engine)
        └────────────┬────────────┘
                 strategy
        (signal, exits, trailing)
                     │
                   data
     (TickStore: parquet SoA, bar aggregation,
      Binance downloader)
                     │
                   core
      (domain types, errors, SQLite writer)

core depends on nothing; each layer above depends only on the layers below it. Full breakdown in docs/ARCHITECTURE.md, module-by-module behaviour in docs/DESIGN.md.

Deliberately out of scope, permanently: order-book / L2 depth (trade ticks only — slippage is modelled as latency plus half-spread), and testnet / paper-trading modes.

Quick start

Requires Rust 1.85+ (edition 2024). Node is needed only if you intend to rebuild the dashboard bundles — they are committed.

Without an exchange account

Leave the API keys empty and everything except order placement still works. The bot boots keys-less, reads exchange metadata from Binance's public endpoints, and brings up the dashboard; the tick archive is public too, so the downloader, the backtester and the optimizer all run. This is the intended way to evaluate the project.

cp .env.example .env               # leave BINANCE_API_* empty
cargo build --release -p bot_live -p bot_download -p bot_backtest_bin

# public tick archive — no key, no account
./target/release/download archive --instrument BTCUSDT.binance \
    --from 2026-06-01 --to 2026-06-07
./target/release/download metadata --instrument BTCUSDT.binance

# run the strategy over those ticks
./target/release/backtest --instrument BTCUSDT.binance \
    --from 2026-06-01 --to 2026-06-07 --initial-balance 10000

./tools/bot_start.sh               # dashboard on http://127.0.0.1:8080/
./tools/bot_stop.sh                # it runs detached; Ctrl+C will not stop it

That week of BTCUSDT is 21.8 M trade ticks: about 120 MB of parquet, under a minute to fetch. The backtest over it prints a trade ledger, an exit breakdown and the risk figures; it also builds a local tick cache under data/, so expect the directory to grow to a few hundred MB.

The first start generates a random admin password, prints it once to stdout, and writes it back into .env.

With an exchange account

Fill BINANCE_API_KEY / BINANCE_API_SECRET in .env before starting. That is the only difference — and it is the point at which the bot places real orders with real money. Re-read the warning above first.

Create the key with Futures trading only, withdrawals disabled, and restricted to the host's IP; .env.example says the same where you paste it in. The bot never withdraws, so a key that cannot move funds is strictly safer and costs nothing.

Operator guide: docs/RUN.md. Deployment to a server: docs/DEPLOY.md.

Repository layout

Path What lives there
lib/core Domain types, errors, constants, SQLite write handle
lib/data Tick storage (parquet, struct-of-arrays), bar aggregation
lib/strategy VolumeSpike signal, exits, trailing
lib/exec Exchange abstraction and the Binance adapter
lib/backtest Fill and fee engine; parity-tested against live
lib/optimize Memoized grid search over the parameter space
bin/live Trading binary plus the dashboard HTTP server
bin/backtest, bin/optimize, bin/download Standalone drivers
frontend/ React + TypeScript dashboard (Vite, bundles committed)
research/ Research pipeline behind docs/WHAT_WORKS.md, mostly Python
research/reports/ Written-up results of that pipeline
optimize_coin/, tools/ Optimization pipeline drivers and operator scripts

Documentation

Document Subject
WHAT_WORKS.md Robustness verdict — start here
ARCHITECTURE.md Module layout and dependency rules
DESIGN.md Per-module behaviour, in depth
RUN.md Running the bot
DEPLOY.md Running it on a server
BACKTEST_VS_LIVE.md Where simulation and live diverge, and why
ROBUSTNESS_PLAN.md The study design behind WHAT_WORKS, and what it measured
FEE_REDUCTION.md Where the trading fees actually go, measured on live orders

Development

cargo check --workspace                        # fast compile check
cargo clippy --workspace --all-targets         # must be warning-free
cargo test --workspace                         # also green under --release
cargo bench -p bot_strategy -p bot_data        # the hot-path numbers above
python3 -m pytest optimize_coin/tests -q       # optimizer pipeline (stdlib only)
cd frontend && npm run build                   # rebuild dashboard bundles
cd frontend && node_modules/.bin/tsc --noEmit  # type-check the dashboard

Dashboard bundles under bin/live/web/pages/ are embedded into the binary at build time and must be committed alongside any change to frontend/src.

License

MIT — see LICENSE.

Two third-party bundles ship vendored under bin/live/web/vendor/, each under its own terms with the original headers intact: Chart.js (MIT) and TradingView Lightweight Charts (Apache-2.0).

About

Rust trading system for crypto perpetual futures: live executor, tick-accurate backtester, and a memoised grid optimizer that has evaluated 17.3B parameter points over an mmap'd tick store running at 226M ticks/s. 185k LOC, 1,881 tests, zero clippy warnings — plus the robustness study that killed its own strategy.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages