mirror of
https://github.com/wassname/options_backtester.git
synced 2026-08-11 11:22:33 +08:00
Fixed a few things and added exit price thresholds as user input
This commit is contained in:
+20
-58
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user