Live Trading·Trading Infrastructure·Intermediate

Multi Strategy Runner

Build a concurrent multi-strategy execution engine capable of running multiple independent trading strategies simultaneously with thread-safe position tracking, strategy-level capital allocation and risk budgeting, and per-strategy performance isolation for independent strategy assessment.

live-tradingtrading-strategies

Running Multiple Strategies Concurrently

1. Introduction: What is Concurrency?

In the context of algorithmic trading, "running multiple strategies concurrently" refers to executing several trading algorithms or components at the same time, rather than one after another. This is a crucial concept for modern trading systems aiming for efficiency, responsiveness, and the ability to capitalize on fleeting market opportunities.

Purpose

The primary purpose of concurrency in trading is to:

  • Improve Responsiveness: React to market events from multiple data sources or for multiple strategies without waiting for one to complete.
  • Increase Throughput: Process more data or execute more decision-making logic per unit of time.
  • Capitalize on Opportunities: Simultaneously monitor various market conditions and execute trades across different assets, exchanges, or strategies.
  • Resource Utilization: Efficiently use available CPU cores or manage I/O operations (like fetching data from APIs).

Importance

The importance of concurrent strategy execution stems from the high-speed and complex nature of financial markets. A system that can run multiple strategies in parallel can:

  1. Reduce Latency: Faster reaction times to market changes.
  2. Diversify Risk: Manage different strategies with varying risk profiles.
  3. Optimize Performance: Potentially achieve better overall returns by combining the strengths of multiple algorithms.
  4. Handle Volume: Process large volumes of market data and orders across different instruments.

This notebook will explore different approaches to achieving concurrency in Python, namely multithreading, multiprocessing, and asynchronous programming, demonstrating their practical application with simple trading strategy simulations.

2. Core Concepts: Concurrency vs. Parallelism

Before diving into implementation, it's essential to understand the distinction between concurrency and parallelism:

  • Concurrency: Deals with managing multiple tasks at the same time. It's about structuring a program such that it can handle many things at once. A single CPU core can achieve concurrency by rapidly switching between tasks, giving the illusion of simultaneous execution.

  • Parallelism: Deals with executing multiple tasks simultaneously. This typically requires multiple CPU cores or processors, where each core genuinely works on a different task at the same time.

In Python, due to the Global Interpreter Lock (GIL), true parallelism for CPU-bound tasks is limited with multithreading. For CPU-bound tasks, multiprocessing is usually preferred to leverage multiple cores, while multithreading is effective for I/O-bound tasks (where the program spends most of its time waiting for external resources like network requests or disk I/O).

Key Challenges in Concurrent Programming:

  • Shared State: Multiple strategies might need to access or modify the same data (e.g., account balance, open positions). Uncontrolled access can lead to incorrect results.
  • Race Conditions: Occur when the outcome of a program depends on the order of execution of operations in different concurrent tasks, leading to unpredictable behavior.
  • Deadlocks: A situation where two or more tasks are blocked indefinitely, waiting for each other to release a resource.
  • Synchronization: Mechanisms (like locks, semaphores, queues) needed to coordinate access to shared resources and prevent race conditions.

We will demonstrate how these concepts play out in the following sections.

3. Approaches to Concurrency in Python

Python offers several modules for concurrent programming. We will focus on the most common ones relevant to building trading systems:

  1. threading: For multithreading, suitable for I/O-bound tasks.
  2. multiprocessing: For multiprocessing, suitable for CPU-bound tasks, bypassing the GIL.
  3. asyncio: For asynchronous programming, suitable for highly I/O-bound and event-driven tasks.
[1]
import time
import random
import threading
import multiprocessing
import asyncio
import numpy as np
import matplotlib.pyplot as plt
from collections import deque

3.1 Multithreading

Multithreading involves running multiple threads within the same process. Threads share the same memory space, which makes communication between them easy but also introduces challenges with shared state. In Python, the Global Interpreter Lock (GIL) limits true parallelism for CPU-bound tasks, meaning only one thread can execute Python bytecode at a time. However, for I/O-bound tasks (where threads spend time waiting for external operations), the GIL is released, allowing other threads to run.

Practical Application: Simulating Trading Strategies (I/O-Bound)

Consider trading strategies that frequently fetch market data, send orders to an exchange, or log information – these are typically I/O-bound operations. Multithreading can be very effective here.

Below, we define a simple trading_strategy_thread function that simulates fetching data and placing an order with a random delay. We then run multiple instances of this strategy concurrently using threading.Thread.

[2]
shared_log = [] # A shared resource to demonstrate potential issues or synchronized access
shared_lock = threading.Lock() # A lock to protect shared_log

