diff --git a/backtester/datahandler/balanced_datahandler.py b/backtester/datahandler/balanced_datahandler.py deleted file mode 100644 index 007460d..0000000 --- a/backtester/datahandler/balanced_datahandler.py +++ /dev/null @@ -1,39 +0,0 @@ -import pandas as pd -from .datahandler import DataHandler -from ..event import MarketEvent - - -class BalancedDataHandler(DataHandler): - """Handler for balanced data set""" - - def __init__(self, data_path, events): - data = pd.read_csv(data_path, parse_dates=["date"]) - - # We will assume bid and ask prices = close - data["bid"] = data["close"] - data["ask"] = data["close"] - - self._data_generator = self._get_data_generator(data) - self.events = events - self.continue_backtest = True - - def get_latest_bars(self, symbol, N=1): - """Returns the latest `N` bars for `symbol` if there are at least N - rows, otherwise returns the all data. - Returns empty dataframe if `symbol` is not in self.data. - """ - return self._current_bar[self._current_bar["symbol"] == symbol].iloc[0] - - def update_bars(self): - """Add new data bar to self.data""" - try: - self.current_date, self._current_bar = next(self._data_generator) - self.events.put(MarketEvent()) - except StopIteration: - self.continue_backtest = False - - def _get_data_generator(self, data): - """Returns generator that yields daily data bars""" - grouped = data.groupby("date") - for date, bars in grouped: - yield date, bars diff --git a/backtester/datahandler/datahandler.py b/backtester/datahandler/datahandler.py deleted file mode 100644 index 241b9e2..0000000 --- a/backtester/datahandler/datahandler.py +++ /dev/null @@ -1,21 +0,0 @@ -from abc import ABCMeta, abstractmethod - - -class DataHandler(metaclass=ABCMeta): - """Interface for the different data handlers""" - - @abstractmethod - def get_latest_bars(self, symbol, N=1): - """ - Returns the last N bars from the latest_symbol list, - or fewer if less bars are available. - """ - raise NotImplementedError("Should implement get_latest_bars()") - - @abstractmethod - def update_bars(self): - """ - Pushes the latest bar to the latest symbol structure - for all symbols in the symbol list. - """ - raise NotImplementedError("Should implement update_bars()") diff --git a/backtester/datahandler/historic_datahandler.py b/backtester/datahandler/historic_datahandler.py deleted file mode 100644 index 9793c20..0000000 --- a/backtester/datahandler/historic_datahandler.py +++ /dev/null @@ -1,35 +0,0 @@ -import pandas as pd -from .datahandler import DataHandler -from ..event import MarketEvent - - -class HistoricDataHandler(DataHandler): - """Handler for Historical Option Data""" - - def __init__(self, data_path, events): - self._data = pd.read_csv( - data_path, parse_dates=["quotedate", - "expiration"]).sort_values(by="date") - - columns = {"quotedate": "date", "optionroot": "symbol"} - self._data.rename(columns=columns, inplace=True) - self._data_index = 0 - self.events = events - self.continue_backtest = True - - def get_latest_bars(self, symbol, N=1): - """Returns the latest `N` bars for `symbol` if there are at least N - rows, otherwise returns the all data. - Returns empty dataframe if `symbol` is not in self._data. - """ - return self._data[(self._data["symbol"] == symbol) - & (self._data["date"] <= self.current_date)][-N:] - - def update_bars(self): - """Add new data bar to self.data""" - if self._data_index < len(self._data): - self.current_date = self._data["date"][self._data_index] - self.events.put(MarketEvent()) - self._data_index += 1 - else: - self.continue_backtest = False diff --git a/backtester/datahandler/historical_options_data.py b/backtester/datahandler/historical_options_data.py index 3e32920..0bc9fc0 100644 --- a/backtester/datahandler/historical_options_data.py +++ b/backtester/datahandler/historical_options_data.py @@ -9,22 +9,38 @@ class HistoricalOptionsData: if schema: assert isinstance(schema, Schema) else: - schema = Schema.canonical() - schema.update({"contract": "optionroot", "date": "quotedate"}) - self.schema = schema + self.schema = HistoricalOptionsData.default_schema() self._data = pd.read_hdf(file, **params) columns = self._data.columns - assert all((col in columns for col in schema)) + 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"}) def __getitem__(self, item): - return self._data[item] + key = self.schema[item].mapping + return self._data[key] def __setitem__(self, item, value): self._data[item] = value def __repr__(self): return self._data.__repr__() + + def default_schema(): + """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" + }) + return schema diff --git a/backtester/datahandler/schema.py b/backtester/datahandler/schema.py index f8acb31..b1503db 100644 --- a/backtester/datahandler/schema.py +++ b/backtester/datahandler/schema.py @@ -3,7 +3,7 @@ class Schema: columns = [ "underlying", "underlying_last", "date", "contract", "type", - "expiration", "strike", "bid", "ask" + "expiration", "strike", "bid", "ask", "volume", "open_interest" ] def canonical(): @@ -26,7 +26,7 @@ class Schema: return Field(key, self._mappings[key]) def __iter__(self): - return iter(self._mappings.values()) + return iter(self._mappings.items()) def __repr__(self): return "Schema({})".format( @@ -77,17 +77,24 @@ class Filter: self.query = query def __and__(self, other): + """Returns logical *and* between `self` and `other`""" assert isinstance(other, Filter) new_query = "({}) & ({})".format(self.query, other.query) return Filter(query=new_query) def __or__(self, other): + """Returns logical *or* between `self` and `other`""" assert isinstance(other, Filter) new_query = "(({}) | ({}))".format(self.query, other.query) return Filter(query=new_query) def __invert__(self): + """Negates filter""" return Filter("!({})".format(self.query)) + def __call__(self, data): + """Returns filtered dataframe""" + return data.query(self.query) + def __repr__(self): return "Filter(query='{}')".format(self.query) diff --git a/backtester/datahandler/spx_datahandler.py b/backtester/datahandler/spx_datahandler.py deleted file mode 100644 index 5fc83b0..0000000 --- a/backtester/datahandler/spx_datahandler.py +++ /dev/null @@ -1,33 +0,0 @@ -import pandas as pd -from .datahandler import DataHandler -from ..event import MarketEvent - - -class SPXDataHandler(DataHandler): - """Handler for SPX test data""" - - def __init__(self, data_path, events): - self._data = pd.read_csv( - data_path, parse_dates=["date"]).sort_values(by="date") - - self._data.rename(columns={"price": "ask"}, inplace=True) - self._data["bid"] = self._data["ask"] - self._data_index = 0 - self.events = events - self.continue_backtest = True - - def get_latest_bars(self, symbol, N=1): - """Returns the latest `N` bars for `symbol` if there are at least N - rows, otherwise returns the all data. - Returns empty dataframe if `symbol` is not in self.data. - """ - return self._data[self._data["date"] <= self.current_date][-N:] - - def update_bars(self): - """Add new data bar to self.data""" - if self._data_index < len(self._data): - self.current_date = self._data["date"][self._data_index] - self.events.put(MarketEvent()) - self._data_index += 1 - else: - self.continue_backtest = False diff --git a/backtester/option.py b/backtester/option.py new file mode 100644 index 0000000..4619684 --- /dev/null +++ b/backtester/option.py @@ -0,0 +1,31 @@ +from enum import Enum + + +class OptionContract: + """Option contract data class""" + + Type = Enum("Type", {"CALL": "call", "PUT": "put"}) + Direction = Enum("Direction", "BUY SELL") + + # Orders: + # BTO: Buy to Open + # BTC: Buy to Close + # STO: Sell to Open + # STC: Sell to Close + Order = Enum("Order", "BTO BTC STO STC") + + def __init__(self, + option_type=Type.CALL, + direction=Direction.BUY, + order=Order.BTO): + assert isinstance(option_type, OptionContract.Type) + assert isinstance(direction, OptionContract.Direction) + assert isinstance(order, OptionContract.Order) + + self._store = {} + self._store["type"] = option_type + self._store["direction"] = direction + self._store["order"] = order + + def __repr__(self): + return "Option({})".format(str(self._store)) diff --git a/backtester/strategy/__init__.py b/backtester/strategy/__init__.py index aecf243..2a8ef20 100644 --- a/backtester/strategy/__init__.py +++ b/backtester/strategy/__init__.py @@ -1,3 +1 @@ from .strategy import Strategy -from .benchmark import Benchmark -from .balanced import Balanced diff --git a/backtester/strategy/strategy.py b/backtester/strategy/strategy.py index 99558c8..5a4ec04 100644 --- a/backtester/strategy/strategy.py +++ b/backtester/strategy/strategy.py @@ -1,11 +1,47 @@ -from abc import ABCMeta, abstractmethod +from ..option import OptionContract +from ..datahandler import Filter -class Strategy(metaclass=ABCMeta): - """Interface for the different investing strategies""" +class Strategy: + """Options strategy class. + Takes in a number of `legs` (option contracts), and filters that determine + entry and exit conditions. + """ - @abstractmethod - def generate_signals(self, event): - """Provides the mechanisms to calculate the list of signals. + def __init__(self, data, entry_filter, exit_filter, legs=[]): + assert all((isinstance(leg, OptionContract) for leg in legs)) + assert isinstance(entry_filter, Filter) + assert isinstance(exit_filter, Filter) + + self.data = data + self.entry = entry_filter + self.exit = exit_filter + self.legs = legs + + def add_leg(self, leg): + """Adds leg to the strategy""" + self.legs.append(leg) + return self + + def remove_leg(self, leg_number): + """Removes leg to the strategy""" + self.legs.pop(leg_number) + return self + + def run(self, data): + """Returns a dataframe of trades executed as a result of + runnning the strategy on the data. """ - raise NotImplementedError("Strategy must implement generate_signals()") + entry_query = self.entry(self._data) + exit_query = self.exit(self._data) + + entry_df = data.query(entry_query) + exit_df = data.query(exit_query) + + return entry_df.merge(exit_df, + on="optionroot", + suffixes=("_entry", "_exit")) + + def __repr__(self): + return "Strategy(entry_filter={}, exit_filter={}, legs={})".format( + self.entry, self.exit, self.legs)