Why Your Backtests Fail in Margin Trading & Securities Lending (And How to Fix It)
DolphinDB22 min read·Just now--
Margin trading and securities lending are forms of leveraged trading that amplify investment returns, providing investors with greater access to capital and more trading opportunities. In quantitative investing, margin trading and securities lending are widely used in a variety of trading practices and strategies, including leveraged and high-frequency trading, hedging, market-neutral, and arbitrage strategies. At present, many quantitative fund managers often overlook margin trading and securities lending transactions when researching and backtesting their trading strategies, relying on standard strategy backtesting. However, they introduce margin trading and securities lending in the actual transactions. As a result, the backtest results differ significantly from the actual results.
Margin trading and securities lending mainly differ from standard trading in the following three aspects:
- In addition to commissions, backtesting a margin trading and securities lending strategy also involves costs such as margin interest and securities lending fee.
- The margin trading and securities lending market is highly dynamic. Key attributes of the underlying securities change with market sentiment, such as the collateral discount rate for the day, the margin usage ratio, and whether a security is eligible for margin trading or securities lending.
- The risk management module for margin trading and securities lending strategy backtesting is more stringent, imposing limits on the account’s maintenance margin ratio, long concentration, and short concentration.
The Backtest plugin of DolphinDB now supports margin trading and securities lending strategy backtesting. This backtesting framework comprehensively incorporates key elements to make the backtest results more closely reflect actual outcomes, including transaction costs, the daily eligible securities for margin trading and securities lending, the collateral discount rate, and the risk management module, thereby narrowing the gap between backtesting results and live trading outcomes.
Backtesting margin trading and securities lending strategy differs significantly from other types of strategy backtesting in terms of operation, risk management, and capital usage. Therefore, their parameter configuration, market data, and methods used also differ. This tutorial demonstrates how to implement margin trading and securities lending strategy backtesting using the Backtest plugin of DolphinDB, covering an introduction to margin trading and securities lending transactions, DolphinDB-based solution for margin trading and securities lending strategy backtesting, and specific backtesting cases.
Note: All scripts in this tutorial are compatible with DolphinDB Server versions 2.00.14.1, 3.00.2.1, and later.
1. Introduction
Margin trading and securities lending are forms of securities margin trading in which investors borrow funds or securities to obtain leveraged trading opportunities to amplify returns or hedge risk. Margin trading refers to borrowing funds from a broker to purchase securities. In contrast, securities lending involves borrowing securities from a broker, selling them, and intending to buy back the same quantity at a lower price in the future and return them to the broker. Margin trading and securities lending generate profits based on expected price movements. Margin trading and securities lending require margin. It can take the form of cash, stocks, or bonds, and brokers assign a collateral discount rate based on the asset quality. Investors must pay costs such as interest, securities lending fees, and transaction fees. Note that for margin trading and securities lending transactions, only marginable securities can be traded, and only securities eligible as collateral can be purchased as collateral. Because of the leverage effect, this mechanism increases both potential gains and losses, exposing investors to challenges such as forced liquidation, market volatility, and cost accumulation. In addition, only stocks that meet specific criteria are eligible for such transactions.
1.1 Margin Requirements
In margin trading and securities lending, the margin system is a prerequisite to protecting the exchange’s interests. When engaging in margin trading and securities lending, investors must trade using a margin account. They are required to transfer cash or eligible collateral securities into the account before engaging in margin trading and securities lending transactions.
- For the SSE, SZSE, and BSE, the margin requirement ratio of securities lending for individual investors shall exceed 100%. While for private fund investment institutions, it shall exceed 120%. (Margin Requirement Ratio = Total Margin / Amount of Margin Trading or Securities Lending * 100%)
1.2 Trading Modes
Margin trading and securities lending are two distinct types of transactions.
- Margin trading: Investors deposit cash or eligible collateral securities into a margin account. They can then borrow funds to purchase securities within the approved limit, using leverage to amplify both potential returns and risks.
- Securities lending: Investors deposit cash or eligible collateral securities into a margin account. They can borrow securities to sell them in the market within the approved limit, using leverage to amplify both potential returns and risks.
In practice, these two transactions represent two opposite directional bets. Margin financing is a long operation, while securities lending is a short operation. Both methods can be used to expand long and short positions and to flatten intra day positions for arbitrage or stop-loss.
1.3 Leverage Effect
Margin trading and securities lending inherently involve leverage, allowing investors to control larger positions with relatively small amounts of capital. The following example illustrates the calculation of the leverage ratio. In margin trading, the investor uses both personal funds and borrowed funds to purchase stocks. Assuming the margin ratio is 100%, half of the investor’s funds comes from the margin account, and the other half is obtained through margin trading. The investor controls 2 units of total capital with 1 unit of equity. Ignoring other factors, the leverage ratio is 2.0. The actual leverage ratio is typically lower. Once the assets in the margin account change, for example, investment in securities, the amount available for margin trading declines after being discounted by a collateral discount rate. At a collateral discount rate of 70%, if the entire balance in the margin account is used for investment, only 70% of the margin can be offset. The actual leverage ratio is 170/100 = 1.7. Leverage ratio formula: Leverage Ratio = Total Available Balance After Margin Trading and Securities Lending / Initial Fund Balance * 100%.
2. DolphinDB-based Solution
The DolphinDB Backtest plugin supports backtesting of the margin trading and securities lending strategy for A-shares listed on the Shanghai and Shenzhen stock exchanges, covering snapshot, minute-level, and daily data. In addition, the Backtest plugin supports calculations for margin interest rate, securities lending fee, and compensation for entitlements, as well as risk management constraints such as the line of credit, eligible marginable securities, long concentration, and short concentration. The following sections introduce the parameter configuration, market data, and methods of the plugin.
2.1 Parameter Configuration
As with other assets such as stocks, you can configure basic parameters, including the start date, end date, initial capital, commission, and market data frequency. When margin trading and securities lending are involved, additional margin interest and securities lending fees are incurred. In addition, the strategy includes constraints such as long and short concentration limits, as well as calculations on compensation for entitlements. When creating the backtesting engine, you can configure parameters such as the margin interest rate, securities lending rate, long and short concentration, dividends and ex-rights adjustments, and set core positions for collateral purchases, margin trading, and securities lending.
The configuration parameters of the backtesting engine are stored in a dictionary. If the parameter strategyGroup is set to securityCreditAccount, margin trading and securities lending strategy backtesting will be performed. Other parameters related to margin trading and securities lending include the line of credit (lineOfCredit), the margin interest rate (marginTradingInterestRate), the securities lending rate (secuLendingInterestRate), the long concentration (longConcentration), and the short concentration (shortConcentration).
Margin trading and securities lending backtesting supports the dynamic processing of dividends and stock splits on holdings acquired through margin trading or collateral purchases, as well as compensation for entitlements. You only need to assign the dividend adjustment table to the stockDividend parameter in the config file. See Table 2 for details.
In margin trading and securities lending backtesting, you can set the initial position when creating the backtesting engine. The initial position is configured through thesetLastDayPositionparameter in the config file. The strategy’s initial equity is the sum of the value of the initial holdings and the initial cash. After the backtesting, the net value of the strategy and the initial position can be retrieved through the netValue and bottomNetValue parameters, respectively, via the Backtest::getDailyTotalPortfolios method. See Table 3 for details.
2.2 Market Data
When backtesting a margin trading and securities lending strategy, the market data format is the same as that for stocks. This example uses dailymarket data. The parameters for minute-level and daily market data:
2.3 Method Reference
The backtesting engine provides methods for you to create engines, submit orders, and retrieve positions and available cash. The following section introduces the different methods of margin trading and securities lending strategy backtesting.
First, the margin trading and securities lending backtesting engine can be created using Backtest::createBacktestEngine method to create. A future release will support the new method Backtest::createBacktester with JIT optimization. When creating a backtesting engine for a margin trading and securities lending strategy, you must specify the securityReference parameter to provide the basic information. This table contains information such as whether the security is eligible as margin collateral, the collateral discount rate, the margin requirement ratios for margin trading and securities lending, and whether it is eligible for margin trading and securities lending. See Table 5 for details.
Backtest::createBacktestEngine(name, config, securityReference , initialize,
beforeTrading, onBar, onSnapshot, onOrder, onTrade, afterTrading, finalize)The method to submit orders is Backtest::submitOrder. Its parameters include the backtesting engine, the order information, and a user-defined order tag. Unlike other asset classes, which have only four order types (buy to open, sell to open, sell to close, and buy to close), margin trading and securities lending transactions have eight order types:
- 1: collateral purchase
- 2: collateral sale
- 3: margin trading
- 4: short selling
- 5: direct repayment
- 6: security sale for repayment
- 7: direct security return
- 8: security purchase for return
For direct repayment (order type 5), the fourth parameter of orderMsg specifies the repayment amount (symbol, order time, order type, repayment amount, order quantity, 5).
Backtest::submitOrder(engine,orderMsg,label)- orderMsgcontains the order information (symbol, order time, order type, order price/repayment amount, order quantity, order type).
Margin trading and securities lending strategy backtesting uses three methods to retrieve position information: Backtest::getMarginSecuPosition for collateral purchase positions, Backtest::getMarginTradingPositio for margin trading positions, and Backtest::getSecuLendingPosition for securities lending positions. These three methods are used to retrieve information such as the position quantity, average position price, and daily trading volume. For daily end-of-day position information, including margin trading and securities lending positions, trading value, profit and loss, and long/short concentration, use the Backtest::getDailyPosition method. Use the Backtest::getTotalPortfolios and Backtest::getDailyTotalPortfolios methods to respectively retrieve real-time and daily strategy equity metrics, including margin trading and securities lending liabilities, interest, maintenance margin ratio, margin balance, yields, profit and loss, and long concentration. Use the Backtest::getReturnSummary method to retrieve metrics such as strategy returns and initial position returns.
3. Key Considerations
3.1 Eligible Securities
Not all securities listed on the Shanghai and Shenzhen Stock Exchanges are eligible for margin trading and securities lending. When creating the backtesting engine, you can pass the basic information table to the engine through the securityReference parameter of the Backtest::createBacktestEngine method. This table includes the lists of securities eligible for margin trading and securities lending, and of securities eligible as margin collateral, as well as the corresponding collateral discount rates, the margin requirement ratio for financing, and the margin requirement ratio for securities lending for each trade date. See Table 5 for details.
3.2 Maintenance Margin Ratio and Concentration Limits
To meet regulations and protect capital, exchanges generally set a warning line for margin trading and securities lending, measured by the maintenance margin ratio. The formula is: Maintenance Margin Ratio = Assets (Equity + Liabilities) / Liabilities * 100%. In actual transactions, when the maintenance margin ratio exceeds 300%, the margin account is considered very safe, and the margin can be withdrawn. When it falls to 140%, the warning line is triggered, and no new margin trading or securities lending is allowed. When it falls to 130%, additional collateral must be posted to restore the maintenance margin ratio above 140%. Otherwise, forced liquidation may occur. When it falls to 110%, forced liquidation is triggered.
Concentration refers to the proportion of total investment allocated to a single security or to a set of top securities within the portfolio. The higher the concentration, the greater the exposure to that security and the higher the concentration risk. Lower concentration indicates better diversification and therefore lower concentration risk.
The risk control module of the backtesting engine automatically controls risk by monitoring metrics such as the maintenance margin ratio, long concentration, and short concentration in real time. If a trade or price movement causes the user account to breach its limits, the engine will either stop the backtest or reject the trade. If the strategy’s maintenance margin ratio falls below the call line, the engine will stop the backtest.
The limits on long concentration and short concentration depend on the real-time maintenance margin ratio:
Note: If long concentration (longConcentration) or short concentration (shortConcentration) is not configured, no corresponding concentration limits will be applied.
3.3 Line of Credit
Exchanges grant investors a line of credit based on their margin balance and credit profile. The line of credit can be divided into margin trading and securities lending components. Increasing the collateral balance in the margin account can raise the line of credit. The actual line of credit is subject to the exchange’s approval. All calculations based on the line of credit assume the maximum possible amount and therefore may not fully reflect the actual transaction limit.
You can set the line of credit through the lineOfCredit parameter. Note that this setting should be set realistically to match actual transactions as closely as possible to improve simulation accuracy.
3.4 Prioritize Closing Margin Position When Selling Collateral
When selling collateral, if only the collateral position exists, it is closed directly. If both collateral purchases and margin positions exist, the margin trading position is closed first, and the remaining quantity closes the collateral position. Retrieve the margin trading and collateral positions through Backtest::getMarginTradingPosition and Backtest::getMarginSecuPosition before selling.
3.5 Other Considerations
- A security sold short on day T cannot be closed on day T.
- Cash from short selling cannot be used to buy stocks or bonds, but it can be used to buy Bond ETFs or Money Market ETFs.
- The backtesting engine currently does not support the rollover mechanism.
4. Backtesting Cases
Margin trading and securities lending strategy is an important mean of improving investment efficiency and expanding investment scale. The DolphinDB-based solution for margin trading and securities lending strategy backtesting provides investors with an efficient, convenient tool to develop and validate these strategies. You can simulate historical market data and backtest different margin trading and securities lending strategy, thereby evaluating their potential returns and risks. This backtesting not only makes strategy design more rigorous but also helps investors optimize decisions and reduce investment risk before actual transactions. This chapter uses two examples to demonstrate how to backtest margin trading and securities lending strategy in DolphinDB.
Note that the two examples in this section are provided solely to illustrate how to implement the relevant logic using the backtesting engine. They do not represent real investment logic, and no legal liability is assumed.
4.1 Add Leverage to Portfolio Strategy Using Margin Trading
When investing in stock strategies, investors can use margin trading to scale up their positions or set aside reserve funds to buy the dip, lowering the average cost of their holdings. This section demonstrates how to implement a leveraged portfolio strategy by applying margin trading in the DolphinDB backtesting engine.
4.1.1 Strategy Logic
This case performs strategy backtesting on a portfolio selected based on factors using the daily market data.
Selection based on factors for the portfolio strategy is not the focus of this section. Here, we assume the selection factor is the signal. This selection factor can be a single factor or a composite signal derived from multiple factors. The logic of this portfolio strategy is implemented as follows:
- First, use the signal as the indicator. At the close of each trading day, select the top 10 stocks from the CSI 300 stocks based on the signal’s ranking. The current total assets are then allocated to these 10 stocks for purchase, based on the signal values.
- At the opening of each trading day, a position is established using 150% of the latest total assets. Specifically, cash is first used to buy holdings, and then margin trading is used to establish the position.
We consider limit-up, limit-down, and suspended trading in the implementation. Stocks cannot be bought during limit-up or suspended trading, and cannot be sold during limit-down or suspended trading. Here, we implement this logic through user-defined functions. The three user-defined functions determine whether a stock can be bought or sold based on whether the limit-up price in the market data equals the current closing price, and whether the daily trading volume is 0. The quantity of stocks purchased must be an integer multiple of 100. For stocks listed on the STAR Market, the quantity must be greater than 200 shares.
def isBuyAble(data){
//Determine if the stock can be bought at the close price
if(data.volume <= 0.001 or (data.upLimitPrice == data.close)){
return false //Buying conditions: The closing price is not at the limit-up price, and the stock is not suspended on that day.
}
return true
}
def isSellAble(data){
//Determine if the stock can be sold at the close price
if(data.volume <= 0.001 or (data.downLimitPrice == data.close)){
return false //Selling conditions: The closing price is not at the limit-down price, and the stock is not suspended on that day.
}
return true
}
def getBuyVolume(istock,mv,close){
if(close <= 0. ) {return 0}
if(istock.substr(0,2) == "68" and int(mv/close) >= 200){
return floor(mv/close)
}
else if(istock.substr(0,2) == "68"){ return 0}
return floor(int(mv/close)/100)*100
}4.1.2 Code Implementation
The backtesting engine employs an event-driven mechanism. We implement the relevant strategy logic within the corresponding event functions. First, in the strategy initialization callback function initialize, we specify the strategy’s global variables. In this case, we specify global variables such as the number of stocks in the portfolio and the leverage multiplier.
def initialize(mutable context, userParam){
print("initialize")
context["N"] = 10 //number of stocks in the portfolio
//Overall portfolio leverage ratio. A value >1 indicates that part of the portfolio is built by margin trading.
context["multiplier"] = 1.5
context["commission"] = userParam["commission"] //commission fee
context["Holdings"] = array(STRING,0,context["N"])//current holding
}In the pre-trading callback function beforeTrading, we initialize the day’s global variables. In this case, we retrieve information on which securities are eligible for margin trading on the current day.
def beforeTrading(mutable context,userParam){
print ("beforeTrading: "+context["tradeDate"])
//retrieve information on which securities are eligible for margin trading on the current day
context["eligibleForMarginTrading"] = userParam[date(context["tradeDate"])]
}The main strategy logic is implemented in the 5 steps within the OHLC callback function onBar:
def onBar(mutable context, msg, indicator){
//At the opening of each trading day, a position is established using 150% of the latest total assets. Specifically, cash is first used to buy holdings, and then margin trading is used to establish the position.
//step 1: Get the buyable stocks and select the top N stocks based on the factor value.
...
//step 2: Get the current holdings, identify non-sellable stocks, and calculate their total values.
...
//step 3:Calculate the allocable market value and the target value for each stock to buy.
...
//step 4: Execute sell operations for the portion that needs to be reduced.
...
//step 5: Execute buy operations. Use cash for non-marginable stocks first.
}Below are the detailed code implementations for each of the 5 steps in the OHLC callback function onBar:
Step 1: Get the buyable stocks and corresponding weights. Select the top context["N"] stocks by factor value and their weights.
//At the opening of each trading day, a position is established using 150% of the latest total assets. Specifically, cash is first used to buy holdings, and then margin trading is used to establish the position.
//step 1: Get the buyable stocks and select the top N stocks based on the factor value.
buyList = array(STRING, 0, context["N"])
buyWeight = array(DOUBLE, 0, context["N"])
eligibleForMarginTradingLaBel = array(INT, 0, context["N"])
//Get the buyable stocks and select the top N stocks based on the previous day's factor value.
//If there are suspended or limit-up stocks, the actual number of buyable stocks may be less than context["N"].
i = 0
for( istock in msg.keys()){
if(i > context["N"] ){ break }
if(msg[istock].signal[0] <0. ){ continue}
if(isBuyAble(msg[istock])){//the stock is buyable
buyList = buyList.append!(istock)
buyWeight = buyWeight.append!(msg[istock].signal[0])
eligibleForMarginTradingLaBel = eligibleForMarginTradingLaBel.append!(int(context["eligibleForMarginTrading"][istock]))
}
i = i+1
}Step 2: Calculate the total market value of stocks that cannot be sold due to suspension or limit-down.
//step 2
//Get current holdings and check for non-sellable stocks
MarginSecuPosition = Backtest::getMarginSecuPosition(context["engine"])
MarginTradingPosition = Backtest::getMarginTradingPosition(context["engine"])
posStock =keys( set(MarginSecuPosition.symbol ) | set(MarginTradingPosition.symbol))
//Record the market value of non-sellable positions
noSellPosMV = 0.
for(istock in posStock){
if(isSellAble(msg[istock]) == false){//non-sellable
//Calculate the market value of the non-sellable stocks
pos = getPos(MarginSecuPosition, istock) + getPos(MarginTradingPosition, istock)
mv = pos*msg[istock].close
if(mv > 0.){
noSellPosMV = noSellPosMV + mv
}
}
}Use Backtest::getMarginSecuPosition to get collateral purchase positions and Backtest::getMarginTradingPosition for margin trading positions. See 2.3 Methods Reference for details.
Step 3: Calculate the target market value for each stock to be purchased after retrieving the buyable stocks and the market value of non-sellable positions.
//step 3
//Normalize the weights
buyWeight = buyWeight\buyWeight.sum()
//Get the current total equity
totalEquity = Backtest::getTotalPortfolios(context["engine"]).totalEquity[0]
//Calculate the target market value to be purchased for each stock
totalMarketValue = (totalEquity - noSellPosMV) *buyWeight*context["multiplier"]Use the Backtest::getDailyTotalPortfolios method to retrieve real-time information, including margin balance, equity, profit, interest, and loss. See 2.3 Methods Reference for details.
Step 4: Sell sellable stocks to free up capital. We do not sell all positions but only the portion that needs to be sold to minimize the commission fee.
//step 4
//Buy based on the target market value to hold
//Sell first and then buy
newStock = (set(buyList) - set(posStock)).keys()
for(istock in posStock){//sell
if(isSellAble(msg[istock])){
//Query current position, which could be collateral position or margin trading position
mv = (getPos(MarginSecuPosition, istock) + getPos(MarginTradingPosition, istock))*msg[istock].close
diffmv = mv - totalMarketValue[at(buyList == istock)].nullFill(0)[0]
pos = getBuyVolume(istock, diffmv, msg[istock].close)
if(diffmv > 0 and pos >0 ){
//Sell collateral
Backtest::submitOrder(context["engine"], (istock,context["tradeTime"], 5, msg[istock].close, pos,2),"collateral sale")
}
}
}Backtest::submitOrder is the method to submit orders. See 2.3 Methods Reference for details. Note that when selling collateral, the engine prioritizes closing margin trading positions. Therefore, we only need to submit a collateral sell here.
Step 5: When constructing the buy portfolio through leveraged trading, we first use cash to purchase stocks ineligible for margin trading, then use margin trading to purchase eligible stocks on the same day.
//step 5: Buy
buyList = buyList[isort(eligibleForMarginTradingLaBel)]
totalMarketValue = totalMarketValue[isort(eligibleForMarginTradingLaBel)]
i = 0
for( istock in buyList){
a = getPos(Backtest::getMarginSecuPosition(context["engine"], [istock]), istock)
b = getPos(Backtest::getMarginTradingPosition(context["engine"], [istock]), istock)
mv = (a + b)*msg[istock].close
diffmv = totalMarketValue[i].nullFill(0)[0] - mv
pos = getBuyVolume(istock, diffmv, msg[istock].close)
availableCash = Backtest::getTotalPortfolios(context["engine"]).availableCash
if(diffmv > 0 and pos >0 and pos*msg[istock].close*(1+context["commission"]) < availableCash){
//Buy collateral
Backtest::submitOrder(context["engine"], (istock,context["tradeTime"], 5, msg[istock].close, pos,1),"buy collateral")
}
else if( diffmv > 0 and pos >0 and pos*msg[istock].close*(1+context["commission"]) > availableCash ){
//Margin trading
Backtest::submitOrder(context["engine"], (istock,context["tradeTime"], 5, msg[istock].close, pos,3),"margin trading")
}
i = i+1
}4.1.3 Parameter Configuration and Backtesting
After completing the strategy script, we need to configure the relevant parameters, create the corresponding backtesting engine, and then retrive market data to run the backtest and view the results.
Configure strategy parameters
When backtesting a margin trading and securities lending strategy, we should configure parameters such as the start and end times of the backtest, the strategy group, the market data type, the line of credit, the maintenance margin ratio, the margin interest rate, and the securities lending fee.
// step 2:Configure strategy parameters and create the engine
config = dict(STRING,ANY)
config["startDate"] = 2020.05.08
config["endDate"] = 2020.07.11
///strategy group, margin account, margin trading and securities lending strategy backtesting///
config["strategyGroup"] = "securityCreditAccount"
config["lineOfCredit"] = 100000000.//line of credit
config["marginTradingInterestRate"] = 0.086
config["secuLendingInterestRate"] = 0.106
config["maintenanceMargin"] = [1.45,1.30,1.20]//maintenance margin ratio
//config["longConcentration"] = [1.45,1.30,1.20]//long concentration limit
//config["shortConcentration"] = [1.45,1.30,1.20]//short concentration limit
config["cash"] = 5000000.
config["commission"] = 0.0///commission fee
config["tax"] = 0.0 //stamp duty
config["dataType"] = 4 //daily data
config["outputOrderInfo"] = true
config["matchingMode"] = 1//order matching at the closing price
config["msgAsTable"] = falseCreate the backtesting engine
engine = Backtest::createBacktestEngine(strategyName, config, securityReference,
initialize{, config},beforeTrading{,eligibleForMarginTrading},onBar,,
onOrder,onTrade,afterTrading,finalize)The Backtest::createBacktestEngine method creates a backtesting engine. See 2.3 Methods Reference for details.
Run the backtest
Run the backtesting as follows. The dayData parameter specifies the market data.
//run the backtesting
Backtest::appendQuotationMsg(engine, dayData)We build a portfolio strategy based on factors. First, put the relevant factor data into the market data’s signal parameter, and then implement logic in the strategy to form a portfolio from the top-ranked stocks by factor value. We also sort dayData by time and factor value before replaying it to the backtesting engine.
View backtesting results
Use the corresponding methods to retrieve daily positions, daily equity, the return summary, trade details, and any user-defined logical context in the strategy.
Use the Backtest::getTradeDetails() method to view the trade details table. A non-null orderInfo indicates the order was rejected with a detailed reason in the column.
Use the Backtest::getDailyTotalPortfolios() method to view the daily net value, equity, and other statistics of the strategy.
totalPortfolios = Backtest::getDailyTotalPortfolios(long(engine))
plot(totalPortfolios.netValue,totalPortfolios.tradeDate,"Net Value")4.2 Amplify Returns When Initial Positions Exist
When investing in stocks, an investor who is already fully invested can use the available margin balance as reserve capital to buy the dip and lower the average cost, or to carry out margin trading and securities lending in other instruments.
4.2.1 Strategy Logic
The example in this section is implemented as follows:
When creating the engine, use 95% of the cash to buy the corresponding stocks.
Monitor the corresponding stocks using the RSI and Bollinger Bands indicators:
- When a stock’s RSI is above 70, and the Bollinger Bands are moving upward and breaking above the upper band, use the account’s available margin balance to execute margin trading.
- When a stock’s RSI is below 30, and the Bollinger Bands are moving downward and breaking below the lower band, use the account’s available margin balance to execute short selling.
Take-profit and stop-loss:
- Close the position when a stock bought on margin or sold short reaches a 5% profit or a 3% loss.
4.2.2 Code Implementation
In strategy backtesting, you can configure the quantity, holding price, and other details for the initial positions using the parameter setLastDayPosition. See Table 3 for details on the initial position table. In the implementation, we assume that 95% of the cash is used to buy two stocks, with the remaining 5% as the strategy’s initial cash.
//Build initial positions, using 95% of cash as the core position
config["cash"] = 5000000.
n = 2//Number of securities in core position
mv = config["cash"] *0.95 / n
setLastDayPosition = select symbol,each(getBuyVolume{, mv, },symbol,open) $ LONG as marginSecuPosition,
open as marginSecuAvgPrice, 0$LONG as marginPosition, 0. as marginBuyValue, 0$LONG as secuLendingPosition,
0. as secuLendingSellValue, open as closePrice, 0.8 as conversionRatio,
0. as tradingMargin, 0. as lendingMargin from getLastClosePrice(config["startDate"], codes[:n])
config["setLastDayPosition"] = setLastDayPosition
//Set cash
config["cash"] = config["cash"] - config["cash"] *0.95*(1 + config["commission"])In the implementation, we use the rsi and bBand functions from the Technical Analysis Indicator Library to compute the RSI and Bollinger Bands indicators.
use ta
def initialize(mutable context, userParam){
print("initialize")
d ={rsi: <ta::rsi(close, 11)>,
bhigh: <ta::bBands(close, 20, 2, 2, 0)[0]>,
blow: <ta::bBands(close, 20, 2, 2, 0)[2]>}
Backtest::subscribeIndicator(context["engine"], "ohlc", d)
context["tp"] = 0.05
context["sl"] = 0.03
context["HoldingsPrice"] = dict(STRING,DOUBLE)
}Use the Backtest::subscribeIndicator method to calculate the indicators. After subscribing to the indicators, retrieve the indicators’ values in the onBar callback function via msg[istock].rsi.
Backtest::subscribeIndicator(engine, quote, indicatorDict)- engine: The backtesting engine instance.
- quote: A string indicating the market data source used to calculate the indicators. Currently supported sources include the snapshot, the trade, and daily and minute-level data.
- indicatorDict: A dictionary in which each key is an indicator name and each value is the corresponding metacode expression for that indicator.
indicatorDict = {rsi: <ta::rsi(close, 11)>,
bhigh: <ta::bBands(close, 20, 2, 2, 0)[0]>,
blow: <ta::bBands(close, 20, 2, 2, 0)[2]>}In the callback function for minute-level data, we use Bollinger Band breakouts and RSI as entry signals for stocks in the universe to execute margin trading or short selling. First, use Backtest::getMarginTradingPosition and Backtest::getSecuLendingPosition methods to query margin trading and securities lending positions.
When there is no position:
- If a security’s RSI is above 70 and the Bollinger Bands are moving upward and breaking above the upper band, execute a margin trading using the account’s available margin balance.
- If the security’s RSI is below 30 and the Bollinger Bands are moving downward and breaking below the lower band, open a short selling using the account’s available margin balance.
When positions are held, use real-time minute data for risk monitoring and execute take-profit or stop-loss actions as needed. For margin trading positions, repay by selling the securities borrowed. For securities lending positions, cover by buying back the securities and returning them.
def onBar(mutable context, msg, indicator){
for( istock in msg.keys()){
longPosition = Backtest::getMarginTradingPosition(context.engine, [istock]).longPosition[0]
shortPosition = Backtest::getSecuLendingPosition(context.engine, [istock]).shortPosition[0]
close = msg[istock].close
open = msg[istock].open
if(msg[istock].rsi > 70. and close > msg[istock].bhigh and close > open and longPosition < 1 and shortPosition < 1 ){
//Execute a margin trading
availableMarginBalance = Backtest::getTotalPortfolios(context.engine).availableMarginBalance[0]
qty = getBuyVolume(istock, availableMarginBalance*0.2, close)
if( qty > 0){
Backtest::submitOrder(context.engine, (istock, context.tradeTime, 5, close, qty, 3), "margin trading")
context["HoldingsPrice"][istock] = close
}
}
if(msg[istock].rsi < 30. and close < msg[istock].blow and close < open and longPosition < 1 and shortPosition <1){
//Do not execute a short selling when a collateral-backed long position exists
pos = Backtest::getMarginSecuPosition(context.engine, [istock]).longPosition[0]
availableMarginBalance = Backtest::getTotalPortfolios(context.engine).availableMarginBalance[0]
qty = getBuyVolume(istock, availableMarginBalance*0.2, close)
if(pos < 1 and qty > 0){
//Execute short selling
Backtest::submitOrder(context.engine, (istock, context.tradeTime, 5, close, qty, 4), "short selling")
context["HoldingsPrice"][istock] = close
}
}
if (longPosition > 0 and (close >= context["HoldingsPrice"][istock] * ( 1+ context["tp"]) or
close <= context["HoldingsPrice"][istock] * ( 1 - context["sl"]))){
//Stop loss or take profit
Backtest::submitOrder(context.engine, (istock, context.tradeTime, 5, close, longPosition, 6), "sell security for repaymen")
}
if (shortPosition > 0 and (close <= context["HoldingsPrice"][istock] * ( 1 - context["tp"]) or
close >= context["HoldingsPrice"][istock] * ( 1 + context["sl"]))){
//Stop loss or take profit
Backtest::submitOrder(context.engine, (istock, context.tradeTime, 5, close, shortPosition, 8), "purchase security for return")
}
}
}Run the backtest:
timer Backtest::appendQuotationMsg(engine, minData)Finally, we use the Backtest::getDailyTotalPortfolios() method to get the net value of the strategy and the initial position.
totalPortfolios = Backtest::getDailyTotalPortfolios(engine)
//view the results
netValue = totalPortfolios.netValue
bottomNetValue = totalPortfolios.bottomNetValue
plot([netValue,bottomNetValue], totalPortfolios.tradeDate, "Net Value")Complete code and sample data are provided in the appendix.
5. Summary
Margin trading and securities lending are trading mechanisms in the stock market. The DolphinDB backtesting engine allows you to configure parameters such as initial positions and dividend adjustment tables, enabling them to backtest margin trading and securities lending strategy and to simulate trading. The engine calculates transaction costs for such trades in real time and includes a built-in risk management system that monitors key metrics, such as concentration.
Although margin trading and securities lending backtesting differ from standard strategies in capital usage, order submission, and investment logic, they follow the same overall workflow: core parameters are stored in the config dictionary and passed to the backtesting engine for centralized management. You only need to specify initial positions and configure relevant parameters, without developing a custom risk management system or manually handling scenarios that may arise during simulated trading in strategy scripts.
This tutorial illustrates the application and operations of margin trading and securities lending strategy backtesting through examples. By implementing two sample strategies, it provides a detailed walkthrough of the process. Future releases will support JIT optimization for margin trading and securities lending strategy backtesting.
6. Appendix
- 4.1: Add Leverage to Portfolio Strategy Using Margin Trading.dos, dayData.csv
- 4.2: Amplify Returns When Initial Positions Exist.dos, MinuteData.csv
Thanks for your reading! To keep up with our latest news, please follow our Twitter @DolphinDB_Inc and Linkedin. You can also join our Slack to chat with the author! 😉