Implementation of Mid-Low Frequency Portfolio Backtesting in Stocks
In quantitative trading, medium to low frequency trading strategies are favored by investors due to their relatively stable return and lower technical barriers. These strategies are typically based on daily or minute-level data, capturing longer time-scale investment opportunities by analyzing market trends, valuation indicators, risk factors, and more. The core of such strategies lies in effective stock selection and position management to maximize returns while controlling risks. Compared to the short-term speculation of high frequency trading, medium to low frequency trading prioritizes adaptability and risk control in complex markets, aiming for higher capital efficiency and more stable returns.
This tutorial will focus on two typical medium to low frequency backtesting cases: one is a stock portfolio trading strategy based on index signals, and the other is a stock index futures trading strategy based on constituent stock signals. Through these cases, we will systematically explain how to use DolphinDB's backtesting engine to construct strategy frameworks, simulate trading processes, and analyze backtesting results.
1. Background Introduction
This tutorial provides a detailed demonstration of how to implement stock portfolio trading strategies and stock index futures trading strategies using DolphinDB’s backtesting engine. Before diving into the examples and scripting details, this chapter offers a brief introduction to the two types of medium to low frequency stock trading strategies for better understanding.
1.1 Stock Portfolio Trading Strategy Based on Index Signals
Recent years have seen increased complexity and uncertainty in financial markets, with higher volatility, stronger correlations between indices and stocks, and diversified multi-factor model application scenarios. In this context, the stock portfolio trading strategy based on index signals has emerged. By combining index signal market trend analysis with multi-factor stock screening, this strategy aims to address complex problems in portfolio management such as dynamic position adjustment, stock selection optimization, and risk control. The core logic of the strategy mainly involves: stock selection, weight allocation, and overall position adjustment.
- Stock selection:
- Screen portfolios that meet the conditions through flexibly configured multi-factor models and preset buy/sell conditions.
- Weight allocation:
- Allocate expected weights based on individual stock conditions (such as technical indicators, risk factors, etc.).
- Overall position adjustment:
- Dynamically adjust overall positions by tracking index signals. If actual positions differ from expected positions, immediately execute buy or sell operations to ensure the portfolio structure always meets expectations.
1.2 Stock Index Futures Trading Strategy Based on Constituent Stock Signals
The stock index futures trading strategy based on constituent stock signals is a strategy that predicts index trends by analyzing signals from index constituent stocks and trades stock index futures accordingly. Its core idea is to use microscopic signals from constituent stocks to capture macroscopic index trends, thereby generating profits in the stock index futures market. Its core logic involves several main steps: individual stock signal generation, constituent stock signal synthesis, index signal decision-making, and risk management.
- Individual stock signal generation:
- Conduct technical and fundamental analysis on index constituent stocks to generate individual stock signals.
- Each constituent stock's signal can be buy, sell, or hold.
- Index signal synthesis:
- At each minute-level snapshot, synthesize signals from all constituent stocks through equal weighting, market cap weighting, liquidity weighting, and other methods to generate the synthesized index signal.
- Index signal decision-making:
- Based on the synthesized index signal, decide whether to buy or sell
stock index futures. Common decision rules include:
- Long signal: Buy stock index futures when the index signal reaches a certain buy threshold.
- Short signal: Sell stock index futures when the index signal reaches a certain sell threshold.
- Hold signal: Maintain current positions when the index signal is between buy and sell thresholds.
- Based on the synthesized index signal, decide whether to buy or sell
stock index futures. Common decision rules include:
2. Implementation of Mid-Low Frequency Stock Portfolio Trading Strategy in DolphinDB
In this chapter, we will explain in detail how to use DolphinDB's built-in scripting language to write event-driven functions to implement a stock medium to low frequency portfolio trading strategy. Content includes strategy logic introduction, strategy implementation process, initial parameter configuration, event function definition, etc.
2.1 Strategy Logic Introduction
This strategy combines technical indicator signals with dynamic risk control mechanisms to screen and adjust stock portfolios, achieving risk control and return optimization. The overall trading logic is shown in Figure 2.1.
- Calculate technical indicators: First call the mytt module to calculate required technical indicators for subsequent operations.
- Initial position target selection: Iterate through all stocks, score and rank stocks based on a series of trend and momentum indicators, and screen stocks that meet buy conditions and don't meet exclusion conditions as the final portfolio. In this strategy, RSI (Relative Strength Index) and long-short moving average crossovers are used to construct the portfolio.
- Individual stock risk control: Conduct individual stock risk control based on stock volatility and RSI, limiting weights of stocks with excessive volatility and limiting maximum and minimum position ratios for individual stocks; when the RSI signal drops below 70, release risk control.
- Individual stock weight normalization: Normalize the final weights determined after risk control as the weights for each stock.
- Position adjustment: Calculate expected market value based on each stock's expected weight, compare it with current holding market value. Execute buy operations when expected market value is greater than holding market value; execute sell operations when expected market value is less than holding market value.
2.2 Strategy Implementation Process
The process of DolphinDB's backtesting engine is shown in Figure 2.2, including six steps. This section will use a general case to introduce how to implement the two types of backtesting modes described above.
2.2.1 Load Market Data and Calculate Technical Indicators
First, before executingthe backtesting logic, use the MyTT module to pre-calculate the following indicators, then merge them into the signal column of the minute-level data table messageTable. (The complete script can be referenced in attachment loadData1.dos.)
- Calculate short-term moving average shortMA and long-term moving
average longMA for each stock to determine trend signals and
identify buy or sell opportunities through moving average
crossovers.
use mytt // Short-term moving average def getShortTermMA(close, period=5){ return MA(close, period) } // Long-term moving average def getLongTermMA(close, period=20){ return MA(close, period) } - Use the
prevfunction to get individual stock's previous period short-term moving average prevShortMA and long-term moving average prevLongMA to judge whether there is a moving average crossover.update facTable set prevShortMA = prev(shortMA).bfill(), prevLongMA = prev(longMA).bfill() - Calculate individual stock's RSI to evaluate overbought or
oversold conditions, helping control market sentiment risk during
trading.
use mytt def getRSI(close, N = 24){ return RSI(close, N = 24) // Directly call the RSI function in mytt module } - Calculate individual stock's volatility to measure price
fluctuations, providing a risk control basis to avoid negative impact of
high-risk assets on the
portfolio.
use mytt def getVolatility(close, period=10){ returns = DIFF(close) / REF(close, 1) return STD(returns, period) } - Trading status check. Define a user-defined function
checkTradingStatusto assess individual stock trading status before sending individual stock trading signals. If there are abnormal situations such as suspension or limit up/down, do not trade.def checkTradingStatus( open, high, low, volume, turnover, close, upLimitPrice, downLimitPrice ){ // Check if suspended isHalted = (open==0 || high==0 || low==0 || volume==0 || turnover==0) // Check if limit up/down isPriceLimited = (close>=upLimitPrice || close<=downLimitPrice) // Return trading status return iif(isHalted || isPriceLimited, 0, 1) }
2.2.2 Configure Parameters
Start and end dates, initial capital, market data type, etc. can all be configured through the parameter config. These parameters allow users to flexibly adjust conditions to simulate different market environments and trading strategy effects. A sample of the initial parameter configuration code is provided below.
config=dict(STRING,ANY) // Store backtest parameters in dictionary format
config["startDate"]= 2024.01.01 // Set backtest start time
config["endDate"]= 2024.12.31 // Set backtest end time
config["strategyGroup"] = "stock" // Select backtest mode, stock mode in this case
config["frequency"] = 0 // Specify frequency for synthesizing snapshots from tick data
config["cash"] = 10000000. // Initial capital, 10 million
config["commission"] = 0.0005 // Trading commission
config["tax"] = 0.0 // Ignore tax
config["dataType"] = 3 // Minute-level
config["matchingMode"] = 3 // Select trading mode, trade at order price in this case
config["msgAsTable"] = false // Pass market data in dictionary form
// Set logic global context
config["context"] = dict(STRING,ANY)
context = dict(STRING,ANY)
context["activeTime"] = 14:30m // Set daily trading time
context["maxPosition"] = 0.1 // Set maximum position ratio for individual stock
context["minPosition"] = 0.001 // Set minimum position ratio for individual stock
config["context"] = context
2.2.3 Define User-defined Callback Functions
- Set Auxiliary Functions
Auxiliary functions are mainly used to support core logic implementation in the
onBarfunction, handling functions such as order generation and daily position updates.getSellVolume: Calculate the sell volume, ensuring compliance with trading rules.
def getSellVolume(price, value, position){ // If sell = position, clear position; if less than position, sell in multiples of 100 // Here / represents integer division vol = int(value/price)/100*100 // Ensure the sell volume is a multiple of 100 return int(iif(vol < position, vol , position)) }getBuyVolume: Calculate the buy volume, ensuring compliance with trading rules (STAR Market requires multiples of 200, others multiples of 100).
def getBuyVolume(symbol, price, value){ // If symbol starts with 688, must be multiples of 200; otherwise multiples of 100 vol = int(value/price) return int(iif(symbol like "688%",vol/200*200, vol/100*100)) }executeSellOrders: Submit sell orders based on strategy logic.- First check if the stock is suspended or limit up/down; if neither, trading can proceed.
- Next call
Backtest::getPositioninterface to get current position, extract closing price from msg, and extract the difference between current holding market value and expected market value fromsellDeltaValueDict. - Calculate the sell volume based on this difference and submit sell order.
def executeSellOrders(mutable context, msg, sellDeltaValueDict, sellSymbol, label="SellClose"){ /* Sell function @Parameters --------------------- context: `DICT` Backtest context msg: `DICT` Market data sellDeltaValueDict: `DICT` Position change value for stocks to be sold sellSymbol: `STRING` Stock code to be sold label: `STRING` Sell order identifier for distinguishing different sell scenarios */ // Check if suspended or limit up/down; skip the sell operation if either condition is met &tradeFlag = checkTradingStatus( msg[sellSymbol]["open"], msg[sellSymbol]["high"], msg[sellSymbol]["low"], msg[sellSymbol]["volume"], msg[sellSymbol]["signal"][0], msg[sellSymbol]["close"], msg[sellSymbol]["upLimitPrice"], msg[sellSymbol]["downLimitPrice"] ) if (tradeFlag == 1){ // Get position information &pos = Backtest::getPosition(context["engine"], sellSymbol)["longPosition"] if (pos == NULL){ pos = 0.0 } &price = msg[sellSymbol]["close"] &value = abs(sellDeltaValueDict[sellSymbol]) // Calculate the sell volume &sellVolume = getSellVolume(price, value, pos) if (pos > 0 and sellVolume > 0) { Backtest::submitOrder(context["engine"], (sellSymbol, context["tradeTime"], 5, price, sellVolume, 3), label) } } }executeBuyOrders: Submit buy orders based on strategy logic.- First check if the stock is suspended or limit up/down; if neither, trading can proceed.
- Next call
Backtest::getPositioninterface to get current position, extract closing price from msg, and extract the difference between current holding market value and expected market value frombuyDeltaValueDict. - Calculate the buy volume based on this difference and submit buy order.
def executeBuyOrders(mutable context, msg, buyDeltaValueDict, buySymbol, label="BuyOpen"){ /* Buy function @Parameters --------------------- context: `DICT` Backtest context msg: `DICT` Market data buyDeltaValueDict: `DICT` Position change value for stocks to be bought buySymbol: `STRING` Stock code to be bought label: `STRING` Buy order identifier for distinguishing different buy scenarios */ // Check if suspended or limit up/down; skip the buy operation if either condition is met &tradeFlag = checkTradingStatus( msg[buySymbol]["open"], msg[buySymbol]["high"], msg[buySymbol]["low"], msg[buySymbol]["volume"], msg[buySymbol]["signal"][0], msg[buySymbol]["close"], msg[buySymbol]["upLimitPrice"], msg[buySymbol]["downLimitPrice"] ) if (tradeFlag == 1){ // Get available cash &cash = Backtest::getTotalPortfolios(context["engine"])["cash"][0] &price = msg[buySymbol]["close"] &value = buyDeltaValueDict[buySymbol] // Calculate the buy volume before conversion &buyVolume = getBuyVolume(buySymbol, price, min(cash, value)) // Check if available cash is sufficient; if not, exit function if (cash < buyVolume * price * (1 + context["commission"])) return if (buyVolume > 0) { Backtest::submitOrder(context["engine"], (buySymbol, context["tradeTime"], 5, price, buyVolume, 1), label) } } }sortDict: Sort a dictionary based on its value.
def sortDict(x, ascending=true){ // Sort dictionary x_idx = isort(x.values(), ascending) res = dict(x.keys()[x_idx], x.values()[x_idx],true) return res } - Set Strategy Callback Functions
(1) Backtest Initialization Function
initializeThis function is triggered at the start of the backtest. It is typically used to define and initialize global variables.
def initialize(mutable context){ // Initialization callback function print("initialize") }(2) Intraday Callback Trading Function
onBarThis strategy optimizes portfolio structure based on market conditions and risk control rules. In the trading callback triggered every minute, this strategy first judges whether the current time is the specified trading minute. If it is, the strategy enters the trading logic to conduct comprehensive analysis of current market data and historical positions, screen stocks that meet buy conditions but not the exclusion conditions, and dynamically adjust position weights during backtest to construct the final position portfolio.
Core Features:
- Concentrated position building: Used for initial position building phase to screen and allocate weights for stocks without positions.
- Buy logic: Short-term moving average crosses above long-term moving average or RSI < 70.
- Exclusion logic: Short-term moving average crosses below long-term moving average or RSI > 80.
- Weight allocation: Based on the number of screened stocks for initial weight allocation, weight set to 1\number of stocks.
- Risk management:
- Reduce weights for high volatility stocks:
- When 0.005 < volatility < 0.01, weight becomes 0.5 of the original.
- When volatility > 0.01, weight becomes 0.1 of the original.
- Limit individual stock weights to fall within minimum and maximum position ratios.
- Reduce weights for high volatility stocks:
- Weight normalization: Ensure total weight equals 1.
- Position adjustment:
- Conduct sell or buy operations based on difference between current positions and expected market value.
- Prioritize sell logic, then execute buy logic.
Trading Logic:
- Check if it's trading time. First check if current time
is the preset trading time (
activeTime). If not, return directly to avoid unnecessary calculations.
if (context["activeTime"] != minute(context["tradeTime"])) { // Return directly if not trading minute return }- Initialize signals and lists. Initialize two lists, buyList and deleteList, to store stocks meeting buy conditions and stocks meeting exclusion conditions respectively.
// Initialize &buyList = array(SYMBOL) &deleteList = array(SYMBOL)- Iterate through each stock, calculate buy/sell signals,
screen stocks to buy and sell.
- Stocks to buy
buyList: When short moving average crosses above long moving average (getCrossreturns[True, False]) or RSI < 70, and if the above conditions are met and trading status is normal, add the stock to buyList. - Stocks to exclude
deleteList: When short moving average crosses below long moving average (getCrossreturns[False, True]) or RSI > 80, and if the above conditions are met and trading status is normal, add the stock to deleteList.
- Stocks to buy
for (istock in msg.keys()){ &istock = istock &pos = Backtest::getPosition(context["engine"], istock)["longPosition"] &price = msg[istock]["close"] &tradeFlag = checkTradingStatus(msg[istock]["open"], msg[istock]["high"], msg[istock]["low"], msg[istock]["volume"], msg[istock]["signal"][0], msg[istock]["close"], msg[istock]["upLimitPrice"], msg[istock]["downLimitPrice"]) // Buy logic: short moving average crosses above long moving average, or RSI < 70 if (getCross(msg[istock]["signal"][3], msg[istock]["signal"][2], msg[istock]["signal"][6], msg[istock]["signal"][5])[0] or msg[istock]["signal"][1] < 70 and tradeFlag) { buyList.append!(istock) } // Exclusion logic: short moving average crosses below long moving average, or RSI > 80 if (getCross(msg[istock]["signal"][3], msg[istock]["signal"][2], msg[istock]["signal"][6], msg[istock]["signal"][5])[1] or msg[istock]["signal"][1] > 80 and tradeFlag) { deleteList.append!(istock) } }- Screen final portfolio list and conduct individual stock risk
control.
- Exclude stocks meeting exclusion conditions to get the
stock list
posListrequiring key attention as the final portfolio list. - Initialize each stock's weight as equal weight allocation (1\number of stocks).
- Adjust weights based on volatility and RSI : when 0.005 < volatility < 0.01, weight becomes 0.5 of the original; when volatility > 0.01, weight becomes 0.1 of the original, record risk control status; when RSI <70, release risk control.
- Limit individual stock weights within maxPosition and minPosition range.
- Exclude stocks meeting exclusion conditions to get the
stock list
// Screen stocks meeting buy conditions but not exclusion conditions; operate on this stock pool &posList = (set(buyList) - set(deleteList)).keys() if (count(posList) != 0) { // Create weight dictionary weightDict = dict(SYMBOL,DOUBLE) for (istock in posList) { // First initialize weight, 1\number of stocks weightDict[istock] = 1 \ posList.size() // Individual stock risk control &volatility = msg[istock]["signal"][4] &RSI = msg[istock]["signal"][1] // Risk control reset logic if (context["riskDict"][istock] == 1 && RSI < 70) { context["riskDict"][istock] = 0 // Set risk control status to 0 } // Individual stock risk control logic: limit weights exceeding volatility threshold if (0.005 < volatility < 0.01) { weightDict[istock] *= 0.5 // Reduce weights for high volatility stocks context["riskDict"][istock] = 1 // Set risk control status to 1 } else if (volatility > 0.01) { weightDict[istock] *= 0.1 // Reduce weights for high volatility stocks context["riskDict"][istock] = 1 // Set risk control status to 1 } // Limit maximum and minimum position ratios for individual stocks weightDict[istock] = min(weightDict[istock], context["maxPosition"]) weightDict[istock] = max(weightDict[istock], context["minPosition"]) }- Individual stock weight normalization. Normalize individual stock weights to make the sum of all individual stock weights equal to 1, ensuring all capital is reasonably allocated to each stock in the portfolio.
// Individual stock weight normalization weightSum = sum(weightDict.values()) if (weightSum != 0) { for (istock in posList) { weightDict[istock] /= weightSum } }- Calculate position market value changes based on expected
market value and current holding market value.
- Create dictionaries for expected market value, holding market value, sell delta, and buy delta, to store each stock's expected market value, current holding market value, position changes for stocks to be sold, and position changes for stocks to be bought, respectively.
- First call
Backtest::getTotalPortfoliosinterface to get current account total equity, obtaining account holding total market value and available cash, and calculate expected market value combined with each stock's weight. - Next call
Backtest::getPositioninterface to get each stock's current position, and calculate current holding market value combined with closing price. - Finally compare expected market value and holding market
value:
- If expected market value < holding market value, add to sell dictionary.
- If expected market value > holding market value, add to buy dictionary.
// Create expected market value, holding market value, and deltaValue dictionaries expectValueDict = dict(SYMBOL,DOUBLE) positionValueDict = dict(SYMBOL,DOUBLE) sellDeltaValueDict = dict(SYMBOL,DOUBLE) buyDeltaValueDict = dict(SYMBOL,DOUBLE) // Get net assets &equity = Backtest::getTotalPortfolios(context["engine"])["totalEquity"][0] // Calculate expected market value and holding market value for (istock in posList) { ExpectValue = equity * weightDict[istock] expectValueDict[istock] = ExpectValue // Use Backtest::getPosition to get daily position information longPosition = Backtest::getPosition(context["engine"], istock)["longPosition"] if (longPosition == NULL) { positionValue = 0.0 } else { positionValue = longPosition * msg[istock]["close"] } positionValueDict[istock] = positionValue deltaValue = ExpectValue - positionValue if (deltaValue > 0) { buyDeltaValueDict[istock] = deltaValue } else if (deltaValue < 0) { sellDeltaValueDict[istock] = deltaValue } }- Iterate through sell dictionary and buy dictionary, and execute sell and buy orders from large to small based on absolute value of difference between expected market value and current holding market value.
// Sell logic sellSymbol = sellDeltaValueDict.keys() if (count(sellSymbol) != 0) { sortedSellDeltaValueDict = sortDict(sellDeltaValueDict, true) // Dictionary sort sortedSellSymbol = sortedSellDeltaValueDict.keys() // Iterate through sell list, sell stocks for (istock in sortedSellSymbol) { executeSellOrders(context, msg, sortedSellDeltaValueDict, istock) // Update position information longPosition = Backtest::getPosition(context["engine"], istock)["longPosition"] if (longPosition == NULL) { positionValue = 0.0 } else { positionValue = longPosition * msg[istock]["close"] } positionValueDict[istock] = positionValue } } // Buy logic buySymbol = buyDeltaValueDict.keys() if (count(buySymbol) != 0) { sortedBuyDeltaValueDict = sortDict(buyDeltaValueDict, false) // Dictionary sort sortedBuySymbol = sortedBuyDeltaValueDict.keys() // Iterate through buy list, buy stocks for (istock in sortedBuySymbol) { executeBuyOrders(context, msg, sortedBuyDeltaValueDict, istock) // Update position information longPosition = Backtest::getPosition(context["engine"], istock)["longPosition"] if (longPosition == NULL) { positionValue = 0.0 } else { positionValue = longPosition * msg[istock]["close"] } positionValueDict[istock] = positionValue } }
2.2.4 Create the Backtesting Engine
Call Backtest::createBacktester to create backtesting
engine.
strategyName="StockBackTest"
callbacks = dict(STRING, ANY)
callbacks["initialize"] = initialize
callbacks["beforeTrading"] = beforeTrading
callbacks["onBar"] = onBar
callbacks["onTick"] = onTick
callbacks["onOrder"] = onOrder
callbacks["onTrade"] = onTrade
callbacks["afterTrading"] = afterTrading
callbacks["finalize"] = finalize
try{Backtest::dropBacktestEngine(strategyName)}catch(ex){print ex}
engine = Backtest::createBacktester(strategyName, config, callbacks,true, )
2.2.5 Run Backtest
After creating the backtesting engine through
Backtest::createBacktester, market data can be passed
into the backtesting engine in the following way to execute backtest.
messageTable is the corresponding market data passed in
dictionary format.
timer Backtest::appendQuotationMsg(engine,messageTable)
a = select * from messageTable where tradeTime=max(tradeTime) limit 1
update a set symbol = "END"
Backtest::appendQuotationMsg(engine, a) // Dictionary structure
2.2.6 Get Backtesting Results
Calling interfaces such as
Backtest::getTradeDetails(engine),
Backtest::getDailyTotalPortfolios(engine),
getReturnSummary(engine) allows viewing corresponding
backtesting results, such as checking order cancellations, viewing the
overall portfolio, and analyzing return performance, etc.
// Get trade details
select * from Backtest::getTradeDetails(engine) where orderStatus==1
// Check order rejection cancellations
select * from Backtest::getTradeDetails(engine) where orderStatus==-1
// View the overall portfolio
dailyTotalPortfolios=Backtest::getDailyTotalPortfolios(engine)
// View return
returnSummary=Backtest::getReturnSummary(long(engine))
3. Implementation of Stock Index Futures Trading Strategy in DolphinDB
In this chapter, we will introduce how to use DolphinDB's built-in scripting to write time-driven functions to implement stock index futures trading strategies. Content includes the implementation process of strategy logic, initialization parameter configuration, how to generate individual stock signals and index signals in strategy logic context, etc.
3.1 Strategy Logic Introduction
Below is a simple stock index futures strategy based on moving average crossovers and RSI, mainly used to generate buy/sell signals for individual stocks, and decide whether to open or close positions in stock index futures by synthesizing these signals. The core logic of this strategy includes individual stock signal generation and index signal synthesis, ultimately simulating trading through the backtesting engine.
- Stock signal generation: First generate individual stock opening signals (long signal marked as 1, short signal marked as -1) and closing signals (marked as 0) based on indicators such as dual moving averages, RSI, volatility, etc.
- Index signal synthesis: Based on individual stock signals, obtain index signals through equal weighting, and get corresponding index target opening signals (marked as 1) and closing signals (marked as 0)
In this case, we take CSI 300 stock index futures as an example, passing stock index futures and constituent stocks as minute-level futures market data into the backtesting engine. The strategy’s orders are executed at the order prices. Additionally, the backtest process of this case is similar to the previously introduced portfolio backtest case, and repeated steps will not be explained in detail.
3.2 Strategy Implementation Process
3.2.1 Individual Stock Signal Generation
- Initialization Function
First, strategy parameters can be set through the parameter context in the initialization function
initialize.initializeis only triggered once after creating the engine. In this case, dual moving averages, RSI, volatility and other indicators must be subscribed to viasubscribeIndicatorin the initialization function, and global parameters such as buy/sellSignal, stockSignal, indexSignal, shortLongMA, etc. must be defined in the initialization function.def initialize(mutable contextDict){ d = dict(STRING, ANY) d["shortMA"] = <MA(close, 5)> d["longMA"] = <MA(close, 20)> d["RSI"] = <RSI(close, 24)> d["volatility"] = <getVolatility(close, 10)> Backtest::subscribeIndicator(contextDict["engine"], "kline", d) // Buy signal threshold contextDict["buySignal"] = 0.995 // Sell signal threshold contextDict["sellSignal"] = 0.995 // Signal dictionary contextDict["stockSignal"] = dict(STRING, ANY) contextDict["indexSignal"] = dict(STRING, ANY) // Signal dictionary for ShortMA and LongMA // set to -1 when the ShortMA is greater than the LongMA, // set to 1 when the LongMA is greater than the ShortMA, // and set to 0 otherwise contextDict["shortLongMA"] = dict(STRING, ANY) } - Open Signal Detection
After subscribing to the above indicators, the strategy logic function
onBargenerates opening signals based on indicators such as shortMA, longMA, RSI, and volatility, and the global dictionary shortLongMA:- At market open, when the short MA crosses above the long MA, RSI is less than 70, and volatility is below 0.02, a long signal is generated (marked as 1).
- At market open, when the long MA crosses above the short MA, RSI is greater than 80, or volatility exceeds 0.08, a short signal is generated (marked as -1).
def onBar(mutable contextDict, msg, indicator){ lowDict = contextDict["lowPrice"] highDict = contextDict["highPrice"] stockSignalDict = contextDict["stockSignal"] // Stock signal dictionary indexSignalDict = contextDict["indexSignal"] // Index signal dictionary keysAll = msg.keys() // Generate stock signals (e.g., CSI 300 constituents + index IF) keys1 = keysAll[!(keysAll like "IF%")] for (i in keys1){ &istock = msg[i].symbol &close = msg[i].close &shortMA = indicator[i].shortMA &longMA = indicator[i].longMA &RSI = indicator[i].RSI &volatility = indicator[i].volatility // Opening signal if (minute(contextDict["tradeTime"])==09:31m){ stockSignalDict[istock] = iif( contextDict[istock]==1 && shortMA>longMA && RSI<70 && volatility<0.02, 1, iif( (contextDict[istock]==-1 && longMA>shortMA) || RSI>80 || volatility>0.05, -1, 0 ) ) } // Record short vs. long MA relationship at market close if (minute(contextDict["tradeTime"])==15:00m){ shortLongMADict[istock] = iif( shortMA>longMA, -1, iif(shortMA<longMA, 1, 0) ) } // Additional logic omitted here } - Close Signal Detection
After generating individual stock opening signals, the
onBarfunction generates closing signals based on the stock’s minute-level closing price and the short MA:- Long position: If the closing price falls below the short MA, a closing signal is generated (marked as 0).
- Short position: If the closing price rises above the short MA, a closing signal is generated (marked as 0).
// Closing signals if (stockSignalDict[istock]==1){ stockSignalDict[istock] = iif(close<shortMA, 0, stockSignalDict[istock]) } else if (stockSignalDict[istock]==-1){ stockSignalDict[istock] = iif(close>shortMA, 0, stockSignalDict[istock]) }
3.2.2 Index Signal Synthesis
After generating signals for all constituent stocks, this case takes the CSI 300 Index Futures as an example.
In the onBar function, the index signal indexSignal
is synthesized through equal-weight aggregation of the constituent stock
signals. Based on this index signal, corresponding open and close signals
are generated and submitted by Backtest::submitOrder.
- Opening Signals:
- When indexSignal > 0.75, open a long position.
- When indexSignal < -0.75, open a short position.
- Closing Signals:
- If long and indexSignal < 0.25, close the position.
- If short and indexSignal > -0.25, close the position.
// Index synthesis logic
keys2 = keysAll[keysAll like "IF%"]
for (i in keys2){
&index = msg[i].symbol
&symbolSource = msg[i].symbolSource
// Equal-weighted index signal
&indexSignal = stockSignalDict.values().sum()/300
&price = msg[i].close
if (indexSignal>0.75){
// Mark long open
indexSignalDict[index] = 1
Backtest::submitOrder(contextDict["engine"],
(index, symbolSource,contextDict["tradeTime"],
5, price+0.02, 0., 2, 1, 0), "buyOpen")
}
if (indexSignal<-0.75){
// Mark short open
indexSignalDict[index] = -1
Backtest::submitOrder(contextDict["engine"],
(index, symbolSource,contextDict["tradeTime"],
5, price+0.02, 0., 2, 2, 0), "sellOpen")
}
if (indexSignalDict[index]==1 and indexSignal<0.25){
// Close position
indexSignalDict[index] = 0
Backtest::submitOrder(contextDict["engine"],
(index, symbolSource,contextDict["tradeTime"],
5, price-0.02, 0., 2, 3, 0), "sellClose")
}
if (indexSignalDict[index]==-1 and indexSignal>-0.25){
// Close position
indexSignalDict[index] = 0
Backtest::submitOrder(contextDict["engine"],
(index, symbolSource,contextDict["tradeTime"],
5, price-0.02, 0., 2, 4, 0), "buyClose")
}
}
4. Summary
We successfully built two medium to low frequency porfolio trading strategies for stocks and index futures based on DolphinDB, covering the complete process from signal generation and backtesting execution to strategy optimization. With its outstanding distributed architecture and high-performance time-series data processing capabilities, DolphinDB demonstrates exceptional efficiency and stability in real-time data analysis, signal generation, dynamic adjustment, and strategy validation.
Through modular design, we achieved flexible switching among multiple strategies, ensuring their stability and feasibility.
5. Appendix
- Portfolio Backtest
- Data Loading Script: loadData1.dos
- Backtest Script: backTest1.dos
- Index Futures Backtest
- Data Simulation Script: loadData2.dos
- Backtest Script: backtest2.dos
