Hello! Welcome to the final module of our course.
In our last lesson, we built a crucial piece of our system's resilience: the position synchronizer. We established that at startup, our framework must reconcile its internal state with the broker's "ground truth" before any trading begins. This ensures we always operate from a correct and verified position state.
With all our core components now designed—data handling, strategy logic, and execution management—we face the final challenge: making them all work together. This lesson marks the beginning of our final module on system orchestration. Our learning outcome is to implement the main event loop to drive the flow of data, strategy evaluation, and execution. This loop is the heart of our trading system, pumping events between components and bringing our framework to life.
1. The System's Heartbeat: Event-Driven Architecture
At its core, a real-time trading system is reactive. It doesn't follow a simple, linear script from start to finish. Instead, it responds to a continuous stream of unpredictable events: a market price update, a signal from a strategy, or a confirmation that an order has been filled.
The most effective way to model this is with an Event-Driven Architecture (EDA). In this design, the flow of the program is determined by events.
The basic components are:
- Event Producers: Components that generate events (e.g., our
DataHandlerlistening to the broker's market data feed). - Event Queue: A central channel where all events are placed in the order they occur.
- Event Loop: A continuous process that takes events from the queue one by one.
- Event Consumers/Handlers: Components that are tasked with processing specific types of events (e.g., a
Strategyclass processes aMarketDataEvent, theExecutionHandlerprocesses aSignalEvent).
This diagram provides a clear visual of the process.

Event-Driven Architecture in Python for Trading
To get a more formal understanding of these components in a trading context, the article 'Event-Driven Architecture in Python for Trading' provides a solid introduction.
Please read the section 'Understanding Event-Driven Architecture', including the 'Key Components of EDA'. This will formalize the concepts we just discussed and place them firmly in a financial context.
2. The Challenge: Handling Concurrent Operations
Our trading system needs to do several things at once:
- Listen for a continuous stream of market data.
- Listen for order status updates from the broker.
- Continuously evaluate strategy logic.
- Potentially listen for user commands (e.g., to shut down).
These are all I/O-bound operations—they spend most of their time waiting for data to arrive from an external source (like a network socket). Trying to manage this with traditional, synchronous code would be incredibly inefficient. One task would block all others while it waits.
This is where Python's asyncio library becomes essential. It allows us to manage many I/O-bound tasks concurrently within a single thread, using a concept that should now sound very familiar: an event loop.
Asyncio Finally Explained: What the Event Loop Really Does
The video 'Asyncio Finally Explained' by ArjanCodes offers an excellent conceptual overview of what an event loop does and why it's powerful for I/O-bound tasks.
Please watch the first segment (00:57 - 02:37) for a clear definition of I/O-bound operations and a visual of how the event loop manages tasks. Then, watch the second segment (07:55 - 09:06) which explains why asyncio is often superior to traditional threading in Python for these kinds of tasks due to the Global Interpreter Lock (GIL).
3. Asynchronous Python: async, await, and Tasks
asyncio gives us special syntax to tell the event loop when a task can be paused to let another one run.
async def: Defines a function as a coroutine. When you call a coroutine, it doesn't execute immediately. Instead, it returns a coroutine object that can be run by the event loop.await: This keyword is used inside a coroutine. It tells the event loop, "I'm about to do something that involves waiting (like a network request). You can pause me here and go run another task. Wake me up when my result is ready."asyncio.Task: This is an object that manages the execution of a coroutine within the event loop. You create tasks to run multiple coroutines "in the background" concurrently.asyncio.run(coroutine): This is the main entry point. It starts the event loop, runs the given main coroutine until it's complete, and then shuts the loop down.
Asyncio in Python - Full Tutorial
For a practical look at how to use these keywords, the 'Asyncio in Python - Full Tutorial' by Tech With Tim provides clear, code-based examples.
First, watch the segment on coroutines (03:32 - 06:25). Focus on the difference between calling a regular function and a coroutine, and how asyncio.run() is used to start the process. Then, watch the segment on Tasks (10:25 - 13:17) to see how asyncio.create_task allows multiple coroutines to run concurrently, drastically reducing total execution time for waiting operations.
4. Implementing the Main Event Loop
Now, let's sketch out the structure of our main loop. We'll use an asyncio.Queue, which is a thread-safe and coroutine-aware queue, perfect for our event-driven design.
Our main loop will be an async function that runs forever, pulling events from this queue and dispatching them.
import asyncio
import queue # For placeholder events
# Assume we have defined Event classes from previous lessons
# e.g., MarketDataEvent, SignalEvent, OrderFillEvent
async def run_event_loop(event_queue: asyncio.Queue, data_handler, strategy, execution_handler):
"""
The main event loop of the trading system.
"""
print("Event loop started. Waiting for events...")
while True:
try:
# The 'await' here pauses the loop until an item is available in the queue.
# This is highly efficient; it consumes no CPU while waiting.
event = await event_queue.get()
except asyncio.CancelledError:
print("Event loop cancelled.")
break
print(f"New Event: {type(event).__name__}")
# --- Event Dispatching Logic ---
if event.type == 'MARKET':
# A MarketDataEvent has arrived. Pass it to the strategy for evaluation.
strategy.on_market_data(event)
elif event.type == 'SIGNAL':
# The strategy has generated a SignalEvent. Pass it to the execution handler.
execution_handler.on_signal(event)
elif event.type == 'FILL':
# The execution handler confirmed a fill. Update portfolio/strategy state.
strategy.on_fill(event)
# A more advanced version would have a dedicated PortfolioManager.
elif event.type == 'ORDER_STATUS':
# The execution handler received an order status update.
strategy.on_order_status(event)
# Mark the task as done. Necessary for proper queue management.
event_queue.task_done()
Test your understanding!
Imagine our DataHandler runs in a separate concurrent task. It receives a tick for SPY, creates a MarketDataEvent, and puts it on the event_queue. A moment later, our ExecutionHandler (also in a separate task) receives a fill confirmation for a different trade, creates an OrderFillEvent, and puts it on the queue.
What will the run_event_loop function do, and in what order?
Show answer
The asyncio.Queue ensures First-In, First-Out (FIFO) processing.
- The
await event_queue.get()call will first return theMarketDataEvent. - The
if event.type == 'MARKET'block will be executed, and the event will be passed to thestrategy.on_market_data()method. - The loop will complete and immediately call
await event_queue.get()again. - This time, it will retrieve the
OrderFillEvent. - The
elif event.type == 'FILL'block will be executed, and thestrategy.on_fill()method will be called.
The asyncio structure ensures that even though the events were generated concurrently, they are processed sequentially and in the correct order by the main loop, preventing race conditions.
5. Orchestrating the Components
The final step is to create a main entry point for our application that initializes all the components and starts their respective concurrent tasks. We'll use asyncio.TaskGroup (available in Python 3.11+) as it provides better error handling and resource management than the older asyncio.gather.
import asyncio
# --- Assume these are our component classes from previous lessons ---
# from data import DataHandler
# from strategy import MyStrategy
# from execution import ExecutionHandler
# from utils import load_config
async def main():
"""
Main entry point to initialize and run the trading system.
"""
# 1. Create the central event queue
event_queue = asyncio.Queue()
# 2. Load configuration
# config = load_config('config.yaml')
# 3. Initialize components, passing the queue to producers and consumers
# data_handler = DataHandler(event_queue, ...)
# strategy = MyStrategy(event_queue, ...)
# execution_handler = ExecutionHandler(event_queue, ...)
print("--- Starting Trading System ---")
try:
# 4. Use a TaskGroup to manage concurrent tasks
async with asyncio.TaskGroup() as tg:
# Task 1: The main event processing loop
tg.create_task(
run_event_loop(event_queue, data_handler, strategy, execution_handler)
)
# Task 2: The data handler's connection and streaming loop
tg.create_task(
data_handler.run() # Assume the data_handler has a 'run' method that connects and streams data
)
# Task 3: The execution handler's connection loop
tg.create_task(
execution_handler.run() # Assume the execution handler also needs a persistent connection
)
# The 'async with' block will wait here until all tasks within the group are complete.
except* Exception as e:
print(f"An error occurred: {e.errors}")
finally:
print("--- Trading System Shutting Down ---")
# Perform cleanup (e.g., close connections)
# await execution_handler.disconnect()
# await data_handler.disconnect()
if __name__ == "__main__":
# 5. Start the asyncio event loop
# For a real application, you'd handle KeyboardInterrupt (Ctrl+C) gracefully here
# to cancel the tasks and shut down cleanly.
try:
asyncio.run(main())
except KeyboardInterrupt:
print("System shutdown requested by user.")
Note: The except* syntax is for handling ExceptionGroups raised by TaskGroup.
This structure provides a robust, concurrent, and scalable foundation. Each component is encapsulated and communicates through the central event queue, orchestrated by the main asyncio event loop.
Conclusion
Today, we've designed the very heart of our trading system. We've moved from a collection of independent components to a fully orchestrated application driven by a powerful, asynchronous event loop.
Key Takeaways:
- A trading system is best designed using an Event-Driven Architecture (EDA), where components communicate via an event queue.
asynciois the ideal Python library for this task, as it efficiently handles multiple I/O-bound operations (like network data streams) concurrently.- The core of the system is a main
async defloop that continuously pulls events from anasyncio.Queueand dispatches them to the appropriate handlers. - We use
asyncio.TaskGrouporasyncio.gatherto launch and manage all the system's long-running processes (the event loop, data connections, etc.) concurrently.
Preview of the Next Lesson:
Our main() function is doing a lot of work: initialization, task creation, and orchestration. To make our framework cleaner and more reusable, we need to encapsulate this logic. In the next lesson, we will create a main Framework class to initialize and connect all components based on the configuration file, providing a single, clean entry point to start our entire system.