Skip to content

minitrade.backtest¤

minitrade.backtest.core.backtesting ¤

Core framework data structures. Objects from this module can also be imported from the top-level module directly, e.g.

from minitrade.backtest import Backtest, Strategy

Allocation ¤

Allocation(tickers: list)

The Allocation class manages the allocation of values among different assets in a portfolio. It provides methods for creating and managing asset buckets, assigning weights to assets, and merging the weights into the parent allocation object.

Allocation is not meant to be instantiated directly. Instead, it is created automatically when a new Strategy object is created. The Allocation object is accessed through the Strategy.alloc property.

The Allocation object is used as an input to the Strategy.rebalance() method to rebalance the portfolio according to the current weight allocation.

Allocation has the following notable properties:

  • tickers: A list of tickers representing the asset space in the allocation.
  • weights: The weight allocation to be used in the current rebalance cycle.
  • previous_weights: The weight allocation used in the previous rebalance cycle.
  • unallocated: Unallocated equity weight, i.e. 1 minus the sum of weights already allocated to assets.
  • bucket: Bucket accessor for weight allocation.

Allocation provides two ways to assign weights to assets:

  1. Explicitly assign weights to assets using Allocation.weights property.

    It's possible to assign weights to individual asset or to all assets in the asset space as a whole. Not all weights need to be specified. If an asset is not assigned a weight, it will have a weight of 0.

    Example:

    # Assign weight to individual asset
    strategy.alloc.weights['A'] = 0.5
    
    # Assign weight to all assets
    strategy.alloc.weights = pd.Series([0.1, 0.2, 0.3], index=['A', 'B', 'C'])
    

  2. Use Bucket to assign weights to logical groups of assets, then merge the weights into the parent allocation object.

    A Bucket is a container that groups assets together and provieds methods for weight allocation. Assets can be added to the bucket by appending lists or filtering conditions. Weights can be assigned to the assets in the bucket using different allocation methods. Multiple buckets can be created for different groups of assets. Once the weight allocation is done at bucket level , the weights of the buckets can be merged into those of the parent allocation object.

    Example:

    # Create a bucket and add assets to it
    bucket = strategy.alloc.bucket['bucket1']
    bucket.append(['A', 'B', 'C'])
    
    # Assign weights to the assets in the bucket
    bucket.weight_explicitly([0.1, 0.2, 0.3])
    
    # Merge the bucket into the parent allocation object
    bucket.apply('update')
    

The state of the Allocation object is managed by the Strategy object across rebalance cycles. A rebalance cycle involves:

  1. Initializing the weight allocation at the beginning of the cycle by calling either Allocation.assume_zero() to reset all weights to zero or Allocation.assume_previous() to inherit the weights from the previous cycle. This must be done before any weight allocation attempts.
  2. Adjusting the weight allocation using either explicitly assignment or Bucket method.
  3. Calling Strategy.rebalance() to rebalance the portfolio according to the current allocation plan.

After each rebalance cycle, the weight allocation is reset, and the process starts over. At any point, the weight allocation from the previous cycle can be accessed using the previous_weights property.

A rebalance cycle is not necessarily equal to the simulation time step. For example, simulation can be done at daily frequency, while the portfolio is rebalanced every month. In this case, the weight allocation is maintained across multiple time steps until the next time Strategy.rebalance() is called.

Example:

class MyStrategy(Strategy):
    def init(self):
        pass

    def next(self):
        # Initialize the weight allocation
        self.alloc.assume_zero()

        # Adjust the weight allocation
        self.alloc.bucket['equity'].append(['A', 'B', 'C']).weight_equally(sum_=0.4).apply('update')
        self.alloc.bucket['bond'].append(['D', 'E']).weight_equally(sum=_0.4).apply('update')
        self.alloc.weights['gold'] = self.alloc.unallocated

        # Rebalance the portfolio
        self.rebalance()

bucket property ¤

bucket: BucketGroup

bucket provides access to a dictionary of buckets.

A bucket can be accessed with a string key. If the bucket does not exist, one will be created automatically.

Buckets are cleared after each rebalance cycle.

Example:

# Access the bucket named 'equity'
bucket = strategy.alloc.bucket['equity']

modified property ¤

modified

True if weight allocation is changed from previous values.

previous_weights property ¤

previous_weights: Series

Previous weight allocation. This is a read-only property.

tickers property ¤

tickers: list

Assets representing the asset space. This is a read-only property

unallocated property ¤

unallocated: float

