Main Content

backtestStrategy

Create backtestStrategy object to define portfolio allocation strategy

Since R2020b

Description

Create a backtestStrategy object which defines a portfolio allocation strategy.

Use this workflow to develop and run a backtest:

  1. Define the strategy logic using a backtestStrategy object to specify how the strategy rebalances a portfolio of assets.

  2. Use backtestEngine to create a backtestEngine object that specifies the parameters of the backtest.

  3. Use runBacktest to run the backtest against historical asset price data and, optionally, trading signal data.

  4. Use equityCurve to plot the equity curves of each strategy.

  5. Use summary to summarize the backtest results in a table format.

For more detailed information on this workflow, see Backtest Investment Strategies Using Financial Toolbox.

Creation

Description

example

strategy = backtestStrategy(name,rebalanceFcn) creates a backtestStrategy object.

example

strategy = backtestStrategy(___,Name,Value) sets properties using name-value pair arguments and any of the arguments in the previous syntax. You can specify multiple name-value pair arguments. For example, strat = backtestStrategy('MyStrategy',@myRebalFcn,'TransactionCost',0.005,'LookbackWindow',20).

Input Arguments

expand all

Strategy name, specified as a string.

Data Types: string

Rebalance function, specified as a function handle which computes new portfolio weights during the backtest. The rebalanceFcn argument implements the core logic of the trading strategy.

The signature of the rebalanceFcn depends on whether UserData and EngineDataList name-value arguments are specified for the backtestStrategy function. For an example of rebalanceFcn where UserData is specified, see Backtest with Brinson Attribution to Evaluate Portfolio Performance.

  • If UserData and EngineDataList are Empty

    If the UserData and EngineDataList name-value arguments are empty, then the rebalanceFcn must have one of the following signatures:

    • new_weights = rebalanceFcn(weights,assetPrices)

    • new_weights = rebalanceFcn(weights,assetPrices,signalData)

    If you do not specify UserData, the rebalance function returns a single output argument, new_weights, which is a vector of asset weights specified as decimal percentages.

    • If the new_weights sum to 1, then the portfolio is fully invested.

    • If the new_weights sum to less than 1, then the portfolio has the remainder in cash, earning the RiskFreeRate specified in the backtestEngine object.

    • If the new_weights sum to more than 1, then there is a negative cash position (margin) and the cash borrowed accrues interest at the cash borrowing rate specified in the CashBorrowRate property of the backtestEngine object.

  • If UserData is Empty and EngineDataList is Specified

    If the UserData name-value argument is empty and the EngineDataList name-value argument is specified, then the rebalanceFcn must have one of the following signatures:

    • new_weights = rebalanceFcn(engineData,assetPrices)

    • new_weights = rebalanceFcn(engineData,assetPrices,signalData)

  • If UserData is Specified and EngineDataList is Empty

    If you use the name-value argument for UserData to specify a strategy-specific userData struct but do not specify the name-value argument for EngineDataList, the required syntax for rebalanceFcn must have one of the following signatures:

    • [new_weights,userData] = rebalanceFcn(weights,assetPrices,userData)

    • [new_weights,userData] = rebalanceFcn(weights,assetPrices,signalData,userData)

    If UserData is specified, then in addition to an output for new_weights, there is also an output for userData returned as a struct.

  • If Both UserData and EngineDataList are Specified

    If you use the name-value argument for UserData to specify a strategy-specific userData struct and you use the name-value argument EngineDataList to specify a backtest engine state, the required syntax for rebalanceFcn must have one of the following signatures:

    • [new_weights,userData] = rebalanceFcn(engineData,assetPrices,userData)

    • [new_weights,userData] = rebalanceFcn(engineData,assetPrices,signalData,userData)

    If UserData is specified, then in addition to an output for new_weights, there is also an output for userData returned as a struct.

The rebalanceFcn function is called by the backtestEngine object each time the strategy must be rebalanced as specified in the RebalanceFrequency name-value argument. The backtestEngine object calls the rebalanceFcn function with the following arguments:

  • weights — The current portfolio weights before rebalancing, specified as decimal percentages.

  • assetPrices — A timetable containing a rolling window of adjusted asset prices.

  • signalData — (Optional) A timetable containing a rolling window of signal data.

    If you provide signal data to the backtestEngine object, then the engine object passes it to the strategy rebalance function using the three input argument syntax. If do not provide signal data the backtestEngine object, then the engine object calls the rebalance function with the two input argument syntax.

  • userData — (Optional) A struct to contain strategy-specific user data.

    If the UserData property is set, the userData struct is passed into rebalanceFcn.

  • engineData — (Optional) A struct containing backtest state data.

    If the EngineDataList property is set, the engineData struct is passed into rebalanceFcn.

For more information on developing a rebalanceFcn function handle, see Backtest Investment Strategies Using Financial Toolbox.

Data Types: function_handle

Name-Value Arguments

Specify optional pairs of arguments as Name1=Value1,...,NameN=ValueN, where Name is the argument name and Value is the corresponding value. Name-value arguments must appear after other arguments, but the order of the pairs does not matter.

Before R2021a, use commas to separate each name and value, and enclose Name in quotes.

Example: strat = backtestStrategy('MyStrategy',@myRebalFcn,'TransactionCost',0.005,'LookbackWindow',20)

Rebalance frequency during the backtest, specified as the comma-separated pair consisting of 'RebalanceFrequency' and a scalar integer, duration orcalendarDuration object, or a vector of datetime objects.

The RebalanceFrequency specifies the schedule of dates where the strategy will rebalance its portfolio. The default is 1, meaning the strategy rebalances with each time step.

For more information, see Defining Schedules for Backtest Strategies.

Data Types: double | object | datetime

