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
¤
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:
-
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:
-
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:
The state of the Allocation
object is managed by the Strategy
object across rebalance cycles. A rebalance
cycle involves:
- Initializing the weight allocation at the beginning of the cycle by calling either
Allocation.assume_zero()
to reset all weights to zero orAllocation.assume_previous()
to inherit the weights from the previous cycle. This must be done before any weight allocation attempts. - Adjusting the weight allocation using either explicitly assignment or
Bucket
method. - 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
¤
previous_weights
property
¤
Previous weight allocation. This is a read-only property.
unallocated
property
¤
Unallocated equity weight. It's the remaining weight that can be allocated to assets. This is a read-only property.
weights
property
writable
¤
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:
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:
- 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.
- Weights are assigned to the assets using different allocation methods.
- Once the weight allocation at bucket level is done, the weights of the bucket can be merged into those of the parent allocation object.
weights
property
¤
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:
- A list of assets or anything list-like, all items will be added.
- A boolean Series with assets as the index and a True value to indicate the asset should be added.
- 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:
- A list of assets or anything list-like, all assets will be removed.
- A boolean Series with assets as the index and a True value to indicate the asset should be removed.
- 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)
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_explicitly
¤
weight_explicitly(weight: float | list | Series) -> Bucket
Assign weights to the assets in the bucket.
weight
can be specified in three ways:
- A single weight should be assigned to all assets in the bucket.
- 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.
- 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:
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:
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 all assets inherit the same weight as used in the previous rebalance cycle.
assume_zero
¤
Assume all assets have zero weight to begin with in a new rebalance cycle.
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 mostmax_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 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.
is_contingent
property
¤
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
.
limit
property
¤
Order limit price for limit orders, or None for market orders, which are filled at next available price.
size
property
writable
¤
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
¤
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
¤
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
¤
Arbitrary value (such as a string) which, if set, enables tracking
of this order and the associated Trade
(see Trade.tag
).
tp
property
¤
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
.
Position
¤
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
Strategy
¤
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.
data
property
¤
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 intominitrade.backtest.core.backtesting.Backtest.__init__
(for precomputing indicators and such). However, withinminitrade.backtest.core.backtesting.Strategy.next
,data
arrays are only as long as the current iteration, simulating gradual price point revelation. In each call ofminitrade.backtest.core.backtesting.Strategy.next
(iteratively called byminitrade.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
).
storage
property
¤
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
¤
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
¤
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 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 |
rebalance
¤
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 |
0.01
|
atol |
int
|
Absolute tolerance of the total absolute value difference between current
and previous allocation. If the difference is smaller than |
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 |
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
¤
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 |
Trade
¤
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
.
exit_bar
property
¤
Candlestick bar index of when the trade was exited (or None if the trade is still active).
exit_price
property
¤
Trade exit price (or None if the trade is still active).
exit_time
property
¤
Datetime of when the trade was exited.
sl
property
writable
¤
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
¤
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
¤
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.
close
¤
Place new Order
to close portion
of the trade at next market price.