Fixed a few things and added exit price thresholds as user input

This commit is contained in:
Javier Rodríguez Chatruc
2019-12-02 11:22:15 -03:00
parent 182f24d874
commit 1fe8851554
3 changed files with 68 additions and 81 deletions
+20 -58
View File
@@ -4,19 +4,20 @@ from operator import add
import pandas as pd
from .strategy import Strategy
from .strategy.signal import Order
from .datahandler import HistoricalOptionsData
class Backtest:
"""Processes signals from the Strategy object"""
def __init__(self, capital=1_000_000, shares_per_contract=100):
def __init__(self, qty=1, capital=1_000_000, shares_per_contract=100):
self.capital = capital
self.shares_per_contract = shares_per_contract
self.qty = qty
self._strategy = None
self._data = None
self.inventory = pd.DataFrame()
self.stop_if_broke = True
@property
def strategy(self):
@@ -48,84 +49,45 @@ class Backtest:
for date, entry_signals, exit_signals in self._strategy.signals(
self._data, self):
# self._execute_exit(date, exit_signals)
# self._execute_entry(date, entry_signals)
self._execute_exit_new(date, exit_signals)
self._execute_entry_new(date, entry_signals)
self._execute_exit(date, exit_signals)
self._execute_entry(date, entry_signals)
return self.trade_log
def _execute_exit(self, date, exit_signals):
"""Executes exits and updates `self.inventory` and `self.trade_log`"""
remove_set = set()
for contract, leg, qty, expiration in self.inventory:
if contract in exit_signals[leg]["contract"].values:
row = exit_signals[leg].query("contract == @contract")
price = row["price"].values[0]
order = row["order"].values[0]
profit = price * qty * self.shares_per_contract
profit *= 1 if order == Order.STC.name else -1
self.capital += profit
self._update_trade_log(date, contract, order, qty, profit)
remove_set.add((contract, leg, qty, expiration))
elif expiration <= date:
remove_set.add((contract, leg, qty, expiration))
self.inventory.difference_update(remove_set)
def _execute_entry(self, date, entry_signals):
"""Executes entry orders and updates `self.inventory` and `self.trade_log`"""
orders = self._process_entry_signals(entry_signals)
for leg, (idx, qty) in orders.items():
row = entry_signals[leg].loc[idx, :]
contract = row["contract"]
order = row["order"]
price = row["price"]
expiration = row["expiration"]
cost = price * qty * self.shares_per_contract
cost *= -1 if order == Order.STO.name else 1
if self.capital >= cost:
self.capital -= cost
self.inventory.add((contract, leg, qty, expiration))
self.strategy.register_entry(contract, price)
self._update_trade_log(date, contract, order, qty, -cost)
def _execute_entry_new(self, date, entry_signals):
"""Executes entry orders and updates `self.inventory` and `self.trade_log`"""
if entry_signals.empty:
return
entry, total_price = self._process_entry_signals(entry_signals)
cost = total_price * 1 * self.shares_per_contract
cost = total_price * self.qty * self.shares_per_contract
if self.capital >= cost:
self.capital -= total_price
if (not self.stop_if_broke) or (self.capital >= cost):
self.capital -= cost
self.inventory = self.inventory.append(entry, ignore_index=True)
legs = entry_signals.columns.levels[0]
for leg in legs:
row = entry[leg]
contract = row["contract"]
order = row["order"]
price = row["cost"]
self._update_trade_log(date, contract, order, 1, -price)
price = row["cost"] * self.shares_per_contract
self._update_trade_log(date, contract, order, self.qty, -price)
def _execute_exit_new(self, date, exit_signals):
def _execute_exit(self, date, exit_signals):
"""Executes exits and updates `self.inventory` and `self.trade_log`"""
for contracts, price in exit_signals:
profit = price * 1 * self.shares_per_contract
for contract, order in contracts:
self._update_trade_log(date, contract, order, 1, profit)
profit = price * self.qty * self.shares_per_contract
for contract, order, individual_price in contracts:
self._update_trade_log(
date, contract, order, self.qty,
individual_price * self.shares_per_contract)
self.capital += profit
legs = exit_signals.columns.levels[0]
for leg in legs:
self.inventory = self.inventory.drop(self.inventory[
self.inventory[leg]['optionroot'] == contract].index)
for leg in self._strategy.legs:
self.inventory = self.inventory.drop(
self.inventory[self.inventory[(
leg.name, 'contract')] == contract].index)
def _process_entry_signals(self, entry_signals):
"""Returns a dictionary containing the orders to execute."""
# Pass `qty` of contracts to buy/sell to `Backtest.__init__`
if not entry_signals.empty:
legs = entry_signals.columns.levels[0]
+2 -2
View File
@@ -894,9 +894,9 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.7"
"version": "3.7.3"
}
},
"nbformat": 4,
"nbformat_minor": 2
"nbformat_minor": 4
}
+46 -21
View File
@@ -5,7 +5,7 @@ import pandas as pd
from backtester.datahandler import Schema
from backtester.option import Direction
from .strategy_leg import StrategyLeg
from .signal import Signal, get_order
from .signal import Signal, get_order, Order
Condition = namedtuple('Condition', 'fields legs tolerance')
@@ -22,11 +22,14 @@ class Strategy:
self.legs = []
self.conditions = []
self.entries = set()
self.exit_thresholds = []
self.dte_on_exit = 2
def add_leg(self, leg):
"""Adds leg to the strategy"""
assert isinstance(leg, StrategyLeg)
assert self.schema == leg.schema
leg.name = "leg_{}".format(len(self.legs) + 1)
self.legs.append(leg)
return self
@@ -35,7 +38,7 @@ class Strategy:
for leg in legs:
assert isinstance(leg, StrategyLeg)
assert self.schema == leg.schema
self.legs.extend(legs)
self.add_leg(leg)
return self
def remove_leg(self, leg_number):
@@ -77,10 +80,8 @@ class Strategy:
else:
entry_df = pd.concat(entry_legs, axis=1)
# exit_legs = self._filter_legs(group, signal=Signal.EXIT)
# exit_df = pd.concat(exit_legs, axis=1)
exit_df = self._filter_exits(data, bt.inventory,
['leg_1', 'leg_2'])
exit_df = self._filter_exits(group, bt.inventory)
yield (date, entry_df, exit_df)
def _filter_legs(self, data, signal=Signal.ENTRY):
@@ -120,28 +121,46 @@ class Strategy:
return self._apply_conditions(dfs)
def _filter_exits(self, data, inventory, legs):
def _filter_exits(self, data, inventory):
exits = []
for index, row in inventory.iterrows():
for _, row in inventory.iterrows():
old_price = 0
current_price = 0
contracts = set()
for leg in legs:
contract = row[leg]['contract']
order = get_order(~leg.direction, Signal.EXIT)
contracts.add((contract, order))
old_price += row[leg]['price']
is_empty = False
filters_exit = False
for leg in self.legs:
contract = row[(leg.name, 'contract')]
order = get_order(leg.direction, Signal.EXIT).name
old_price += row[(leg.name, 'cost')]
option = data[data['optionroot'] == contract]
if order[0] == 'B':
current_price -= option['ask']
# This was originally to skip (and then remove) entries that are past their expiration and therefore
# don't have a corresponding exit anymore (i.e, option is empty). It doesn't work, however, because
# option might just be empty because of missing data in the middle. Moreover, even if the entry is
# past its expiration the current code will still execute the other exit legs associated with it,
# which is inaccurate. This last point can only be truly resolved by not executing the entry
# in the first place.
if option.empty:
is_empty = True
contracts.add((contract, order, 0))
continue
#
if order == Order.BTC.name:
ask = option['ask'].values[0]
current_price -= ask
contracts.add((contract, order, -ask))
else:
current_price += option['bid']
if (current_price <= 0.8 * old_price) & (current_price >=
1.2 * old_price):
bid = option['bid'].values[0]
current_price += bid
contracts.add((contract, order, bid))
flt = leg.exit_filter
option = flt(option)
if not option.empty:
filters_exit = True
if is_empty or filters_exit or self._is_past_threshold(
current_price, old_price):
exits.append((contracts, current_price))
else:
# Filter the data according to the exit filters and append to exits the contracts that need to exit
pass
return exits
def _apply_conditions(self, dfs):
@@ -166,6 +185,12 @@ class Strategy:
return dfs
def _is_past_threshold(self, current_price, old_price):
current_abs = abs(current_price)
old_abs = abs(old_price)
return (current_abs <= self.exit_thresholds[0] * old_abs) or (
current_abs >= self.exit_thresholds[1] * old_abs)
def __repr__(self):
return "Strategy(legs={}, conditions={})".format(
self.legs, self.conditions)