def trading_strategy_thread(strategy_id, duration_seconds):
    """
    Simulates a trading strategy that runs for a given duration.

    Inputs:
    - strategy_id (str): Unique identifier for the strategy.
    - duration_seconds (float): The total time this strategy should 'run' in seconds.

    Outputs:
    - None: Prints messages to console and appends to a shared log.
    """
    start_time = time.time()
    print(f"Strategy {strategy_id}: Started at {time.ctime(start_time)}")

    while time.time() - start_time < duration_seconds:
        # Simulate fetching data (I/O-bound operation)
        fetch_delay = random.uniform(0.01, 0.1)
        time.sleep(fetch_delay)

        # Simulate making a decision and placing an order (I/O-bound operation)
        order_delay = random.uniform(0.01, 0.05)
        time.sleep(order_delay)

        action = random.choice(['BUY', 'SELL', 'HOLD'])
        message = f"Strategy {strategy_id}: {action} at {time.ctime()} (Simulated delay: {fetch_delay+order_delay:.2f}s)"

        # Protect shared_log with a lock to prevent race conditions
        with shared_lock:
            shared_log.append(message)
            # print(message) # Uncomment to see frequent updates

    end_time = time.time()
    print(f"Strategy {strategy_id}: Finished at {time.ctime(end_time)}. Total time: {end_time - start_time:.2f}s")

# Demonstrate running multiple strategies using threads
print("\n--- Running multiple strategies with Threads ---")
num_strategies = 3
strategy_durations = [random.uniform(1.5, 3.0) for _ in range(num_strategies)]

threads = []
shared_log.clear() # Clear log for new run

start_concurrent_time = time.time()
for i in range(num_strategies):
    thread = threading.Thread(target=trading_strategy_thread, args=(f"Thread_{i+1}", strategy_durations[i]))
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join() # Wait for all threads to complete

end_concurrent_time = time.time()
print(f"All threaded strategies finished in: {end_concurrent_time - start_concurrent_time:.2f} seconds")

print("\n--- Excerpts from Shared Log (first 5 and last 5 entries) ---")
for entry in shared_log[:5]:
    print(entry)
print("...")
for entry in shared_log[-5:]:
    print(entry)

--- Running multiple strategies with Threads ---
Strategy Thread_1: Started at Wed Jun 10 10:07:07 2026
Strategy Thread_2: Started at Wed Jun 10 10:07:07 2026
Strategy Thread_3: Started at Wed Jun 10 10:07:07 2026
Strategy Thread_2: Finished at Wed Jun 10 10:07:09 2026. Total time: 2.22s
Strategy Thread_3: Finished at Wed Jun 10 10:07:09 2026. Total time: 2.39s
Strategy Thread_1: Finished at Wed Jun 10 10:07:09 2026. Total time: 2.69s
All threaded strategies finished in: 2.69 seconds

--- Excerpts from Shared Log (first 5 and last 5 entries) ---
Strategy Thread_2: BUY at Wed Jun 10 10:07:07 2026 (Simulated delay: 0.10s)
Strategy Thread_3: SELL at Wed Jun 10 10:07:07 2026 (Simulated delay: 0.12s)
Strategy Thread_1: HOLD at Wed Jun 10 10:07:07 2026 (Simulated delay: 0.13s)
Strategy Thread_2: SELL at Wed Jun 10 10:07:07 2026 (Simulated delay: 0.04s)
Strategy Thread_3: BUY at Wed Jun 10 10:07:07 2026 (Simulated delay: 0.06s)
...
Strategy Thread_1: BUY at Wed Jun 10 10:07:09 2026 (Simulated delay: 0.08s)
Strategy Thread_1: SELL at Wed Jun 10 10:07:09 2026 (Simulated delay: 0.06s)
Strategy Thread_1: BUY at Wed Jun 10 10:07:09 2026 (Simulated delay: 0.08s)
Strategy Thread_1: SELL at Wed Jun 10 10:07:09 2026 (Simulated delay: 0.05s)
Strategy Thread_1: BUY at Wed Jun 10 10:07:09 2026 (Simulated delay: 0.05s)

Explanation and Interpretation:

  • The output shows that the Started messages for all strategies appear almost simultaneously, and Finished messages appear in an interleaved fashion, demonstrating concurrent execution.
  • Each strategy runs for its duration_seconds independently.
  • The shared_log demonstrates how multiple threads can contribute to a shared resource. The threading.Lock() ensures that only one thread modifies shared_log at a time, preventing data corruption (a race condition). If you remove the with shared_lock: block, you might observe inconsistent or corrupted log entries in a more complex scenario.
  • Why it matters: This approach is excellent for scenarios where strategies spend a lot of time waiting for external data (e.g., API calls, database queries). While one thread waits, another can execute, making the overall system more efficient.