Transaction costs for trades, specified as the comma-separated pair consisting of 'TransactionCosts' and a scalar numeric, vector, or function handle. You can specify transaction costs in three ways:

  • rate — A scalar decimal percentage charge to both purchases and sales of assets. For example ,if you set TransactionCosts to 0.001, then each transaction (buys and sells) would pay 0.1% in transaction fees.

  • [buyRate, sellRate] — A 1-by-2 vector of decimal percentage rates that specifies separate rates for buying and selling of assets.

  • computeTransactionCostsFcn — A function handle to compute customized transaction fees. If you specify a function handle, the backtestEngine object calls the TransactionCosts function to compute the fees for each rebalance. The user-defined computeTransactionCostsFcn function handle has different signatures depending on whether the optional name-value arguments UserData and EngineDataList are specified and whether the sellCosts, buyCosts, or detailedCosts outputs are used.

    • If UserData is Empty and EngineDataList is Empty Using buyCosts and sellCosts Outputs

      When the optional name-value arguments UserData and EngineDataList are not specified and the sellCosts and buyCosts outputs are used, the computeTransactionCostsFcn function handle has the following signature:

      [buyCosts,sellCosts] = computeTransactionCostsFcn(deltaPositions)

      The user-defined computeTransactionCostsFcn function handle takes a single input argument, deltaPositions, which is a vector of changes in asset positions for all assets (in currency units) as a result of a rebalance. Positive elements in the deltaPositions vector indicate purchases while negative entries represent sales. The user-defined function handle must return two output arguments buyCosts and sellCosts, which contain the total costs (in currency) for the entire rebalance for each type of transaction.

    • If UserData is Empty and EngineDataList is Empty and detailedCosts Output is specified

      When the optional name-value arguments UserData and EngineDataList are not specified and detailedCosts output is specified, the computeTransactionCostsFcn function handle has the following signature:

      detailedCosts = computeTransactionCostsFcn(deltaPositions)

      The user-defined computeTransactionCostsFcn function handle takes a single input argument, deltaPositions, which is a vector of changes in asset positions for all assets (in currency units) as a result of a rebalance. Positive elements in the deltaPositions vector indicate purchases while negative entries represent sales. The user-defined function handle returns one output argument for detailedCosts for per-asset transaction costs.

    • If UserData is Empty and EngineDataList is Specified Using buyCosts and sellCosts Outputs

      When the optional name-value argument UserData is not specified, but the optional name-value argument EngineDataList is specified and the buyCosts and sellCosts output are used, the computeTransactionCostsFcn function handle has the following signatures:

      [buyCosts,sellCosts] = computeTransactionCostsFcn(deltaPositions,engineData)
      

    • If UserData is Empty and EngineDataList is Specified and detailedCosts Output is Specified

      When the optional name-value argument UserData is not specified, but the optional name-value argument EngineDataList is specified and detailedCosts output is specified, the computeTransactionCostsFcn function handle has the following signatures:

      detailedCosts = computeTransactionCostsFcn(deltaPositions,engineData)
      

    • If UserData is Specified and EngineDataList is Empty Using buyCosts and sellCosts Outputs

      When the optional name-value argument UserData is specified, but the optional name-value argument EngineDataList is not specified and buyCosts and sellCosts outputs are used, the computeTransactionCostsFcn function handle has the following signatures:

      [buyCosts,sellCosts,userData] = computeTransactionCostsFcn(deltaPositions,userData)

    • If UserData is Specified and EngineDataList is Empty and detailedCosts Output is specified

      When the optional name-value argument UserData is specified, but the optional name-value argument EngineDataList is not specified and detailedCosts output is specified, the computeTransactionCostsFcn function handle has the following signatures:

      [detailedCosts,userData] = computeTransactionCostsFcn(deltaPositions,userData)

    • If Both UserData and EngineDataList are Specified Using buyCosts and sellCosts Outputs

      If you use the optional name-value argument for UserData to specify a strategy-specific userData struct and the optional name-value argument EngineDataList to specify required backtest engine state data and buyCosts and sellCosts arguments are used, the required syntax for the computeTransactionCostsFcn function handle must have the following signature:

      [buyCosts,sellCosts,userData] = computeTransactionCostsFcn(deltaPositions,engineData,userData)
      If you specify the optional name-value argument UserData, then the strategy userData struct is passed to the computeTransactionCostsFcn cost function as the final input argument. The computeTransactionCostsFcn function handle can read or write any information to the userData struct. The updated userData struct is returned as the third output argument.

    • If Both UserData and EngineDataList are Specified and detailedCosts Output is Specified

      If you use the optional name-value argument for UserData to specify a strategy-specific userData struct and the optional name-value argument EngineDataList to specify required backtest engine state data and the detailedCosts output is specified, the required syntax for the computeTransactionCostsFcn function handle must have the following signature:

      [detailedCosts,userData] = computeTransactionCostsFcn(deltaPositions,engineData,userData)
      If you specify the optional name-value argument UserData, then the strategy userData struct is passed to the computeTransactionCostsFcn cost function as the final input argument. The computeTransactionCostsFcn function handle can read or write any information to the userData struct. The updated userData struct is returned as the second output argument.

Data Types: double | function_handle

Lookback window, specified as the comma-separated pair consisting of 'LookbackWindow' and a 1-by-2 vector of integers, a duration or calendarDuration object.

When using a 1-by-2 vector with integers that defines the minimum and maximum size of the rolling window of data (asset prices and signal data) that you provide to the rebalanceFcn argument, you specify these limits in terms of the number of time steps. When specified as integers, the lookback window is defined in terms of rows of data from the asset (pricesTT) and signal (signalTT) timetables used in the backtest. The lookback minimum sets the minimum number of rows of asset price data that must be available to the rebalance function before a strategy rebalance can occur. The lookback maximum sets the maximum size for the rolling window of price data that is passed to the rebalance function.

For example, if the backtestEngine object is provided with daily price data, then LookbackWindow specifies the size bounds of the rolling window in days. The default is [0 Inf], meaning that all available past data is given to the rebalance function. If you specify a non-zero minimum, then the software does not call rebalanceFcn until enough time steps process to meet the minimum size.