Unallocated equity weight. It's the remaining weight that can be allocated to assets. This is a read-only property.

weights property writable ¤

weights: Series

Current weight allocation. Weight should be non-negative and the total weight should be less than or equal to 1.

It's possible to assign weights to individual asset or to all assets in the asset space as a whole. When assigning weights as a whole, only non-zero weights need to be specified, and other weights are assigned zero automatically.

Example:

# Assign weight to individual asset
strategy.alloc.weights['A'] = 0.5

# Assign weight to all assets
strategy.alloc.weights = pd.Series([0.1, 0.2, 0.3], index=['A', 'B', 'C'])

Bucket ¤

Bucket(alloc: Allocation)

Bucket is a container that groups assets together and applies weight allocation among them. A bucket is associated with a parent allocation object, while the allocation object can be associated with multiple buckets.

Assets in a bucket are identified by their tickers. They are unique within the bucket, but can be repeated in different buckets.

Using Bucket for weight allocation takes 3 steps:

  1. Assets are added to the bucket by appending lists or filtering conditions. The rank of the assets in the bucket is preserved and can be used to assign weights.
  2. Weights are assigned to the assets using different allocation methods.
  3. Once the weight allocation at bucket level is done, the weights of the bucket can be merged into those of the parent allocation object.
tickers property ¤
tickers: list

Assets in the bucket. This is a read-only property.

weights property ¤
weights: Series

Weights of the assets in the bucket. This is only available after weight allocation is done by calling Bucket.weight_*() methods. This is a read-only property.

append ¤
append(ranked_list: list | Series, *conditions: list | Series) -> Bucket

Add assets that are in the ranked list to the end of the bucket.

ranked_list can be specified in three ways:

  1. A list of assets or anything list-like, all items will be added.
  2. A boolean Series with assets as the index and a True value to indicate the asset should be added.
  3. A non-boolean Series with assets as the index and all assets in the index will be added.

The rank of the assets is determined by its order in the list or in the index. The rank of the assets in the bucket is preserved. If an asset is already in the bucket, its rank in bucket will not be affected by appending new list to the bucket, even if the asset is ranked differently in the new list.

Multiple conditions can be specified as filters to exclude certain assets in the ranked list from being added. Assets must satisfy all the conditions in order to be added to the bucket.

conditions can be specified in the same way as ranked_list, only that the asset order in a condition is not important.

Example:

# Append 'A' and 'B' to the bucket
bucket.append(['A', 'B'])

# Append 'A' and 'C' to the bucket
bucket.append(pd.Series([True, False, True], index=['A', 'B', 'C']))

# Append 'C' to the bucket
bucket.append(pd.Series([1, 2, 3], index=['A', 'B', 'C']).nlargest(2), pd.Series([1, 2, 3], index=['A', 'B', 'C']) > 2)

Parameters:

Name Type Description Default
ranked_list list | Series

A list of assets or a Series of assets to be added to the bucket.

required
conditions list | Series

A list of assets or a Series of assets to be used as conditions to filter the assets.

()
apply ¤
apply(method: str = 'update') -> Bucket

Apply the weight allocation to the parent allocation object.

method controls how the bucket weight allocation should be merged into the parent allocation object.

When method is update, the weights of assets in the bucket will update the weights of the same assets in the parent allocation object. If an asset is not in the bucket, its weight in the parent allocation object will not be changed. This is the default method.

When method is overwrite, the weights of the parent allocation object will be replaced by the weights of the assets in the bucket or set to 0 if the asset is not in the bucket.

When method is accumulate, the weights of the assets in the bucket will be added to the weights of the same assets, while the weights of the assets not in the bucket will remain unchanged.

If the bucket is empty, no change will be made to the parent allocation object.

Note that no validation is performed on the weights of the parent allocation object after the bucket weight is merged. It is the responsibility of the user to ensure the final weights are valid before use.

Parameters:

Name Type Description Default
method str

Method to merge the bucket into the parent allocation object. Available methods are 'update', 'overwrite', 'accumulate'.

'update'
remove ¤
remove(*conditions: list | Series) -> Bucket

Remove assets that satisify all the given conditions from the bucket.

conditions can be specified in three ways:

  1. A list of assets or anything list-like, all assets will be removed.
  2. A boolean Series with assets as the index and a True value to indicate the asset should be removed.
  3. A non-boolean Series with assets as the index and all assets in the index will be removed.

Example:

# Remove 'A' and 'B' from the bucket
bucket.remove(['A', 'B'])

