In the previous article we covered the application architecture. In this article we will go through the application’s logic at a high level.
There are different ways to build a backtesting application. I opted for an event-driven pattern because it conceptually mirrors how a live trading system would handle things. The trouble of going with this approach pays dividends later down the line when it comes to adapting it to a live trading.
An event-driven system is in essence composed of parts that communicate with each other by sending events to a message broker/bus. The message broker is the central part in the system and orchestrates and regulates the flow of events within the system. The parts on the other hand, consume events from the message broker according to whatever business logic.
Once the logic is in place, the next important consideration is what to consider an event, that is, what to capture and what to ignore. In the context of backtesting we’re mainly focused on capturing market-related events. This reduces the universe of data to only those that are relevant to buying and selling the financial instruments we’re concerned about.
With that preamble out the way, when all of the above have been taken care of, this is what you should have in terms of driver code (Rust pseudo-code-ish):
fn backester() {
// Engine Components
//
// Message broker
let (transmitter, events_queue) = mpsc::channel();
// Data layer
let data_handler = DataHandler::new();
// Strategy abstraction
let buy_hold_strat = BuyHoldStrategy::new();
// Financial Broker (execution layer)
let broker = Broker::new();
// Portfolio abstraction
let portfolio = Portfolio::new();
// Control loop
loop {
// handle interaction with the outside world here, e.g. market feed, etc.
// and control the outer loop termination condition
if data_handler.market_feed_not_empty() {
data_handler.push_market_event_to_queue()
}
else {
// break or return
break
}
// Inner loop
while events_queue.not_empty() {
match events_queue.get() {
MarketEvent => {
// handle market event, e.g. new data bar from the market feed
buy_hold_strat.parse_market_event(MarketEvent)
// .parse_market_event() could then decide to generate an event
// to open/close a trade
}
SignalEvent => {
portfolio.parse_signal(SignalEvent)
// .parse_signal() could then decide to generate an event to
// instruct the broker to execute an order
}
OrderEvent => {
broker.execute_order(OrderEvent)
// .execute_order() can execute the order based on whatever logic,
// e.g. Market Order, Limit Order, Quantity, etc.
}
FillEvent => {
portfolio.handle_fill(FillEvent)
// .handle_fill() could perform some bookkeeping after the order has
// been filled, or even modify it, if for example there's been an unexpected
// outcome like a partial fill, cancellation, etc.
}
}
}
}
}
The setup above gives you the flexibility and modularity to swap parts in and out as desired, while still simulating the flow of events in a live environment, i.e. in drip-feed.
It is more trouble to set up than a vectorised equivalent, but is truer to the real thing given that events in the real world are not known before hand, unless you’re insider trading of course. But. . .

And that’s the core application logic covered.
In the next article we will cover the parts of the engine in a bit more detail and discuss, where appropriate, changes needed to make it work in a live trading environment.
Feel free to share the article on socials: