Files
options_backtester/asset_backtester/backtester.py
T

122 lines
4.2 KiB
Python

import pandas as pd
import pyprind
from .portfolio import Portfolio
class Backtest:
def __init__(self, schema, initial_capital=1_000_000):
self.schema = schema
self._portfolio = None
self._data = None
self.initial_capital = initial_capital
@property
def portfolio(self):
return self._portfolio
@portfolio.setter
def portfolio(self, portfolio):
assert isinstance(portfolio, Portfolio)
self._portfolio = portfolio
@property
def data(self):
return self._data
@data.setter
def data(self, data):
self._data = data
def run(self, periods=1, sma_days=None):
"""Runs a backtest and returns a dataframe with the daily balance"""
assert self._data is not None
assert self._portfolio is not None
self.current_capital = 0
self.current_cash = self.initial_capital
self.inventory = pd.DataFrame(columns=['symbol', 'cost', 'qty'])
self.balance = pd.DataFrame()
if sma_days:
self._data.sma(sma_days)
data_iterator = self._data.iter_dates()
first_day = self._data['date'].min()
last_day = self._data['date'].max()
rebalancing_days = pd.date_range(first_day, last_day, freq=str(periods) +
'BMS').to_pydatetime() if periods is not None else []
bar = pyprind.ProgBar(data_iterator.ngroups, bar_char='█')
self.balance = pd.DataFrame({
'capital': self.current_cash,
'cash': self.current_cash
},
index=[self._data.start_date - pd.Timedelta(1, unit='day')])
for date, data in data_iterator:
if date == first_day:
self._rebalance_portfolio(data, sma_days)
self._update_balance(date, data)
if date in rebalancing_days:
self._rebalance_portfolio(data, sma_days)
bar.update()
self.balance['% change'] = self.balance['capital'].pct_change()
self.balance['accumulated return'] = (1.0 + self.balance['% change']).cumprod()
return self.balance
def _rebalance_portfolio(self, data, sma_days):
"""Rebalances the portfolio so that the total money is allocated according to the given percentages"""
money_total = self.current_cash + self.current_capital
for asset in self._portfolio.assets:
query = '{} == "{}"'.format(self.schema['symbol'], asset.symbol)
asset_current = data.query(query)
asset_price = asset_current[self.schema['adjClose']].values[0]
if sma_days is not None:
if asset_current['sma'].values[0] < asset_price:
qty = (money_total * asset.percentage) // asset_price
else:
qty = 0
else:
qty = (money_total * asset.percentage) // asset_price
inventory_entry = self.inventory.query(query)
self.inventory.drop(inventory_entry.index, inplace=True)
updated_asset = pd.Series([asset.symbol, asset_price, qty])
updated_asset.index = self.inventory.columns
self.inventory = self.inventory.append(updated_asset, ignore_index=True)
# Update current cash
invested_capital = sum(self.inventory['cost'] * self.inventory['qty'])
self.current_cash = money_total - invested_capital
def _update_balance(self, date, data):
"""Updates self.balance for the given date"""
costs = []
for asset in self._portfolio.assets:
query = '{} == "{}"'.format(self.schema['symbol'], asset.symbol)
asset_current = data.query(query)
inventory_asset = self.inventory.query(query)
cost = asset_current[self.schema['adjClose']].values[0]
qty = inventory_asset['qty'].values[0]
costs.append(cost * qty)
total_value = sum(costs)
self.current_capital = total_value
money_total = total_value + self.current_cash
row = pd.Series({
'total value': total_value,
'cash': self.current_cash,
'capital': money_total,
}, name=date)
self.balance = self.balance.append(row)