Minute-Frequency Futures CTA Strategy Backtesting
Commodity trading advisor (CTA) strategies play a vital role in modern financial markets. By employing technical analysis and trend following, they help traders capture market movements, hedge risks, and maximize profits. In medium- and high-frequency trading, CTA strategies are particularly effective at improving trading efficiency and profitability.
Before deploying quantitative medium- and high-frequency strategies in live trading, investors should backtest them on historical data to verify their validity and feasibility. With its high-performance computing engine and powerful data processing capabilities, DolphinDB is an ideal choice for backtesting medium- and high-frequency trading strategies. This tutorial walks you through a backtesting case for a minute-frequency futures CTA strategy. It demonstrates how to use the DolphinDB medium- and high-frequency backtesting engine and shows the engine's strong performance in practice.
1. Introduction
This tutorial shows in detail how to combine multiple technical indicators, such as the Average True Range (ATR) and the Relative Strength Index (RSI), to implement a CTA strategy, with specific rules for opening positions and setting stop-losses.
Before diving into the implementation of the minute-frequency futures CTA strategy, this chapter provides a brief overview of CTA strategies and the medium- and high-frequency backtesting solution to help you better understand the case.
1.1 Background on CTA Strategies
CTA strategies primarily trade in futures and options markets through technical analysis and systematic trading methods. There are two kinds of CTA strategies: discretionary CTA and systematic CTA. Discretionary CTA strategies rely on the manager's judgment, generating returns through fundamental analysis and subjective assessments of market trends. Systematic CTA strategies, on the other hand, make investment decisions based on buy and sell signals generated by quantitative models.
These strategies depend on technical analysis tools and indicators, such as the moving averages (MA), the RSI, and ATR, to identify market trends and trading opportunities. CTA strategies also place a strong emphasis on risk management, typically setting stop-loss and take-profit points to protect the portfolio from significant losses. The advantages of futures trading, including the ability to go both long and short and the T+0 settlement system, allow quantitative CTA strategies to adapt quickly to market conditions and to capture opportunities arising from market changes more intuitively. Consequently, when the futures market experiences sharp upward or downward swings, quantitative CTA strategies can often profit effectively from them.
Based on their trading logic, quantitative CTA strategies can be classified into trend following, trend reversal, and arbitrage hedging.
-
Trend Following: Profits by riding established price trends, holding long positions during uptrends and short positions during downtrends, and closing them when the trend ends.
-
Trend Reversal: Profits from price reversion by identifying inflection points, holding a long position when the signal indicates an upward reversal and a short position when it indicates a downward reversal, and closing the position when the price reverts.
-
Arbitrage Hedging: Trades related varieties or contracts. It can go long on undervalued varieties and short on overvalued ones, or profit from price convergence, including spot-futures arbitrage, calendar spread arbitrage, inter-commodity arbitrage, and cross-market arbitrage.
This article implements a trend reversal strategy for minute-frequency futures CTA using DolphinDB.
1.2 Overview of the Medium- and High-Frequency Backtesting Solution
Implementing a backtesting platform for medium- and high-frequency quantitative trading involves three key components: chronological market data replay, order matching, and strategy development with performance evaluation. Backtesting medium- and high-frequency strategies often faces several challenges:
First, the massive volume of medium- and high-frequency trading data places extremely high demands on the query and computation performance of the backtesting engine.
Second, to ensure the effectiveness of strategy evaluation and optimization, the backtesting process should simulate actual trading as closely as possible, considering factors such as whether orders can be filled, fill price, trading volume, and market impact.
Furthermore, the backtesting engine should have a flexible architecture that supports the implementation of various trading strategies and technical indicators and is easy to extend to meet different market and trading needs.
To address these challenges, DolphinDB provides an extensible, high-performance backtesting solution for medium- and high-frequency quantitative trading, built on a distributed storage and computing architecture. This solution implements three core components: in-database market data replay, an order matching simulator, and an event-driven backtesting engine. It supports developing and testing medium- and high-frequency strategies using DolphinScript, Python, or C++. Specifically, the solution covers the following three modules:
-
replayfunction: Supports replaying data from one or more DFS tables with different schemas into a stream table strictly by time or a specified multi-column sort order. This addresses the need for a unified environment for factor calculation and strategy development and production. -
Order matching simulator: Supports Level-2 tick and snapshot market data from the Shanghai and Shenzhen stock exchanges. It provides high-precision matching consistent with the exchange's "price priority, time priority" rule, multiple matching modes based on various market data types, and rich configuration options for simulating live trading environments.
-
Backtesting engine: Allows you to define custom indicators and supports strategy backtesting based on tick, snapshot, minute, and daily frequency market data, providing results such as returns, positions, and trade details. High-precision strategy backtesting based on tick and snapshot data enables you to achieve integrated simulation and backtesting for strategy validation.
Note that these three modules integrate well with third-party components. Even if you have already implemented a solution for a specific component, DolphinDB can integrate with it to form a complete backtesting solution.
2. Implement a Futures CTA Trend Reversal Strategy with DolphinDB
This case implements a futures CTA trend reversal strategy based on minute-frequency data, combining the ATR and RSI technical analysis indicators. The specific strategy logic is as follows:
-
Calculate Indicators:
-
Calculate the absolute difference between the high, low, and previous closing price of the underlying within 1 minute (the TR indicator), and then compute the average of the TR indicator (ATR).
-
Calculate the RSI indicator using closing price data from the past 10 minutes.
-
-
Open Positions:
-
When the RSI value is greater than 70 and the ATR is greater than its 10-minute average, open a long position.
-
When the RSI value is less than 30 and the ATR is greater than its 10-minute average, open a short position.
-
-
Stop-Loss:
-
When holding a long position, if the price retraces 0.004 from the highest point, sell to close.
-
When holding a short position, if the price rebounds 0.004 from the lowest point, buy to close.
-
2.1 Write a Custom Strategy
First, define two important technical analysis indicators based on minute-frequency market data: the ATR and the RSI. ATR measures market volatility, while RSI assesses overbought or oversold market conditions.
To calculate price-volume factors and similar indicators from medium- and high-frequency market data, the DolphinDB backtesting engine uses a reactive stateful engine. It supports unified stream-batch computation and efficiently processes stateful high-frequency factors. For details on defining indicators, see createReactiveStateEngine. The following code demonstrates how to define the ATR and RSI technical indicators:
@state
def atr(high, low, close, m=14, n=10) {
prevClosePrice = prev(close)
tr = rowMax([high-low, abs(high-prevClosePrice), abs(low-prevClosePrice)])
atr = ema(tr, m)
mAtr = mavg(atr, n)
return atr, mAtr
}
@state
def rsi(close, timePeriod=14) {
deltaClose = deltas(close)
up = iif(nullCompare( > , deltaClose, 0), deltaClose, 0)
down = iif(nullCompare( > , deltaClose, 0), 0, - deltaClose)
upAvg = ema(up, timePeriod)
downAvg = ema(down, timePeriod)
return 100.0 * upAvg / (upAvg + downAvg)
}
In medium- and high-frequency backtesting, strategies are typically event-driven, and a single strategy usually needs to handle multiple types of events, such as the arrival of new market data or the execution of new orders. The backtesting engine uses an event-driven architecture and provides a comprehensive set of event functions, including callbacks for strategy initialization, pre-market processing, market data, and daily post-market processing. You can implement strategies by writing the strategy logic in these callback functions. In addition, user-defined callback functions support JIT compilation, significantly improving the execution efficiency. The following example shows how these event functions are implemented.
In the strategy initialization function, first subscribe to the high, low, and
closing price market data, which has been processed with dominant contract
backward-adjusted (smoothed) prices, to calculate the ATR, its 10-minute
average, and the RSI indicator. Here, the signal field is an array
vector that stores the backward-adjusted high, low, and latest closing prices of
the dominant contract. The subscribeIndicator method takes the
backtesting engine name, the data type to calculate, and a dictionary of
indicators to compute (where the key is the indicator name for later access and
the value is the metacode for the indicator calculation). The calculation
results are then passed to strategy callback functions such as
onBar.
def initialize(mutable contextDict){
d=dict(STRING,ANY)
d["ATR"]=<atr(signal[0], signal[1], signal[2], 14,10)[0]>
d["mATR"]=<atr(signal[0], signal[1], signal[2], 14,10)[1]>
d["RSI"]=<rsi(signal[2], 14)>
Backtest::subscribeIndicator(contextDict["engine"], "kline", d)
}
In the OHLC bar callback function
onBar, the system generates buy or sell signals by
retrieving the subscribed ATR and RSI indicators. When a long or short
position exists and the market shows a pullback, the system performs the
corresponding stop-loss operation. The msg parameter of the
onBar function contains the latest minute-frequency market
data sent by the backtesting engine, along with the indicator calculation
results defined in initialize. msg is a dictionary where
each key is a futures symbol and the corresponding value contains the market
data and calculated indicator results for that symbol. The following code
demonstrates the logic for determining whether to open a long or short
position:
def onBar(mutable contextDict, msg, indicator){
lowDict = contextDict["lowPrice"]
highDict = contextDict["highPrice"]
keys = msg.keys()
for (i in keys){
istock = msg[i].symbol
symbolSource = msg[i].symbolSource
pos = Backtest::getPosition(contextDict["engine"], istock)
longPos = pos.longPosition
shortPos = pos.shortPosition
price = msg[i].close
atr = indicator[i].ATR
matr = indicator[i].mATR
rsi = indicator[i].RSI
// Buy when there is no long position and an uptrend is detected
if (longPos < 1 and shortPos < 1 and atr > matr and rsi > contextDict["buySignalRSI"] and matr > 0){
Backtest::submitOrder(contextDict["engine"], (istock, symbolSource,
contextDict["tradeTime"], 5, price+0.02, 0., 2, 1, 0), "buyOpen")
highDict[istock] = price
continue
}
// Sell when there is no short position and a downtrend is detected
if (longPos < 1 and shortPos < 1 and atr > matr and rsi < contextDict["sellSignalRSI"] and matr > 0){
Backtest::submitOrder(contextDict["engine"], (istock,
symbolSource, contextDict["tradeTime"], 5, price-0.02, 0., 2, 2, 0), "sellOpen")
lowDict[istock] = price
continue
}
if (longPos > 0 and price > highDict[istock]){//update high price
highDict[istock] = max(
price, highDict[istock])
} else if (longPos > 0 and price <= highDict[istock]*(1-contextDict["closeLine"])){//平仓
Backtest::submitOrder(contextDict["engine"], (istock, symbolSource,
contextDict["tradeTime"], 5, price-0.02, 0., 2, 3, 0), "sellClose")
}
if (shortPos > 0 and price < lowDict[istock]){//update low price
lowDict[istock] = min(
price, lowDict[istock])
} else if (shortPos > 0 and price >= lowDict[istock]*(1+contextDict["closeLine"])){//平仓
Backtest::submitOrder(contextDict["engine"],
(istock, symbolSource, contextDict["tradeTime"], 5, price+0.02, 0., 2, 4, 0), "buyClose")
}
}
contextDict["lowPrice"] = lowDict
contextDict["highPrice"] = highDict
}
Here, Backtest::submitOrder is the order placement method
provided by the backtesting engine:
Backtest::submitOrder(engine, msg, label="")
//engine: an engine instance
//msg: an order information tuple or table, (stock code, order time, order type, order price, order quantity, buy/sell direction)
//label: (optional) used to classify orders
In addition to implementing strategy logic that reacts to incoming market data, the medium- and high-frequency backtesting engine also supports writing strategy logic to handle order status changes, trade executions, daily post-market account statistics, and other business logic that must be processed before the strategy ends. For a complete list of callback functions supported by the medium- and high-frequency backtesting engine, see Backtest Method References.
2.2 Configure Strategy Parameters
You can configure the backtest start and end dates, initial capital, commissions and stamp duty, market data type, order delay, dynamic dividend adjustments, and other settings. These parameters help you flexibly adjust backtesting conditions to simulate different market environments and evaluate the performance of various trading strategies. Since this example backtests a futures strategy, the strategy type parameter should be set to "futures" to ensure the backtesting engine correctly recognizes futures trading and accounts for its specific rules and conventions. Additionally, you can set global variables for the strategy through the strategy logic context parameter context. The following code shows an example of the initial parameter configuration:
userConfig = dict(STRING, ANY)
userConfig["startDate"] = 2023.02.06
userConfig["endDate"] = 2023.02.08
userConfig["strategyGroup"] = "futures"
userConfig["cash"] = 10000000
userConfig["commission"] = 1.5
userConfig["tax"] = 0.0 // useless in future goods backtest engine
userConfig["dataType"] = 3
userConfig["msgAsTable"] = false
userConfig["frequency"] = 0
userConfig["outputOrderInfo"] = true
userConfig["depth"] = 5
userConfig["matchingMode"] = 3
Context = dict(STRING, ANY)
Context["buySignalRSI"] = 70.
Context["sellSignalRSI"] = 30.
Context["closeLine"] = 0.004
Context["lowPrice"] = dict(STRING, ANY)
Context["highPrice"] = dict(STRING, ANY)
userConfig["context"] = Context
When creating a futures backtest, you must configure a basic information table for futures contracts, including the contract multiplier, trading unit, commission, and commission calculation method (per lot or based on trading value). For detailed configuration, see Backtest Method References.
2.3 Create a Backtesting Engine
After setting the engine name, simulated exchange name, engine configuration
options, the schema and field mapping dictionary for real market data, the
schema and field mapping dictionary for orders, the order detail output table,
and the synthetic snapshot output table, you can create a backtesting engine
instance using the createBacktester method. The fourth
parameter of the createBacktester method specifies whether to
enable JIT optimization. The default value is false, which disables JIT
optimization. To enable JIT optimization, set this parameter to true.
strategyName = "Backtest_test1"
callbacks = dict(STRING, ANY)
callbacks["initialize"] = initialize
callbacks["beforeTrading"] = beforeTrading
callbacks["onBar"] = onBar
callbacks["onOrder"] = onOrder
callbacks["onTrade"] = onTrade
callbacks["afterTrading"] = afterTrading
callbacks["finalize"] = finalized
try{Backtest::dropBacktestEngine(strategyName)}catch(ex){print ex}
engine = Backtest::createBacktester(strategyName, userConfig, callbacks, , basicInfo)
2.4 Run a Backtest
After creating the backtesting engine with
Backtest::createBacktester, you can run a backtest as
follows. Here, tb is the corresponding minute-frequency market data. For field
and type descriptions of the market data, refer to Backtest Method
References.
Backtest::appendQuotationMsg(engine, tb)
2.5 Retrieve Backtest Results
After the backtest is complete, you can use the corresponding methods to retrieve daily positions, daily equity, a return summary, execution details, and the user-defined logical context within the strategy. For all methods used to retrieve backtest results, refer to Backtest Method References. The following figure shows the daily position data obtained in this example:
3. Performance Test
This section measures the performance of DolphinDB's medium- and high-frequency backtesting engine and the effect of JIT optimization. We used two datasets of 1-minute futures data from February 2023: one for a single AG main contract and another for 77 main contracts. The test mode was set to single-threaded. The test results are shown in Table 3-1.
The test environment used in this article is configured as follows:
-
DolphinDB v3.00.1 single node
-
CPU: Intel(R) Xeon(R) Silver 4216 CPU @ 2.10GHz, 64 cores
-
Memory: 400 GB
-
Disk: SSD, 12 Gbps
| Number of Symbols | Number of Orders | Total Time Without JIT Optimization (In Seconds) | Total Time With JIT Optimization (In Seconds) |
|---|---|---|---|
| 1 AG main contract, 1 month, 10,011 records | 149 | 0.25 | 0.08 |
| 77 main contracts, 1 month, 350,218 records | 2185 | 7.7 | 2.2 |
The table shows that DolphinDB processes medium- and high-frequency data efficiently and that enabling JIT optimization noticeably improve the backtesting performance.
4. Summary
This tutorial demonstrates how to backtest a medium- and high-frequency futures strategy using the DolphinDB backtesting engine, highlighting its excellent performance, rich strategy triggering mechanisms, and comprehensive results. We will continue to publish backtesting examples for stocks, options, and other instruments to further demonstrate DolphinDB's application across different markets and strategies.