If you specify LookbackWindow as a single scalar value, then the value is both the minimum and maximum of the LookbackWindow (that is, a fixed-sized window).

If using a duration or calendarDuration object, the lookback window minimum and maximum are defined in terms of timespans relative to the time at a rebalance. For example if the lookback minimum was set to five days (that is, days(5)), the rebalance will only occur if the backtest start time is at least five days prior to the rebalance time. Similarly, if the lookback maximum was set to six months (that is, calmonths(6)), the lookback window would contain only data that occurred at six months prior to the rebalance time or later.

Note

Alternatively, the LookbackWindow can be set to a single scalar value indicating that the rolling window should be exactly that size (either in terms of rows or a time duration). The minimum and maximum size will both be set to the provided value.

Data Types: double | object

Initial portfolio weights, specified as the comma-separated pair consisting of 'InitialWeights' and a vector. The InitialWeights vector sets the portfolio weights before the backtestEngine object begins the backtest. The size of the initial weights vector must match the number of assets used in the backtest.

Alternatively, you can set the InitialWeights name-value pair argument to empty ([]) to indicate the strategy will begin with no investments and in a 100% cash position. The default for InitialWeights is empty ([ ]).

Data Types: double

Since R2022b

Management fee charged as an annualized percent of the total portfolio value, specified as the comma-separated pair consisting of 'ManagementFee' and a numeric scalar. The management fee is the annualized fee rate paid to cover the costs of managing a strategy's portfolio. The management fee is charged based on the strategy portfolio balance at the start of each date specified by the ManagementFeeSchedule. The default is 0, meaning no management fee is paid.

For more information, see Management Fees.

Data Types: double

Since R2022b

Management fee schedule, specified as the comma-separated pair consisting of 'ManagementFeeSchedule' and a numeric scalar, a duration or calendarDuration object, or alternatively, as a vector of datetimes.

The ManagementFeeSchedule specifies the schedule of dates where the management fee is charged (if a ManagementFee is defined). The default is calyears(1), meaning the management fee is charged annually.

For more information, see Defining Schedules for Backtest Strategies.

Data Types: double | datetime | object

Since R2022b

Performance fee charged as a percent of the total portfolio growth, specified as the comma-separated pair consisting of 'PerformanceFee' and a numeric scalar. The performance fee, sometimes called an incentive fee, is a fee paid to reward a fund manager based on performance of the fund. The fee is paid periodically based on the schedule specified by the PerformanceFeeSchedule. The default is 0, meaning no performance fee is paid.

For more information, see Performance Fees.

Data Types: double

Since R2022b

Annualized hurdle rate or column in assetPrices or signalData to act as hurdle asset, specified as the comma-separated pair consisting of 'PerformanceHurdle' and a numeric scalar or a string. The PerformanceHurdle sets a minimum level of performance that a strategy must achieve before the performance fee is paid.

  • If specified as a numeric scalar, the hurdle represents an annualized growth rate, for example 0.05 indicates a 5% growth rate. In this case, the performance fee is only paid if, on a performance fee date, the strategy portfolio value is greater than the high-water mark and the portfolio has produced a return greater than 5% annualized since the start of the backtest. The fee is paid only on the growth in excess of both the hurdle rate and the previous high-water mark.

  • If specified as a string, the hurdle must match a column in either the assetPrices or signalData timetables of the backtest and it indicates a hurdle asset. In this case, the performance fee is paid only if the portfolio value is greater than the high-water mark and the portfolio has produced a return greater than the hurdle asset return since the start of the backtest. The performance fee is charged only on the portfolio growth in excess of both the hurdle asset growth and the previous high-water mark.

For more information, see Performance Hurdle.

Data Types: double | string

Since R2022b

Performance fee schedule, specified as the comma-separated pair consisting of 'PerformanceFeeSchedule' and a numeric scalar, a duration or calendarDuration object, or alternatively as a vector of datetimes.

PerformanceFeeSchedule specifies the schedule of dates where the performance fee is charged (if PerformanceFee is defined). The default is calyears(1), meaning the performance fee is charged annually.

For more information, see Defining Schedules for Backtest Strategies.

Data Types: double | datetime | object

Since R2023a

Strategy-specific user data for use in rebalanceFcn, specified as a struct. When UserData is set to a struct, the backtestEngine provides all user-defined rebalanceFcn function handle functions with the userData struct as an additional input argument. The rebalanceFcn function handle functions can read and write to the userData struct. The rebalanceFcn function handle functions return the updated userData struct as an additional output argument.

For an example of using UserData, see Use UserData Property to Create Stop Loss Trading Strategy and Backtest with Brinson Attribution to Evaluate Portfolio Performance.

Data Types: struct

Since R2023a

Specify a set of optional backtest state data that a strategy needs in the rebalanceFcn function handle, specified as a scalar string or string array. By default, the EngineDataList property is empty, indicating that no additional engine data is required.

If a strategy requires engine data, you can specify EngineDataList as a vector of strings containing the names of the required engineData. The backtestEngine creates an engineData struct with a field corresponding to each element in the EngineDataList. Valid engineData values are:

  • Weights — The current portfolio weights before rebalancing,specified as decimal percentages. This is the default first argument to the rebalance function (RebalanceFcn).

  • WeightsHistory — A timetable of Weights vectors for each past date in the backtest. This timetable holds the end-of-day asset weights for each asset for each day. Future dates in the timetable contain NaN values.

  • PortfolioValue — The current portfolio value at the time the rebalance function (RebalanceFcn) is called, specified as a numeric scalar. This PortfolioValue is potentially different from the "end of day" portfolio value reported by the backtestEngine at the end of the backtest. Transaction costs and other fees are paid after the rebalance function is called, so the PortfolioValue passed into the rebalance function by the engineData struct may be different from the final portfolio value reported for that day after the backtest completes.

  • PortfolioValueHistory — A timetable of portfolio values for the backtest. The timetable contains two columns. The BeforeExpenses column holds the portfolio value for each day before any expenses (transaction costs or fees). The EndOfDay column holds the portfolio value for each day, inclusive of all transaction costs and other fees. The values in the second column (EndOfDay) match the final end-of-day portfolio values reported at the end of the backtest. Future dates in the timetable contain NaN values.

  • FeesHistory — A timetable of fees charged for each day in the backtest. The FeesHistory timetable has two columns: Management and Performance for the management and performance fees, respectively. The management and performance fees are defined using the ManagementFee and PerformanceFee name-value arguments. Future dates in the timetable contain NaN values.

  • TransactionCostHistory — A timetable of per-asset transaction costs charged for each day in the backtest. Future dates in the timetable contain NaN values. The TransactionCostHistory cannot be set if the TransactionCosts function generate aggregate costs. The TransactionCosts computeTransactionCostsFcn function handle must specify an output for detailedCosts to set TransactionCostHistory.

  • CashAsset — A string array for items in the pricesTT timetable specified as cash assets.

  • DebtAsset — A string array for items in the pricesTT timetable specified as debt assets.

