DEV Community

John Doe
John Doe

Posted on

The Hardest Part of a Trading Bot Isn't the Strategy

A trading strategy is usually the most visible part of an automated trading system.

You can see the indicators, the entry conditions, the signal, and eventually the order.

But after working on CryptoBot, I've started to think that the strategy is actually one of the easier parts.

The difficult part is everything that happens when reality doesn't behave as expected.

What happens when the WebSocket disconnects?

What happens when market data becomes stale?

What happens when an order is submitted but the response is lost?

What happens when the application restarts and its local state no longer matches the exchange?

These aren't unusual edge cases.

They are normal problems in a long-running distributed system.

The Happy Path Is Easy

A simple trading bot can be represented like this:

Market Data
    ↓
Strategy
    ↓
Signal
    ↓
Risk Check
    ↓
Order
    ↓
Exchange
Enter fullscreen mode Exit fullscreen mode

Everything works.

The exchange responds, the network stays connected, market data arrives, and the order is accepted.

For a prototype, this is enough.

For software that should run continuously, it isn't.

Eventually something will fail.

The important question is not whether the system will fail.

The important question is what it will do when it does.

Reconnecting Isn't Recovery

Real-time market data is often delivered through WebSockets.

A connection can disappear because of network problems, exchange maintenance, server-side disconnects, or client-side failures.

A naive system does:

connect
receive data
process data
repeat
Enter fullscreen mode Exit fullscreen mode

A more realistic system needs:

connect
    ↓
receive data
    ↓
connection fails
    ↓
detect failure
    ↓
reconnect
    ↓
restore state
    ↓
validate data
    ↓
continue
Enter fullscreen mode Exit fullscreen mode

The important difference is the recovery step.

Suppose the connection was down for five seconds.

The bot reconnects successfully.

But what happened during those five seconds?

It may have missed market updates or order events.

Simply reconnecting doesn't restore the state that was lost.

The application may need to synchronize with the exchange before trading again.

Stale Data Can Be Worse

A disconnected connection is easy to detect.

Stale data is harder.

Imagine the application is still running, but the last market update arrived thirty seconds ago.

The strategy still has a price.

It's just an old price.

That creates a dangerous situation:

Market Data Stops
      ↓
Local State Remains Unchanged
      ↓
Strategy Keeps Running
      ↓
Signal Generated
      ↓
Order Submitted
Enter fullscreen mode Exit fullscreen mode

A safer approach is to make data freshness part of the decision.

Market Data
    ↓
Freshness Check
    ↓
Fresh?
  /   \
Yes    No
 ↓      ↓
Trade   Stop
Enter fullscreen mode Exit fullscreen mode

Sometimes the correct trading decision is simply:

I don't have reliable data, so I am not going to trade.

The Hardest Case: An Unknown Order

Consider a simple order request:

POST /order
Enter fullscreen mode Exit fullscreen mode

The application sends it.

Then the network connection disappears.

No response arrives.

Did the exchange receive the order?

Maybe.

Did it execute?

Maybe.

The application cannot know from the timeout alone.

This is why:

timeout = failure
Enter fullscreen mode Exit fullscreen mode

is dangerous.

The real state may be:

UNKNOWN
Enter fullscreen mode Exit fullscreen mode

Blindly retrying can create a duplicate order if the first request actually succeeded.

A safer flow is:

Order Request
    ↓
Timeout
    ↓
UNKNOWN
    ↓
Query Exchange
    ↓
Determine Actual State
    ↓
Update Local State
Enter fullscreen mode Exit fullscreen mode

This is one of the reasons an order should be treated as a state machine rather than just a function call.

An Order Has a Lifecycle

A simplified lifecycle can look like:

Created
   ↓
Submitted
   ↓
Accepted
   ↓
Partially Filled
   ↓
Filled
Enter fullscreen mode Exit fullscreen mode

There can also be rejected, cancelled, expired, failed, or unknown states.

Partial fills make this even more interesting.

An order for 1.0 BTC might first be filled for 0.2 BTC, then 0.5 BTC, and finally 1.0 BTC.