# Remove 'A' and 'C' from the bucket
bucket.remove(pd.Series([True, False, True], index=['A', 'B', 'C']))

# Remove 'A' and 'B' from the bucket
bucket.remove(pd.Series([1, 2, 3], index=['A', 'B', 'C']).nsmallest(2))

# Remove 'B' from the bucket
bucket.remove(pd.Series([1, 2, 3], index=['A', 'B', 'C']) > 1, pd.Series([1, 2, 3], index=['A', 'B', 'C']) < 3)
Args: conditions: A list of assets or a Series of assets to be used as conditions to filter the assets.

trim ¤
trim(limit: int) -> Bucket

Trim the bucket to a maximum number of assets.

Parameters:

Name Type Description Default
limit int

Maximum number of assets should be included

required
weight_equally ¤
weight_equally(sum_: float = None) -> Bucket

Allocate equity value equally to the assets in the bucket.

sum_ should be between 0 and 1, with 1 means 100% of value should be allocated.

Example:

bucket.append(['A', 'B', 'C']).weight_equally(0.5)

Parameters:

Name Type Description Default
sum_ float

Total weight that should be allocated.

None
weight_explicitly ¤
weight_explicitly(weight: float | list | Series) -> Bucket

Assign weights to the assets in the bucket.

weight can be specified in three ways:

  1. A single weight should be assigned to all assets in the bucket.
  2. A list of weights should be assigned to the assets in the bucket in rank order. If more weights are provided than the number of assets in the bucket, the extra weights are ignored. If fewer weights are provided, the remaining assets will be assigned a weight of 0.
  3. A Series with assets as the index and the weight as the value. If no weight is provided for an asset, it will be assigned a weight of 0. If a weight is provided for an asset that is not in the bucket, it will be ignored.

Example:

bucket.append(['A', 'B', 'C']).weight_explicitly(0.2)
bucket.append(['A', 'B', 'C']).weight_explicitly([0.1, 0.2, 0.3])
bucket.append(['A', 'B', 'C']).weight_explicitly(pd.Series([0.1, 0.2, 0.3], index=['A', 'B', 'C']))
Args: weight: A single value, a list of values or a Series of weights.

weight_proportionally ¤
weight_proportionally(relative_weights: list, sum_: float = None) -> Bucket

Allocate equity value proportionally to the assets in the bucket.

sum_ should be between 0 and 1, with 1 means 100% of value should be allocated.

Example:

bucket.append(['A', 'B', 'C']).weight_proportionally([1, 2, 3], 0.5)

Parameters:

Name Type Description Default
relative_weights list

A list of relative weights. The length of the list should be the same as the number of assets in the bucket.

required
sum_ float

Total weight that should be allocated.

None

assume_previous ¤

assume_previous()

Assume all assets inherit the same weight as used in the previous rebalance cycle.

assume_zero ¤

assume_zero()

Assume all assets have zero weight to begin with in a new rebalance cycle.

normalize ¤

normalize()

Normalize the weight allocation so that the sum of weights equals 1.

Backtest ¤

Backtest(data: DataFrame, strategy: Type[Strategy], *, cash: float = 10000, holding: dict = {}, commission: float = 0.0, margin: float = 1.0, trade_on_close=False, hedging=False, exclusive_orders=False, trade_start_date=None, lot_size=1, fail_fast=True, storage: dict | None = None)

Backtest a particular (parameterized) strategy on particular data.

Upon initialization, call method minitrade.backtest.core.backtesting.Backtest.run to run a backtest instance, or minitrade.backtest.core.backtesting.Backtest.optimize to optimize it.

data is a pd.DataFrame with 2-level columns: 1st level is a list of tickers, and 2nd level is Open, High, Low, Close, and Volume. If the strategy works only on one asset, the 1st level can be dropped. If any columns are missing, set them to what you have available, e.g.

df['Open'] = df['High'] = df['Low'] = df['Close']
df['Volumn'] = 0

The passed data frame can contain additional columns that can be used by the strategy (e.g. sentiment info). DataFrame index can be either a datetime index (timestamps) or a monotonic range index (i.e. a sequence of periods).

strategy is a minitrade.backtest.core.backtesting.Strategy subclass (not an instance).

cash is the initial cash to start with.

holding is a mapping of preexisting assets and their sizes before backtest begins, e.g.

{'AAPL': 10, 'MSFT': 5}

commission is the commission ratio. E.g. if your broker's commission is 1% of trade value, set commission to 0.01. Note, if you wish to account for bid-ask spread, you can approximate doing so by increasing the commission, e.g. set it to 0.0002 for commission-less forex trading where the average spread is roughly 0.2‰ of asking price.

margin is the required margin (ratio) of a leveraged account. No difference is made between initial and maintenance margins. To run the backtest using e.g. 50:1 leverge that your broker allows, set margin to 0.02 (1 / leverage).

If trade_on_close is True, market orders will be filled with respect to the current bar's closing price instead of the next bar's open.

If hedging is True, allow trades in both directions simultaneously. If False, the opposite-facing orders first close existing trades in a FIFO manner.

If exclusive_orders is True, each new order auto-closes the previous trade/position, making at most a single trade (long or short) in effect at each time.

If trade_start_date is not None, orders generated before the date are surpressed and ignored in backtesting.

lot_size is the minimum increment of shares you buy in one order. Order size will be rounded to integer multiples during rebalance.

fail_fast, when True, instructs the backtester to bail out when cash is not enough to cover an order. This can be used in live trading to detect issues early. If False, backtesting will ignore the order and continue, which can be convenient during algorithm research.

storage, when not None, is a dictionary that contains saved states from past runs. Modification to storage is persisted and can be made available for future runs.

optimize ¤

optimize(*, maximize: Union[str, Callable[[Series], float]] = 'SQN', method: str = 'grid', max_tries: Optional[Union[int, float]] = None, constraint: Optional[Callable[[dict], bool]] = None, return_heatmap: bool = False, return_optimization: bool = False, random_state: Optional[int] = None, **kwargs) -> Union[Series, Tuple[Series, Series], Tuple[Series, Series, dict]]

Optimize strategy parameters to an optimal combination. Returns result pd.Series of the best run.

maximize is a string key from the minitrade.backtest.core.backtesting.Backtest.run-returned results series, or a function that accepts this series object and returns a number; the higher the better. By default, the method maximizes Van Tharp's System Quality Number.

method is the optimization method. Currently two methods are supported:

  • "grid" which does an exhaustive (or randomized) search over the cartesian product of parameter combinations, and
  • "skopt" which finds close-to-optimal strategy parameters using model-based optimization, making at most max_tries evaluations.

max_tries is the maximal number of strategy runs to perform. If method="grid", this results in randomized grid search. If max_tries is a floating value between (0, 1], this sets the number of runs to approximately that fraction of full grid space. Alternatively, if integer, it denotes the absolute maximum number of evaluations. If unspecified (default), grid search is exhaustive, whereas for method="skopt", max_tries is set to 200.

constraint is a function that accepts a dict-like object of parameters (with values) and returns True when the combination is admissible to test with. By default, any parameters combination is considered admissible.

If return_heatmap is True, besides returning the result series, an additional pd.Series is returned with a multiindex of all admissible parameter combinations, which can be further inspected or projected onto 2D to plot a heatmap (see backtesting.lib.plot_heatmaps()).

If return_optimization is True and method = 'skopt', in addition to result series (and maybe heatmap), return raw scipy.optimize.OptimizeResult for further inspection, e.g. with scikit-optimize plotting tools.

If you want reproducible optimization results, set random_state to a fixed integer random seed.

Additional keyword arguments represent strategy arguments with list-like collections of possible values. For example, the following code finds and returns the "best" of the 7 admissible (of the 9 possible) parameter combinations:

backtest.optimize(sma1=[5, 10, 15], sma2=[10, 20, 40],
                  constraint=lambda p: p.sma1 < p.sma2)

.. TODO:: Improve multiprocessing/parallel execution on Windos with start method 'spawn'.

plot ¤

plot(*, results: Series = None, filename=None, plot_width=None, plot_equity=True, plot_return=False, plot_pl=True, plot_volume=False, plot_drawdown=False, plot_trades=True, smooth_equity=False, relative_equity=True, superimpose: Union[bool, str] = False, resample=True, reverse_indicators=False, show_legend=True, open_browser=True, plot_allocation=False, relative_allocation=True, plot_indicator=True)

Plot the progression of the last backtest run.

If results is provided, it should be a particular result pd.Series such as returned by minitrade.backtest.core.backtesting.Backtest.run or minitrade.backtest.core.backtesting.Backtest.optimize, otherwise the last run's results are used.

filename is the path to save the interactive HTML plot to. By default, a strategy/parameter-dependent file is created in the current working directory.

plot_width is the width of the plot in pixels. If None (default), the plot is made to span 100% of browser width. The height is currently non-adjustable.

