Crypto markets never close. Which means something a person cannot watch, but a program can.
That is why I wrote QuantDesk: an engine that runs around the clock, makes its own decisions and keeps its own book. It trades on paper; real money requires two separate locks and both are shut.
Two processes, one database
Exchange (REST + WebSocket)
│
┌───────────▼────────────┐
│ engine (Node) │ 24/7 decision loop
│ │
│ market store │ candles + top of book, in memory
│ features │ ~45 per symbol/timeframe
│ regime │ TREND · RANGE · SQUEEZE · CHAOS
│ 4 strategies │ each gated to the regimes it suits
│ ensemble │ best + corroboration, adaptive weights
│ risk │ sizing, expectancy gate, breakers
│ execution │ paper | live — one contract
└───────────┬────────────┘
│
┌─────▼──────┐
│ MySQL │
└─────┬──────┘
│
┌───────────▼────────────┐
│ dashboard (PHP) │ read-mostly
└────────────────────────┘
The database is the only link between them. The reason is simple: the engine has to react to a WebSocket frame in milliseconds and hold indicator state across thousands of bars. The dashboard has to render dense tables and survive a reload. Two different shapes, two different runtimes.
Thanks to that split, restarting the dashboard never touches an open position.
First: “which market are we in right now?”
A strategy is not good or bad; it is in the wrong market. Trend following bleeds in a sideways market. Mean reversion is crushed in a strong trend.
So the engine classifies first:
| regime | meaning | strategies enabled |
|---|---|---|
| TREND (up/down) | directional move, expanding volatility | trend following, breakout |
| RANGE | sideways, bounded | mean reversion |
| SQUEEZE | volatility compressed, a move is building | breakout preparation |
| CHAOS | no direction, high volatility | none |
The last row is the most valuable. Sometimes an engine’s most profitable behaviour is not trading. After the “chaos” regime was added, trade count fell and results improved.
The expectancy gate
A signal does not open a trade. First one question: can this trade pay for itself?
p is the strategy’s realised hit rate in that regime. win/loss are the target and stop distances. cost is fees plus order-book slippage.
This gate alone eliminated one thing: on a quiet major, a 5-minute bar’s range simply cannot cover a round trip. The same symbol’s 1-hour bar clears it comfortably.
So the reason the engine watches several timeframes is not diversification — it is finding a move that can pay the cost.
The largest term: fees
Early on I hard-coded the fee rate. Then I did the arithmetic:
40 trades a day at 0.1% round trip is 8% a day. If the strategy’s gross return is below that, it is a net loss — however good the signal.
So in short-horizon trading fees are the single largest line in the P&L. That was the last thing that should have been a constant; it became a parameter with measured sensitivity. That measurement eliminated an entire strategy that looked excellent gross.
The control group
When the curve turns up, what you want to do is celebrate. Instead I ran a second engine: one buying at random from the same universe.
| parameter | strategy | control |
|---|---|---|
| Period · universe · size · costs | same | same |
| Selection rule | signal | random |
The strategy beat the control — by less than I thought. Most of the return came not from the strategy but from the direction of that period. The remaining difference was still meaningful; now I knew which part actually belonged to the engine.
And the broken instrument
At one point the engine’s “missed opportunity” counter was far higher than it should be. I went looking and found the bug in the counter itself: under a certain condition it counted the same event twice.
Which meant the last three decisions I had made from that counter were void. A measurement taken with a broken instrument is not evidence about a strategy.
Since then I have a habit: when adding a metric I add a small check that validates it — “this number must equal the sum of those two”. If it does not hold, the instrument is broken.
Record the trades you refuse
You add a filter and some trades are blocked. But is the filter right?
The only way to know is to follow what would have happened. The engine now records every trade it refuses and tracks it as if it had opened:
refusal: {
reason: "insufficient liquidity",
signal_at: …,
tracked_outcome: +2.3% (24h) ← the filter was wrong here
}
After a few weeks the picture was clear: two filters genuinely prevented losses, one was on average removing good trades. The third was deleted.
Two locks on real money
For the engine to send a real order, three things must be true at once: the mode must be live, a separate permission flag must be set, and credentials must be present. Switching modes is also refused while paper positions are open.
Above that sit circuit breakers: reaching maximum drawdown halts the book entirely until a human clears it; a daily loss limit halts only that session and lifts itself at the next day rollover.
And a per-strategy breaker: a strategy with enough trades in its window whose expectancy has turned negative shuts itself off.
What I learned
Perhaps half the code in this project is measurement code. Not the strategy, but the infrastructure for knowing whether the strategy works.
The three most valuable pieces: the control group, the record of refused trades, and the checks that validate the instruments themselves.
If you cannot measure how good a system is, you cannot claim to be improving it. You are only changing it.