Run
Overview¶
The core of the system is the run() function, which connects all components in a streaming event loop.
The run is used for every stage in the 4 stages, from back-testing to live-trading.
Each of these five components in the run has its own responsibilities:
| Component | Responsibility |
|---|---|
| Feed | Provides (market) data events |
| Strategy | Generates trading signals from events |
| Trader | Converts signals into orders (risk/sizing) |
| Broker | Executes orders, maintains account state |
| Journal | Logs/tracks every step (read-only) |
Each component is implemented an Abstract Base Class with pluggable implementations, making every part of the pipeline independently swappable.
Step-by-step¶
It is helpful to understand some of the details of the run loop.
for event in feed.play(...):
account = broker.sync(event) # 1 syncs and updates the account
signals = strategy.create_signals(event) # 2 generate signals from market data
orders = trader.create_orders(signals, ...) # 3 apply risk rules, create orders
broker.place_orders(orders) # 4 place orders at broker
journal.track(...) # 5 record metrics (optional)broker.sync(event)— Sync with the state of the underlying broker. Returns an updated Account object which reflects the latest state and market data. Orders and positions that are closed, are not included in the returned account object.strategy.create_signals(event)— The strategy examines the event’s price data and returns a list of Signal objects. Each signal has an asset, a rating (typically -1.0 to 1.0), and a type (ENTRY,EXIT, orENTRY_EXIT). Strategies are pure decision-makers — they know nothing about cash, positions, or risk.trader.create_orders(signals, event, account)— The trader applies risk management rules (position sizing, shorting constraints, order limits) and converts signals into concreteOrderobjects. Unlike strategies, traders have full access to the Account (cash, positions, buying power).broker.place_orders(orders)— New orders are submitted to the broker. In SimBroker, they are stored and only evaluated for execution when the next event arrives.journal.track(...)— Optional logging and metrics collection. Journals are passive observers that never modify state.
The run function also has many defaults for its parameters in case they are not provided:
if no broker is provider, the SimBroker is used
if no trader is provided, the SimpleTrader is used
if no journal is provided, this step is skipped all together
if None is provided as a strategy, this step is skipped and the trader is provided with an empty list of signals
Basic Backtest¶
A simple back test that iterates over all the historic data in the feed, just requires a few lines of code.
import roboquant as rq
feed = rq.feeds.YahooFeed("JPM", "IBM", start_date="2015-01-01")
strategy = rq.strategies.EMACrossover()
account = rq.run(feed, strategy)
print(account)buying power : 2,018,593@USD
cash : 2,018,593@USD
equity : 2,018,593@USD
positions : none
trades : 188
mkt value : 0@USD
orders : none
last update : 2026-09-30 04:00:00+00:00
This works because run() provides sensible defaults: SimBroker (USD 1M deposit, 0% slippage) and SimpleTrader.
Custom Backtest¶
The following snippets shows how to override many of the default settings.
from roboquant import USD
feed = rq.feeds.YahooFeed("AAPL", "MSFT", start_date="2020-01-01")
strategy = rq.strategies.EMACrossover()
trader = rq.traders.FlexTrader(shorting=True)
broker = rq.brokers.SimBroker(deposit=500_000@USD)
journal = rq.journals.MetricsJournal()
account = rq.run(feed, strategy, trader=trader, broker=broker, journal=journal)Walk Forward¶
Walk-forward analysis is a backtesting technique that mimics real-world trading by splitting historical data into successive periods. Below is very simple example of a walk forward that provides insights into the performance in different timeframes.
timeframes = feed.timeframe().split(5)
for timeframe in timeframes:
strategy = rq.strategies.EMACrossover(13, 26)
account = rq.run(feed, strategy, timeframe=timeframe)
print(f"{timeframe.strftime('%Y-%m-%d')} equity={account.equity():.0f}")[2020-01-02 ― 2021-05-08> equity=1359561@USD
[2021-05-08 ― 2022-09-13> equity=972719@USD
[2022-09-13 ― 2024-01-18> equity=1286717@USD
[2024-01-18 ― 2025-05-25> equity=884136@USD
[2025-05-25 ― 2026-09-30] equity=1122342@USD
Often a walk forward is used in combination with hyperparameter tuning. This is known as Walk-Forward Optimization (WFO). The idea is:
Split the historical data into a sequence of time windows (e.g. 5 periods).
For each window, use the current window as the in-sample (training) period to find the best parameters.
Test those parameters on the next window, the out-of-sample (testing) period.
Move forward one window and repeat.
This approach helps detect overfitting: if a parameter set performs well in-sample but poorly out-of-sample, the strategy likely doesn’t generalize. By measuring performance only on unseen data, you get a more realistic estimate of how the strategy would have performed in production.
In practice, you might also track metrics across all out-of-sample periods (e.g. average Sharpe ratio, win rate) rather than just the final equity value, giving a fuller picture of robustness.
from collections import namedtuple
Best = namedtuple("Best", "equity param")
timeframes = feed.timeframe().split(5)
params = [(3,5), (13,26), (20, 31)]
for idx in range(len(timeframes) - 1):
best = Best(-1_000_000.0, None)
# Find the best parameter
for param in params:
strategy = rq.strategies.EMACrossover(*param)
account = rq.run(feed, strategy, timeframe=timeframes[idx])
equity = account.equity_value()
if equity > best.equity:
best = Best(equity, param)
# Validate
strategy = rq.strategies.EMACrossover(*best.param)
account = rq.run(feed, strategy, timeframe=timeframes[idx+1])
equity = account.equity_value()
print(f"param={best.param} training={best.equity:,.0f} testing={equity:,.0f}")param=(20, 31) training=1,423,592 testing=897,870
param=(3, 5) training=1,196,387 testing=1,119,791
param=(20, 31) training=1,292,065 testing=1,007,585
param=(3, 5) training=1,067,922 testing=1,314,897
Multi-run¶
A Multi-run samples a number of random timeframes and then runs a back test on each of them.
timeframes = feed.timeframe().sample(100, "365 days")
equities = []
for timeframe in timeframes:
strategy = rq.strategies.EMACrossover(13, 26)
account = rq.run(feed, strategy, timeframe=timeframe)
equities.append(account.equity()[USD])
print(f"min={min(equities):.0f}, max={max(equities):.0f}")min=863494, max=1373082
Live and paper-trade run¶
A live or paper-trade run is only different from a back test in the implementation of two of the components selected.
Instead of the SimBroker a real broker is selected and instead of a historic data feed, a live datafeed is used.
import os
from dotenv import load_dotenv
from roboquant.third_party.alpaca import AlpacaLiveFeed, AlpacaBroker
load_dotenv()
# Setup the real broker
api_key = os.environ["ALPACA_API_KEY"]
secret_key = os.environ["ALPACA_SECRET"]
broker = AlpacaBroker(api_key, secret_key)
# Setup the live feed
alpaca_feed = AlpacaLiveFeed(api_key, secret_key, market="iex")
symbols = ["TSLA", "MSFT", "NVDA", "AMD", "AAPL"]
alpaca_feed.subscribe_trades(*symbols)
# Run a strategy
strategy = rq.strategies.EMACrossover(13, 26)
timeframe = rq.Timeframe.next("120 min")
account = rq.run(feed, strategy, broker=broker, timeframe=timeframe)Early stopping¶
It is possible to stop a run before all the events are handled. You do so
by having one of the components call the stop_run() function.
For example a custom Journal could track some metrics and based on the results
invoke the stop_run() function. See also journal
Under the hood it will throw a special type of exception that is handled gracefully within the run loop.