Specifying an EngineDataList changes the required syntax of the backtestStrategy function handles that you define using rebalanceFcn. For an example of using EngineDataList, see Use EngineDataList Property to Enforce Trading Strategy of Whole Shares.

Data Types: string

Properties

expand all

Strategy name, specified as a string.

Data Types: string

Rebalance function, specified as a function handle.

Data Types: function_handle

Rebalance frequency during the backtest, specified as a scalar numeric, duration or calendarDuration object, or vector of datetimes.

Data Types: double | object | datetime

Transaction costs, specified as a scalar numeric, vector, or function handle.

Data Types: double | function_handle

Lookback window, specified as a scalar numeric or vector.

Data Types: double

Initial weights, specified as a vector.

Data Types: double

Since R2022b

Management fee charged as an annualized percent of the total portfolio value, specified as a numeric scalar.

Data Types: double

Since R2022b

Management fee schedule, specified as scalar numeric, a vector of datetimes, or a duration or calendarDuration object.

Data Types: double | datetime | object

Since R2022b

Performance fee charged as an absolute percent of the total portfolio growth, specified as a numeric scalar.

Data Types: double

Since R2022b

Annualized hurdle rate or column in assetPrices or signalData to act as hurdle asset, specified as a numeric scalar or a string in either assetPrices or signalData as a hurdle asset.

Data Types: double | string

Since R2022b

Performance fee schedule, specified as a scalar numeric, vector of datetimes, or a duration or calendarDuration object.

Data Types: datetime | object

Since R2023a

Strategy-specific user data for use in rebalanceFcn, specified as a struct.

Data Types: struct

Since R2023a

Specify set of optional backtest state data a strategy needs in rebalanceFcn, specified as a scalar string or string array.

Data Types: string

Examples

collapse all

Define a backtest strategy by using a backtestStrategy object. backtestStrategy objects contain properties specific to a trading strategy, such as the rebalance frequency, transaction costs, and a rebalance function. The rebalance function implements the core logic of the strategy and is used by the backtesting engine during the backtest to allow the strategy to change its asset allocation and to make trades. In this example, to illustrate how to create and use backtest strategies in MATLAB®, you prepare two simple strategies for backtesting:

  1. An equal weighted strategy

  2. A strategy that attempts to "chase returns"

The strategy logic for these two strategies is defined in the rebalance functions.

Set Strategy Properties

A backtestStrategy object has several properties that you set using parameters for the backtestStrategy function.

Initial Weights

The InitialWeights property contains the asset allocation weights at the start of the backtest. The default value for InitialWeights is empty ([]), which indicates that the strategy begins the backtest uninvested, meaning that 100% of the capital is in cash earning the risk-free rate.

Set the InitialWeights to a specific asset allocation. The size of the initial weights vector must match the number of assets in the backtest.

% Initialize the strategies with 30 weights, since the backtest
% data comes from a year of the 30 DJIA stocks.
numAssets = 30;

% Give the initial weights for both strategies equal weighting. Weights
% must sum to 1 to be fully invested.
initialWeights = ones(1,numAssets);
initialWeights = initialWeights / sum(initialWeights);

Transaction Costs

The TransactionCosts property allows you to set the fees that the strategy pays for trading assets. Transaction costs are paid as a percentage of the total change in position for each asset. Specify costs in decimal percentages. For example, if TransactionCosts is set to 1% (0.01) and the strategy buys $100 worth of a stock, then the transaction costs incurred are $1.

Transaction costs are set using a 1-by-2 vector that sets separate fee rates for purchases and sales of assets. In this example, both strategies pay the same transaction costs — 25 basis points for asset purchases and 50 basis points for sales.

% Define the Transaction costs as [buyCosts sellCost] and specify the costs
% as decimal percentages.
tradingCosts = [0.0025 0.005];

You can also set the TransactionCosts property to a function handle if you need to implement arbitrarily complex transaction cost structures. For more information on creating transaction cost functions, see backtestStrategy.

Rebalance Frequency

The RebalanceFrequency property determines how often the backtesting engine rebalances and reallocates the portfolio of a strategy using the rebalance function. Set the RebalanceFrequency in terms of time steps in the backtest. For example, if the backtesting engine is testing a strategy with a set of daily price data, then set the rebalance function in days. Essentially, RebalanceFrequency represents the number of rows of price data to process between each call to the strategy rebalance function.

% Both strategies rebalance every 4 weeks (20 days).
rebalFreq = 20;

Lookback Window

Each time the backtesting engine calls a strategy rebalance function, a window of asset price data (and possibly signal data) is passed to the rebalance function. The rebalance function can then make trading and allocation decisions based on a rolling window of market data. The LookbackWindow property sets the size of these rolling windows. Set the window in terms of time steps. The window determines the number of rows of data from the asset price timetable that are passed to the rebalance function.

The LookbackWindow property can be set in two ways. For a fixed-sized rolling window of data (for example, "50 days of price history"), the LookbackWindow property is set to a single scalar value (N = 50). The software then calls the rebalance function with a price timetable containing exactly N rows of rolling price data.

