From 182f24d874bb0512f6329c9264814d8331e2a428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Rodr=C3=ADguez=20Chatruc?= Date: Thu, 28 Nov 2019 10:43:27 -0300 Subject: [PATCH] Changed inventory to a dataframe and added exit filter by price --- backtester/backtester.py | 50 +++++++++++++++++++++++++++------ backtester/strategy/strategy.py | 34 ++++++++++++++++++---- 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/backtester/backtester.py b/backtester/backtester.py index 8281206..c0ca8c1 100644 --- a/backtester/backtester.py +++ b/backtester/backtester.py @@ -16,7 +16,7 @@ class Backtest: self.shares_per_contract = shares_per_contract self._strategy = None self._data = None - self._inventory = set() + self.inventory = pd.DataFrame() @property def strategy(self): @@ -47,9 +47,11 @@ class Backtest: columns=["date", "contract", "order", "qty", "profit", "capital"]) for date, entry_signals, exit_signals in self._strategy.signals( - self._data): - self._execute_exit(date, exit_signals) - self._execute_entry(date, entry_signals) + self._data, self): + # self._execute_exit(date, exit_signals) + # self._execute_entry(date, entry_signals) + self._execute_exit_new(date, exit_signals) + self._execute_entry_new(date, entry_signals) return self.trade_log @@ -57,7 +59,7 @@ class Backtest: """Executes exits and updates `self.inventory` and `self.trade_log`""" remove_set = set() - for contract, leg, qty, expiration in self._inventory: + for contract, leg, qty, expiration in self.inventory: if contract in exit_signals[leg]["contract"].values: row = exit_signals[leg].query("contract == @contract") price = row["price"].values[0] @@ -70,7 +72,7 @@ class Backtest: elif expiration <= date: remove_set.add((contract, leg, qty, expiration)) - self._inventory.difference_update(remove_set) + self.inventory.difference_update(remove_set) def _execute_entry(self, date, entry_signals): """Executes entry orders and updates `self.inventory` and `self.trade_log`""" @@ -87,10 +89,40 @@ class Backtest: cost *= -1 if order == Order.STO.name else 1 if self.capital >= cost: self.capital -= cost - self._inventory.add((contract, leg, qty, expiration)) + self.inventory.add((contract, leg, qty, expiration)) self.strategy.register_entry(contract, price) self._update_trade_log(date, contract, order, qty, -cost) + def _execute_entry_new(self, date, entry_signals): + """Executes entry orders and updates `self.inventory` and `self.trade_log`""" + if entry_signals.empty: + return + entry, total_price = self._process_entry_signals(entry_signals) + cost = total_price * 1 * self.shares_per_contract + + if self.capital >= cost: + self.capital -= total_price + self.inventory = self.inventory.append(entry, ignore_index=True) + legs = entry_signals.columns.levels[0] + for leg in legs: + row = entry[leg] + contract = row["contract"] + order = row["order"] + price = row["cost"] + self._update_trade_log(date, contract, order, 1, -price) + + def _execute_exit_new(self, date, exit_signals): + """Executes exits and updates `self.inventory` and `self.trade_log`""" + for contracts, price in exit_signals: + profit = price * 1 * self.shares_per_contract + for contract, order in contracts: + self._update_trade_log(date, contract, order, 1, profit) + self.capital += profit + legs = exit_signals.columns.levels[0] + for leg in legs: + self.inventory = self.inventory.drop(self.inventory[ + self.inventory[leg]['optionroot'] == contract].index) + def _process_entry_signals(self, entry_signals): """Returns a dictionary containing the orders to execute.""" # Pass `qty` of contracts to buy/sell to `Backtest.__init__` @@ -98,9 +130,9 @@ class Backtest: if not entry_signals.empty: legs = entry_signals.columns.levels[0] costs = reduce(add, (entry_signals[leg]["cost"] for leg in legs)) - return entry_signals.loc[costs.idxmin()] + return entry_signals.loc[costs.idxmin()], costs.min() else: - return entry_signals + return entry_signals, 0 def _update_trade_log(self, date, contract, order, qty, profit): """Adds entry for the given order to `self.trade_log`.""" diff --git a/backtester/strategy/strategy.py b/backtester/strategy/strategy.py index 542a150..7d0dc80 100644 --- a/backtester/strategy/strategy.py +++ b/backtester/strategy/strategy.py @@ -63,7 +63,7 @@ class Strategy: given profit/loss levels""" self.entries.add(contract) - def signals(self, data): + def signals(self, data, bt): """Iterates over `data` and yields a tuple of `(date, entry_signals, exit_signals)` for each time step. """ @@ -77,10 +77,10 @@ class Strategy: else: entry_df = pd.concat(entry_legs, axis=1) - exit_legs = self._filter_legs(group, signal=Signal.EXIT) - exit_df = pd.concat(exit_legs, axis=1) - # entry_df.legs = exit_df.legs = exit_df.columns.levels[0] - + # exit_legs = self._filter_legs(group, signal=Signal.EXIT) + # exit_df = pd.concat(exit_legs, axis=1) + exit_df = self._filter_exits(data, bt.inventory, + ['leg_1', 'leg_2']) yield (date, entry_df, exit_df) def _filter_legs(self, data, signal=Signal.ENTRY): @@ -120,6 +120,30 @@ class Strategy: return self._apply_conditions(dfs) + def _filter_exits(self, data, inventory, legs): + exits = [] + for index, row in inventory.iterrows(): + old_price = 0 + current_price = 0 + contracts = set() + for leg in legs: + contract = row[leg]['contract'] + order = get_order(~leg.direction, Signal.EXIT) + contracts.add((contract, order)) + old_price += row[leg]['price'] + option = data[data['optionroot'] == contract] + if order[0] == 'B': + current_price -= option['ask'] + else: + current_price += option['bid'] + if (current_price <= 0.8 * old_price) & (current_price >= + 1.2 * old_price): + exits.append((contracts, current_price)) + else: + # Filter the data according to the exit filters and append to exits the contracts that need to exit + pass + return exits + def _apply_conditions(self, dfs): """Applies conditions on the specified legs."""