From 5fa8ae97c857c2f4a8665433973edea8faa45ee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Rodr=C3=ADguez=20Chatruc?= Date: Fri, 8 Nov 2019 10:53:48 -0300 Subject: [PATCH] Added strangle and option for reading csv --- .../datahandler/historical_options_data.py | 13 ++- backtester/datahandler/schema.py | 5 +- backtester/strategy/__init__.py | 2 + backtester/strategy/strangle.py | 80 +++++++++++++++++++ backtester/strategy/strategy.py | 1 - 5 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 backtester/strategy/strangle.py diff --git a/backtester/datahandler/historical_options_data.py b/backtester/datahandler/historical_options_data.py index 697e5bb..909d421 100644 --- a/backtester/datahandler/historical_options_data.py +++ b/backtester/datahandler/historical_options_data.py @@ -1,17 +1,26 @@ import pandas as pd +import os from .schema import Schema class HistoricalOptionsData: """Historical Options Data container class.""" - def __init__(self, file, schema=None, **params): if schema: assert isinstance(schema, Schema) else: self.schema = HistoricalOptionsData.default_schema() - self._data = pd.read_hdf(file, **params) + file_extension = os.path.splitext(file)[1] + + 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 + ] + self._data = pd.read_csv(file, **params) + columns = self._data.columns assert all((col in columns for _key, col in self.schema)) diff --git a/backtester/datahandler/schema.py b/backtester/datahandler/schema.py index 14df704..db63729 100644 --- a/backtester/datahandler/schema.py +++ b/backtester/datahandler/schema.py @@ -33,7 +33,7 @@ class Schema: def __setitem__(self, key, value): self._mappings[key] = value - + def __getitem__(self, key): """Returns mapping of given `key`""" return self._mappings[key] @@ -45,6 +45,9 @@ class Schema: return "Schema({})".format( [Field(k, m) for k, m in self._mappings.items()]) + def __eq__(self, other): + return self._mappings == other._mappings + class Field: """Encapsulates data fields to build filters used by strategies""" diff --git a/backtester/strategy/__init__.py b/backtester/strategy/__init__.py index 48a22bf..3bb3bf1 100644 --- a/backtester/strategy/__init__.py +++ b/backtester/strategy/__init__.py @@ -1,2 +1,4 @@ from .strategy import Strategy from .strategy_leg import StrategyLeg +from .straddle import Straddle +from .strangle import Strangle diff --git a/backtester/strategy/strangle.py b/backtester/strategy/strangle.py new file mode 100644 index 0000000..7d4bfde --- /dev/null +++ b/backtester/strategy/strangle.py @@ -0,0 +1,80 @@ +import pandas as pd + + +class Strangle: + def __init__(self, + underlying, + strike, + dte, + strike_diff, + shares_per_contract=100, + capital=1000000.0): + self.underlying = underlying + self.strike = strike + self.dte = dte + self.strike_diff = strike_diff + self.inventory = set() + self.shares_per_contract = shares_per_contract + self.capital = capital + + def execute_entry(self, date, group): + calls = group.loc[(group.type == 'call') + & (group.strike >= self.strike[0]) & + (group.strike <= self.strike[1]) & + (group.dte >= self.dte[0]) + & (group.dte <= self.dte[1])] + puts = group.loc[group.type == 'put'] + merge = calls.merge(puts, on=['dte'], suffixes=('_call', '_put')) + merge['ask_sum'] = merge['ask_call'] + merge['ask_put'] + merge['strike_diff'] = abs(merge['strike_call'] - merge['strike_put']) + merge_strangle = merge.loc[merge['strike_diff'] <= self.strike_diff] + if merge_strangle.empty: + return + entry_index = merge_strangle['ask_sum'].idxmin() + entry = merge_strangle.loc[entry_index] + cost = sum([entry['ask_sum'] * self.shares_per_contract]) + if cost <= self.capital: + self.capital -= cost + self.inventory.add((entry.optionroot_call, entry.dte)) + self.inventory.add((entry.optionroot_put, entry.dte)) + self._update_trade_log(date, entry.optionroot_call, + entry.type_call, + -entry.ask_call * self.shares_per_contract) + self._update_trade_log(date, entry.optionroot_put, entry.type_put, + -entry.ask_put * self.shares_per_contract) + + def execute_exits(self, inventory, date, group): + exits = [] + remove_set = set() + for entry in inventory: + exit = group.loc[(group.optionroot == entry[0]) & (group.dte == 1)] + if not exit.empty: + exits.append(exit) + remove_set.add(entry) + for exit in exits: + profit = exit.bid.values[0] * self.shares_per_contract + contract = exit.optionroot.values[0] + type_ = exit.type.values[0] + self.capital += profit + self._update_trade_log(date, contract, type_, profit) + self.inventory.difference_update(remove_set) + + def run(self, data): + self.trade_log = pd.DataFrame( + columns=["date", "contract", "type", "profit", "capital"]) + + for date, group in self._iter_dates(data): + self.execute_entry(date, group) + self.execute_exits(self.inventory, date, group) + + return self.trade_log + + def _update_trade_log(self, date, contract, type_, profit): + """Adds entry for the given order to `self.trade_log`.""" + self.trade_log.loc[len( + self.trade_log)] = [date, contract, type_, profit, self.capital] + + def _iter_dates(self, data): + """Returns `pd.DataFrameGroupBy` with the given underlying and with contracts grouped by date""" + df = data._data.loc[data._data.underlying == self.underlying] + return df.groupby(data.schema["date"]) diff --git a/backtester/strategy/strategy.py b/backtester/strategy/strategy.py index 19b3a38..d1ce0ed 100644 --- a/backtester/strategy/strategy.py +++ b/backtester/strategy/strategy.py @@ -10,7 +10,6 @@ class Strategy: Takes in a number of `legs` (option contracts), and filters that determine entry and exit conditions. """ - def __init__(self, schema): assert isinstance(schema, Schema) self.schema = schema