3.2 Multiprocessing

Multiprocessing involves running multiple processes, each with its own Python interpreter and memory space. This completely bypasses the GIL, allowing for true parallelism on multi-core systems. Communication between processes is more complex than between threads, typically requiring explicit inter-process communication (IPC) mechanisms like queues, pipes, or shared memory.

Practical Application: Simulating Trading Strategies (CPU-Bound)

Consider strategies that perform intensive mathematical calculations, complex optimization problems, or Monte Carlo simulations – these are typically CPU-bound. Multiprocessing is the ideal choice here.

We'll define a trading_strategy_process function that simulates a CPU-intensive calculation. We then run multiple instances concurrently using multiprocessing.Process.

[3]
def complex_calculation(strategy_id, iterations):
    """
    Simulates a CPU-intensive calculation.

    Inputs:
    - strategy_id (str): Unique identifier for the strategy.
    - iterations (int): Number of iterations for the calculation.

    Outputs:
    - result (float): The final calculated value.
    """
    result = 0
    for i in range(iterations):
        result += np.sin(i) * np.cos(i) * np.tan(i) # CPU-intensive operations
    return result

def trading_strategy_process(strategy_id, calculation_iterations, result_queue=None):
    """
    Simulates a trading strategy that performs a CPU-intensive calculation.

    Inputs:
    - strategy_id (str): Unique identifier for the strategy.
    - calculation_iterations (int): Number of iterations for the complex calculation.
    - result_queue (multiprocessing.Queue, optional): A queue to put the result into.

    Outputs:
    - None: Prints messages and optionally puts result into queue.
    """
    start_time = time.time()
    print(f"Strategy {strategy_id}: Started CPU-bound task at {time.ctime(start_time)}")

    # Perform CPU-intensive calculation
    final_result = complex_calculation(strategy_id, calculation_iterations)

    end_time = time.time()
    message = f"Strategy {strategy_id}: Finished CPU-bound task at {time.ctime(end_time)}. Total time: {end_time - start_time:.2f}s. Result: {final_result:.2f}"
    print(message)

    if result_queue:
        result_queue.put((strategy_id, final_result, end_time - start_time))

# Demonstrate running multiple strategies using processes
print("\n--- Running multiple strategies with Processes ---")
num_strategies_mp = 3
calculation_iterations = [random.randint(5_000_000, 10_000_000) for _ in range(num_strategies_mp)]

processes = []
result_queue = multiprocessing.Queue() # Queue for inter-process communication

start_concurrent_time_mp = time.time()
for i in range(num_strategies_mp):
    process = multiprocessing.Process(target=trading_strategy_process,
                                      args=(f"Process_{i+1}", calculation_iterations[i], result_queue))
    processes.append(process)
    process.start()

# Wait for all processes to complete
for process in processes:
    process.join()

end_concurrent_time_mp = time.time()
print(f"All processed strategies finished in: {end_concurrent_time_mp - start_concurrent_time_mp:.2f} seconds")

print("\n--- Results from Queue ---")
while not result_queue.empty():
    print(result_queue.get())

--- Running multiple strategies with Processes ---
Strategy Process_1: Started CPU-bound task at Wed Jun 10 10:07:10 2026
Strategy Process_2: Started CPU-bound task at Wed Jun 10 10:07:10 2026
Strategy Process_3: Started CPU-bound task at Wed Jun 10 10:07:10 2026
Strategy Process_3: Finished CPU-bound task at Wed Jun 10 10:08:45 2026. Total time: 95.32s. Result: 2897142.04
Strategy Process_1: Finished CPU-bound task at Wed Jun 10 10:08:45 2026. Total time: 95.83s. Result: 2817477.67
Strategy Process_2: Finished CPU-bound task at Wed Jun 10 10:08:53 2026. Total time: 103.25s. Result: 3559320.96
All processed strategies finished in: 103.32 seconds

--- Results from Queue ---
('Process_3', np.float64(2897142.0423151446), 95.32118773460388)
('Process_1', np.float64(2817477.6691653733), 95.82800269126892)
('Process_2', np.float64(3559320.959638267), 103.25441980361938)

Explanation and Interpretation:

  • Similar to threading, the Started messages appear concurrently, and Finished messages are interleaved. However, here each process runs on a separate CPU core (if available), achieving true parallelism for the CPU-bound complex_calculation.
  • The multiprocessing.Queue is used to safely collect results from different processes. Since processes have separate memory spaces, they cannot directly access each other's variables. IPC mechanisms are essential for sharing data.
  • Why it matters: This approach is critical for strategies that are computationally intensive, such as backtesting complex models, running machine learning predictions, or performing large-scale optimizations. It allows you to fully utilize all available CPU cores on your machine.

