How-To Guides#
This page contains short, focused recipes for common tasks in QF-Lib. Each section answers one specific question with a minimal, runnable code example.
How to use stop losses#
Stop losses automatically close a position when it moves against you by a configurable amount. They
are built into AlphaModelStrategy through
the use_stop_losses=True flag and the fraction_at_risk field of each
AlphaModel.
How stop losses are calculated#
When use_stop_losses=True, the strategy computes a stop-loss price for every open position:
Long position:
stop_price = entry_price * (1 - fraction_at_risk)Short position:
stop_price = entry_price * (1 + fraction_at_risk)
The fraction_at_risk is a property of the alpha model - it is typically estimated from recent
Average True Range (ATR) normalised by price. The risk_estimation_factor parameter on the alpha
model scales this estimate.
Enabling stop losses#
import matplotlib.pyplot as plt
plt.ion()
from qf_lib.backtesting.events.time_event.regular_time_event.calculate_and_place_orders_event import \
CalculateAndPlaceOrdersRegularEvent
from qf_lib.backtesting.position_sizer.initial_risk_position_sizer import InitialRiskPositionSizer
from qf_lib.backtesting.strategies.alpha_model_strategy import AlphaModelStrategy
from qf_lib.backtesting.trading_session.backtest_trading_session_builder import BacktestTradingSessionBuilder
from qf_lib.common.enums.frequency import Frequency
from qf_lib.common.utils.dateutils.string_to_date import str_to_date
from qf_lib.documents_utils.document_exporting.pdf_exporter import PDFExporter
from qf_lib.documents_utils.excel.excel_exporter import ExcelExporter
from demo_scripts.backtester.moving_average_alpha_model import MovingAverageAlphaModel
from demo_scripts.common.utils.dummy_ticker import DummyTicker
from demo_scripts.demo_configuration.demo_data_provider import daily_data_provider
from demo_scripts.demo_configuration.demo_settings import get_demo_settings
def main():
start_date = str_to_date('2016-01-01')
end_date = str_to_date('2017-12-31')
settings = get_demo_settings()
pdf_exporter = PDFExporter(settings)
excel_exporter = ExcelExporter(settings)
session_builder = BacktestTradingSessionBuilder(settings, pdf_exporter, excel_exporter)
session_builder.set_frequency(Frequency.DAILY)
session_builder.set_data_provider(daily_data_provider)
session_builder.set_backtest_name("Stop Loss Demo")
# Use InitialRiskPositionSizer - sizes positions so that the stop loss means
# at most 3% of portfolio is at risk per trade
session_builder.set_position_sizer(InitialRiskPositionSizer, initial_risk=0.03)
ts = session_builder.build(start_date, end_date)
model = MovingAverageAlphaModel(
fast_time_period=5,
slow_time_period=20,
risk_estimation_factor=1.25, # scales the ATR estimate used as fraction_at_risk
data_provider=ts.data_provider,
)
model_tickers_dict = {model: [DummyTicker('AAA'), DummyTicker('BBB')]}
# Enable stop losses here
strategy = AlphaModelStrategy(ts, model_tickers_dict, use_stop_losses=True)
CalculateAndPlaceOrdersRegularEvent.set_daily_default_trigger_time()
CalculateAndPlaceOrdersRegularEvent.exclude_weekends()
strategy.subscribe(CalculateAndPlaceOrdersRegularEvent)
ts.start_trading()
Tip
Stop losses work best with
InitialRiskPositionSizer.
That sizer calculates position size so that if the stop loss is hit, the portfolio loses at most
initial_risk percent of its value - making risk per trade predictable.
How to trade multiple assets with Alpha Models#
Trading multiple tickers simultaneously requires only a list of tickers in model_tickers_dict.
The AlphaModelStrategy calls calculate_exposure() for each ticker independently every bar.
Multiple tickers with a single model#
from demo_scripts.common.utils.dummy_ticker import DummyTicker
from demo_scripts.backtester.moving_average_alpha_model import MovingAverageAlphaModel
tickers = [DummyTicker('AAA'), DummyTicker('BBB'), DummyTicker('CCC'),
DummyTicker('DDD'), DummyTicker('EEE')]
model = MovingAverageAlphaModel(
fast_time_period=5, slow_time_period=20,
risk_estimation_factor=1.25, data_provider=ts.data_provider,
)
# Map one model to all five tickers
model_tickers_dict = {model: tickers}
strategy = AlphaModelStrategy(ts, model_tickers_dict, use_stop_losses=True)
Multiple models, each covering different tickers#
You can also run several models in parallel. Each model is responsible for its own subset of tickers:
fast_model = MovingAverageAlphaModel(5, 20, 1.25, ts.data_provider)
slow_model = MovingAverageAlphaModel(10, 50, 1.25, ts.data_provider)
model_tickers_dict = {
fast_model: [DummyTicker('AAA'), DummyTicker('BBB')],
slow_model: [DummyTicker('CCC'), DummyTicker('DDD')],
}
strategy = AlphaModelStrategy(ts, model_tickers_dict)
Pre-loading data for multiple tickers#
When running with many tickers, pre-loading all price data before the backtest loop avoids repeated network calls and speeds things up significantly:
all_tickers = [DummyTicker('AAA'), DummyTicker('BBB'), DummyTicker('CCC')]
# Builds a PrefetchingDataProvider internally over the backtest date range
ts.use_data_preloading(all_tickers)
ts.start_trading()
How to use order filters#
An OrdersFilter intercepts orders just before
they are sent to the broker and adjusts or removes them based on custom criteria. The most common built-in
filter is VolumeOrdersFilter, which
caps each order’s quantity so it does not exceed a given fraction of the asset’s average daily volume.
Adding a VolumeOrdersFilter#
from qf_lib.backtesting.orders_filter.volume_orders_filter import VolumeOrdersFilter
from qf_lib.backtesting.trading_session.backtest_trading_session_builder import BacktestTradingSessionBuilder
session_builder = BacktestTradingSessionBuilder(settings, pdf_exporter, excel_exporter)
session_builder.set_frequency(Frequency.DAILY)
session_builder.set_data_provider(daily_data_provider)
# Limit each order to at most 10% of the 5-day average daily volume
session_builder.add_orders_filter(VolumeOrdersFilter, volume_percentage_limit=0.10)
ts = session_builder.build(start_date, end_date)
Note
Slippage models also support volume limiting via the max_volume_share_limit parameter (see
Customize your backtest). The key difference is that
VolumeOrdersFilter cancels the
excess quantity before the order reaches the simulated exchange, whereas the slippage
max_volume_share_limit limits the fill at execution time. Depending on your model, you may
want to use both, one, or neither.
Writing a custom order filter#
To implement your own filter, subclass
OrdersFilter and implement adjust_orders():
from typing import List
from qf_lib.backtesting.order.order import Order
from qf_lib.backtesting.orders_filter.orders_filter import OrdersFilter
class MaxPositionsFilter(OrdersFilter):
"""Rejects all new orders if the portfolio already holds max_positions assets."""
def __init__(self, data_provider, max_positions: int):
super().__init__(data_provider)
self.max_positions = max_positions
def adjust_orders(self, orders: List[Order]) -> List[Order]:
# Count currently open positions via the data_provider or external state
# (This is a simplified illustration - in practice you would inspect ts.portfolio)
buy_orders = [o for o in orders if o.quantity > 0]
sell_orders = [o for o in orders if o.quantity < 0]
if len(buy_orders) > self.max_positions:
# Keep only the first max_positions buy orders
buy_orders = buy_orders[: self.max_positions]
return buy_orders + sell_orders
How to export results to Excel and PDF#
Excel export#
ExcelExporter writes any
QFDataFrame or
QFSeries to an .xlsx file:
from qf_lib.documents_utils.excel.excel_exporter import ExcelExporter
from demo_scripts.demo_configuration.demo_settings import get_demo_settings
settings = get_demo_settings()
excel_exporter = ExcelExporter(settings)
# Assuming prices_df is a QFDataFrame of close prices
excel_exporter.export_container(
container=prices_df,
file_path="output/prices.xlsx",
sheet_name="Close Prices",
)
Excel import#
ExcelImporter reads a sheet back into a
QFDataFrame:
from qf_lib.documents_utils.excel.excel_importer import ExcelImporter
from qf_lib.containers.dataframe.qf_dataframe import QFDataFrame
excel_importer = ExcelImporter(settings)
df = excel_importer.import_container(
file_path="output/prices.xlsx",
starting_cell="A1",
ending_cell="B25",
sheet_name="Close Prices",
container_type=QFDataFrame,
)
PDF export#
Tearsheets and analysis sheets are built on top of
PDFExporter. In most cases you do
not need to interact with it directly - just pass it to the tearsheet constructor and call
tearsheet.save(). See Analysing Backtest Results for full examples.
How to suppress backtest output#
By default, each backtest writes several files to the output directory. During a parameter sweep or unit
test this is undesirable. Use BacktestMonitorSettings.no_stats() to disable all file output:
from qf_lib.backtesting.monitoring.backtest_monitor import BacktestMonitorSettings
session_builder.set_monitor_settings(BacktestMonitorSettings.no_stats())
This keeps the portfolio in-memory (ts.portfolio) fully functional so you can still read
ts.portfolio.portfolio_eod_series() and ts.portfolio.closed_positions() after the backtest.