If the application disconnects between those events, its local state may become inaccurate.

That's why reconciliation matters.

Local State Is Not Reality

The bot might have:

Local:
BTC position = 0.0
Enter fullscreen mode Exit fullscreen mode

while the exchange has:

Exchange:
BTC position = 0.1
Enter fullscreen mode Exit fullscreen mode

The local state is wrong.

The exchange is the system that actually holds the account and executes the trade.

This means the application needs a way to verify and rebuild its state.

A useful model is:

Exchange
    ↓
External State
    ↓
Reconciliation
    ↓
Local State
    ↓
Strategy
Enter fullscreen mode Exit fullscreen mode

WebSocket events provide fast updates.

REST APIs can provide snapshots and help verify state.

One gives responsiveness.

The other provides a way to recover when events are missed.

Application Restarts Matter Too

Imagine:

Bot Running
    ↓
Order Submitted
    ↓
Order Filled
    ↓
Application Crashes
Enter fullscreen mode Exit fullscreen mode

When the application starts again, it needs to know:

  • What positions do I have?
  • Which orders are still open?
  • Which orders were filled?
  • What happened while the bot was offline?

This is why startup recovery matters.

Instead of immediately starting to trade:

Application Start
       ↓
Query Exchange
       ↓
Compare State
       ↓
Reconcile
       ↓
Validate
       ↓
Start Trading
Enter fullscreen mode Exit fullscreen mode

The bot should not assume that its previous local state is correct just because it was saved.

Recovery Should Be a Real State

A simple:

connected = true
Enter fullscreen mode Exit fullscreen mode

isn't enough.

The system can be connected while still having stale or inconsistent state.

A better model is:

CONNECTING
    ↓
SYNCING
    ↓
READY
    ↓
RUNNING
    ↓
RECOVERING
    ↓
SYNCING
    ↓
RUNNING
Enter fullscreen mode Exit fullscreen mode

There should also be a state where the bot refuses to trade.

For example, if the local position doesn't match the exchange position:

Mismatch Detected
      ↓
Stop Trading
      ↓
Reconcile
      ↓
Restore Consistency
      ↓
Resume Only If Safe
Enter fullscreen mode Exit fullscreen mode

Not every error should automatically result in another retry.

Sometimes stopping is the correct behavior.

Logging Is Part of Reliability

When an automated system makes a decision, you should be able to understand why.

A useful log trail might look like:

Market data received
    ↓
Strategy evaluated
    ↓
Signal generated
    ↓
Risk check passed
    ↓
Order submitted
    ↓
Exchange response received
    ↓
Order filled
Enter fullscreen mode Exit fullscreen mode

During recovery, state transitions are equally important:

WebSocket disconnected
RUNNING → RECOVERING
Reconnect successful
State synchronized
Market data refreshed
RECOVERING → RUNNING
Enter fullscreen mode Exit fullscreen mode

If something happens at 3 AM while nobody is watching, the logs should make it possible to reconstruct what the bot believed and what it did.

Final Thoughts

When I started CryptoBot, I was mainly interested in automating trading strategies.

Over time, the more interesting questions became different:

What if the connection disappears?

What if the response disappears?

What if the data becomes stale?

What if the application restarts?

What if the local state is wrong?

What if an order exists but the bot doesn't know about it?

Those questions don't make the trading strategy smarter.

They make the software more reliable.

A real trading bot isn't just:

Strategy → Order
Enter fullscreen mode Exit fullscreen mode

It is closer to:

Market Data
     ↓
Strategy
     ↓
Risk Management
     ↓
Execution
     ↓
Exchange
     ↓
Reconciliation
     ↓
Recovery
Enter fullscreen mode Exit fullscreen mode

The hardest part isn't making the bot do something.

It's making sure that when something goes wrong, the bot knows enough to stop, recover, reconcile, and continue safely.

That's the part of CryptoBot I find most interesting.

https://github.com/pavloaser23/crypto-trading-bot

Top comments (0)