Alternatively, you can define the LookbackWindow property by using a 1-by-2 vector [min max] that specifies the minimum and maximum size for an expanding window of data. In this way, you can set flexible window sizes. For example:

  • [10 Inf] — At least 10 rows of data

  • [0 50] — No more than 50 rows of data

  • [0 Inf] — All available data (that is, no minimum, no maximum); this is the default value

  • [20 20] — Exactly 20 rows of data; this is equivalent to setting LookbackWindow to the scalar value 20

The software does not call the rebalance function if the data is insufficient to create a valid rolling window, regardless of the value of the RebalanceFrequency property.

If the strategy does not require any price or signal data history, then you can indicate that the rebalance function requires no data by setting the LookbackWindow property to 0.

% The equal weight strategy does not require any price history data.
ewLookback = 0;

% The "chase returns" strategy bases its decisions on the trailing
% 10-day asset returns. The lookback window is set to 11 since computing 10 days 
% of returns requires the close price from day 0.
chaseLookback = 11;

Rebalance Function

The rebalance function (rebalanceFcn) is the user-authored function that contains the logic of the strategy. The backtesting engine calls the strategy rebalance function with a fixed set of parameters and expects it to return a vector of asset weights representing the new, desired portfolio allocation after a rebalance. For more information, see the rebalance functions.

Create Strategies

Using the prepared strategy properties, you can create the two strategy objects.

% Create the equal weighted strategy. The rebalance function @equalWeights
% is defined in the Rebalance Functions section at the end of this example.
equalWeightStrategy = backtestStrategy("EqualWeight",@equalWeight, ...
    'RebalanceFrequency',rebalFreq, ...
    'TransactionCosts',tradingCosts, ...
    'LookbackWindow',ewLookback, ...
    'InitialWeights',initialWeights)
equalWeightStrategy = 
  backtestStrategy with properties:

                      Name: "EqualWeight"
              RebalanceFcn: @equalWeight
        RebalanceFrequency: 20
          TransactionCosts: [0.0025 0.0050]
            LookbackWindow: 0
            InitialWeights: [0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333]
             ManagementFee: 0
     ManagementFeeSchedule: 1y
            PerformanceFee: 0
    PerformanceFeeSchedule: 1y
         PerformanceHurdle: 0
                  UserData: [0x0 struct]
            EngineDataList: [0x0 string]

% Create the "chase returns" strategy.  The rebalance function
% @chaseReturns is defined in the Rebalance Functions section at the end of this example.
chaseReturnsStrategy = backtestStrategy("ChaseReturns",@chaseReturns, ...
    'RebalanceFrequency',rebalFreq, ...
    'TransactionCosts',tradingCosts, ...
    'LookbackWindow',chaseLookback, ...
    'InitialWeights',initialWeights)
chaseReturnsStrategy = 
  backtestStrategy with properties:

                      Name: "ChaseReturns"
              RebalanceFcn: @chaseReturns
        RebalanceFrequency: 20
          TransactionCosts: [0.0025 0.0050]
            LookbackWindow: 11
            InitialWeights: [0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333 0.0333]
             ManagementFee: 0
     ManagementFeeSchedule: 1y
            PerformanceFee: 0
    PerformanceFeeSchedule: 1y
         PerformanceHurdle: 0
                  UserData: [0x0 struct]
            EngineDataList: [0x0 string]

Set Up Backtesting Engine

To backtest the two strategies, use the backtestEngine object. The backtesting engine sets parameters of the backtest that apply to all strategies, such as the risk-free rate and initial portfolio value. For more information, see backtestEngine.

% Create an array of strategies for the backtestEngine.
strategies = [equalWeightStrategy chaseReturnsStrategy];

% Create backtesting engine to test both strategies.
backtester = backtestEngine(strategies);

Rebalance Functions

Strategy rebalance functions defined using the rebalanceFcn argument for backtestStrategy must adhere to a fixed API that the backtest engine expects when interacting with each strategy. Rebalance functions must implement one of the following two syntaxes:

function new_weights = exampleRebalanceFcn(current_weights,assetPriceTimeTable)

function new_weights = exampleRebalanceFcn(current_weights,assetPriceTimeTable,signalDataTimeTable)

All rebalance functions take as their first input argument the current allocation weights of the portfolio. current_weights represents the asset allocation just before the rebalance occurs. During a rebalance, you can use current_weights in a variety of ways. For example, you can use current_weights to determine how far the portfolio allocation has drifted from the target allocation or to size trades during the rebalance to limit turnover.

The second and third arguments of the rebalance function syntax are the rolling windows of asset prices and optional signal data. The two tables contain the trailing N rows of the asset and signal timetables that are passed to the runBacktest function, where N is set using the LookbackWindow property of each strategy.

If optional signal data is provided to the runBacktest function, then the backtest engine passes the rolling window of signal data to each strategy that supports it.

The equalWeight strategy simply invests equally across all assets.

function new_weights = equalWeight(current_weights,assetPrices) %#ok<INUSD> 

% Invest equally across all assets.
num_assets = numel(current_weights);
new_weights = ones(1,num_assets) / num_assets;

end

The chaseReturns strategy invests only in the top X stocks based on their rolling returns in the lookback window. This naive strategy is used simply as an illustrative example.

function new_weights = chaseReturns(current_weights,assetPrices) 

% Set number of stocks to invest in.
numStocks = 15;

% Compute rolling returns from lookback window.
rollingReturns = assetPrices{end,:} ./ assetPrices{1,:};

% Select the X best performing stocks over the lookback window
[~,idx] = sort(rollingReturns,'descend');
bestStocksIndex = idx(1:numStocks);

% Initialize new weights to all zeros.
new_weights = zeros(size(current_weights));

% Invest equally across the top performing stocks.
new_weights(bestStocksIndex) = 1;
new_weights = new_weights / sum(new_weights);

end

Since R2023a