3.3 Asynchronous Programming with asyncio

asyncio is Python's library for writing concurrent code using the async/await syntax. It's particularly well-suited for I/O-bound and high-concurrency network applications, such as interacting with multiple trading APIs, websocket data streams, or performing many non-blocking HTTP requests. It uses a single thread and a single process, managing multiple tasks through an event loop.

Practical Application: High-Frequency Data Feeds / Multiple API Calls

Consider strategies that need to monitor multiple assets across different exchanges or receive real-time data from various sources. These are prime candidates for asyncio.

We'll define an async_trading_strategy that simulates fetching data (an I/O wait) and then processing it, without blocking the entire program.

[4]
async def async_trading_strategy(strategy_id, num_api_calls):
    """
    Simulates an asynchronous trading strategy making multiple API calls.

    Inputs:
    - strategy_id (str): Unique identifier for the strategy.
    - num_api_calls (int): Number of simulated asynchronous API calls.

    Outputs:
    - None: Prints messages to console.
    """
    start_time = time.time()
    print(f"Strategy {strategy_id}: Started async task at {time.ctime(start_time)}")

    for i in range(num_api_calls):
        # Simulate an asynchronous API call (I/O wait)
        api_call_delay = random.uniform(0.05, 0.2)
        await asyncio.sleep(api_call_delay) # Non-blocking wait

        current_time = time.time()
        print(f"  {strategy_id} API call {i+1} completed at {time.ctime(current_time)}")

    end_time = time.time()
    print(f"Strategy {strategy_id}: Finished async task at {time.ctime(end_time)}. Total time: {end_time - start_time:.2f}s")

async def main_async():
    print("\n--- Running multiple strategies with Asyncio ---")
    num_strategies_async = 3
    api_calls_per_strategy = [random.randint(3, 6) for _ in range(num_strategies_async)]

    tasks = []
    for i in range(num_strategies_async):
        task = async_trading_strategy(f"Async_{i+1}", api_calls_per_strategy[i])
        tasks.append(task)

    start_concurrent_time_async = time.time()
    await asyncio.gather(*tasks) # Run all tasks concurrently
    end_concurrent_time_async = time.time()

    print(f"All asyncio strategies finished in: {end_concurrent_time_async - start_concurrent_time_async:.2f} seconds")

# Run the main asynchronous function
await main_async()

--- Running multiple strategies with Asyncio ---
Strategy Async_1: Started async task at Wed Jun 10 10:08:53 2026
Strategy Async_2: Started async task at Wed Jun 10 10:08:53 2026
Strategy Async_3: Started async task at Wed Jun 10 10:08:53 2026
  Async_2 API call 1 completed at Wed Jun 10 10:08:53 2026
  Async_1 API call 1 completed at Wed Jun 10 10:08:53 2026
  Async_2 API call 2 completed at Wed Jun 10 10:08:53 2026
  Async_3 API call 1 completed at Wed Jun 10 10:08:53 2026
  Async_2 API call 3 completed at Wed Jun 10 10:08:53 2026
  Async_1 API call 2 completed at Wed Jun 10 10:08:53 2026
  Async_3 API call 2 completed at Wed Jun 10 10:08:53 2026
  Async_3 API call 3 completed at Wed Jun 10 10:08:53 2026
  Async_1 API call 3 completed at Wed Jun 10 10:08:53 2026
Strategy Async_1: Finished async task at Wed Jun 10 10:08:53 2026. Total time: 0.44s
  Async_2 API call 4 completed at Wed Jun 10 10:08:53 2026
  Async_3 API call 4 completed at Wed Jun 10 10:08:53 2026
  Async_2 API call 5 completed at Wed Jun 10 10:08:53 2026
Strategy Async_2: Finished async task at Wed Jun 10 10:08:53 2026. Total time: 0.57s
  Async_3 API call 5 completed at Wed Jun 10 10:08:53 2026
Strategy Async_3: Finished async task at Wed Jun 10 10:08:53 2026. Total time: 0.61s
All asyncio strategies finished in: 0.61 seconds

Explanation and Interpretation:

  • The output clearly shows the interleaved nature of asyncio. While one await asyncio.sleep() is happening (simulating an API call), the event loop switches to another task, allowing all strategies to make progress "simultaneously" even though it's still single-threaded.
  • The total execution time is significantly less than the sum of individual delays, as tasks don't block each other during I/O waits.
  • Why it matters: asyncio is extremely powerful for building highly concurrent I/O-bound applications. It's often used for real-time data processing, managing connections to multiple exchanges, and building responsive user interfaces or backend services that handle many concurrent requests.

4. Visualizations

To better understand the implications of different concurrency models, let's visualize two aspects: performance comparison and the interleaving of tasks.