If plot_equity is True, the resulting plot will contain an equity (initial cash plus assets) graph section. This is the same as plot_return plus initial 100%.

If plot_return is True, the resulting plot will contain a cumulative return graph section. This is the same as plot_equity minus initial 100%.

If plot_pl is True, the resulting plot will contain a profit/loss (P/L) indicator section.

If plot_volume is True, the resulting plot will contain a trade volume section.

If plot_drawdown is True, the resulting plot will contain a separate drawdown graph section.

If plot_trades is True, the stretches between trade entries and trade exits are marked by hash-marked tractor beams.

If smooth_equity is True, the equity graph will be interpolated between fixed points at trade closing times, unaffected by any interim asset volatility.

If relative_equity is True, scale and label equity graph axis with return percent, not absolute cash-equivalent values.

If superimpose is True, superimpose larger-timeframe candlesticks over the original candlestick chart. Default downsampling rule is: monthly for daily data, daily for hourly data, hourly for minute data, and minute for (sub-)second data. superimpose can also be a valid Pandas offset string, such as '5T' or '5min', in which case this frequency will be used to superimpose. Note, this only works for data with a datetime index.

If resample is True, the OHLC data is resampled in a way that makes the upper number of candles for Bokeh to plot limited to 10_000. This may, in situations of overabundant data, improve plot's interactive performance and avoid browser's Javascript Error: Maximum call stack size exceeded or similar. Equity & dropdown curves and individual trades data is, resample can also be a Pandas offset string, such as '5T' or '5min', in which case this frequency will be used to resample, overriding above numeric limitation. Note, all this only works for data with a datetime index.

If reverse_indicators is True, the indicators below the OHLC chart are plotted in reverse order of declaration.

If show_legend is True, the resulting plot graphs will contain labeled legends.

If open_browser is True, the resulting filename will be opened in the default web browser.

If plot_allocation is True, the resulting plot will contain an equity allocation graph section.

If relative_allocation is True, scale and label equity allocation graph axis with return percent, not absolute cash-equivalent values.

If plot_indicator is True, the resulting plot will contain a section for each indicator used in the strategy.

run ¤

run(**kwargs) -> Series

Run the backtest. Returns pd.Series with results and statistics.

Keyword arguments are interpreted as strategy parameters.