This example shows how to use the UserData property of backtestStrategy to create a stop loss trading strategy. A stop loss strategy sets a threshold to define how much money that you are willing to lose before closing a position. For example, if you bought a stock at $100, you could set a stop loss at $95, and if the stock price falls below that point, then you sell your position. You can then set a new price that is your buy-in price to buy back into the stock.

Load Data

% Load equity adjusted price data and convert to timetable
T = readtable('dowPortfolio.xlsx');
pricesTT = table2timetable(T(:,[1 3:end]),'RowTimes','Dates')
pricesTT=251×30 timetable
       Dates        AA       AIG      AXP      BA        C       CAT      DD       DIS      GE       GM       HD       HON      HPQ      IBM     INTC      JNJ      JPM      KO       MCD      MMM      MO       MRK     MSFT      PFE      PG        T       UTX      VZ       WMT      XOM 
    ___________    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____

    03-Jan-2006    28.72    68.41    51.53    68.63    45.26    55.86    40.68    24.18     33.6    17.82    39.79    36.14    28.35    80.13    24.57    59.08    37.78    38.98    32.72    75.93    52.27    30.73    26.19    22.16    56.38     22.7    54.94    26.79     44.9    56.64
    04-Jan-2006    28.89    68.51    51.03    69.34    44.42    57.29    40.46    23.77    33.56     18.3    39.05    35.99    29.18    80.03     24.9    59.99    37.56    38.91    33.01    75.54    52.65    31.08    26.32    22.88    56.48    22.87    54.61    27.58    44.99    56.74
    05-Jan-2006    29.12     68.6    51.57    68.53    44.65    57.29    40.38    24.19    33.47    19.34    38.67    35.97    28.97    80.56    25.25    59.74    37.67     39.1    33.05    74.85    52.52    31.13    26.34     22.9     56.3    22.92    54.41     27.9    44.38    56.45
    06-Jan-2006    29.02    68.89    51.75    67.57    44.65    58.43    40.55    24.52     33.7    19.61    38.96    36.53     29.8    82.96    25.28    60.01    37.94    39.47    33.25    75.47    52.95    31.08    26.26    23.16    56.24    23.21    54.58    28.01    44.56    57.57
    09-Jan-2006    29.37    68.57    53.04    67.01    44.43    59.49    40.32    24.78    33.61    21.12    39.38    36.23    30.17    81.76    25.44    60.38    38.55    39.66    33.88    75.84    53.11    31.58    26.21    23.16    56.67     23.3     55.2    28.12     44.4    57.54
    10-Jan-2006    28.44    69.18    52.88    67.33    44.57    59.25     40.2    25.09    33.43    20.79    40.33    36.17    30.33     82.1     25.1    60.49    38.61     39.7    33.91    75.37    53.04    31.27    26.35    22.77    56.45    23.16    55.24    28.24    44.54    57.99
    11-Jan-2006    28.05     69.6    52.59     68.3    44.98    59.28    38.87    25.33    33.66    20.61    41.44    36.19    30.88    82.19    25.12    59.91    38.58    39.72     34.5    75.22    53.31    31.39    26.63    23.06    56.65    23.34    54.41    28.58    45.23    58.38
    12-Jan-2006    27.68    69.04     52.6     67.9    45.02    60.13    38.02    25.41    33.25    19.76    41.05    35.77    30.57    81.61    24.96    59.63    37.87     39.5    33.96    74.57    53.23    31.41    26.48     22.9    56.02    23.24     53.9    28.69    44.43    57.77
    13-Jan-2006    27.81    68.84     52.5     67.7    44.92    60.24    37.86    25.47    33.35     19.2    40.43    35.85    31.43    81.22    24.78    59.26    37.84    39.37    33.65    74.38    53.29     31.4    26.53    22.99    56.49    23.27     54.1    28.75     44.1    59.06
    17-Jan-2006    27.97    67.84    52.03    66.93    44.47    60.85    37.75    25.15     33.2    18.68    40.11    35.56     31.2    81.05    24.52    58.74    37.64    39.11    33.77    73.99    52.85    31.16    26.34    22.63    56.25    23.13    54.41    28.12    43.66    59.61
    18-Jan-2006    27.81    67.42    51.84    66.58    44.41    60.04    37.54    24.97    33.08    18.98    40.42    35.71    31.21    81.83    21.72    59.61    37.24    38.91    34.16    74.07    52.89    30.99    26.18    22.36    56.54    23.11    54.08    27.83    43.88    58.78
    19-Jan-2006    28.33    66.92    51.66    66.42    44.02    60.66    37.69       26    32.95     19.1    39.83    35.88    31.77    81.14    21.53     59.6    37.03    39.01    34.36    73.81    52.68    31.26    26.36    23.27    56.39    23.18    54.41    28.07    44.49    59.57
    20-Jan-2006    27.67    65.55    50.49    64.79    41.95    58.98    37.34    25.49     31.7     18.9    38.76    34.57    31.28    79.45    20.91    58.28    36.07    38.21    35.01    72.21    52.18     31.2    25.77    23.03    55.74    23.01    53.46    27.64    43.71    58.63
    23-Jan-2006    28.03    65.46    50.53     65.3    42.24    59.23    37.37    25.29    31.63     20.6    38.29    34.78    30.88     79.5    20.52    58.66    36.28     38.6    34.86    72.65    52.09     30.8    25.71    23.19    55.55    22.77    52.94    27.69    43.95    59.28
    24-Jan-2006    28.16    65.39    51.89    65.92    42.25    59.53    37.09    25.76    31.32    21.73    39.03    35.25    30.91    78.95    20.45     56.9    36.13    38.98       35    71.21    51.74    30.76    25.64    22.91    55.77    22.96    54.86     27.6    44.41    59.05
    25-Jan-2006    28.57    64.67    51.97    65.19    42.45    60.23    37.05    25.21    31.13    22.48    38.57    34.79    31.64    79.01    20.38    56.08    36.48    39.21    34.32    70.06    51.49    31.14    25.76    23.14    56.35    23.47     55.4    28.03    44.65    58.32
      ⋮

