Building a trading bot is surprisingly straightforward.
Get some market data. Calculate a few indicators. Generate a BUY or SELL signal.
Maybe connect it to an exchange.
Congratulations. You have a trading bot.
But here's the question that changed how I approached NERV Trade, my Java-based trading engine:
What happens after the signal?
What if the broker accepts your order but never responds?
What if an order is partially filled?
What if the same execution arrives twice?
What if the application crashes while updating a position?
And what happens when the application restarts while orders are still active?
These aren't hypothetical edge cases you can safely ignore when building an automated trading system.
They're architectural problems.
And solving them is what separates a simple trading bot from a reliable trading engine.
1. The Trading Bot Everyone Starts With
Most trading projects begin with an architecture similar to this:
Market Data
|
v
Technical Indicators
|
v
Trading Strategy
|
v
BUY / SELL / HOLD
For example, imagine a simple Java strategy:
public enum Signal {
BUY,
SELL,
HOLD
}
public Signal evaluate(
BigDecimal fastMovingAverage,
BigDecimal slowMovingAverage) {
int comparison =
fastMovingAverage.compareTo(slowMovingAverage);
if (comparison > 0) {
return Signal.BUY;
}
if (comparison < 0) {
return Signal.SELL;
}
return Signal.HOLD;
}
This is a simplified illustration, not NERV Trade's actual strategy implementation.
It demonstrates the basic idea.
Given market observations, produce a trading signal.
You can improve the strategy by introducing:
- Multiple indicators
- Multi-timeframe confirmation
- Volatility analysis
- Market regime detection
- Risk-adjusted signals
But regardless of how sophisticated the analysis becomes, the output is still fundamentally a decision.
BUY
SELL
HOLD
And a decision isn't an execution.
That's where the interesting engineering begins.
2. A BUY Signal Is Not an Order
Suppose our strategy produces:
BUY BTCUSDT
What does that mean?
Does the strategy want to buy Bitcoin?
Has an order been created?
Has the exchange accepted the order?
Has the order been filled?
These are different states.
A more realistic architecture separates them:
Market Data
|
v
Analysis
|
v
Strategy
|
v
Risk Evaluation
|
v
Trade Intent
|
v
Local Order
|
v
Broker Submission
|
v
Execution Reports
Each component has a specific responsibility.
| Component | Responsibility |
|---|---|
| Analysis | Interpret market conditions |
| Strategy | Decide what action to propose |
| Risk | Determine whether the action is permitted |
| Trade Intent | Own the trading lifecycle |
| Local Order | Represent the requested broker action |
| Broker Adapter | Communicate with the external venue |
| Execution Processor | Accept and account for actual fills |
This separation is important because a trading decision can lead to multiple orders.
And one order can produce multiple executions.
A strategy should not need to understand those execution details.
Its responsibility is to express an intention.
The execution engine is responsible for determining what actually happened.
3. An Accepted Order Is Not an Executed Order
Consider an order to buy 10 units.
The simplest implementation assumes:
BUY 10
|
v
FILLED 10
But a broker might produce:
BUY 10
|
v
ACKNOWLEDGED
|
v
PARTIALLY FILLED: 3
|
v
PARTIALLY FILLED: 7
|
v
FILLED: 10
The order has a lifecycle.
Its state changes over time.
A simplified representation might look like:
public enum OrderStatus {
CREATED,
SUBMITTED,
ACKNOWLEDGED,
PARTIALLY_FILLED,
FILLED,
CANCELLED,
REJECTED
}
Again, this is an illustrative model rather than NERV Trade's exact domain API.
The important distinction is that an order's status is not the same thing as its economic effect.
An order might be acknowledged without any execution.
It might be partially filled and later cancelled.
It might be fully filled through several independent executions.
The engine must track both:
What the broker says about the order
and
What executions have actually occurred.
Those are related but different responsibilities.
4. Partial Fills Introduce Accounting
Let's take the previous example.
Order: BUY 10
Execution 1: BUY 3 @ 100
Execution 2: BUY 4 @ 101
Execution 3: BUY 3 @ 102
The engine cannot simply record:
BUY 10 @ 100
That would misrepresent the actual execution.
Instead, it needs to account for each fill.
For this example:
Total quantity = 10
Total cost =
(3 × 100)
+ (4 × 101)
+ (3 × 102)
Total cost = 1,010
Average price = 101
And that is before considering fees.
In Java, the arithmetic might look like:
BigDecimal totalCost = executions.stream()
.map(execution ->
execution.price()
.multiply(execution.quantity()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
The arithmetic is easy.
The lifecycle surrounding it is not.
What happens if an execution is delivered twice?
What if the order is cancelled after a partial fill?
What if the application crashes before the accounting is committed?
What if reconciliation discovers an execution that was never received through the live stream?
These questions introduce a requirement that extends far beyond calculating an average execution price.
Execution accounting must remain correct under duplicate delivery, failure, and recovery.
5. The Same Execution Can Arrive More Than Once
This is where trading systems begin to resemble other distributed systems.
Imagine the broker reports:
Execution ID: 12345
BUY 0.001 BTC
The application processes the fill.
Its exposure increases by 0.001 BTC.
Then the connection drops.
After reconnecting, the application receives the same execution again.
Or perhaps it discovers that execution through a reconciliation request.
If the engine processes it again:
Actual purchased: 0.001 BTC
Recorded exposure: 0.002 BTC
The internal state is now wrong.
This is why execution identity matters.
A simplified execution-processing contract might be:
public interface ExecutionProcessor {
ExecutionResult accept(ExecutionReport report);
}
The implementation must determine whether the execution has already been accepted.
Conceptually:
Receive Execution
|
v
Check Durable Identity
|
+----+----+
| |
v v
Already New
Accepted Execution
| |
v v
No New Apply Accounting
Effect Atomically
The word durable is important.
An in-memory Set of execution IDs might prevent duplicates while the application is running.
But it doesn't survive a restart.
And a restart is precisely when previously observed executions may be encountered again.
In NERV Trade, accepted execution identities are persisted so duplicate delivery does not create duplicate economic effects.
The principle is straightforward:
The same execution can be observed multiple times, but its accounting effects must be applied only once.
We'll explore this problem in much greater detail later in the series.
6. The Most Dangerous Failure Is an Unknown Outcome
Consider a common backend pattern:
try {
broker.submit(order);
} catch (Exception exception) {
retry(order);
}
For ordinary read operations, retrying after a transient failure can be reasonable.
For order submission, this pattern can be dangerous.
Imagine the following sequence:
Trading Engine Broker
| |
|------ Submit BUY -------->|
| |
| Order Accepted
| |
X |
Connection Lost |
The broker accepted the order.
But the engine never received the response.
From the application's perspective, the request failed.
From the broker's perspective, the order exists.
If the engine blindly retries with a new identity, it may create another order.
Now the account has more exposure than intended.
This is an example of an uncertain external side effect.
The system knows that a request was attempted.
It does not know whether the external operation succeeded.
A safer conceptual flow is:
Submit Order
|
v
Response Received?
|
+---+---+
| |
YES NO
| |
v v
Record Preserve Uncertain
Result Submission State
|
v
Reconcile With Broker
|
v
Establish Actual Outcome
The engine needs stable order correlation and a recovery mechanism.
It must not assume that a missing response means the broker rejected the request.
This is one of the architectural challenges I found particularly interesting because it closely resembles problems in payment gateways.
A payment request might succeed even when the client times out.
A trading order might be accepted even when the application loses its connection.
In both cases, blindly repeating the operation can be worse than temporarily preserving uncertainty.
7. The Database Becomes Part of the Execution Engine
At some point, the application must survive a restart.
That means persisting state.
But saving an order isn't enough.
When an execution is accepted, several related records may need to change:
Execution Identity
+
Execution Details
+
Order Accounting
+
Trade Exposure
+
Cost Basis
+
Realized P&L
+
Lifecycle State
+
Journal Outcome
These changes represent one logical economic operation.
Consider what happens if the application crashes halfway through processing.
Execution recorded: YES
Exposure updated: NO
Now the database contains an execution that isn't reflected in the position.
The opposite failure is equally problematic:
Execution recorded: NO
Exposure updated: YES
After restarting, the engine may accept the same execution again.
NERV Trade eventually moved toward atomic execution processing using PostgreSQL transactions.
The requirement is:
Either all accounting effects of an accepted execution commit, or none of them do.
This is more than a database implementation detail.
It affects how the domain model and execution-processing boundaries are designed.
A transaction must encompass the complete state transition.
Otherwise, persistence can preserve an inconsistent result perfectly.
8. Restart Recovery Changes the Definition of State
Suppose the application has been running for several hours.
It has:
- Active trade intents
- Submitted orders
- Partially filled orders
- Accepted executions
- Open exposure
- Completed lifecycles
Then the process stops.
When it restarts, it cannot simply create a new empty state.
The broker still knows about its orders.
The account still has its assets.
And the market may have continued executing orders while the application was offline.
The engine needs to reconstruct its durable state and reconcile unresolved activity with the broker.
This means recovering more than the latest aggregate position.
It needs to understand which trade intents own the exposure, which executions have already been accepted, and which orders remain unresolved.
A restart must not turn an existing trade into a new trade.
And recovery must not silently invent an outcome that hasn't been confirmed.
This is why recovery became an architectural responsibility in NERV Trade rather than just a startup routine.
9. Market Data Has Similar Problems
Execution isn't the only place where a simple design becomes complicated.
Market data introduces its own consistency requirements.
Suppose a strategy analyzes five-minute candles.
M5
Then we introduce higher-timeframe context:
M5
M15
H1
H4
Now the engine must answer questions such as:
- Which higher-timeframe candles were available at a particular decision time?
- Has the current candle closed?
- Can a partially formed candle be used?
- What happens when market-data events arrive late?
- How are missing intervals detected?
- Can the same historical input reproduce the same analysis?
The most dangerous mistake is accidentally using information that wasn't available when a historical decision was made.
That creates look-ahead bias.
A backtest might look excellent while relying on future information.
For NERV Trade, deterministic analysis and multi-timeframe consistency became important foundations.
We'll cover both in dedicated articles.
10. What the Architecture Eventually Became
The original idea was simple:
Market Data
|
v
Analysis
|
v
BUY / SELL
But as the project evolved, each newly discovered problem introduced another responsibility.
The resulting architecture became closer to:
Market Data
|
v
Multi-Timeframe State
|
v
Analysis
|
v
Strategy
|
v
Risk
|
v
Trade Intent
|
v
Order Lifecycle
|
v
BrokerPort
|
+----------+----------+
| |
v v
Simulator Binance Spot
| |
+----------+----------+
|
v
Execution Reports
|
v
Execution Accounting
|
v
PostgreSQL
|
v
Recovery / Reconciliation
These components weren't introduced simply to make the system more sophisticated.
They appeared because the earlier design couldn't reliably answer increasingly difficult questions.
That distinction matters.
Complexity is not an achievement by itself.
Every architectural component should exist because it protects an important responsibility or invariant.
11. A Trading Bot Makes Decisions. A Trading Engine Owns Their Consequences.
I don't think there's a universally useful definition that separates every trading bot from every trading engine.
But for NERV Trade, the distinction became clear.
A trading bot primarily answers:
What should I trade?
A trading engine must also answer:
What happened?
What do I currently own?
Which strategy owns that exposure?
What has already been processed?
What remains uncertain?
Can I recover after a failure?
Does my internal state still agree with the broker?
These questions shaped the engineering direction of NERV Trade.
And they changed how I measured progress.
Early progress meant generating signals and improving analysis.
Later progress meant proving that:
- Duplicate executions don't duplicate exposure.
- Partial fills produce correct accounting.
- Failed transactions don't leave half-committed state.
- Uncertain submissions aren't blindly repeated.
- Restart recovery preserves lifecycle identity.
- Broker reconciliation doesn't invent new trades.
Those improvements aren't always visible on a trading chart.
But they're fundamental to building a system that can operate reliably.
12. Before Everything Else, We Needed Determinism
There's one more important part of this story.
Before implementing broker adapters, persistence, or restart recovery, NERV Trade needed to answer a more fundamental question.
Suppose we provide the same sequence of market data twice.
Should the engine produce the same analysis and trading decision?
It sounds obvious.
But timestamps, mutable state, event ordering, candle boundaries, and partially formed market data can introduce subtle differences.
If analysis isn't reproducible, debugging everything downstream becomes much harder.
A strategy might produce different decisions during replay.
A backtest might accidentally use information from the future.
And a failure might be impossible to reproduce.
That led to one of the earliest design principles behind NERV Trade:
Determinism.
Before worrying about whether the engine could reliably execute a decision, we needed confidence that it could reliably reproduce one.
And that's where the engineering journey really begins.
What's Next?
This article is Part 2 of the NERV Trade engineering series.
The series documents the real development journey behind building a Java trading engine, including architectural decisions, failures, testing, persistence, broker integration, and recovery.
Previous article:
I Built a Trading Engine in Java — Here's What It Took
Next article:
Deterministic Trading: Why the Same Market Data Must Produce the Same Decision
We'll explore why deterministic analysis matters, how reproducibility affects trading architecture, and why reliable market-data processing must come before execution.
About NERV Trade
NERV Trade is part of NERV — Next-Generation Engineering for Runtime Velocity, an open-source engineering initiative focused on building reusable Java and Spring-based systems.
The project explores:
- Deterministic market analysis
- Multi-timeframe trading
- Strategy and risk boundaries
- Trade lifecycle management
- Execution accounting
- PostgreSQL persistence
- Broker integration
- Recovery and reconciliation
This article is adapted from the original CzetsuyaTech publication:
A Trading Bot Is Easy. A Trading Engine Is Not.
This series focuses on software engineering and trading infrastructure, not financial advice or claims of trading profitability.

Top comments (0)