Skip to content

[WIP] Support multiple instruments(symbols) backtest #639

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
fix linter issue
  • Loading branch information
robert1003 committed May 8, 2022
commit 82ace33999b7a6afac6c46c4a825f6abc52c0942
1 change: 1 addition & 0 deletions backtesting/_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ def f(s, new_index=pd.Index(df.index.view(int)), bars=trades[column]):


def plot(*, results: pd.Series,
symbols: List[str],
df: pd.DataFrame,
indicators: List[_Indicator],
filename='', plot_width=None,
Expand Down
7 changes: 3 additions & 4 deletions backtesting/_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,10 @@ def _round_timedelta(value, _period=_data_period(index)):
r = []
for symbol in symbols:
c = ohlc_data[f"{symbol}_Close"].values
_r = (c[-1] - c[0]) / c[0] * 100
s.loc[f'Buy & Hold Return for {symbol} [%]'] = _r # long-only return
_r = (c[-1] - c[0]) / c[0] * 100
s.loc[f'Buy & Hold Return for {symbol} [%]'] = _r # long-only return
r.append(_r)
s.loc[f'Avg Buy & Hold Return [%]'] = np.mean(r)

s.loc['Avg Buy & Hold Return [%]'] = np.mean(r)

gmean_day_return: float = 0
day_returns = np.array(np.nan)
Expand Down
60 changes: 36 additions & 24 deletions backtesting/backtesting.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class Strategy(metaclass=ABCMeta):
`backtesting.backtesting.Strategy.next` to define
your own strategy.
"""

def __init__(self, broker, data, params):
self._indicators = []
self._broker: _Broker = broker
Expand Down Expand Up @@ -287,6 +288,7 @@ class _Orders(tuple):
"""
TODO: remove this class. Only for deprecation.
"""

def cancel(self):
"""Cancel all non-contingent (i.e. SL/TP) orders."""
for order in self:
Expand Down Expand Up @@ -314,6 +316,7 @@ class Position:
if self.position:
... # we have a position, either long or short
"""

def __init__(self, broker: '_Broker'):
self.__broker = broker

Expand Down Expand Up @@ -378,6 +381,7 @@ class Order:
[filled]: https://www.investopedia.com/terms/f/fill.asp
[Good 'Til Canceled]: https://www.investopedia.com/terms/g/gtc.asp
"""

def __init__(self, broker: '_Broker',
symbol: str,
size: float,
Expand All @@ -402,15 +406,17 @@ def _replace(self, **kwargs):
return self

def __repr__(self):
return '<Order symbol={}, {}>'.format(self.__symbol, ', '.join(f'{param}={round(value, 5)}'
for param, value in (
('size', self.__size),
('limit', self.__limit_price),
('stop', self.__stop_price),
('sl', self.__sl_price),
('tp', self.__tp_price),
('contingent', self.is_contingent),
) if value is not None))
return '<Order symbol={}, {}>'.format(self.__symbol, \
', '.join(f'{param}={round(value, 5)}'
for param, value in (
('size', self.__size),
('limit', self.__limit_price),
('stop', self.__stop_price),
('sl', self.__sl_price),
('tp', self.__tp_price),
('contingent',
self.is_contingent),
) if value is not None))

def cancel(self):
"""Cancel the order."""
Expand Down Expand Up @@ -523,6 +529,7 @@ class 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`.
"""

def __init__(self, broker: '_Broker', symbol: str, size: int, entry_price: float, entry_bar):
self.__broker = broker
self.__symbol = symbol
Expand All @@ -535,7 +542,8 @@ def __init__(self, broker: '_Broker', symbol: str, size: int, entry_price: float
self.__tp_order: Optional[Order] = None

def __repr__(self):
return f'<Trade symbol={self.__symbol} size={self.__size} time={self.__entry_bar}-{self.__exit_bar or ""} ' \
return f'<Trade symbol={self.__symbol} size={self.__size} ' \
f'time={self.__entry_bar}-{self.__exit_bar or ""} ' \
f'price={self.__entry_price}-{self.__exit_price or ""} pl={self.pl:.0f}>'

def _replace(self, **kwargs):
Expand Down Expand Up @@ -703,7 +711,7 @@ def __init__(self, *, symbols, data, cash, commission, margin,
self._equity = np.tile(np.nan, len(index))
self.orders: List[Order] = []
self.trades: List[Trade] = []
self.positions = {symbol:Position(self) for symbol in symbols}
self.positions = {symbol: Position(self) for symbol in symbols}
self.closed_trades: List[Trade] = []

@property
Expand Down Expand Up @@ -769,32 +777,31 @@ def new_order(self,

return order

#@property
# @property
def last_price(self, symbol) -> float:
""" Price at the last (current) close. """
return self._data[f"{symbol}_Close"][-1]

#@property
# @property
def prev_close(self, symbol) -> float:
""" Price at the previous close. """
return self._data[f"{symbol}_Close"][-2]

#@property
# @property
def last_open(self, symbol) -> float:
""" Price at the last open. """
return self._data[f"{symbol}_Open"][-1]

#@property
# @property
def last_high(self, symbol) -> float:
""" Price at the last open. """
return self._data[f"{symbol}_High"][-1]

#@property
# @property
def last_low(self, symbol) -> float:
""" Price at the last open. """
return self._data[f"{symbol}_Low"][-1]


def _adjusted_price(self, symbol, size=None, price=None) -> float:
"""
Long/short `price`, adjusted for commisions.
Expand Down Expand Up @@ -835,9 +842,10 @@ def _process_orders(self):

# Process orders
for order in list(self.orders): # type: Order
open, high, low = self.last_open(order.symbol), self.last_high(order.symbol), self.last_low(order.symbol)
open, high, low = self.last_open(order.symbol), self.last_high(
order.symbol), self.last_low(order.symbol)
prev_close = self.prev_close(order.symbol)

# Related SL/TP order was already removed
if order not in self.orders:
continue
Expand Down Expand Up @@ -907,7 +915,7 @@ def _process_orders(self):
# Adjust price to include commission (or bid-ask spread).
# In long positions, the adjusted price is a fraction higher, and vice versa.
adjusted_price = self._adjusted_price(order.symbol, order.size, price)

# If order size was specified proportionally,
# precompute true size in units, accounting for margin and spread/commissions
size = order.size
Expand All @@ -920,7 +928,7 @@ def _process_orders(self):
continue
assert size == round(size)
need_size = int(size)

if not self._hedging:
# Fill position by FIFO closing/reducing existing opposite-facing trades.
# Existing trades are closed at unadjusted price, because the adjustment
Expand Down Expand Up @@ -953,7 +961,8 @@ def _process_orders(self):

# Open a new trade
if need_size:
self._open_trade(order.symbol, adjusted_price, need_size, order.sl, order.tp, time_index)
self._open_trade(order.symbol, adjusted_price, need_size,
order.sl, order.tp, time_index)

# We need to reprocess the SL/TP orders newly added to the queue.
# This allows e.g. SL hitting in the same bar the order was open.
Expand Down Expand Up @@ -1034,6 +1043,7 @@ class Backtest:
instance, or `backtesting.backtesting.Backtest.optimize` to
optimize it.
"""

def __init__(self,
symbols: List[str],
data: pd.DataFrame,
Expand Down Expand Up @@ -1095,7 +1105,8 @@ def __init__(self,
if not (isinstance(strategy, type) and issubclass(strategy, Strategy)):
raise TypeError('`strategy` must be a Strategy sub-type')
if not isinstance(symbols, list) or not all([isinstance(s, str) for s in symbols]) or len(symbols) == 0:
raise TypeError('`symbols` must be list of string with size > 0 representing symbols in data')
raise TypeError(
'`symbols` must be list of string with size > 0 representing symbols in data')
if not isinstance(data, pd.DataFrame):
raise TypeError("`data` must be a pandas.DataFrame with columns")
if not isinstance(commission, Number):
Expand All @@ -1121,7 +1132,8 @@ def __init__(self,

if len(data) == 0:
raise ValueError('OHLC `data` is empty')
base_columns = [f"{symbol}_{column}" for column in ['Open', 'High', 'Low', 'Close', 'Volume']]
base_columns = [f"{symbol}_{column}" for column in [
'Open', 'High', 'Low', 'Close', 'Volume']]
if len(data.columns.intersection(set(base_columns))) != 5:
raise ValueError("`data` must be a pandas.DataFrame with columns "
"'symbol_Open', 'symbol_High', 'symbol_Low', 'symbol_Close',"
Expand Down
5 changes: 3 additions & 2 deletions backtesting/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from itertools import compress
from numbers import Number
from inspect import currentframe
from typing import Sequence, Optional, Union, Callable
from typing import Sequence, Optional, Union, Callable, List

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -167,6 +167,7 @@ def quantile(series: Sequence, quantile: Union[None, float] = None):

def compute_stats(
*,
symbols: List[str],
stats: pd.Series,
data: pd.DataFrame,
trades: pd.DataFrame = None,
Expand Down Expand Up @@ -194,7 +195,7 @@ def compute_stats(
equity[:] = stats._equity_curve.Equity.iloc[0]
for t in trades.itertuples(index=False):
equity.iloc[t.EntryBar:] += t.PnL
return _compute_stats(trades=trades, equity=equity, ohlc_data=data,
return _compute_stats(symbols=symbols, trades=trades, equity=equity, ohlc_data=data,
risk_free_rate=risk_free_rate, strategy_instance=stats._strategy)


Expand Down
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy