From c60ddb0e992e8a3a39037ae8ddda23b8f3c2895b Mon Sep 17 00:00:00 2001 From: Juan Pablo Amoroso Date: Wed, 27 Nov 2019 17:59:02 -0300 Subject: [PATCH] Added calculation of total costs for signals. Moved check of 0 priced options to StrategyLeg --- backtester/backtester.py | 61 +++++++++++++---------------- backtester/strategy/strategy.py | 16 +++++--- backtester/strategy/strategy_leg.py | 20 ++++++++-- 3 files changed, 55 insertions(+), 42 deletions(-) diff --git a/backtester/backtester.py b/backtester/backtester.py index e59591c..8281206 100644 --- a/backtester/backtester.py +++ b/backtester/backtester.py @@ -1,3 +1,6 @@ +from functools import reduce +from operator import add + import pandas as pd from .strategy import Strategy @@ -50,7 +53,26 @@ class Backtest: return self.trade_log - def _execute_entry(self, date, orders, entry_signals): + def _execute_exit(self, date, exit_signals): + """Executes exits and updates `self.inventory` and `self.trade_log`""" + remove_set = set() + + 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] + order = row["order"].values[0] + profit = price * qty * self.shares_per_contract + profit *= 1 if order == Order.STC.name else -1 + self.capital += profit + self._update_trade_log(date, contract, order, qty, profit) + remove_set.add((contract, leg, qty, expiration)) + elif expiration <= date: + remove_set.add((contract, leg, qty, expiration)) + + self._inventory.difference_update(remove_set) + + def _execute_entry(self, date, entry_signals): """Executes entry orders and updates `self.inventory` and `self.trade_log`""" orders = self._process_entry_signals(entry_signals) @@ -73,39 +95,12 @@ class Backtest: """Returns a dictionary containing the orders to execute.""" # Pass `qty` of contracts to buy/sell to `Backtest.__init__` - orders = {} - if not entry_signals.empty: - for leg in entry_signals.legs: - leg_signals = entry_signals[leg] - # Filter out zero priced options - leg_signals = leg_signals.query("price > 0.0") - if leg_signals.empty: - return {} - if (leg_signals["order"] == Order.BTO.name).any(): - orders[leg] = (leg_signals["price"].idxmin(), 1) - else: - orders[leg] = (leg_signals["price"].idxmax(), 1) - return orders - - def _execute_exit(self, date, exit_signals): - """Executes exits and updates `self.inventory` and `self.trade_log`""" - remove_set = set() - - 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] - order = row["order"].values[0] - profit = price * qty * self.shares_per_contract - profit *= 1 if order == Order.STC.name else -1 - self.capital += profit - self._update_trade_log(date, contract, order, qty, profit) - remove_set.add((contract, leg, qty, expiration)) - elif expiration <= date: - remove_set.add((contract, leg, qty, expiration)) - - self._inventory.difference_update(remove_set) + legs = entry_signals.columns.levels[0] + costs = reduce(add, (entry_signals[leg]["cost"] for leg in legs)) + return entry_signals.loc[costs.idxmin()] + else: + return entry_signals 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 a5c1223..542a150 100644 --- a/backtester/strategy/strategy.py +++ b/backtester/strategy/strategy.py @@ -3,6 +3,7 @@ from collections import namedtuple import pandas as pd from backtester.datahandler import Schema +from backtester.option import Direction from .strategy_leg import StrategyLeg from .signal import Signal, get_order @@ -78,7 +79,7 @@ class Strategy: 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] + # entry_df.legs = exit_df.legs = exit_df.columns.levels[0] yield (date, entry_df, exit_df) @@ -88,13 +89,13 @@ class Strategy: """ schema = self.schema dfs = [] - for number, leg in enumerate(self.legs, start=1): + for leg in self.legs: if signal == Signal.ENTRY: flt = leg.entry_filter - price = leg.direction.value + cost = leg.direction.value else: flt = leg.exit_filter - price = (~leg.direction).value + cost = (~leg.direction).value df = flt(data) fields = { @@ -103,13 +104,18 @@ class Strategy: schema["expiration"]: "expiration", schema["type"]: "type", schema["strike"]: "strike", - schema[price]: "price" + schema[cost]: "cost" } subset_df = df.loc[:, fields.keys()] subset_df.rename(columns=fields, inplace=True) order = get_order(leg.direction, signal) subset_df["order"] = order.name + + # Change sign of cost for SELL orders + if leg.direction == Direction.SELL: + subset_df["cost"] = -subset_df["cost"] + dfs.append(subset_df.reset_index(drop=True)) return self._apply_conditions(dfs) diff --git a/backtester/strategy/strategy_leg.py b/backtester/strategy/strategy_leg.py index 0e78d3a..be6ab0f 100644 --- a/backtester/strategy/strategy_leg.py +++ b/backtester/strategy/strategy_leg.py @@ -13,8 +13,9 @@ class StrategyLeg: self.schema = schema self.type = option_type self.direction = direction - self._entry_filter = self.schema.type == self.type.value - self._exit_filter = self.schema.type == self.type.value + + self._entry_filter = self._base_entry_filter() + self._exit_filter = self._base_exit_filter() @property def entry_filter(self): @@ -24,7 +25,7 @@ class StrategyLeg: @entry_filter.setter def entry_filter(self, flt): """Sets the entry filter""" - self._entry_filter = (self.schema.type == self.type.value) & flt + self._entry_filter = self._base_entry_filter() & flt @property def exit_filter(self): @@ -34,7 +35,18 @@ class StrategyLeg: @exit_filter.setter def exit_filter(self, flt): """Sets the exit filter""" - self._exit_filter = (self.schema.type == self.type.value) & flt + self._exit_filter = self._base_exit_filter() & flt + + def _base_entry_filter(self): + if self.direction == Direction.BUY: + return (self.schema.type == self.type.value) & (self.schema.ask > + 0) + else: + return (self.schema.type == self.type.value) & (self.schema.bid > + 0) + + def _base_exit_filter(self): + return self.schema.type == self.type.value def __repr__(self): return "StrategyLeg(type={}, direction={}, entry_filter={}, exit_filter={})".format(