EYThe LogEmre Yakut
← all entries
Simulation · Metering · deep

I wrote a trading engine that decides on its own

QuantDesk watches the market around the clock, decides which regime it is in, enables the strategies that suit it, and rejects any trade whose expectancy cannot pay the fees. Its most valuable part is not the strategy — it is the measurement.

TRENDtrend takibi · kırılma ARALIKortalamaya dönüş SIKIŞMAkırılma hazırlığı KAOSHİÇBİRİ BEKLENEN DEĞER KAPISIE = p·kazanç − (1−p)·kayıp − gidiş_dönüş_maliyeti > 0geçemeyen sinyal işleme dönüşmez risk · boyut · devre kesici kâğıt | gerçek — aynı sözleşme strateji iyi ya da kötü değildir — YANLIŞ PİYASADA olurgünde 40 işlem × gidiş-dönüş %0,1 = günlük %8 maliyetkomisyon, bu ölçekte P&L’in EN BÜYÜK terimi — sabit yazılmaz, parametre olur

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:

regimemeaningstrategies enabled
TREND (up/down)directional move, expanding volatilitytrend following, breakout
RANGEsideways, boundedmean reversion
SQUEEZEvolatility compressed, a move is buildingbreakout preparation
CHAOSno direction, high volatilitynone

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?

the condition to pass
E = p · win − (1−p) · loss − round_trip_cost > 0

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:

the weight of transaction cost
total cost = trades × 2 × (fee + slippage)

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.

parameterstrategycontrol
Period · universe · size · costssamesame
Selection rulesignalrandom

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

the most expensive lesson here

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.

SimulationMetering