From 0df22363557c6ac6257c931a3fc0d4ef6903fa90 Mon Sep 17 00:00:00 2001 From: Juan Pablo Amoroso Date: Thu, 26 Dec 2019 14:52:45 -0300 Subject: [PATCH] Filter contracts already in inventory. Added monthly iteration --- backtester/backtester.py | 33 ++++++++------ .../datahandler/historical_options_data.py | 11 ++--- backtester/strategy/strategy.py | 43 ++++++++----------- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/backtester/backtester.py b/backtester/backtester.py index 22ae18d..6bd18d6 100644 --- a/backtester/backtester.py +++ b/backtester/backtester.py @@ -6,7 +6,6 @@ from .datahandler import HistoricalOptionsData class Backtest: """Processes signals from the Strategy object""" - def __init__(self, qty=1, capital=1_000_000, shares_per_contract=100): self.capital = capital self.shares_per_contract = shares_per_contract @@ -34,24 +33,34 @@ class Backtest: assert isinstance(data, HistoricalOptionsData) self._data = data - def run(self): - """Runs the backtest and returns a `pd.DataFrame` of the orders executed (`self.trade_log`)""" + def run(self, monthly=False): + """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. + + Returns: + 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 self.trade_log = pd.DataFrame() - for date, options in self._data.iter_dates(): - entry_signals = self._strategy.filter_entries(options) + data_iterator = self._data.iter_months() if monthly else self._data.iter_dates() + + for _date, options in data_iterator: + entry_signals = self._strategy.filter_entries(options, self.inventory) exit_signals = self._strategy.filter_exits(options, self.inventory) - self._execute_exit(date, exit_signals) - self._execute_entry(date, entry_signals) + self._execute_exit(exit_signals) + self._execute_entry(entry_signals) return self.trade_log - def _execute_entry(self, date, entry_signals): + 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) @@ -60,7 +69,7 @@ class Backtest: self.trade_log = self.trade_log.append(entry, ignore_index=True) self.capital -= total_price - def _execute_exit(self, date, exit_signals): + def _execute_exit(self, exit_signals): """Executes exits and updates `self.inventory` and `self.trade_log`""" if exit_signals is None: return @@ -75,11 +84,9 @@ class Backtest: if not entry_signals.empty: costs = entry_signals['totals']['cost'] - return entry_signals.loc[costs.idxmin():costs.idxmin()], costs.min( - ) + return entry_signals.loc[costs.idxmin():costs.idxmin()], costs.min() else: return entry_signals, 0 def __repr__(self): - return "Backtest(capital={}, strategy={})".format( - self.capital, self._strategy) + return "Backtest(capital={}, strategy={})".format(self.capital, self._strategy) diff --git a/backtester/datahandler/historical_options_data.py b/backtester/datahandler/historical_options_data.py index 909d421..256e0ea 100644 --- a/backtester/datahandler/historical_options_data.py +++ b/backtester/datahandler/historical_options_data.py @@ -16,16 +16,13 @@ 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._data["dte"] = (self._data["expiration"] - self._data["quotedate"]).dt.days self.schema.update({"dte": "dte"}) def apply_filter(self, f): @@ -36,6 +33,10 @@ class HistoricalOptionsData: """Returns `pd.DataFrameGroupBy` that groups contracts by date""" return self._data.groupby(self.schema["date"]) + def iter_months(self): + """Returns `pd.DataFrameGroupBy` that groups contracts by month""" + return self._data.groupby(pd.Grouper(key=self.schema["date"], freq="MS")) + def __getattr__(self, attr): """Pass method invocation to `self._data`""" diff --git a/backtester/strategy/strategy.py b/backtester/strategy/strategy.py index a1c5f6c..089569e 100644 --- a/backtester/strategy/strategy.py +++ b/backtester/strategy/strategy.py @@ -17,7 +17,6 @@ class Strategy: Takes in a number of `StrategyLeg`'s (option contracts), and filters that determine entry and exit conditions. """ - def __init__(self, schema): assert isinstance(schema, Schema) self.schema = schema @@ -71,16 +70,22 @@ class Strategy: assert loss_pct >= 0 self.exit_thresholds = (profit_pct, loss_pct) - def filter_entries(self, options): + def filter_entries(self, options, inventory): """Returns the entry signals chosen by the strategy for the given (daily) options. Args: options (pd.DataFrame): DataFrame of (daily) options + inventory (pd.DataFrame): Inventory of current positions Returns: pd.DataFrame: Entry signals """ - return self._filter_legs(options, Signal.ENTRY) + + # Remove contracts already in inventory + inventory_contracts = pd.concat([inventory[leg.name]['contract'] for leg in self.legs]) + subset_options = options[~options[self.schema['contract']].isin(inventory_contracts)] + + return self._filter_legs(subset_options, Signal.ENTRY) def filter_exits(self, options, inventory): """Returns the exit signals chosen by the strategy for the given @@ -97,20 +102,16 @@ class Strategy: if inventory.empty: return - underlying_col, spot_col = self.schema['underlying'], self.schema[ - 'underlying_last'] - underlying_symbols = options.loc[:, ( - underlying_col, spot_col)].drop_duplicates(underlying_col) + 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 + self._exit_candidates(l.direction, inventory[l.name], options, spot_prices) 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) + threshold_exits = self._filter_thresholds(inventory['totals']['cost'], total_costs) filter_mask = [] for i, leg in enumerate(self.legs): @@ -118,12 +119,11 @@ class Strategy: filter_mask.append(flt(leg_candidates[i])) fields = self._signal_fields((~leg.direction).value) leg_candidates[i] = leg_candidates[i].loc[:, fields.values()] - leg_candidates[i].columns = pd.MultiIndex.from_product( - [["leg_{}".format(i + 1)], leg_candidates[i].columns]) + leg_candidates[i].columns = pd.MultiIndex.from_product([["leg_{}".format(i + 1)], + leg_candidates[i].columns]) totals = pd.DataFrame.from_dict({"cost": total_costs}) - totals.columns = pd.MultiIndex.from_product([["totals"], - totals.columns]) + totals.columns = pd.MultiIndex.from_product([["totals"], totals.columns]) leg_candidates.append(totals) filter_mask = reduce(lambda x, y: x | y, filter_mask) exits_mask = threshold_exits | filter_mask @@ -209,12 +209,10 @@ class Strategy: cost = sum(leg["cost"] for leg in dfs) totals = pd.DataFrame.from_dict({"cost": cost}) - totals.columns = pd.MultiIndex.from_product([["totals"], - totals.columns]) + totals.columns = pd.MultiIndex.from_product([["totals"], totals.columns]) for i in range(len(dfs)): - dfs[i].columns = pd.MultiIndex.from_product( - [["leg_{}".format(i + 1)], dfs[i].columns]) + dfs[i].columns = pd.MultiIndex.from_product([["leg_{}".format(i + 1)], dfs[i].columns]) dfs.append(totals) @@ -239,9 +237,7 @@ class Strategy: # the daily data the values will all be NaN and the filters should all yield False. fields = self._signal_fields((~direction).value) options = options.rename(columns=fields) - candidates = inventory_leg[['contract']].merge(options, - how='left', - on='contract') + candidates = inventory_leg[['contract']].merge(options, how='left', on='contract') order = get_order(direction, Signal.EXIT) candidates['order'] = order.name @@ -275,5 +271,4 @@ class Strategy: return (excess_return >= profit_pct) | (excess_return <= -loss_pct) def __repr__(self): - return "Strategy(legs={}, conditions={})".format( - self.legs, self.conditions) + return "Strategy(legs={}, conditions={})".format(self.legs, self.conditions)