4.1 Visualization 1: Performance Comparison (Sequential vs. Concurrent)

This visualization compares the total execution time of a set of tasks when run sequentially versus concurrently (using multithreading for I/O-bound tasks and multiprocessing for CPU-bound tasks). We expect to see significant speedups for concurrent execution, especially as the number of tasks increases.

[5]
def sequential_io_strategy(strategy_id, duration_seconds):
    start = time.time()
    time.sleep(duration_seconds) # Simulate I/O wait
    return time.time() - start

def sequential_cpu_strategy(strategy_id, iterations):
    start = time.time()
    complex_calculation(strategy_id, iterations) # Use the complex_calculation from before
    return time.time() - start

def run_benchmark(num_tasks, task_type, concurrency_model):
    total_time_taken = 0
    if task_type == 'io':
        task_durations = [random.uniform(0.1, 0.5) for _ in range(num_tasks)]
    else: # cpu
        task_durations = [random.randint(500_000, 1_000_000) for _ in range(num_tasks)] # iterations

    if concurrency_model == 'sequential':
        start_time = time.time()
        for i in range(num_tasks):
            if task_type == 'io':
                sequential_io_strategy(f"Seq_IO_{i+1}", task_durations[i])
            else:
                sequential_cpu_strategy(f"Seq_CPU_{i+1}", task_durations[i])
        total_time_taken = time.time() - start_time
    elif concurrency_model == 'threading' and task_type == 'io':
        threads = []
        start_time = time.time()
        for i in range(num_tasks):
            thread = threading.Thread(target=sequential_io_strategy, args=(f"Thread_IO_{i+1}", task_durations[i]))
            threads.append(thread)
            thread.start()
        for thread in threads:
            thread.join()
        total_time_taken = time.time() - start_time
    elif concurrency_model == 'multiprocessing' and task_type == 'cpu':
        processes = []
        queue = multiprocessing.Queue()
        start_time = time.time()
        for i in range(num_tasks):
            process = multiprocessing.Process(target=trading_strategy_process, args=(f"Process_CPU_{i+1}", task_durations[i], queue))
            processes.append(process)
            process.start()
        for process in processes:
            process.join()
        total_time_taken = time.time() - start_time
        while not queue.empty():
            queue.get() # Clear the queue
    else:
        return None # Invalid combination

    return total_time_taken

# --- Benchmark Configuration ---
num_tasks_range = [1, 5, 10, 20]

# I/O Bound Benchmark
io_sequential_times = []
io_threaded_times = []
for n in num_tasks_range:
    io_sequential_times.append(run_benchmark(n, 'io', 'sequential'))
    io_threaded_times.append(run_benchmark(n, 'io', 'threading'))

# CPU Bound Benchmark (Note: May take longer for higher num_tasks)
cpu_sequential_times = []
cpu_processed_times = []
for n in num_tasks_range:
    cpu_sequential_times.append(run_benchmark(n, 'cpu', 'sequential'))
    cpu_processed_times.append(run_benchmark(n, 'cpu', 'multiprocessing'))

# --- Plotting ---
fig, axes = plt.subplots(1, 2, figsize=(16, 6))

# I/O Bound Plot
axes[0].plot(num_tasks_range, io_sequential_times, marker='o', label='Sequential I/O')
axes[0].plot(num_tasks_range, io_threaded_times, marker='x', label='Threaded I/O')
axes[0].set_title('I/O-Bound Task Performance (Simulated)')
axes[0].set_xlabel('Number of Strategies/Tasks')
axes[0].set_ylabel('Total Execution Time (seconds)')
axes[0].legend()
axes[0].grid(True)

# CPU Bound Plot
axes[1].plot(num_tasks_range, cpu_sequential_times, marker='o', label='Sequential CPU')
axes[1].plot(num_tasks_range, cpu_processed_times, marker='x', label='Multiprocessed CPU')
axes[1].set_title('CPU-Bound Task Performance (Simulated)')
axes[1].set_xlabel('Number of Strategies/Tasks')
axes[1].set_ylabel('Total Execution Time (seconds)')
axes[1].legend()
axes[1].grid(True)

plt.tight_layout()
plt.show()
Strategy Process_CPU_1: Started CPU-bound task at Wed Jun 10 10:09:10 2026
Strategy Process_CPU_1: Finished CPU-bound task at Wed Jun 10 10:09:13 2026. Total time: 2.85s. Result: 309340.03
Strategy Process_CPU_1: Started CPU-bound task at Wed Jun 10 10:09:32 2026
Strategy Process_CPU_2: Started CPU-bound task at Wed Jun 10 10:09:32 2026Strategy Process_CPU_3: Started CPU-bound task at Wed Jun 10 10:09:32 2026

