diff --git a/backtester/datahandler/__init__.py b/backtester/datahandler/__init__.py index 1293d94..da64028 100644 --- a/backtester/datahandler/__init__.py +++ b/backtester/datahandler/__init__.py @@ -1,4 +1,2 @@ -from .datahandler import DataHandler -from .historic_datahandler import HistoricDataHandler -from .spx_datahandler import SPXDataHandler -from .balanced_datahandler import BalancedDataHandler +from .schema import * +from .historical_options_data import HistoricalOptionsData diff --git a/backtester/datahandler/historical_options_data.py b/backtester/datahandler/historical_options_data.py new file mode 100644 index 0000000..8ff18cd --- /dev/null +++ b/backtester/datahandler/historical_options_data.py @@ -0,0 +1,30 @@ +import pandas as pd +from .schema import Schema + + +class HistoricalOptionsData: + """Historical Options Data container class.""" + + def __init__(self, file, key=None, query=None, schema=None): + if schema: + assert isinstance(schema, Schema) + else: + schema = Schema.canonical() + schema.update({"contract": "optionroot", "date": "quotedate"}) + self.schema = schema + + self._data = pd.read_hdf(file, key=key, where=query) + columns = self._data.columns + assert all((col in columns for col in schema)) + + self._data["dte"] = (self._data["expiration"] - + self._data["quotedate"]).dt.days + + def __getitem__(self, item): + return self._data[item] + + def __setitem__(self, item, value): + self._data[item] = value + + def __repr__(self): + return self._data.__repr__() diff --git a/backtester/datahandler/schema.py b/backtester/datahandler/schema.py new file mode 100644 index 0000000..a704322 --- /dev/null +++ b/backtester/datahandler/schema.py @@ -0,0 +1,93 @@ +class Schema: + """Data schema class (used to run validations)""" + + columns = [ + "underlying", "underlying_last", "date", "contract", "type", + "expiration", "strike", "bid", "ask" + ] + + def canonical(): + """Builder method that returns a `Schema` with default mappings""" + mappings = {key: key for key in Schema.columns} + return Schema(mappings) + + def __init__(self, mappings): + assert all((key in mappings for key in Schema.columns)) + + self._mappings = mappings + + def update(self, mappings): + self._mappings.update(mappings) + + def __setitem__(self, key, value): + self._mappings[key] = value + + def __getitem__(self, key): + return Field(key, self._mappings[key]) + + def __iter__(self): + return iter(self._mappings.values()) + + def __repr__(self): + return "Schema({})".format( + [Field(k, m) for k, m in self._mappings.items()]) + + +class Field: + """Encapsulates data fields to build filters used by strategies""" + + def __init__(self, name, mapping): + self.name = name + self.mapping = mapping + + def _create_filter(self, operator, value): + query = "{field} {op} {value}".format(field=self.mapping, + op=operator, + value=value) + return Filter(query) + + def __lt__(self, value): + return self._create_filter("<", value) + + def __le__(self, value): + return self._create_filter("<=", value) + + def __gt__(self, value): + return self._create_filter(">", value) + + def __ge__(self, value): + return self._create_filter(">=", value) + + def __eq__(self, value): + return self._create_filter("==", value) + + def __ne__(self, value): + return self._create_filter("!=", value) + + def __repr__(self): + return "Field(name='{}', mapping='{}')".format(self.name, self.mapping) + + +class Filter: + """This class determines entry/exit conditions for strategies""" + + def __init__(self, query): + self.query = query + + def _compose_filter(self, operator, other): + assert isinstance(other, Filter) + + new_query = "({}) {} ({})".format(self.query, operator, other.query) + return Filter(query=new_query) + + def __and__(self, other): + return self._compose_filter("&", other) + + def __or__(self, other): + return self._compose_filter("|", other) + + def __invert__(self): + return Filter("!({})".format(self.query)) + + def __repr__(self): + return "Filter(query='{}')".format(self.query)