From 59f16bac0803d1fe5afe6f7e3ebe63f28fabd36c Mon Sep 17 00:00:00 2001 From: Juan Pablo Amoroso Date: Fri, 27 Dec 2019 13:05:11 -0300 Subject: [PATCH] Fixed monthly iteration. Changed default enter and exit profit thresholds to math.inf --- .../datahandler/historical_options_data.py | 14 ++++-- backtester/strategy/strategy.py | 48 +++++++------------ 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/backtester/datahandler/historical_options_data.py b/backtester/datahandler/historical_options_data.py index 256e0ea..4be6c08 100644 --- a/backtester/datahandler/historical_options_data.py +++ b/backtester/datahandler/historical_options_data.py @@ -35,7 +35,12 @@ class HistoricalOptionsData: def iter_months(self): """Returns `pd.DataFrameGroupBy` that groups contracts by month""" - return self._data.groupby(pd.Grouper(key=self.schema["date"], freq="MS")) + date_col = self.schema["date"] + iterator = self._data.groupby( + pd.Grouper(key=date_col, + freq="MS")).apply(lambda g: g[g[date_col] == g[date_col].min()]).reset_index(drop=True).groupby( + pd.Grouper(key=date_col, freq="MS")) + return iterator def __getattr__(self, attr): """Pass method invocation to `self._data`""" @@ -51,8 +56,11 @@ class HistoricalOptionsData: return method def __getitem__(self, item): - key = self.schema[item] - return self._data[key] + if isinstance(item, pd.Series): + return self._data[item] + else: + key = self.schema[item] + return self._data[key] def __setitem__(self, key, value): self._data[key] = value diff --git a/backtester/strategy/strategy.py b/backtester/strategy/strategy.py index 1cbff39..dfbb80c 100644 --- a/backtester/strategy/strategy.py +++ b/backtester/strategy/strategy.py @@ -1,9 +1,10 @@ +import math from collections import namedtuple +from functools import reduce import pandas as pd import numpy as np -from functools import reduce from backtester.datahandler import Schema from backtester.option import Direction from .strategy_leg import StrategyLeg @@ -17,7 +18,6 @@ class Strategy: Takes in a number of `StrategyLeg`'s (option contracts), and filters that determine entry and exit conditions. """ - def __init__(self, schema, qty=1, shares_per_contract=100): assert isinstance(schema, Schema) self.schema = schema @@ -62,12 +62,12 @@ class Strategy: self.conditions.append(Condition(fields, legs, tolerance)) return self - def add_exit_thresholds(self, profit_pct=0.0, loss_pct=0.0): + def add_exit_thresholds(self, profit_pct=math.inf, loss_pct=math.inf): """Adds maximum profit/loss thresholds. Both **must** be >= 0.0 Args: - profit_pct (float, optional): Max profit level. Defaults to 0.0 - loss_pct (float, optional): Max loss level. Defaults to 0.0 + profit_pct (float, optional): Max profit level. Defaults to math.inf + loss_pct (float, optional): Max loss level. Defaults to math.inf """ assert profit_pct >= 0 assert loss_pct >= 0 @@ -85,10 +85,8 @@ class Strategy: """ # 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)] + 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) @@ -103,20 +101,16 @@ class Strategy: pd.DataFrame: Exit signals """ - 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): @@ -124,12 +118,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 @@ -212,12 +205,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) @@ -242,9 +233,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 @@ -276,5 +265,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)