Strategy Process_CPU_4: Started CPU-bound task at Wed Jun 10 10:09:32 2026
Strategy Process_CPU_5: Started CPU-bound task at Wed Jun 10 10:09:32 2026
Strategy Process_CPU_5: Finished CPU-bound task at Wed Jun 10 10:09:49 2026. Total time: 16.94s. Result: 319983.97
Strategy Process_CPU_4: Finished CPU-bound task at Wed Jun 10 10:09:50 2026. Total time: 18.52s. Result: 349924.53
Strategy Process_CPU_2: Finished CPU-bound task at Wed Jun 10 10:09:51 2026. Total time: 18.91s. Result: 357063.30
Strategy Process_CPU_3: Finished CPU-bound task at Wed Jun 10 10:09:51 2026. Total time: 19.09s. Result: 393775.04
Strategy Process_CPU_1: Finished CPU-bound task at Wed Jun 10 10:09:52 2026. Total time: 19.98s. Result: 463372.37
Strategy Process_CPU_1: Started CPU-bound task at Wed Jun 10 10:10:31 2026
Strategy Process_CPU_2: Started CPU-bound task at Wed Jun 10 10:10:31 2026Strategy Process_CPU_3: Started CPU-bound task at Wed Jun 10 10:10:31 2026

Strategy Process_CPU_4: Started CPU-bound task at Wed Jun 10 10:10:31 2026Strategy Process_CPU_5: Started CPU-bound task at Wed Jun 10 10:10:31 2026

Strategy Process_CPU_6: Started CPU-bound task at Wed Jun 10 10:10:31 2026
Strategy Process_CPU_7: Started CPU-bound task at Wed Jun 10 10:10:31 2026
Strategy Process_CPU_8: Started CPU-bound task at Wed Jun 10 10:10:31 2026
Strategy Process_CPU_9: Started CPU-bound task at Wed Jun 10 10:10:31 2026
Strategy Process_CPU_10: Started CPU-bound task at Wed Jun 10 10:10:31 2026
Strategy Process_CPU_5: Finished CPU-bound task at Wed Jun 10 10:10:59 2026. Total time: 28.20s. Result: 254846.71
Strategy Process_CPU_8: Finished CPU-bound task at Wed Jun 10 10:10:59 2026. Total time: 28.24s. Result: 258695.27
Strategy Process_CPU_9: Finished CPU-bound task at Wed Jun 10 10:11:01 2026. Total time: 29.58s. Result: 269706.99
Strategy Process_CPU_6: Finished CPU-bound task at Wed Jun 10 10:11:04 2026. Total time: 33.16s. Result: 304059.02
Strategy Process_CPU_1: Finished CPU-bound task at Wed Jun 10 10:11:04 2026. Total time: 33.54s. Result: 302712.95
Strategy Process_CPU_4: Finished CPU-bound task at Wed Jun 10 10:11:06 2026. Total time: 35.54s. Result: 348189.71
Strategy Process_CPU_3: Finished CPU-bound task at Wed Jun 10 10:11:07 2026. Total time: 36.03s. Result: 371216.89
Strategy Process_CPU_2: Finished CPU-bound task at Wed Jun 10 10:11:08 2026. Total time: 36.77s. Result: 388684.05
Strategy Process_CPU_10: Finished CPU-bound task at Wed Jun 10 10:11:08 2026. Total time: 37.04s. Result: 417758.00
Strategy Process_CPU_7: Finished CPU-bound task at Wed Jun 10 10:11:09 2026. Total time: 38.03s. Result: 498616.46
Strategy Process_CPU_1: Started CPU-bound task at Wed Jun 10 10:12:26 2026
Strategy Process_CPU_3: Started CPU-bound task at Wed Jun 10 10:12:26 2026Strategy Process_CPU_2: Started CPU-bound task at Wed Jun 10 10:12:26 2026Strategy Process_CPU_4: Started CPU-bound task at Wed Jun 10 10:12:26 2026


Strategy Process_CPU_5: Started CPU-bound task at Wed Jun 10 10:12:26 2026
Strategy Process_CPU_6: Started CPU-bound task at Wed Jun 10 10:12:26 2026
Strategy Process_CPU_7: Started CPU-bound task at Wed Jun 10 10:12:26 2026
Strategy Process_CPU_8: Started CPU-bound task at Wed Jun 10 10:12:26 2026
Strategy Process_CPU_9: Started CPU-bound task at Wed Jun 10 10:12:26 2026Strategy Process_CPU_10: Started CPU-bound task at Wed Jun 10 10:12:26 2026

Strategy Process_CPU_11: Started CPU-bound task at Wed Jun 10 10:12:26 2026Strategy Process_CPU_12: Started CPU-bound task at Wed Jun 10 10:12:26 2026

