Filter contracts already in inventory. Added monthly iteration

This commit is contained in:
Juan Pablo Amoroso
2019-12-26 14:52:45 -03:00
parent 461ea8da52
commit 0df2236355
3 changed files with 45 additions and 42 deletions
+20 -13
View File
@@ -6,7 +6,6 @@ from .datahandler import HistoricalOptionsData
class Backtest:
"""Processes signals from the Strategy object"""
def __init__(self, qty=1, capital=1_000_000, shares_per_contract=100):
self.capital = capital
self.shares_per_contract = shares_per_contract
@@ -34,24 +33,34 @@ class Backtest:
assert isinstance(data, HistoricalOptionsData)
self._data = data
def run(self):
"""Runs the backtest and returns a `pd.DataFrame` of the orders executed (`self.trade_log`)"""
def run(self, monthly=False):
"""Runs the backtest and returns a `pd.DataFrame` of the orders executed (`self.trade_log`)
Args:
monthly (bool, optional): Iterates through data monthly rather than daily. Defaults to False.
Returns:
pd.DataFrame: Log of the trades executed.
"""
assert self._data is not None
assert self._strategy is not None
assert self._data.schema == self._strategy.schema
self.trade_log = pd.DataFrame()
for date, options in self._data.iter_dates():
entry_signals = self._strategy.filter_entries(options)
data_iterator = self._data.iter_months() if monthly else self._data.iter_dates()
for _date, options in data_iterator:
entry_signals = self._strategy.filter_entries(options, self.inventory)
exit_signals = self._strategy.filter_exits(options, self.inventory)
self._execute_exit(date, exit_signals)
self._execute_entry(date, entry_signals)
self._execute_exit(exit_signals)
self._execute_entry(entry_signals)
return self.trade_log
def _execute_entry(self, date, entry_signals):
def _execute_entry(self, entry_signals):
"""Executes entry orders and updates `self.inventory` and `self.trade_log`"""
entry, total_price = self._process_entry_signals(entry_signals)
@@ -60,7 +69,7 @@ class Backtest:
self.trade_log = self.trade_log.append(entry, ignore_index=True)
self.capital -= total_price
def _execute_exit(self, date, exit_signals):
def _execute_exit(self, exit_signals):
"""Executes exits and updates `self.inventory` and `self.trade_log`"""
if exit_signals is None:
return
@@ -75,11 +84,9 @@ class Backtest:
if not entry_signals.empty:
costs = entry_signals['totals']['cost']
return entry_signals.loc[costs.idxmin():costs.idxmin()], costs.min(
)
return entry_signals.loc[costs.idxmin():costs.idxmin()], costs.min()
else:
return entry_signals, 0
def __repr__(self):
return "Backtest(capital={}, strategy={})".format(
self.capital, self._strategy)
return "Backtest(capital={}, strategy={})".format(self.capital, self._strategy)
@@ -16,16 +16,13 @@ class HistoricalOptionsData:
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
]
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))
self._data["dte"] = (self._data["expiration"] -
self._data["quotedate"]).dt.days
self._data["dte"] = (self._data["expiration"] - self._data["quotedate"]).dt.days
self.schema.update({"dte": "dte"})
def apply_filter(self, f):
@@ -36,6 +33,10 @@ class HistoricalOptionsData:
"""Returns `pd.DataFrameGroupBy` that groups contracts by date"""
return self._data.groupby(self.schema["date"])
def iter_months(self):
"""Returns `pd.DataFrameGroupBy` that groups contracts by month"""
return self._data.groupby(pd.Grouper(key=self.schema["date"], freq="MS"))
def __getattr__(self, attr):
"""Pass method invocation to `self._data`"""
+19 -24
View File
@@ -17,7 +17,6 @@ class Strategy:
Takes in a number of `StrategyLeg`'s (option contracts), and filters that determine
entry and exit conditions.
"""
def __init__(self, schema):
assert isinstance(schema, Schema)
self.schema = schema
@@ -71,16 +70,22 @@ class Strategy:
assert loss_pct >= 0
self.exit_thresholds = (profit_pct, loss_pct)
def filter_entries(self, options):
def filter_entries(self, options, inventory):
"""Returns the entry signals chosen by the strategy for the given
(daily) options.
Args:
options (pd.DataFrame): DataFrame of (daily) options
inventory (pd.DataFrame): Inventory of current positions
Returns:
pd.DataFrame: Entry signals
"""
return self._filter_legs(options, Signal.ENTRY)
# Remove contracts already in inventory
inventory_contracts = pd.concat([inventory[leg.name]['contract'] for leg in self.legs])
subset_options = options[~options[self.schema['contract']].isin(inventory_contracts)]
return self._filter_legs(subset_options, Signal.ENTRY)
def filter_exits(self, options, inventory):
"""Returns the exit signals chosen by the strategy for the given
@@ -97,20 +102,16 @@ class Strategy:
if inventory.empty:
return
underlying_col, spot_col = self.schema['underlying'], self.schema[
'underlying_last']
underlying_symbols = options.loc[:, (
underlying_col, spot_col)].drop_duplicates(underlying_col)
underlying_col, spot_col = self.schema['underlying'], self.schema['underlying_last']
underlying_symbols = options.loc[:, (underlying_col, spot_col)].drop_duplicates(underlying_col)
spot_prices = underlying_symbols.set_index(underlying_col).to_dict()
leg_candidates = [
self._exit_candidates(l.direction, inventory[l.name], options,
spot_prices) for l in self.legs
self._exit_candidates(l.direction, inventory[l.name], options, spot_prices) for l in self.legs
]
total_costs = sum([l['cost'] for l in leg_candidates])
threshold_exits = self._filter_thresholds(inventory['totals']['cost'],
total_costs)
threshold_exits = self._filter_thresholds(inventory['totals']['cost'], total_costs)
filter_mask = []
for i, leg in enumerate(self.legs):
@@ -118,12 +119,11 @@ class Strategy:
filter_mask.append(flt(leg_candidates[i]))
fields = self._signal_fields((~leg.direction).value)
leg_candidates[i] = leg_candidates[i].loc[:, fields.values()]
leg_candidates[i].columns = pd.MultiIndex.from_product(
[["leg_{}".format(i + 1)], leg_candidates[i].columns])
leg_candidates[i].columns = pd.MultiIndex.from_product([["leg_{}".format(i + 1)],
leg_candidates[i].columns])
totals = pd.DataFrame.from_dict({"cost": total_costs})
totals.columns = pd.MultiIndex.from_product([["totals"],
totals.columns])
totals.columns = pd.MultiIndex.from_product([["totals"], totals.columns])
leg_candidates.append(totals)
filter_mask = reduce(lambda x, y: x | y, filter_mask)
exits_mask = threshold_exits | filter_mask
@@ -209,12 +209,10 @@ class Strategy:
cost = sum(leg["cost"] for leg in dfs)
totals = pd.DataFrame.from_dict({"cost": cost})
totals.columns = pd.MultiIndex.from_product([["totals"],
totals.columns])
totals.columns = pd.MultiIndex.from_product([["totals"], totals.columns])
for i in range(len(dfs)):
dfs[i].columns = pd.MultiIndex.from_product(
[["leg_{}".format(i + 1)], dfs[i].columns])
dfs[i].columns = pd.MultiIndex.from_product([["leg_{}".format(i + 1)], dfs[i].columns])
dfs.append(totals)
@@ -239,9 +237,7 @@ class Strategy:
# the daily data the values will all be NaN and the filters should all yield False.
fields = self._signal_fields((~direction).value)
options = options.rename(columns=fields)
candidates = inventory_leg[['contract']].merge(options,
how='left',
on='contract')
candidates = inventory_leg[['contract']].merge(options, how='left', on='contract')
order = get_order(direction, Signal.EXIT)
candidates['order'] = order.name
@@ -275,5 +271,4 @@ class Strategy:
return (excess_return >= profit_pct) | (excess_return <= -loss_pct)
def __repr__(self):
return "Strategy(legs={}, conditions={})".format(
self.legs, self.conditions)
return "Strategy(legs={}, conditions={})".format(self.legs, self.conditions)