Backtest#
In the development of quantitative trading strategies, a high-performance and easy-to-use backtesting system can significantly improve the efficiency of strategy validation and the pace of strategy iteration. To facilitate the development of multi-asset, medium/low-frequency, and high-frequency strategies, Swordfish provides a backtesting framework that covers strategy writing, engine creation, backtest execution, and results analysis. The framework is designed for developers with Python skills and basic quantitative trading knowledge, helping them quickly get started and efficiently develop and validate strategies.
Basic Framework#
Swordfish’s backtesting framework supports strategy validation for various asset types, including stocks, futures, options, bonds, and cryptocurrencies. The framework consists of the following core modules:
Module |
Description |
Example |
|---|---|---|
|
Base class for strategy templates: used to define
callback functions such as |
# Define a custom trading strategy class inheriting from StrategyTemplate
class MyStrategy(backtest.StrategyTemplate):
# Initialization function called once when backtesting starts.
def initialize(self, context):
pass # The initialization logic is omitted here.
# Function called each time when an OHLC is generated.
def on_bar(self, context, msg, indicator):
pass # The trading logic is omitted here.
|
|
Provides asset-specific trading interfaces, such as
|
class MyStrategy(backtest.StrategyTemplate,
backtest.StockOrderMixin):
def initialize(self, context):
pass
def on_bar(self, context, msg, indicator):
self.submit_stock_order(…)
# Place stock trading orders (you must set required parameters).
|
|
Backtesting configuration class. Each asset type has
its own configuration class, such as |
# Create a configuration object for stock backtesting
config = backtest.StockConfig()
|
|
Backtester class: used to load the strategy configuration and data and execute backtesting. |
# Create a backtester.
backtest.Backtester(MyStrategy, config)
|
The backtesting workflow can be summarized in the following five steps:
Strategy development: Write strategy logic in the class inherting from
StrategyTemplate.Parameter configuration: Set the asset type, backtesting time range, initial capital, etc.
Backtester creation: Create a
Backtesterobject.Backtest execution: Insert data and start backtesting.
Result analysis: Obtain, visualize, and evaluate backtesting results.
This modular, decoupled design greatly enhances strategy reusability and framework maintainability, making strategy development more efficient and flexible.
The following code example demonstrates how to develop the simplest executable strategy backtesting demo using Swordfish’s backtesting framework.
# Import the backtesting module and required utility libraries.
import swordfish.plugins.backtest as backtest
import swordfish as sf
import swordfish.function as F
import pandas as pd
# Define a strategy class inheriting from StrategyTemplate provided by the backtesting framework
class MyBasicStrategy(backtest.StrategyTemplate):
def initialize(self, context):
"""
Strategy initialization function, called when backtesting starts.
context: the backtesting context, such as time, capital, and asset type.
"""
print("Initial Strategy Context:", context)
def on_bar(self, context, msg, indicator):
"""
Called each time when an OHLC is generated.
Write the trading logic in the function.
context: the current context, such as capital and positions.
msg: the current OHLC's market data, such as time, prices, and trading volume.
indicator: the indicators, which are used to support trading decisions.
"""
pass # You can add buy/sell logic, such as logic on detecting moving average golden/death crosses.
# Create a backtesting configuration object.
config = backtest.StockConfig()
config.start_date = sf.scalar("2021.01.01", type="DATE") # Start date of backtesting
config.end_date = sf.scalar("2021.12.31", type="DATE") # End date of backtesting
config.asset_type = backtest.AssetType.STOCK # Asset type: stock
config.cash = 100_000_000 # Initial capital: 100 million yuan
config.commission = 0.00015 # Commission rate: 0.00015
config.data_type = backtest.MarketType.MINUTE # Market data: minute-level OHLC
# Create a backtester object and pass in the strategy and configuration.
backtester = backtest.Backtester(MyBasicStrategy, config)
# When this line is executed, the strategy will be initialized, and the following information will be printed (from the initialize function):
# Starting Strategy Context: tradeDate->2021.01.01
# tradeTime->1970.01.01T00:00:00.000
# barTime->1970.01.01T00:00:00.000
# engine->53135104
# Prepare market data (the following data is an example; you must provide real minute-level market data).
stocks = pd.DataFrame({
# Sample fields (define the fields based on the framework): symbol, datetime, open, high, low, close, volume, etc.
# Here, only one piece of data is mocked.
'symbol': ['000001.XSHE'],
'tradeTime': [sf.scalar("2021.01.01T09:30:00", type="TIMESTAMP")],
'open': [10.0],
'high': [10.5],
'low': [9.5],
'close': [10.2],
'volume': [1000],
'upLimitPrice': [11.0],
'downLimitPrice': [9.0],
'prevClosePrice': [10.0]
})
# Convert a pandas DataFrame into a Swordfish table.
stocks = F.table(stocks)
# Insert market data into the backtester.
backtester.append_data(stocks)
# When this line is executed, the strategy will be initialized again, and the same information will be printed:
# Starting Strategy Context: tradeDate->2021.01.01
# tradeTime->1970.01.01T00:00:00.000
# barTime->1970.01.01T00:00:00.000
# engine->53135104
The code above forms a basic backtesting workflow, which includes the following steps:
Define the strategy class: Define a class inheriting from
StrategyTemplateand implement key callback functions such asinitialize()andon_bar().Configure backtesting parameters: Set the asset type, start and end times, initial capital, commission rate, data frequency, and other parameters.
Create the backtester object: Create a
Backtesterobject using the strategy class and the backtesting configuration.Prepare market data: Convert the market data into a Swordfish table using
swordfish.function.table.Insert data and start backtesting: Once the backtester receives the data, it automatically executes the strategy logic in time order (e.g., calling
on_bar()).
Note
The initialize() function is automatically called when the backtester is created. It is suitable for tasks such as loading parameters, setting initial state, and registering indicators.
Edit Strategy#
Swordfish’s backtesting framework is built on an event-driven mechanism. It supports a variety of event callback functions covering strategy initialization, daily pre-market and post-market processing, tick data, snapshot data, OHLC data, as well as order and trade executions. You can define logic, compute indicators, or place orders in the corresponding event functions, depending on your requirements on strategies.
Event Functions Supported by the Framework#
Event Function |
Description |
|---|---|
|
The strategy initialization function (triggered only once), which is used to set the initial state, load data, or initialize indicators. |
|
The daily pre-market callback, which is suitable for daily preparation, such as subscribing to market data and clearing variables. |
|
The tick data callback, which handles each order or trade and is suitable for high-frequency strategies. |
|
The snapshot data callback, which captures the complete market state at a specific point in time. |
|
The OHLC data callback, which is suitable for medium/low-frequency strategies, such as minute-level or daily strategy. |
|
The trade detail callback, which is only supported for bonds on the Shanghai Stock Exchange. |
|
The callback triggered on order status changes, which is used to track the lifecycle of orders. |
|
The trade execution callback, which is triggered when a trade is actually executed. |
|
The daily post-market callback, which is used to calculate daily profits, summarize positions, etc. |
|
The callback triggered once before the backtesting ends, which is used for outputting results, cleaning up resources, etc. |
|
The timer callback triggered at a specified time or frequency, which is suitable for executing strategy logic on a regular schedule. |
Callback Parameter Description#
context: A dictionary indicating the strategy context object, which is used to store user-defined variables during strategy execution. The framework also provides the following four built-in global attributes:
context.tradeTime: The latest timestamp of the current market data.context.tradeDate: The current trading date.context.BarTime: The timestamp of the current OHLC data, which is used for low-frequency snapshot aggregation.context.engine: The backtesting engine instance, which provides control interfaces during strategy execution.
msg: A dictionary indicating the market data object, with structure varying depending on the data type:
High-frequency data (e.g.,
on_tick,on_snapshot): Each tick/snapshot is a dictionary. The price can be accessed viamsg.lastPriceormsg.price.OHLC data (e.g.,
on_bar): A nested dictionary, where the first level is the asset’s symbol code (e.g.,msg['600000']) and the second level contains the market data fields of the asset (e.g.,msg['600000'].close).
indicator: The subscribed indicators in the strategy. Its structure matches that of the corresponding
msg. They are commonly used inon_baroron_tickto make decisions based on technical indicators.orders: A dictionary indicating the order information, passed in
on_order; fields vary by asset type.trades: A dictionary indicating the trade information, passed in
on_trade; represents actually executed orders.
Note
context is an object that persists throughout the strategy and can be regarded as a shared runtime dictionary for storing and passing strategy states, parameters, and cached data. It allows variables to be set during initialization and shared or modified in subsequent functions. Examples include:
Strategy parameters (e.g., maximum position size, take-profit/stop-loss levels)
State flags (e.g., whether a position is open, whether a signal has been triggered)
Data cache (e.g., historical prices, intermediate indicator values)
Custom metrics (e.g., cumulative profit, number of signal triggers)
Set Backtesting Parameters#
Before starting a backtest, you must create and configure a backtesting configuration object. Swordfish’s backtesting framework provides dedicated configuration classes for different asset types, making strategy development more flexible and extensible. Examples include:
StockConfig: Configuration for stock backtesting.FutureConfig: Configuration for futures backtesting.BacktestBasicConfig: General configuration (suitable for multi-asset portfolios).
View Default Parameters#
If you are unsure which parameters are included in a configuration class or want to check their default values, you can instantiate the class and print it:
import swordfish.plugins.backtest as backtest
# Create a default stock strategy configuration object for setting backtesting parameters.
config = backtest.StockConfig()
# Print the current configuration object to view the default parameters or for debugging purposes.
print(config)
The output will display all supported parameters along with their default values, for example:
{
'add_time_column_in_indicator': False,
# Whether to add a time column in indicator data (usually for debugging or result alignment)
'benchmark': None,
# Performance benchmark (e.g., CSI 300) used for comparing strategy returns
'callback_for_snapshot': 0,
# Snapshot callback mode
'cash': None,
# Initial capital in yuan, e.g., 100_000_000 represents 100 million yuan
'commission': None,
# Commission rate, e.g., 0.00015 means 0.015% (1.5 basis points)
'context': None,
# Runtime context (usually auto-filled by the framework, no manual setting needed)
'data_retention_window': None,
# Data retention window, used for indicator lookback, e.g., keep the last 30 OHLCs
'enable_indicator_optimize': False,
# Whether to enable indicator computation optimization (improves speed but may reduce flexibility)
'enable_subscription_to_tick_quotes': False,
# Whether to subscribe to tick data (for high-frequency or order book strategies)
'frequency': 0,
# Data frequency, e.g., minute, daily, corresponding to MarketType enumerations
'is_backtest_mode': True,
# Whether in backtesting mode (True for backtesting, False for live trading)
'latency': None,
# Latency simulation (e.g., network or matching latency), usually for a simulated environment
'matching_mode': None,
# Matching mode, e.g., price or time priority, as defined by the framework
'matching_ratio': 1.0,
# Matching ratio for regular orders (1.0 means fully traded; <1 means partially traded)
'msg_as_table': False,
# Whether to structure market messages into tables for easier operation
'orderbook_matching_ratio': 1.0,
# Matching ratio based on order book (advanced scenarios)
'output_order_info': False,
# Whether to output detailed information for each order (for debugging)
'output_queue_position': 0,
# Whether to output the position of orders in the queue (advanced order flow simulation)
'prev_close_price': None,
# Previous trading day's closing price (for use as an opening reference)
'set_last_day_position': None,
# Whether to automatically include the previous day's positions (useful for continuity testing)
'stock_dividend': None,
# Stock dividend configuration (whether to consider dividends and stock splits)
'tax': None,
# Stamp tax (e.g., 0.001 applied on sell orders)
'universe': None
# Stock pool (e.g., specify the list of stocks to trade)
}
Customize Parameters#
After the configuration object is created, it can be modified based on actual requirements, such as setting the backtesting time range, asset type, initial capital, commission rate, etc.
# Import the swordfish module that includes utility methods such as scalar().
import swordfish as sf
# Set the start date for backtesting (use sf.scalar to create an object of the DATE type).
config.start_date = sf.scalar("2022.04.11", type="DATE")
# Set the end date for backtesting (same as start date means backtesting is performed on a single day).
config.end_date = sf.scalar("2022.04.11", type="DATE")
# Set the asset type to stock (can also be option, ETF, etc., depending on the framework).
config.asset_type = backtest.AssetType.STOCK
# Set the market data type to snapshot.
# SNAPSHOT usually indicates hourly snapshot data, suitable for medium/high-frequency strategies.
# Other possible values include: MarketType.MINUTE (minute-level), MarketType.DAILY (daily), etc.
config.data_type = backtest.MarketType.SNAPSHOT
# Set initial capital to 100 million yuan.
config.cash = 100_000_000
# Set stamp tax (usually only applied on sell orders).
config.tax = 0.001
Insert Data#
Swordfish’s backtesting engine accepts Swordfish tables as market data
input. You can construct a table using sf.table() or convert a
pandas DataFrame into a Swordfish table, and then pass the data to the
backtester via backtester.append_data().
msg_table = sf.table({
'symbol': sf.vector(["000001.XSHE", "000001.XSHE"], type="STRING"),
'timestamp': sf.vector(["2022.04.11 10:10:00.000", "2022.04.11 10:10:03.000"], type="TIMESTAMP"),
'lastPrice': sf.vector([7.0, 7.5], type="DOUBLE"),
'prevClosePrice': sf.vector([6.5, 7.0], type="DOUBLE"),
'offerPrice': sf.array_vector([[7.1], [7.6]], type="DOUBLE")
})
Alternatively, you can use F.loadText() to import a CSV file.
Regardless of the method used to prepare the data, it must be inserted
via the backtester.append_data() method:
backtester = backtest.Backtester(MyBasicStrategy, config)
backtester.append_data(msg_table)
Different asset types and market data frequencies have different requirements for input data fields. The following table lists the required fields and their meanings for a stock snapshot strategy:
Field |
Type |
Description |
|---|---|---|
symbol |
SYMBOL |
Stock code |
symbolSource |
STRING |
“XSHG”(Shanghai Stock Exchange) or “XSHE” (Shenzhen Stock Exchange) |
timestamp |
TIMESTAMP |
Timestamp |
lastPrice |
DOUBLE |
Latest trade price |
upLimitPrice |
DOUBLE |
Limit-up price |
downLimitPrice |
DOUBLE |
Limit-down price |
totalBidQty |
LONG |
Total bid quantity in the interval |
totalOfferQty |
LONG |
Total offer quantity in the interval |
bidPrice |
DOUBLE[] |
List of bid prices |
bidQty |
LONG[] |
List of bid quantities |
offerPrice |
DOUBLE[] |
List of ask prices |
offerQty |
LONG[] |
List of ask quantities |
For the required market data fields for other frequencies and asset types, refer to the backtesting tutorial for specific asset type.
Example: Simple Trend-following Strategy#
In this section, Swordfish’s backtesting framework is used to develop and backtest a simple trend-following strategy based on minute-level OHLC data. The strategy logic is as follows:
If the current closing price of an asset is lower than the previous OHLC’s closing price, attempt to buy.
If the asset is currently held and the current price is higher than the previous OHLC’s closing price, attempt to sell.
Each trade involves a fixed quantity of 1,000 shares.
The strategy operates on minute-level OHLC data.
Import modules
import swordfish.plugins.backtest as backtest
import swordfish as sf
import swordfish.function as F
Write strategy
# Define a strategy class, inheriting from StrategyTemplate (includes the framework lifecycle) and StockOrderMixin (assists in ordering).
class MyStrategy(backtest.StrategyTemplate, backtest.StockOrderMixin):
def initialize(self, context):
"""
Strategy initialization function, called once before backtesting starts.
Used to define and subscribe to the required indicators.
"""
# Use the metacode module to construct indicator logic
with sf.meta_code() as m:
lastp = F.prev(m.col("close")) # Calculate the previous OHLC's closing price (prev(close))
# Subscribe to the indicator from the framework: type = KLINE (OHLC), name = 'lastp'
self.subscribe_indicator(backtest.MarketDataType.KLINE, {
'lastp': lastp
})
def on_bar(self, context, msg, indicator):
"""
Triggered once per OHLC (e.g., every minute/day, depending on data_type).
Executes the buy/sell logic.
"""
for istock in msg.keys():
prevp = indicator[istock]["lastp"] # Obtain the previous OHLC's closing price
lastPrice = msg[istock]["close"] # Closing price of the current OHLC
# Obtain the long position quantity in the current default account
position = self.accounts[backtest.AccountType.DEFAULT].get_position(symbol=istock)["longPosition"]
# === Buy logic ===
if position == 0:
if lastPrice < prevp: # Current price below previous OHLC: oversold buy signal
print(context["tradeTime"], "buy", istock, lastPrice)
# Submit a buy order
# Parameters: symbol, time, order book level, price, quantity, direction (1 = buy), label
self.submit_stock_order(
istock, context["tradeTime"], 5, lastPrice, 1000, 1, label="buy"
)
# === Sell logic ===
else:
if lastPrice > prevp: # Current price above previous OHLC: rebound sell signal
print(context["tradeTime"], "sell", istock, lastPrice)
# Submit a sell order
# Direction parameter = 2 indicates sell
self.submit_stock_order(
istock, context["tradeTime"], 5, lastPrice, 1000, 2, label="sell"
)
In initialize:
Use
sf.meta_code()to construct an indicator: the previous OHLC’s closing price (prev(close)).Use
subscribe_indicatorto subscribe to this indicator with the type KLINE (OHLC data).The subscription result will automatically be available later in the
on_barcallback.
In on_bar:
Iterate over all symbols at the current timestamp.
Use
indicator[istock]["lastp"]to obtain the previous OHLC’s closing price.Obtain the position information about the current asset.
Check buy/sell conditions and place orders using
submit_stock_order.
Set backtesting parameters
Asset type: stocks, using minute-level OHLC data.
Initial capital: 100 million yuan.
Commission rate: 0.015% with no stamp tax.
Set
frequency = 0to perform backtesting without downsampling and use original data timestamps directly.
# Create a backtest configuration
config = backtest.StockConfig()
# Set the backtesting time range
config.start_date = sf.scalar("2021.01.01", type="DATE")
config.end_date = sf.scalar("2021.12.31", type="DATE")
# Set the asset type and data frequency
config.asset_type = backtest.AssetType.STOCK
config.frequency = 0 # Avoid backtesting in lower frequencies
# Initial capital and trading costs
config.cash = 100000000
config.commission = 0.00015 # Commission rate
config.tax = 0.0 # Stamp tax
# Use minute-level market data
config.data_type = backtest.MarketType.MINUTE
Load data
# Load stock data file
stocks = F.loadText('PATH_TO_DATA.csv')
# SQL query: group by symbol and sort by time, extracting key fields
msg_table = sf.sql("""
select symbol,
timestamp(tradeTime) as tradeTime, // Timestamp
open, low, high, // Open, low, and high prices of the current OHLC
prev(open) as close, // Previous OHLC's open price used as closing price
long(volume) as volume, // Trading volume
amount, // Trading amount
double(upLimitPrice) as upLimitPrice, // Limit-up price
double(downLimitPrice) as downLimitPrice,// Limit-down price
prevClose as prevClosePrice // Previous closing price
from stocks
context by symbol // Group by symbol (time-series context)
order by tradeTime // Sort by time
""", vars={'stocks': stocks})
Use
loadTextto load market data in the CSV format.Construct a standardized OHLC table via SQL.
Include required backtesting fields such as symbol, open, close, volume, prevClose, etc.
Insert data and start backtesting
# Create a backtester with the custom strategy and configuration
backtester = backtest.Backtester(MyStrategy, config)
# Insert market data into the backtester
backtester.append_data(msg_table)
After completing the above steps, the backtester will automatically execute the strategy and generate buy/sell signals.
Analyze Results#
After the backtest is completed, you can access various backtesting
results through the account object, including daily positions, net
account value, return summary, and trade details.
# Obtain the default account
account = backtester.accounts[backtest.AccountType.DEFAULT]
# Print common backtesting results
print("Daily positions:", account.get_daily_position()) # Daily stock positions
print("Daily total portfolios:", account.daily_total_portfolios) # Daily total asset/equity data
print("Return summary", account.return_summary) # Overall return (e.g., annualized return, max drawdown)
print("Trade records:", account.trade_details) # Detailed records of each transaction
Common methods/properties for backtesting results#
Method/Property |
Data Type/Form |
Description |
|---|---|---|
|
DOUBLE |
Current available cash in the account. |
|
TABLE |
Detailed trade records, including timestamp, trade direction, price, quantity, etc. |
|
TABLE |
Daily position information, including symbol, trade direction, position size, floating profit/loss, etc. |
|
TABLE |
Overall account indicators throughout the strategy execution, including current market value, net asset value, rate of return, etc. |
|
TABLE |
Daily account indicators, including daily market value, net asset value, profit/loss, rate of return, etc. |
|
TABLE |
Return summary of the backtest, including cumulative return, annualized return, Sharpe ratio, etc. (fields are provided by the framework) |
Advanced: Subscribe to Indicators#
In this section, we’ll introduce a simple price change indicator to the strategy and show how to place orders based on it. Swordfish’s backtesting framework provides interfaces for registering, subscribing to, and using such indicators.
The complete code is included in the Script for section 7 in
Appendix. Since @F.swordfish_udf cannot be used directly in the
interactive command line, please save the full script as a .py file
and run it from the command line using python file_name.py.
Define Indicator Function#
# Define a stateful UDF (is_state=True) to calculate price change ratio
@F.swordfish_udf(mode="translate", is_state=True)
def zdf(last_price, prev_close_price):
return last_price / prev_close_price - 1 # Price change ratio = (current price / previous closing price) - 1
This function calculates the price change ratio [(current price / previous closing price) − 1].
@swordfish_udf(...): Registers the function as an indicator usable in backtesting.mode="translate": Indicates that the function will be converted into a metadata expression, which can be used withsubscribe_indicator().
Subscribe to the Indicator in Strategy#
Subscribe to the indicator in the initialize() function so it can be
used when new market data arrives.
def initialize(self, context):
# Set the maximum position size for each asset
context["max_pos"] = 500
# Create a dictionary to store open position information for each asset
context["open"] = sf.dictionary(key_type="STRING", val_type="BOOL")
# Define the indicator logic (using the previously defined price change function)
with sf.meta_code() as m:
m_zdf = zdf(m.col("lastPrice"), m.col("prevClosePrice"))
# Subscribe to the custom indicator (zdf) in snapshot market data
self.subscribe_indicator(backtest.MarketDataType.SNAPSHOT, {
'zdf': m_zdf
})
The
contextdictionary stores the maximum position size and open position information for each asset.sf.meta_code()converts an indicator into a metacode object.m.col("...")obtains field references from the market data.subscribe_indicator()subscribes to the user-defined indicator. The first parameter, backtest.MarketDataType.SNAPSHOT, means the system will automatically calculate the indicator at each snapshot timestamp and pass it toon_snapshot()for use. The second parameter is a dictionary where the key is the indicator name and the value is the corresponding metacode.
Trigger Trading Using the Indicator#
In on_snapshot(), the indicator values are used to make decisions
and place orders.
def on_snapshot(self, context, msg, indicator):
symbol = msg["symbol"] # Current stock symbol
best_ask = msg["offerPrice"][0] # Best offer price
long_pos = self.accounts[backtest.AccountType.DEFAULT]\
.get_position(symbol)["longPosition"] # Current position size
opens = context["open"] # Dictionary of opened positions
# Place a buy order if all conditions are met:
# 1. Price change exceeds 1%
# 2. Current position size is below the maximum limit
# 3. No previous order has been placed for this stock
if indicator["zdf"] > 0.01 and long_pos <= context["max_pos"] and not opens.get(symbol, False):
self.submit_stock_order(
symbol, context["tradeTime"], # Stock symbol and current time
5, best_ask, 100, 1, # Order parameters: buy limit, price, quantity, etc.
label="buy"
)
opens[symbol] = True # Mark this stock as ordered to prevent duplicate buys
context["open"] = opens # Update the context state
Strategy logic explanation:
Retrieve the price change indicator for the current asset:
indicator["zdf"].If the price increase exceeds 1%, the current position size is below the maximum limit, and no order has been placed for this asset yet, place a buy order.
Use
submit_stock_order()to place the order.Mark the asset as opened in
context["open"]to prevent duplicate trades.
The full code is available in the Appendix.
Best Practices#
This section covers important points to keep in mind when using the Swordfish’s backtesting framework, including the backtesting configuration, code logic design, and data processing.
Backtesting Configuration#
Indicator optimization
Swordfish supports real-time indicator subscription in backtesting. If
your strategy involves a large number of technical indicator
computations (e.g., moving averages, MACD, price changes), you can set
config.enable_indicator_optimize = True. With the configuration
enabled, Swordfish will batch-optimize indicator computation to avoid
redundant work on similar indicators.
Recommended indicator calculation methods (from high to low priority)
Load pre-computed indicators.
Subscribe to indicators for real-time computation.
Compute indicators in callbacks.
Proper order matching settings
Complex order matching, such as simulating order queue position or
splitting partial fills, increases computational overhead. If fill-rate
validation is not required during backtesting, you can use a simplified
order matching mode. In this case, setting config.matching_mode = 3
will directly fill orders at the order price and bypass time-consuming
order matching logic.
Code logic design#
Use context properly
Callbacks (e.g., on_bar, on_tick) can be triggered thousands of
times per second in high-frequency strategies. Creating objects (e.g.,
lists, dicts, DataFrames) dynamically in callbacks leads to frequent
memory allocation and garbage collection, which is a major performance
killer. The solution is to pre-define reusable objects via context
in initialize, and reuse them in callbacks.
Reduce redundant work in callbacks
Avoid I/O operations in callbacks, such as print, file writing, and
database queries. Instead, store the required information in context
and export it using finalize. For example, if you need to log
parameters each time a position is opened, refer to the following code:
def initialize(self, context):
context["entry_log"] = []
def on_bar(self, context, msg, indicator):
context["entry_log"].append(...)
# Trading logic...
Short-circuit logic first
When writing conditional logic, place the conditions that are most likely to be false first to reduce unnecessary checks.
# Bad case
if indicator["ma5"] < msg['close'] and long_pos == 0: # Check the indicator first and then the position
# Trading logic...
# Good case
# Check the position first (simpler condition) and then the indicator
if long_pos == 0 and indicator["ma5"] < msg['close']:
# Trading logic...
Data processing#
Compute static indicators offline
For indicators that do not require real-time updates, avoid subscribing
to them in backtesting. Instead, pre-process the data using sf.sql
with vectorized operations rather than Python loops, fully leveraging
Swordfish’s computation performance.
Batch load market data
During backtesting, Swordfish injects batch market data into the engine through event callbacks. Loading market data in batches significantly improves backtesting performance by enabling the engine to fetch cached data, speeding up market data parsing. Therefore, batch data loading is recommended in data processing.
Summary#
This tutorial introduces how to develop and backtest quantitative trading strategies using Swordfish’s backtesting framework, covering strategy writing, backtesting parameter configuration, market data loading, user-defined indicator subscription, and trading logic implementation.
Swordfish supports high-performance computation for multi-asset and multi-frequency strategies. Its strong scalability makes it suitable for researching and validating mid- to high-frequency strategies, multi-asset portfolios, or combination strategies.
Appendix#
Script for section 1:
simplest_framework.pyMock data for section 5:
mink_data.csvScript for section 5:
trend_following_strategy.pyScript for section 7:
indicator_subscription.py