Strategy Process_CPU_13: Started CPU-bound task at Wed Jun 10 10:12:26 2026
Strategy Process_CPU_14: Started CPU-bound task at Wed Jun 10 10:12:26 2026
Strategy Process_CPU_15: Started CPU-bound task at Wed Jun 10 10:12:26 2026
Strategy Process_CPU_16: Started CPU-bound task at Wed Jun 10 10:12:26 2026
Strategy Process_CPU_17: Started CPU-bound task at Wed Jun 10 10:12:26 2026
Strategy Process_CPU_18: Started CPU-bound task at Wed Jun 10 10:12:26 2026Strategy Process_CPU_19: Started CPU-bound task at Wed Jun 10 10:12:26 2026

Strategy Process_CPU_20: Started CPU-bound task at Wed Jun 10 10:12:27 2026
Strategy Process_CPU_15: Finished CPU-bound task at Wed Jun 10 10:13:24 2026. Total time: 58.03s. Result: 253413.47
Strategy Process_CPU_4: Finished CPU-bound task at Wed Jun 10 10:13:27 2026. Total time: 60.94s. Result: 275662.18
Strategy Process_CPU_18: Finished CPU-bound task at Wed Jun 10 10:13:28 2026. Total time: 61.18s. Result: 277642.33
Strategy Process_CPU_9: Finished CPU-bound task at Wed Jun 10 10:13:33 2026. Total time: 67.35s. Result: 303190.46
Strategy Process_CPU_20: Finished CPU-bound task at Wed Jun 10 10:13:39 2026. Total time: 72.11s. Result: 331393.24
Strategy Process_CPU_16: Finished CPU-bound task at Wed Jun 10 10:13:40 2026. Total time: 73.69s. Result: 345135.45
Strategy Process_CPU_7: Finished CPU-bound task at Wed Jun 10 10:13:50 2026. Total time: 84.24s. Result: 405026.68
Strategy Process_CPU_17: Finished CPU-bound task at Wed Jun 10 10:13:51 2026. Total time: 84.86s. Result: 408642.65
Strategy Process_CPU_6: Finished CPU-bound task at Wed Jun 10 10:13:52 2026. Total time: 85.93s. Result: 417991.24
Strategy Process_CPU_1: Finished CPU-bound task at Wed Jun 10 10:13:53 2026. Total time: 86.94s. Result: 436894.28
Strategy Process_CPU_3: Finished CPU-bound task at Wed Jun 10 10:13:53 2026. Total time: 87.03s. Result: 429385.67
Strategy Process_CPU_10: Finished CPU-bound task at Wed Jun 10 10:13:54 2026. Total time: 87.94s. Result: 446041.52
Strategy Process_CPU_13: Finished CPU-bound task at Wed Jun 10 10:13:54 2026. Total time: 87.91s. Result: 439130.02
Strategy Process_CPU_14: Finished CPU-bound task at Wed Jun 10 10:13:54 2026. Total time: 88.10s. Result: 438944.41
Strategy Process_CPU_11: Finished CPU-bound task at Wed Jun 10 10:13:55 2026. Total time: 89.33s. Result: 467524.75
Strategy Process_CPU_8: Finished CPU-bound task at Wed Jun 10 10:13:55 2026. Total time: 89.54s. Result: 472544.49
Strategy Process_CPU_12: Finished CPU-bound task at Wed Jun 10 10:13:56 2026. Total time: 89.84s. Result: 470459.79
Strategy Process_CPU_5: Finished CPU-bound task at Wed Jun 10 10:13:56 2026. Total time: 90.14s. Result: 485313.23
Strategy Process_CPU_2: Finished CPU-bound task at Wed Jun 10 10:13:56 2026. Total time: 90.22s. Result: 489752.46
Strategy Process_CPU_19: Finished CPU-bound task at Wed Jun 10 10:13:56 2026. Total time: 89.60s. Result: 474507.53
cell output

Interpretation of Visualization 1:

  • I/O-Bound Tasks (Left Plot): You should observe that the "Threaded I/O" line is significantly flatter and lower than the "Sequential I/O" line, especially as the number of tasks increases. This demonstrates the efficiency of multithreading for I/O-bound operations; while one thread waits, others can proceed, leading to a much shorter total execution time.
  • CPU-Bound Tasks (Right Plot): Similarly, the "Multiprocessed CPU" line should show better performance (flatter/lower) compared to "Sequential CPU". This highlights how multiprocessing leverages multiple CPU cores to truly run computations in parallel, overcoming the GIL limitation for CPU-intensive workloads.

This visualization clearly illustrates the performance benefits of choosing the right concurrency model for the right type of task.

4.2 Visualization 2: Task Interleaving (Timeline)