>>> Backtest(GOOG, SmaCross).run()
Start                     2004-08-19 00:00:00
End                       2013-03-01 00:00:00
Duration                   3116 days 00:00:00
Exposure Time [%]                     93.9944
Equity Final [$]                      51959.9
Equity Peak [$]                       75787.4
Return [%]                            419.599
Buy & Hold Return [%]                 703.458
Return (Ann.) [%]                      21.328
Volatility (Ann.) [%]                 36.5383
Sharpe Ratio                         0.583718
Sortino Ratio                         1.09239
Calmar Ratio                         0.444518
Max. Drawdown [%]                    -47.9801
Avg. Drawdown [%]                    -5.92585
Max. Drawdown Duration      584 days 00:00:00
Avg. Drawdown Duration       41 days 00:00:00
# Trades                                   65
Win Rate [%]                          46.1538
Best Trade [%]                         53.596
Worst Trade [%]                      -18.3989
Avg. Trade [%]                        2.35371
Max. Trade Duration         183 days 00:00:00
Avg. Trade Duration          46 days 00:00:00
Profit Factor                         2.08802
Expectancy [%]                        8.79171
SQN                                  0.916893
Kelly Criterion                        0.6134
_strategy                            SmaCross
_equity_curve                           Eq...
_trades                       Size  EntryB...
_orders                              Ticke...
_positions                           {'GOO...
_trade_start_bar                           0
dtype: object

.. warning:: You may obtain different results for different strategy parameters. E.g. if you use 50- and 200-bar SMA, the trading simulation will begin on bar 201. The actual length of delay is equal to the lookback period of the Strategy.I indicator which lags the most. Obviously, this can affect results.

Order ¤

Order(broker: _Broker, ticker: str, size: float, limit_price: Optional[float] = None, stop_price: Optional[float] = None, sl_price: Optional[float] = None, tp_price: Optional[float] = None, parent_trade: Optional[Trade] = None, entry_time: datetime = None, tag: object = None)

Place new orders through Strategy.buy() and Strategy.sell(). Query existing orders through Strategy.orders.

When an order is executed or filled, it results in a Trade.

If you wish to modify aspects of a placed but not yet filled order, cancel it and place a new one instead.

All placed orders are Good 'Til Canceled.

entry_time property ¤

entry_time: datetime

Time of when the order is created.

is_contingent property ¤

is_contingent

True for contingent orders, i.e. OCO stop-loss and take-profit bracket orders placed upon an active trade. Remaining contingent orders are canceled when their parent Trade is closed.

You can modify contingent orders through Trade.sl and Trade.tp.

is_long property ¤

is_long

True if the order is long (order size is positive).

is_short property ¤

is_short

True if the order is short (order size is negative).

limit property ¤

limit: Optional[float]

Order limit price for limit orders, or None for market orders, which are filled at next available price.

size property writable ¤

size: float

Order size (negative for short orders).

If size is a value between 0 and 1, it is interpreted as a fraction of current available liquidity (cash plus Position.pl minus used margin). A value greater than or equal to 1 indicates an absolute number of units.

sl property ¤

sl: Optional[float]

A stop-loss price at which, if set, a new contingent stop-market order will be placed upon the Trade following this order's execution. See also Trade.sl.

stop property ¤

stop: Optional[float]

Order stop price for stop-limit/stop-market order, otherwise None if no stop was set, or the stop price has already been hit.

tag property ¤

tag

Arbitrary value (such as a string) which, if set, enables tracking of this order and the associated Trade (see Trade.tag).

tp property ¤

tp: Optional[float]

A take-profit price at which, if set, a new contingent limit order will be placed upon the Trade following this order's execution. See also Trade.tp.

cancel ¤

cancel()

Cancel the order.

Position ¤

Position(broker: _Broker, ticker: str)

Currently held asset position, available as minitrade.backtest.core.backtesting.Strategy.position within minitrade.backtest.core.backtesting.Strategy.next. Can be used in boolean contexts, e.g.

if self.position():
    ...  # we have a position, either long or short

is_long property ¤

is_long: bool

True if the position is long (position size is positive).

is_short property ¤

is_short: bool

True if the position is short (position size is negative).

pl property ¤

pl: float

Profit (positive) or loss (negative) of the current position in cash units.

pl_pct property ¤

pl_pct: float

Profit (positive) or loss (negative) of the current position in percent.

size property ¤

size: float

Position size in units of asset. Negative if position is short.

close ¤

close(portion: float = 1.0)

Close portion of position by closing portion of each active trade. See Trade.close.

Strategy ¤

Strategy(broker, data, params)

Bases: ABC

A trading strategy base class. Extend this class and override methods minitrade.backtest.core.backtesting.Strategy.init and minitrade.backtest.core.backtesting.Strategy.next to define your own strategy.

alloc property ¤

alloc: Allocation

Allocation instance that manages the weight allocation among assets.

closed_trades property ¤

closed_trades: Tuple[Trade, ...]

List of settled trades (see Trade).

data property ¤

data: _Data

Price data, roughly as passed into minitrade.backtest.core.backtesting.Backtest.__init__, but with two significant exceptions:

  • data is not a DataFrame, but a custom structure that serves customized numpy arrays for reasons of performance and convenience. Besides OHLCV columns, .index and length, it offers .pip property, the smallest price unit of change.
  • Within minitrade.backtest.core.backtesting.Strategy.init, data arrays are available in full length, as passed into minitrade.backtest.core.backtesting.Backtest.__init__ (for precomputing indicators and such). However, within minitrade.backtest.core.backtesting.Strategy.next, data arrays are only as long as the current iteration, simulating gradual price point revelation. In each call of minitrade.backtest.core.backtesting.Strategy.next (iteratively called by minitrade.backtest.core.backtesting.Backtest internally), the last array value (e.g. data.Close[-1]) is always the most recent value.
  • If you need data arrays (e.g. data.Close) to be indexed Pandas Series or DataFrame, you can call their .df accessor (e.g. data.Close.df). If you need the whole of data as a DataFrame, use .df accessor (i.e. data.df).

equity property ¤

equity: float

Current account equity (cash plus assets).

orders property ¤

orders: List[Order]

List of orders (see Order) waiting for execution.

storage property ¤

storage: dict | None

Storage is a dictionary for saving custom data across backtest runs when used in the context of automated trading in incremental mode.

If backtest finishes successfully, any modification to the dictionary is persisted and can be accessed in future runs. If backtest fails due to any error, the modification is not saved. If backtest runs in dryrun mode, the modification is not saved.

No storage is provided when trading in "strict" mode, in which case storage is None.

I ¤

I(funcval: Union[DataFrame, Series, Callable], *args, name=None, plot=True, overlay=None, color=None, scatter=False, **kwargs) -> Union[DataFrame, Series]

Declare an indicator. An indicator is just an array of values, but one that is revealed gradually in minitrade.backtest.core.backtesting.Strategy.next much like minitrade.backtest.core.backtesting.Strategy.data is. Returns DataFrame in init() and np.ndarray of indicator values in next().

funcval is either a function that returns the indicator array(s) of same length as minitrade.backtest.core.backtesting.Strategy.data, or the indicator array(s) itself as a DataFrame, Series, or arrays.

In the plot legend, the indicator is labeled with function name, DataFrame column name, or Series name, unless name overrides it.

If plot is True, the indicator is plotted on the resulting minitrade.backtest.core.backtesting.Backtest.plot.

If overlay is True, the indicator is plotted overlaying the price candlestick chart (suitable e.g. for moving averages). If False, the indicator is plotted standalone below the candlestick chart.

color can be string hex RGB triplet or X11 color name. By default, the next available color is assigned.

If scatter is True, the plotted indicator marker will be a circle instead of a connected line segment (default).

Additional *args and **kwargs are passed to func and can be used for parameters.

For example, using simple moving average function from TA-Lib:

def init():
    self.sma = self.I(ta.SMA, self.data.Close, self.n_sma)

buy ¤

buy(*, ticker: str = None, size: float = _FULL_EQUITY, limit: Optional[float] = None, stop: Optional[float] = None, sl: Optional[float] = None, tp: Optional[float] = None, tag: object = None)

Place a new long order. For explanation of parameters, see Order and its properties.

For single asset strategy, ticker can be left as None.

See Position.close() and Trade.close() for closing existing positions.

See also Strategy.sell().

init abstractmethod ¤

init()

Initialize the strategy. Override this method. Declare indicators (with minitrade.backtest.core.backtesting.Strategy.I). Precompute what needs to be precomputed or can be precomputed in a vectorized fashion before the strategy starts.

If you extend composable strategies from minitrade.backtest.core.backtesting.lib, make sure to call: super().init()

next abstractmethod ¤

next()

Main strategy runtime method, called as each new minitrade.backtest.core.backtesting.Strategy.data instance (row; full candlestick bar) becomes available. This is the main method where strategy decisions upon data precomputed in minitrade.backtest.core.backtesting.Strategy.init take place.

If you extend composable strategies from minitrade.backtest.core.backtesting.lib, make sure to call: super().next()

position ¤

position(ticker: str = None) -> Position

Instance of minitrade.backtest.core.backtesting.Position.

For single asset strategy, ticker can be left as None, which returns the position of the only asset.

prepare_data classmethod ¤

prepare_data(tickers: List[str], start: str) -> DataFrame | None

Prepare data for trading.

This class method can be overridden in a Strategy implementation to provide data for trading. The can be useful when the data is not provided externally and the strategy wants to bring its own data, e.g. from a database.

Parameters:

Name Type Description Default
tickers List[str]

List of tickers to fetch data for.

required
start str

Start date of the data to fetch.

required

Returns:

Type Description
DataFrame | None

A pd.DataFrame with 2-level columns as required by Backtest() or None.

rebalance ¤

rebalance(force: bool = False, rtol: float = 0.01, atol: int = 0, cash_reserve: float = 0.1)

Rebalance the portfolio according to the current weight allocation.

If the weight allocation is not changed from the previous cycle, the rebalance is skipped. This behavior can be overridden by setting force to True, which will force rebalance even if the weight allocation is unchanged. This is useful when the actual portfolio value deviates from the target value due to price changes and should be corrected.

When a rebalance should be performed, the difference between the target and actual portfolio, defined as the sum of absolute difference of individual assets, is calculated. If the difference is too small compared to the relative tolerance rtol or the absolute tolerance atol, the rebalance is again skipped. This can be used to avoid unnecessary transaction cost. An exception is when the target weight of an asset is zero, in which case the position of the asset, if exists, is always closed.

cash_reserve is the ratio of total equity reserved as cash to account for order quantity rounding and sudden price changes between order placement and execution. It is recommended to set this value to a small positive number to avoid order rejection due to insufficient cash. The minimum value may depend on the volatility of the assets.

Parameters:

Name Type Description Default
force bool

If True, rebalance will be performed even if the current weight allocation is not changed from the previous.

False
rtol float

Relative tolerance of the total absolute value difference between current and previous allocation vs. total portfolio value. If the difference is smaller than rtol, rebalance will not be performed.

0.01
atol int

Absolute tolerance of the total absolute value difference between current and previous allocation. If the difference is smaller than atol, rebalance will not be performed.

0
cash_reserve float

Ratio of total equity reserved as cash to account for order quantity rounding and sudden price changes between order placement and execution.

0.1

record ¤

record(name: str = None, plot: bool = True, overlay: bool = None, color: str = None, scatter: bool = False, **kwargs)

Record arbitrary key-value pairs as time series. This can be used for diagnostic data collection or for plotting custom data.

Values to be recorded should be passed as keyword arguments. The value can be a scalar, a dictionary, or a pandas Series. If a dictionary or a Series is passed, its keys will be used as names for time series.

Example:

# Record a scalar value
self.record(my_key=42)

# Record a dictionary
self.record(my_dict={'a': 1, 'b': 2})

# Record a pandas Series
self.record(my_series=pd.Series({'a': 1, 'b': 2}))

Parameters:

Name Type Description Default
name str

Name of the time series. If not provided, the name will be the same as the keyword argument.

None
plot bool

If True, the time series will be plotted on the resulting minitrade.backtest.core.backtesting.Backtest.plot.

True
overlay bool

If True, the time series will be plotted overlaying the price candlestick chart. If False, the time series will be plotted standalone below the candlestick chart.

None
color str

Color of the time series. If not provided, the next available color will be assigned.

None
scatter bool

If True, the plotted time series marker will be a circle instead of a connected line segment.

False

sell ¤

sell(*, ticker: str = None, size: float = _FULL_EQUITY, limit: Optional[float] = None, stop: Optional[float] = None, sl: Optional[float] = None, tp: Optional[float] = None, tag: object = None)

Place a new short order. For explanation of parameters, see Order and its properties.

For single asset strategy, ticker can be left as None.

See also Strategy.buy().

.. note:: If you merely want to close an existing long position, use Position.close() or Trade.close().

start_on_day ¤

start_on_day(n: int)

Hint to start the backtest on a specific day.

This can be used to define a warm-up period, ensuring at least n days of data are available when next() is called for the first time.

When the backtest starts depends both on n and on the availability of indicators. If indicators are defined, the backtest will start when all indicators have valid data or on the n-th day, whichever comes later.

This method should be called in init().

Parameters:

Name Type Description Default
n int

Day index to start on. Must be within [0, len(data)-1].

required

trades ¤

trades(ticker: str = None) -> Tuple[Trade, ...]

List of active trades (see Trade).

Trade ¤

Trade(broker: _Broker, ticker: str, size: int, entry_price: float, entry_bar, tag)

When an Order is filled, it results in an active Trade. Find active trades in Strategy.trades and closed, settled trades in Strategy.closed_trades.

entry_bar property ¤

entry_bar: int

Candlestick bar index of when the trade was entered.

entry_price property ¤

entry_price: float

Trade entry price.

entry_time property ¤

entry_time: Union[Timestamp, int]

Datetime of when the trade was entered.

exit_bar property ¤

exit_bar: Optional[int]

Candlestick bar index of when the trade was exited (or None if the trade is still active).

exit_price property ¤

exit_price: Optional[float]

Trade exit price (or None if the trade is still active).

exit_time property ¤

exit_time: Optional[Union[Timestamp, int]]

Datetime of when the trade was exited.

is_long property ¤

is_long

True if the trade is long (trade size is positive).

is_short property ¤

is_short

True if the trade is short (trade size is negative).

pl property ¤

pl

Trade profit (positive) or loss (negative) in cash units.

pl_pct property ¤

pl_pct

Trade profit (positive) or loss (negative) in percent.

size property ¤

size

Trade size (volume; negative for short trades).

sl property writable ¤

sl

Stop-loss price at which to close the trade.

This variable is writable. By assigning it a new price value, you create or modify the existing SL order. By assigning it None, you cancel it.

tag property ¤

tag

A tag value inherited from the Order that opened this trade.

This can be used to track trades and apply conditional logic / subgroup analysis.

See also Order.tag.

tp property writable ¤

tp

Take-profit price at which to close the trade.

This property is writable. By assigning it a new price value, you create or modify the existing TP order. By assigning it None, you cancel it.

value property ¤

value

Trade total value in cash (volume × price).

close ¤

close(portion: float = 1.0, finalize=False)

Place new Order to close portion of the trade at next market price.