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

StrategyTemplate

Base class for strategy templates: used to define callback functions such as initialize, on_snapshot, and on_bar.

# 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.

AssetMixin

Provides asset-specific trading interfaces, such as StockOrderMixin (for stocks) and FuturesOrderMixin (for futures).

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).

Config

Backtesting configuration class. Each asset type has its own configuration class, such as StockConfig and OptionConfig.

# Create a configuration object for stock backtesting
config = backtest.StockConfig()

Backtester

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:

pys_backtesting_workflow.png
  1. Strategy development: Write strategy logic in the class inherting from StrategyTemplate.

  2. Parameter configuration: Set the asset type, backtesting time range, initial capital, etc.

  3. Backtester creation: Create a Backtester object.

  4. Backtest execution: Insert data and start backtesting.

  5. 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:

  1. Define the strategy class: Define a class inheriting from StrategyTemplate and implement key callback functions such as initialize() and on_bar().

  2. Configure backtesting parameters: Set the asset type, start and end times, initial capital, commission rate, data frequency, and other parameters.

  3. Create the backtester object: Create a Backtester object using the strategy class and the backtesting configuration.

  4. Prepare market data: Convert the market data into a Swordfish table using swordfish.function.table.

  5. 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

def initialize(self, context)

The strategy initialization function (triggered only once), which is used to set the initial state, load data, or initialize indicators.

def before_trading(self, context)

The daily pre-market callback, which is suitable for daily preparation, such as subscribing to market data and clearing variables.

def on_tick(self, context, msg, indicator)

The tick data callback, which handles each order or trade and is suitable for high-frequency strategies.

def on_snapshot(self, context, msg, indicator)

The snapshot data callback, which captures the complete market state at a specific point in time.

def on_bar(self, context, msg, indicator)

The OHLC data callback, which is suitable for medium/low-frequency strategies, such as minute-level or daily strategy.

def on_transaction(self, context, msg, indicator)

The trade detail callback, which is only supported for bonds on the Shanghai Stock Exchange.

def on_order(self, context, orders)

The callback triggered on order status changes, which is used to track the lifecycle of orders.

def on_trade(self, context, trades)

The trade execution callback, which is triggered when a trade is actually executed.

def after_trading(self, context)

The daily post-market callback, which is used to calculate daily profits, summarize positions, etc.

def finalize(self, context)

The callback triggered once before the backtesting ends, which is used for outputting results, cleaning up resources, etc.

def on_timer(self, context, msg, indicator)

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 via msg.lastPrice or msg.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 in on_bar or on_tick to 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_indicator to subscribe to this indicator with the type KLINE (OHLC data).

  • The subscription result will automatically be available later in the on_bar callback.

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 = 0 to 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 loadText to 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

cash

DOUBLE

Current available cash in the account.

trade_details

TABLE

Detailed trade records, including timestamp, trade direction, price, quantity, etc.

get_daily_position()

TABLE

Daily position information, including symbol, trade direction, position size, floating profit/loss, etc.

total_portfolios

TABLE

Overall account indicators throughout the strategy execution, including current market value, net asset value, rate of return, etc.

daily_total_portfolios

TABLE

Daily account indicators, including daily market value, net asset value, profit/loss, rate of return, etc.

return_summary

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 with subscribe_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 context dictionary 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 to on_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)

  1. Load pre-computed indicators.

  2. Subscribe to indicators for real-time computation.

  3. 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#