When I started building CryptoBot, I thought the hardest part would be the trading strategy.
RSI, MACD, moving averages, momentum, backtesting...
That was the fun part.
But after spending more time on the project, I started running into a different kind of problem.
The strategy could be completely fine.
The software around it wasn't always fine.
A bot can generate a perfectly reasonable BUY signal and still make the wrong decision because the market data is stale, the exchange connection disappeared, or the application doesn't know whether an order was actually executed.
That's when I started looking at the project differently.
Instead of:
Strategy → Signal → Order
I started thinking about:
Market Data → Validation → Strategy → Risk Management → Execution → Exchange → Reconciliation → Recovery
The strategy is only one part of the system.
The Happy Path Is Easy
The basic flow of a trading bot is almost too simple:
┌──────────────┐
│ Market Data │
└──────┬───────┘
↓
┌──────────────┐
│ Strategy │
└──────┬───────┘
↓
┌──────────────┐
│ Risk Check │
└──────┬───────┘
↓
┌──────────────┐
│ Order │
└──────┬───────┘
↓
┌──────────────┐
│ Exchange │
└──────────────┘
Everything works.
The data arrives.
The strategy runs.
The order is accepted.
The exchange responds.
Great.
But that's the happy path.
Real systems don't stay there for very long.
Eventually:
- the WebSocket disconnects;
- market data becomes stale;
- an API request times out;
- an order is partially filled;
- the application crashes;
- an event is missed;
- local state doesn't match the exchange.
And this is where the interesting engineering starts.
A WebSocket Can Reconnect and Still Be Wrong
CryptoBot can use WebSockets for real-time market data.
Normally:
CONNECT
↓
RECEIVE DATA
↓
PROCESS DATA
↓
CONTINUE
Then something happens:
CONNECT
↓
RECEIVE DATA
↓
CONNECTION LOST
The obvious solution is to reconnect.
But reconnecting isn't the same as recovering.
Imagine the connection was down for ten seconds.
During those ten seconds:
- the price may have changed;
- an order may have been filled;
- an order may have been cancelled;
- multiple events may have happened.
The WebSocket reconnects.
But what did the bot miss?
That's the important question.
Connected
≠
Synchronized
A safer recovery flow looks more like:
Connection Lost
↓
Reconnect
↓
Synchronize
↓
Validate State
↓
Resume Trading
The bot shouldn't necessarily start trading immediately after reconnecting.
It first needs to know what happened while it was disconnected.
Stale Data Is More Dangerous Than a Crash
A crash is easy to notice.
The process is dead.
Stale data is different.
The application can be running perfectly while making decisions using old information.
Imagine the last BTC update arrived 30 seconds ago.
The bot still has:
last_price = 105200
So the strategy runs.
Maybe it sees an entry condition.
Maybe it generates:
BUY BTC
But the market data is 30 seconds old.
The actual problem is not necessarily the strategy.
The strategy was given bad input.
A simple freshness check can prevent this:
MAX_DATA_AGE = 5
if time.time() - last_market_update > MAX_DATA_AGE:
trading_enabled = False
The idea is simple:
Market Data
↓
Is data fresh?
/ \
YES NO
↓ ↓
Strategy STOP
↓
Trade
Sometimes the best decision a trading bot can make is:
I don't trust the data, so I'm not trading.
That's not a failure.
That's risk control.
The Worst Case: "Did My Order Happen?"
This is probably the failure mode I find most interesting.
Imagine:
Bot
↓
POST /order
↓
Exchange
The request is sent.
Then the network disappears.
No response.
Now the bot has a problem.
Did the exchange receive the order?
Maybe.
Did it accept it?
Maybe.
Did it execute it?
Maybe.
The bot doesn't know.
And that's very different from:
Order Failed
A timeout really means:
The client didn't receive a response.
It doesn't necessarily mean:
The exchange didn't process the request.
This is why blindly retrying can be dangerous.
For example:
Create Order
↓
Timeout
↓
Retry
↓
Create Order
If the first order actually succeeded, you may now have a duplicate.
A better approach is to treat the result as unknown:
┌─────────────────┐
│ Submit Order │
└────────┬────────┘
↓
Timeout
↓
┌─────────────────┐
│ UNKNOWN │
└────────┬────────┘
↓
Query Exchange
↓
Determine State
↓
Update Local State
This is one of those concepts that applies far beyond trading.
Whenever you communicate with an external system, there can be a difference between:
"It failed."
and:
"I don't know what happened."
Those are not the same thing.
An Order Is a State Machine
It's also tempting to think of an order as a simple function call:
order = create_order(...)
But in reality, an order has a lifecycle.
For example:
CREATED
↓
SUBMITTED
↓
ACCEPTED
↓
PARTIALLY_FILLED
↓
FILLED
It can also go another way:
CREATED
↓
REJECTED
Or:
ACCEPTED
↓
CANCELLED
Or even:
SUBMITTED
↓
UNKNOWN
Partial fills make things even more interesting.
Suppose the bot wants to buy 1.0 BTC.
The exchange might report:
0.2 BTC filled
Then:
0.5 BTC filled
And eventually:
1.0 BTC filled
If the bot misses one event, its local state can become incorrect.
The exchange may know that 0.5 BTC has been filled.
The bot may still think it's 0.2 BTC.
Now the rest of the system is operating on the wrong state.
Local State Is Not Reality
This is probably the most important thing I've learned while working on the project.
Imagine:
LOCAL STATE
BTC Position = 0.0
But the exchange says:
EXCHANGE STATE
BTC Position = 0.1
Which one is correct?
The exchange.
The local state is only the application's current understanding of reality.
It can become wrong because:
Missed Event
↓
Incorrect Local State
or:
Network Failure
↓
Incomplete State
or:
Application Restart
↓
State Must Be Rebuilt
This is why reconciliation matters.
┌───────────────┐
│ Exchange │
│ Actual State │
└───────┬───────┘
↓
┌───────────────┐
│ Reconciliation│
└───────┬───────┘
↓
┌───────────────┐
│ Local State │
└───────┬───────┘
↓
┌───────────────┐
│ Strategy │
└───────────────┘
The goal isn't to eliminate every possible failure.
The goal is to have a way to recover from them.
WebSocket + REST
This is where WebSocket and REST APIs complement each other.
WebSocket is great for fast updates:
WebSocket
↓
Real-Time Events
↓
Local State
REST can provide a snapshot:
REST API
↓
Current State
↓
Reconciliation
Together:
WebSocket
↓
Fast Updates
↓
Something Goes Wrong
↓
REST Snapshot
↓
Reconciliation
↓
Correct State
This pattern is useful because events are fast, but snapshots are useful for recovery.
If the application missed something, it needs another way to find out what the current state actually is.
connected = true Is Not Enough
One of the simplest implementations would be:
if connected:
trade()
But consider this:
WebSocket: CONNECTED
Market Data: STALE
Position: UNKNOWN
Order State: UNSYNCHRONIZED
Technically:
connected = True
But should the bot trade?
No.
This is why application state matters.
Instead of a single boolean, I prefer something closer to:
CONNECTING
↓
SYNCING
↓
READY
↓
RUNNING
And if something goes wrong:
RUNNING
↓
RECOVERING
↓
SYNCING
↓
READY
↓
RUNNING
There should also be a state where trading is explicitly blocked:
STATE MISMATCH
↓
STOP TRADING
↓
RECONCILE
↓
VALIDATE
↓
RESUME
The goal isn't:
Keep the bot running no matter what.
It's:
Keep the bot running when it is safe to do so.
Risk Management Should Be Separate
The strategy might say:
BUY BTC
That doesn't automatically mean the order should be sent.
CryptoBot also includes risk-management concepts such as:
- stop-loss;
- take-profit;
- trailing stops;
- position sizing;
- capital allocation;
- portfolio rebalancing.
The basic idea is:
Strategy
↓
BUY
↓
Risk Management
↓
Allowed?
/ \
YES NO
↓ ↓
ORDER REJECT
I like this separation because the strategy answers:
What do I want to do?
While risk management answers:
Should I actually do it?
Those are different questions.
Backtesting Doesn't Test Reality
CryptoBot also supports backtesting.
That's useful for testing strategies against historical data:
Historical Data
↓
Strategy
↓
Simulated Execution
↓
Results
You can analyze things like:
- profit and loss;
- win rate;
- drawdown;
- number of trades;
- trading costs;
- strategy performance.
But there is something a backtest usually doesn't reproduce very well:
the ugly parts of reality.
A backtest doesn't randomly lose a WebSocket connection.
It doesn't receive half of an execution event.
It doesn't restart after an order was filled.
It doesn't have to wonder whether an API request timed out after the exchange accepted it.
So a strategy performing well in a backtest doesn't automatically mean the live trading system is reliable.
These are two different engineering problems.
This Changed How I Look at CryptoBot
When I started CryptoBot, I was mostly interested in strategies.
Now I'm just as interested in what happens around them.
The project includes:
- automated cryptocurrency trading;
- real-time market data;
- technical-analysis strategies;
- backtesting;
- risk management;
- automated execution;
- multi-exchange support;
- custom strategies;
- stop-loss and take-profit;
- trailing stops;
- portfolio rebalancing;
- performance analytics;
- experimental machine-learning approaches.
But the questions I find most interesting are often simpler:
What happens when the connection disappears?
What happens when the data is stale?
What happens when the response disappears?
What happens when the order actually succeeded?
What happens when the application crashes?
What happens when the local state is wrong?
What happens when the bot simply doesn't know what happened?
These aren't strategy questions.
They're system questions.
And I think they're what separates a trading script from a trading system.
Final Thought
A trading bot isn't just:
Strategy → Order
It's more like:
Market Data
↓
Validation
↓
Strategy
↓
Risk Management
↓
Execution
↓
Exchange
↓
Reconciliation
↓
Recovery
The strategy decides what the bot wants to do.
The rest of the system has to make sure the bot understands what is actually happening.
And sometimes the safest decision isn't:
BUY
or:
SELL
It's:
STOP
↓
RECOVER
↓
RECONCILE
↓
VALIDATE
↓
RESUME
That's probably the biggest lesson I've learned while building CryptoBot.
I'm not trying to build a bot that trades all the time.
I'm trying to build one that knows when it shouldn't trade.
If you're building a trading bot or another event-driven system, what's the failure mode you find hardest to handle?
Stale data?
Duplicate orders?
Lost events?
Exchange outages?
State reconciliation?
Or something completely different?
I'd be interested to hear about it.
CryptoBot
CryptoBot is an ongoing project focused on cryptocurrency trading automation, algorithmic strategies, market analysis, backtesting, risk management, and experimentation with different approaches to automated trading.
Disclaimer: CryptoBot is provided for development, testing, research, and educational purposes. Cryptocurrency trading involves significant financial risk and can result in the loss of capital. Past backtesting results do not guarantee future performance or profits. The software does not guarantee profits.
Top comments (0)