mirror of
https://github.com/wassname/options_backtester.git
synced 2026-07-20 12:30:38 +08:00
Added daily/monthly balance (pd.DataFrame that keeps track of the value of current positions)
This commit is contained in:
+52
-12
@@ -11,7 +11,6 @@ class Backtest:
|
||||
def __init__(self):
|
||||
self._strategy = None
|
||||
self._data = None
|
||||
self.inventory = pd.DataFrame()
|
||||
self.stop_if_broke = True
|
||||
|
||||
@property
|
||||
@@ -22,7 +21,7 @@ class Backtest:
|
||||
def strategy(self, strat):
|
||||
assert isinstance(strat, Strategy)
|
||||
self._strategy = strat
|
||||
self.current_capital = strat.initial_capital
|
||||
self.current_cash = strat.initial_capital
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
@@ -37,22 +36,27 @@ class Backtest:
|
||||
"""Runs the backtest and returns a `pd.DataFrame` of the orders executed (`self.trade_log`)
|
||||
|
||||
Args:
|
||||
monthly (bool, optional): Iterates through data monthly rather than daily. Defaults to False.
|
||||
monthly (bool, optional): Iterates through data monthly rather than daily. Defaults to False.
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: Log of the trades executed.
|
||||
pd.DataFrame: Log of the trades executed.
|
||||
"""
|
||||
|
||||
assert self._data is not None
|
||||
assert self._strategy is not None
|
||||
assert self._data.schema == self._strategy.schema
|
||||
|
||||
index = pd.MultiIndex.from_product(
|
||||
columns = pd.MultiIndex.from_product(
|
||||
[[l.name for l in self._strategy.legs],
|
||||
['contract', 'underlying', 'expiration', 'type', 'strike', 'cost', 'order']])
|
||||
index_totals = pd.MultiIndex.from_product([['totals'], ['cost', 'qty', 'date']])
|
||||
self.inventory = pd.DataFrame(columns=index.append(index_totals))
|
||||
totals = pd.MultiIndex.from_product([['totals'], ['cost', 'qty', 'date']])
|
||||
self.inventory = pd.DataFrame(columns=columns.append(totals))
|
||||
self.trade_log = pd.DataFrame()
|
||||
self.balance = pd.DataFrame({
|
||||
'capital': self.current_cash,
|
||||
'cash': self.current_cash
|
||||
},
|
||||
index=[self.data.start_date - pd.Timedelta(1, unit='day')])
|
||||
|
||||
data_iterator = self._data.iter_months() if monthly else self._data.iter_dates()
|
||||
bar = pyprind.ProgBar(data_iterator.ngroups, bar_char='█')
|
||||
@@ -63,19 +67,23 @@ class Backtest:
|
||||
|
||||
self._execute_exit(exit_signals)
|
||||
self._execute_entry(entry_signals)
|
||||
self._update_balance(date, options)
|
||||
|
||||
bar.update()
|
||||
|
||||
self.balance['% change'] = self.balance['capital'].pct_change()
|
||||
self.balance['accumulated return'] = (1.0 + self.balance['% change']).cumprod() - 1
|
||||
|
||||
return self.trade_log
|
||||
|
||||
def _execute_entry(self, entry_signals):
|
||||
"""Executes entry orders and updates `self.inventory` and `self.trade_log`"""
|
||||
entry, total_price = self._process_entry_signals(entry_signals)
|
||||
|
||||
if (not self.stop_if_broke) or (self.current_capital >= total_price):
|
||||
if (not self.stop_if_broke) or (self.current_cash >= total_price):
|
||||
self.inventory = self.inventory.append(entry, ignore_index=True)
|
||||
self.trade_log = self.trade_log.append(entry, ignore_index=True)
|
||||
self.current_capital -= total_price
|
||||
self.current_cash -= total_price
|
||||
|
||||
def _execute_exit(self, exit_signals):
|
||||
"""Executes exits and updates `self.inventory` and `self.trade_log`"""
|
||||
@@ -83,10 +91,10 @@ class Backtest:
|
||||
|
||||
self.trade_log = self.trade_log.append(exits, ignore_index=True)
|
||||
self.inventory.drop(self.inventory[exits_mask].index, inplace=True)
|
||||
self.current_capital -= sum(total_costs)
|
||||
self.current_cash -= sum(total_costs)
|
||||
|
||||
def _process_entry_signals(self, entry_signals):
|
||||
"""Returns a dictionary containing the orders to execute."""
|
||||
"""Returns the entry signals to execute and their cost."""
|
||||
|
||||
if not entry_signals.empty:
|
||||
# costs = entry_signals['totals']['cost']
|
||||
@@ -96,6 +104,38 @@ class Backtest:
|
||||
else:
|
||||
return entry_signals, 0
|
||||
|
||||
def _update_balance(self, date, options):
|
||||
"""Updates positions and calculates statistics for the current date.
|
||||
|
||||
Args:
|
||||
date (pd.Timestamp): Current date.
|
||||
options (pd.DataFrame): DataFrame of (daily/monthly) options.
|
||||
"""
|
||||
|
||||
leg_candidates = [
|
||||
self._strategy._exit_candidates(l.direction, self.inventory[l.name], options) for l in self._strategy.legs
|
||||
]
|
||||
|
||||
calls_value = -np.sum(
|
||||
np.sum(leg['cost'] * self.inventory['totals']['qty']
|
||||
for leg in leg_candidates if (leg['type'] == 'call').any()))
|
||||
puts_value = -np.sum(
|
||||
np.sum(leg['cost'] * self.inventory['totals']['qty']
|
||||
for leg in leg_candidates if (leg['type'] == 'put').any()))
|
||||
|
||||
capital = calls_value + puts_value + self.current_cash
|
||||
|
||||
row = pd.Series(
|
||||
{
|
||||
'qty': self.inventory['totals']['qty'].sum(),
|
||||
'calls value': calls_value,
|
||||
'puts value': puts_value,
|
||||
'cash': self.current_cash,
|
||||
'capital': capital,
|
||||
},
|
||||
name=date)
|
||||
self.balance = self.balance.append(row)
|
||||
|
||||
def summary(self):
|
||||
"""Returns a table with summary statistics about the trade log"""
|
||||
df = self.trade_log
|
||||
@@ -153,4 +193,4 @@ class Backtest:
|
||||
return summary
|
||||
|
||||
def __repr__(self):
|
||||
return "Backtest(capital={}, strategy={})".format(self.current_capital, self._strategy)
|
||||
return "Backtest(capital={}, strategy={})".format(self.current_cash, self._strategy)
|
||||
|
||||
@@ -16,14 +16,20 @@ class HistoricalOptionsData:
|
||||
if file_extension == '.h5':
|
||||
self._data = pd.read_hdf(file, **params)
|
||||
elif file_extension == '.csv':
|
||||
params["parse_dates"] = [self.schema.expiration.mapping, self.schema.date.mapping]
|
||||
params['parse_dates'] = [self.schema.expiration.mapping, self.schema.date.mapping]
|
||||
self._data = pd.read_csv(file, **params)
|
||||
|
||||
columns = self._data.columns
|
||||
assert all((col in columns for _key, col in self.schema))
|
||||
|
||||
self._data["dte"] = (self._data["expiration"] - self._data["quotedate"]).dt.days
|
||||
self.schema.update({"dte": "dte"})
|
||||
date_col = self.schema['date']
|
||||
expiration_col = self.schema['expiration']
|
||||
|
||||
self._data['dte'] = (self._data[expiration_col] - self._data[date_col]).dt.days
|
||||
self.schema.update({'dte': 'dte'})
|
||||
|
||||
self.start_date = self._data[date_col].min()
|
||||
self.end_date = self._data[date_col].max()
|
||||
|
||||
def apply_filter(self, f):
|
||||
"""Apply Filter `f` to the data. Returns a `pd.DataFrame` with the filtered rows."""
|
||||
@@ -31,11 +37,11 @@ class HistoricalOptionsData:
|
||||
|
||||
def iter_dates(self):
|
||||
"""Returns `pd.DataFrameGroupBy` that groups contracts by date"""
|
||||
return self._data.groupby(self.schema["date"])
|
||||
return self._data.groupby(self.schema['date'])
|
||||
|
||||
def iter_months(self):
|
||||
"""Returns `pd.DataFrameGroupBy` that groups contracts by month"""
|
||||
date_col = self.schema["date"]
|
||||
date_col = self.schema['date']
|
||||
iterator = self._data.groupby(pd.Grouper(
|
||||
key=date_col,
|
||||
freq="MS")).apply(lambda g: g[g[date_col] == g[date_col].min()]).reset_index(drop=True).groupby(date_col)
|
||||
@@ -45,7 +51,7 @@ class HistoricalOptionsData:
|
||||
"""Pass method invocation to `self._data`"""
|
||||
|
||||
method = getattr(self._data, attr)
|
||||
if hasattr(method, "__call__"):
|
||||
if hasattr(method, '__call__'):
|
||||
|
||||
def df_method(*args, **kwargs):
|
||||
return method(*args, **kwargs)
|
||||
@@ -76,14 +82,14 @@ class HistoricalOptionsData:
|
||||
"""Returns default schema for Historical Options Data"""
|
||||
schema = Schema.canonical()
|
||||
schema.update({
|
||||
"contract": "optionroot",
|
||||
"date": "quotedate",
|
||||
"last": "last",
|
||||
"open_interest": "openinterest",
|
||||
"impliedvol": "impliedvol",
|
||||
"delta": "delta",
|
||||
"gamma": "gamma",
|
||||
"theta": "theta",
|
||||
"vega": "vega"
|
||||
'contract': 'optionroot',
|
||||
'date': 'quotedate',
|
||||
'last': 'last',
|
||||
'open_interest': 'openinterest',
|
||||
'impliedvol': 'impliedvol',
|
||||
'delta': 'delta',
|
||||
'gamma': 'gamma',
|
||||
'theta': 'theta',
|
||||
'vega': 'vega'
|
||||
})
|
||||
return schema
|
||||
|
||||
@@ -101,14 +101,7 @@ class Strategy:
|
||||
pd.DataFrame: Exit signals
|
||||
"""
|
||||
|
||||
underlying_col, spot_col = self.schema['underlying'], self.schema['underlying_last']
|
||||
underlying_symbols = options.loc[:, (underlying_col, spot_col)].drop_duplicates(underlying_col)
|
||||
spot_prices = underlying_symbols.set_index(underlying_col).to_dict()
|
||||
|
||||
leg_candidates = [
|
||||
self._exit_candidates(l.direction, inventory[l.name], options, spot_prices) for l in self.legs
|
||||
]
|
||||
|
||||
leg_candidates = [self._exit_candidates(l.direction, inventory[l.name], options) for l in self.legs]
|
||||
total_costs = sum([l['cost'] for l in leg_candidates])
|
||||
threshold_exits = self._filter_thresholds(inventory['totals']['cost'], total_costs)
|
||||
|
||||
@@ -219,18 +212,16 @@ class Strategy:
|
||||
|
||||
return pd.concat(dfs, axis=1)
|
||||
|
||||
def _exit_candidates(self, direction, inventory_leg, options, spot_prices):
|
||||
def _exit_candidates(self, direction, inventory_leg, options):
|
||||
"""Returns the exit candidates for the given inventory leg with their order and cost (positive for STC orders).
|
||||
|
||||
Args:
|
||||
direction (option.Direction): Direction of the leg for `Signal.EXIT`
|
||||
inventory_leg (pd.DataFrame): DataFrame of contracts in the inventory leg
|
||||
options (pd.DataFrame): Options in the current time step
|
||||
spot_prices (dict): Dictionary mapping underlying symbols to their spot prices
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: DataFrame with a `current_cost` column with the
|
||||
(possibly imputed) cost for the contracts in `inventory_leg`
|
||||
pd.DataFrame: DataFrame with the cost for the contracts in `inventory_leg`
|
||||
"""
|
||||
|
||||
# FIXME: Leaky abstraction (inventory schema)
|
||||
|
||||
Reference in New Issue
Block a user