Create Backtest Stop Loss Strategy

The stop loss strategy will either be 100% invested in the stock or 100% in cash. You can use two fields in the UserData struct for backtestStrategy to set the stop loss limits:

  • StopLossPercent — How much you are willing to lose before selling, as a decimal percent

  • BuyInPercent — How much further a stock must fall after you sell it before we buy back in, as a percent

In addition, the UserData struct has the following fields:

  • Asset — Ticker symbol AIG for the asset you want to trade

  • StopLossPrice — Threshold to sell the asset. If the price falls below this stop loss price, sell the asset and go to cash.

  • BuyInPrice — Target price to buy the asset. If the asset is at this price or lower, buy the asset.

stopLossStrat = backtestStrategy("StopLoss",@stopLossSingleAsset,RebalanceFrequency=1);

% Setup the initial UserData
stopLossStrat.UserData = struct(StopLossPercent=0.05,BuyInPercent=0.02,Asset="AIG",StopLossPrice=nan,BuyInPrice=Inf);

The backtest strategy starts 100% in cash. Normally the BuyInPrice is set by the strategy after you sell a stock, but in this case, you can initialize the BuyInPrice to be Inf. This triggers a stock buy on the first rebalance (since any price is less than Inf). You don't need to set the StopLossPrice at this point (it is set to nan) because the strategy sets the stop loss price once a stock purchase is made.

The strategy specifies that you are only willing to lose 5% of your capital before the stop loss is triggered and you sell. Then you will buy back into the stock if it falls by an additional 2%. You can set the rebalance frequency to 1 because this type of strategy has to "rebalance" every day in order to check the price to determine if any action is needed.

Run Backtest

Create a backtestEngine object and run the backtest using runBacktest.

backtester = backtestEngine(stopLossStrat);
backtester = runBacktest(backtester,pricesTT);

Use equityCurve to generate a plot the stop loss trading strategy.

equityCurve(backtester)

Figure contains an axes object. The axes object with title Equity Curve, xlabel Time, ylabel Portfolio Value contains an object of type line. This object represents StopLoss.

The flat parts of the equity curve are where the stop loss strategy has sold AIG and then later buys back into AIG.

Local Functions

function [new_weights,user_data] = incrementCounter(current_weights,prices,user_data)

% don't mess with the weights
new_weights = current_weights;

% increment the user data counter
user_data.Counter = user_data.Counter + 1;

end
function [new_weights,user_data] = stopLossSingleAsset(current_weights,prices,user_data)

% start with the existing weights
new_weights = current_weights;

% find column of the asset
asset = char(user_data.Asset);
assetIdx = find(strncmpi(asset,prices.Properties.VariableNames,numel(asset)));
assetPrice = prices{end,assetIdx};

% determine if we're currently invested or in cash
inCash = sum(current_weights) < 1e-5;

if inCash

    % We are in cash, see if we should buy back in
    if assetPrice <= user_data.BuyInPrice

        % Buy back in
        new_weights(assetIdx) = 1;
        % Set new stop loss price
        user_data.StopLossPrice = assetPrice * (1 - user_data.StopLossPercent);
    end

else

    % We are in the stock, see if we should sell
    if assetPrice <= user_data.StopLossPrice

        % Sell the stock
        new_weights(assetIdx) = 0;
        % Set new buy-in price
        user_data.BuyInPrice = assetPrice * (1 - user_data.BuyInPercent);
    end

end

end

Since R2023a

This example shows how to use the EngineDataList property of backtestStrategy to enforce a trading strategy with a whole shares constraint.

Load Data

% Load equity adjusted price data and convert the data to timetable
T = readtable('dowPortfolio.xlsx');
pricesTT = table2timetable(T(:,[1 3:end]),'RowTimes','Dates')
pricesTT=251×30 timetable
       Dates        AA       AIG      AXP      BA        C       CAT      DD       DIS      GE       GM       HD       HON      HPQ      IBM     INTC      JNJ      JPM      KO       MCD      MMM      MO       MRK     MSFT      PFE      PG        T       UTX      VZ       WMT      XOM 
    ___________    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____

    03-Jan-2006    28.72    68.41    51.53    68.63    45.26    55.86    40.68    24.18     33.6    17.82    39.79    36.14    28.35    80.13    24.57    59.08    37.78    38.98    32.72    75.93    52.27    30.73    26.19    22.16    56.38     22.7    54.94    26.79     44.9    56.64
    04-Jan-2006    28.89    68.51    51.03    69.34    44.42    57.29    40.46    23.77    33.56     18.3    39.05    35.99    29.18    80.03     24.9    59.99    37.56    38.91    33.01    75.54    52.65    31.08    26.32    22.88    56.48    22.87    54.61    27.58    44.99    56.74
    05-Jan-2006    29.12     68.6    51.57    68.53    44.65    57.29    40.38    24.19    33.47    19.34    38.67    35.97    28.97    80.56    25.25    59.74    37.67     39.1    33.05    74.85    52.52    31.13    26.34     22.9     56.3    22.92    54.41     27.9    44.38    56.45
    06-Jan-2006    29.02    68.89    51.75    67.57    44.65    58.43    40.55    24.52     33.7    19.61    38.96    36.53     29.8    82.96    25.28    60.01    37.94    39.47    33.25    75.47    52.95    31.08    26.26    23.16    56.24    23.21    54.58    28.01    44.56    57.57
    09-Jan-2006    29.37    68.57    53.04    67.01    44.43    59.49    40.32    24.78    33.61    21.12    39.38    36.23    30.17    81.76    25.44    60.38    38.55    39.66    33.88    75.84    53.11    31.58    26.21    23.16    56.67     23.3     55.2    28.12     44.4    57.54
    10-Jan-2006    28.44    69.18    52.88    67.33    44.57    59.25     40.2    25.09    33.43    20.79    40.33    36.17    30.33     82.1     25.1    60.49    38.61     39.7    33.91    75.37    53.04    31.27    26.35    22.77    56.45    23.16    55.24    28.24    44.54    57.99
    11-Jan-2006    28.05     69.6    52.59     68.3    44.98    59.28    38.87    25.33    33.66    20.61    41.44    36.19    30.88    82.19    25.12    59.91    38.58    39.72     34.5    75.22    53.31    31.39    26.63    23.06    56.65    23.34    54.41    28.58    45.23    58.38
    12-Jan-2006    27.68    69.04     52.6     67.9    45.02    60.13    38.02    25.41    33.25    19.76    41.05    35.77    30.57    81.61    24.96    59.63    37.87     39.5    33.96    74.57    53.23    31.41    26.48     22.9    56.02    23.24     53.9    28.69    44.43    57.77
    13-Jan-2006    27.81    68.84     52.5     67.7    44.92    60.24    37.86    25.47    33.35     19.2    40.43    35.85    31.43    81.22    24.78    59.26    37.84    39.37    33.65    74.38    53.29     31.4    26.53    22.99    56.49    23.27     54.1    28.75     44.1    59.06
    17-Jan-2006    27.97    67.84    52.03    66.93    44.47    60.85    37.75    25.15     33.2    18.68    40.11    35.56     31.2    81.05    24.52    58.74    37.64    39.11    33.77    73.99    52.85    31.16    26.34    22.63    56.25    23.13    54.41    28.12    43.66    59.61
    18-Jan-2006    27.81    67.42    51.84    66.58    44.41    60.04    37.54    24.97    33.08    18.98    40.42    35.71    31.21    81.83    21.72    59.61    37.24    38.91    34.16    74.07    52.89    30.99    26.18    22.36    56.54    23.11    54.08    27.83    43.88    58.78
    19-Jan-2006    28.33    66.92    51.66    66.42    44.02    60.66    37.69       26    32.95     19.1    39.83    35.88    31.77    81.14    21.53     59.6    37.03    39.01    34.36    73.81    52.68    31.26    26.36    23.27    56.39    23.18    54.41    28.07    44.49    59.57
    20-Jan-2006    27.67    65.55    50.49    64.79    41.95    58.98    37.34    25.49     31.7     18.9    38.76    34.57    31.28    79.45    20.91    58.28    36.07    38.21    35.01    72.21    52.18     31.2    25.77    23.03    55.74    23.01    53.46    27.64    43.71    58.63
    23-Jan-2006    28.03    65.46    50.53     65.3    42.24    59.23    37.37    25.29    31.63     20.6    38.29    34.78    30.88     79.5    20.52    58.66    36.28     38.6    34.86    72.65    52.09     30.8    25.71    23.19    55.55    22.77    52.94    27.69    43.95    59.28
    24-Jan-2006    28.16    65.39    51.89    65.92    42.25    59.53    37.09    25.76    31.32    21.73    39.03    35.25    30.91    78.95    20.45     56.9    36.13    38.98       35    71.21    51.74    30.76    25.64    22.91    55.77    22.96    54.86     27.6    44.41    59.05
    25-Jan-2006    28.57    64.67    51.97    65.19    42.45    60.23    37.05    25.21    31.13    22.48    38.57    34.79    31.64    79.01    20.38    56.08    36.48    39.21    34.32    70.06    51.49    31.14    25.76    23.14    56.35    23.47     55.4    28.03    44.65    58.32
      ⋮

Create Backtest Strategies

This example runs a backtest on two equal-weighted strategies and then compares the results. The first backtest strategy uses an exact equal-weight rebalance function that allows fractional shares. The second backtest strategy uses a EngineDataList property for PortfolioValue to enforce a whole shares constraint.

Use backtestStrategy to create the first strategy for partial shares.

% Partial shares and equal weight for assets
partialRebal = @(~,prices) ones(1,width(prices)) / width(prices);
ewPartial = backtestStrategy("PartialShares",partialRebal,RebalanceFrequency=1);

Use backtestStrategy to create the second strategy for whole shares.

% Whole shares and equal weight for assets
ewWhole = backtestStrategy("WholeShares",@equalWeightShares,RebalanceFrequency=1,EngineDataList="PortfolioValue");

Run Backtest

Aggregate the two strategy objects, create a backtestEngine object, and then run the backtest using runBacktest.

strats = [ewPartial, ewWhole];
backtester = backtestEngine(strats,RiskFreeRate=0.02);
backtester = runBacktest(backtester,pricesTT);

Use equityCurve to generate a plot for both of the trading strategies.

equityCurve(backtester);

Figure contains an axes object. The axes object with title Equity Curve, xlabel Time, ylabel Portfolio Value contains 2 objects of type line. These objects represent PartialShares, WholeShares.

You can verify that the second strategy traded in whole shares.

% Plot the number of shares of each asset
plot(backtester.Positions.PartialShares{:,2:end} ./ pricesTT{:,:});
title('Number of Shares per Asset (Partial Shares)')

Figure contains an axes object. The axes object with title Number of Shares per Asset (Partial Shares) contains 30 objects of type line.

plot(backtester.Positions.WholeShares{:,2:end} ./ pricesTT{:,:})
title('Number of Shares per Asset (Whole Shares)')

Figure contains an axes object. The axes object with title Number of Shares per Asset (Whole Shares) contains 30 objects of type line.

Local Functions

function new_weights = equalWeightShares(engine_data,prices)

numAssets = width(prices);

% Dollars per asset for equal weight with partial shares
dollars_per_asset = engine_data.PortfolioValue / numAssets;
% Compute shares (partial shares first, then round down)
asset_shares = floor(dollars_per_asset ./ prices{end,:});
% Convert number of shares into dollars
asset_dollars = asset_shares .* prices{end,:};
% Convert dollars into weights
new_weights = asset_dollars / engine_data.PortfolioValue;

end

More About

expand all

Version History

Introduced in R2020b

expand all