This visualization aims to show when each task starts and ends, helping to visually distinguish between sequential execution and the interleaved execution of concurrent tasks. We'll simulate tasks with varying durations and record their start and end times relative to a common benchmark start time.

[6]
def record_task_execution(strategy_id, duration_seconds, timeline_data):
    start = time.time()
    time.sleep(duration_seconds)
    end = time.time()
    timeline_data.append({'id': strategy_id, 'start': start, 'end': end})

def run_sequential_timeline(num_tasks):
    timeline_data = []
    base_time = time.time()
    for i in range(num_tasks):
        duration = random.uniform(0.1, 0.5)
        record_task_execution(f"Seq_{i+1}", duration, timeline_data)

    # Adjust times relative to base_time for plotting
    for item in timeline_data:
        item['start'] -= base_time
        item['end'] -= base_time
    return timeline_data

def run_threaded_timeline(num_tasks):
    timeline_data = deque() # Use deque for thread-safe appends
    threads = []
    base_time = time.time()
    for i in range(num_tasks):
        duration = random.uniform(0.1, 0.5)
        thread = threading.Thread(target=record_task_execution, args=(f"Thread_{i+1}", duration, timeline_data))
        threads.append(thread)
        thread.start()
    for thread in threads:
        thread.join()

    # Convert deque to list and adjust times
    timeline_list = list(timeline_data)
    for item in timeline_list:
        item['start'] -= base_time
        item['end'] -= base_time
    return timeline_list

# --- Generate Timeline Data ---
num_tasks_timeline = 5

sequential_timeline = run_sequential_timeline(num_tasks_timeline)
threaded_timeline = run_threaded_timeline(num_tasks_timeline)

# --- Plotting ---
fig, axes = plt.subplots(2, 1, figsize=(12, 8), sharex=True)

def plot_timeline(ax, data, title):
    for i, task in enumerate(data):
        ax.barh(task['id'], task['end'] - task['start'], left=task['start'], height=0.6, color=plt.cm.viridis(i / len(data)))
    ax.set_title(title)
    ax.set_xlabel('Time (seconds)')
    ax.set_ylabel('Strategy ID')
    ax.grid(axis='x', linestyle='--', alpha=0.7)

plot_timeline(axes[0], sequential_timeline, 'Sequential Task Execution Timeline')
plot_timeline(axes[1], threaded_timeline, 'Threaded Task Execution Timeline (Interleaved)')

plt.tight_layout()
plt.show()
cell output

Interpretation of Visualization 2:

  • Sequential Timeline (Top Plot): You will see tasks arranged end-to-end. Each task begins only after the previous one has fully completed. The total time taken is the sum of all individual task durations.
  • Threaded Timeline (Bottom Plot): Here, the tasks will appear to overlap significantly. Multiple bars will start around the same time, and their execution (represented by the bars) will be interleaved. The total duration from the start of the first task to the end of the last task will be much shorter than the sequential execution, demonstrating the power of concurrency in reducing overall wall-clock time.

This visualization vividly illustrates the difference in execution patterns, showing how concurrent tasks make progress together rather than waiting in line.

5. Conclusion: Choosing the Right Model

Running multiple strategies concurrently is a powerful technique for building high-performance algorithmic trading systems. The choice of concurrency model in Python largely depends on the nature of your tasks:

  • Multithreading (threading): Best for I/O-bound tasks (e.g., fetching market data, sending orders, logging) where threads spend most of their time waiting for external operations. It's efficient due to low overhead and shared memory, but limited by the GIL for CPU-bound tasks.

  • Multiprocessing (multiprocessing): Best for CPU-bound tasks (e.g., complex calculations, backtesting, simulations) that can truly benefit from multiple CPU cores. It bypasses the GIL by creating separate processes, but has higher overhead and requires explicit IPC for data sharing.

  • Asynchronous Programming (asyncio): Excellent for highly I/O-bound and event-driven tasks (e.g., handling multiple real-time data feeds, managing many simultaneous API connections) in a single-threaded, non-blocking manner. It provides high concurrency with minimal resource overhead, but requires careful structuring of async/await code.

Best Practices:

  • Identify Task Type: Determine if your strategies are I/O-bound or CPU-bound.
  • Manage Shared State: Always use appropriate synchronization primitives (locks, queues) when multiple concurrent units access shared data to prevent race conditions.
  • Error Handling: Implement robust error handling and logging for concurrent tasks, as debugging can be more complex.
  • Start Simple: Begin with the simplest model that meets your needs and only introduce more complex solutions (like distributed systems) when necessary.

By understanding and appropriately applying these concurrency models, you can build robust, efficient, and highly responsive algorithmic trading systems capable of managing multiple strategies effectively.