mirror of
https://github.com/wassname/options_backtester.git
synced 2026-06-27 18:05:27 +08:00
Filter contracts already in inventory. Added monthly iteration
This commit is contained in:
+20
-13
@@ -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`"""
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user