mirror of
https://github.com/wassname/options_backtester.git
synced 2026-09-09 11:28:08 +08:00
Drop entry signals with qty == 0. _buy_stocks and _sell_some_options now update self.current_cash
This commit is contained in:
+36
-35
@@ -4,7 +4,7 @@ import numpy as np
|
||||
import pandas as pd
|
||||
import pyprind
|
||||
|
||||
from .enums import Stock, Signal, Direction, get_order
|
||||
from .enums import *
|
||||
from .datahandler import HistoricalOptionsData, TiingoData
|
||||
from .strategy import Strategy
|
||||
|
||||
@@ -117,10 +117,9 @@ class Backtest:
|
||||
|
||||
for date, stocks, options in data_iterator:
|
||||
if (date in rebalancing_days):
|
||||
|
||||
previous_rb_date = rebalancing_days[rebalancing_days.get_loc(date) -
|
||||
1] if rebalancing_days.get_loc(date) != 0 else date
|
||||
self._update_balance(previous_rb_date, date, self._stocks_data, self._options_data)
|
||||
self._update_balance(previous_rb_date, date)
|
||||
self._rebalance_portfolio(date, stocks, options, sma_days)
|
||||
|
||||
bar.update()
|
||||
@@ -184,42 +183,30 @@ class Backtest:
|
||||
options_allocation = self.allocation['options'] * total_capital
|
||||
|
||||
# buy stocks
|
||||
|
||||
stocks_allocation = self.allocation['stocks'] * total_capital
|
||||
self._stocks_inventory = pd.DataFrame(columns=['symbol', 'price', 'qty'])
|
||||
self._buy_stocks(stocks, stocks_allocation, sma_days)
|
||||
stock_capital = self._current_stock_capital(stocks)
|
||||
|
||||
stock_cash = stocks_allocation - stock_capital
|
||||
|
||||
# exit/enter contracts
|
||||
|
||||
if options_allocation >= options_capital:
|
||||
options_cash = self._execute_option_entries(date, options, options_allocation - options_capital)
|
||||
if self.allocation['options'] * total_capital >= options_capital:
|
||||
self._execute_option_entries(date, options, options_allocation - options_capital)
|
||||
else:
|
||||
to_sell = options_capital - options_allocation
|
||||
options_value = self._get_current_option_quotes(options)
|
||||
|
||||
options_cash = self._sell_some_options(date, to_sell, options_value)
|
||||
|
||||
self.current_cash = stock_cash + options_cash
|
||||
self._sell_some_options(date, to_sell, options_value)
|
||||
|
||||
def _sell_some_options(self, date, to_sell, options_value):
|
||||
|
||||
sold = 0
|
||||
values_by_row = [0] * len(options_value[0])
|
||||
for i in range(len(self._options_strategy.legs)):
|
||||
values_by_row += options_value[i]['cost'].values # sum in each row all the values in the leg
|
||||
for i, (contract_per_row, inventory_row) in enumerate(zip(values_by_row, self._options_inventory.iterrows())):
|
||||
total_costs = np.sum([options_value[i]['cost'] for i in range(len(options_value))])
|
||||
for i, (contract_per_row, inventory_row) in enumerate(zip(total_costs, self._options_inventory.iterrows())):
|
||||
if to_sell - sold < -contract_per_row * inventory_row[1]['totals']['qty']:
|
||||
|
||||
qty_to_sell = to_sell // contract_per_row
|
||||
|
||||
self._options_inventory.at[i, ('totals', 'date')] = date
|
||||
self._options_inventory.at[i, ('totals', 'qty')] += qty_to_sell
|
||||
|
||||
sold -= (qty_to_sell * contract_per_row)
|
||||
return to_sell - sold
|
||||
|
||||
self.current_cash += to_sell - sold
|
||||
|
||||
def _current_stock_capital(self, stocks):
|
||||
"""Return the current value of the stocks inventory.
|
||||
@@ -250,6 +237,7 @@ class Backtest:
|
||||
|
||||
def _buy_stocks(self, stocks, allocation, sma_days):
|
||||
"""Buys stocks according to their given weight, optionally using an SMA entry filter.
|
||||
Updates `self._stocks_inventory` and `self.current_cash`.
|
||||
|
||||
Args:
|
||||
stocks (pd.DataFrame): Stocks data for the current time step.
|
||||
@@ -268,15 +256,20 @@ class Backtest:
|
||||
else:
|
||||
qty = (allocation * stock_percentages) // stock_prices
|
||||
|
||||
self.current_cash -= np.sum(stock_prices * qty)
|
||||
self._stocks_inventory = pd.DataFrame({'symbol': stock_symbols, 'price': stock_prices, 'qty': qty})
|
||||
|
||||
def _update_balance(self, start_date, end_date, stocks, options):
|
||||
def _update_balance(self, start_date, end_date):
|
||||
"""Updates self.balance in batch in a certain period between rebalancing days"""
|
||||
stocks_data = stocks.query('(date >= "{}") & (date < "{}")'.format(start_date, end_date))
|
||||
options_data = options.query('(quotedate >= "{}") & (quotedate < "{}")'.format(start_date, end_date))
|
||||
stocks_date_col = self._stocks_schema['date']
|
||||
stocks_data = self._stocks_data.query('({date_col} >= "{start_date}") & ({date_col} < "{end_date}")'.format(
|
||||
date_col=stocks_date_col, start_date=start_date, end_date=end_date))
|
||||
options_date_col = self._options_schema['date']
|
||||
options_data = self._options_data.query('({date_col} >= "{start_date}") & ({date_col} < "{end_date}")'.format(
|
||||
date_col=options_date_col, start_date=start_date, end_date=end_date))
|
||||
|
||||
calls_value = pd.Series(0, index=options_data['quotedate'].unique())
|
||||
puts_value = pd.Series(0, index=options_data['quotedate'].unique())
|
||||
calls_value = pd.Series(0, index=options_data[options_date_col].unique())
|
||||
puts_value = pd.Series(0, index=options_data[options_date_col].unique())
|
||||
|
||||
for leg in self._options_strategy.legs:
|
||||
leg_inventory = self._options_inventory[leg.name]
|
||||
@@ -284,15 +277,17 @@ class Backtest:
|
||||
for contract in leg_inventory['contract']:
|
||||
leg_inventory_contract = leg_inventory.query('contract == "{}"'.format(contract))
|
||||
qty = self._options_inventory.loc[leg_inventory_contract.index]['totals']['qty'].values[0]
|
||||
options_contract_col = self._options_schema['contract']
|
||||
current = leg_inventory_contract[['contract']].merge(options_data,
|
||||
how='left',
|
||||
left_on='contract',
|
||||
right_on='optionroot').set_index('quotedate')
|
||||
right_on=options_contract_col)
|
||||
current.set_index(options_date_col, inplace=True)
|
||||
|
||||
if cost_field == 'ask':
|
||||
if cost_field == Direction.BUY.value:
|
||||
current[cost_field] = -current[cost_field]
|
||||
|
||||
if (leg_inventory_contract['type'] == 'call').any():
|
||||
if (leg_inventory_contract['type'] == Type.CALL.value).any():
|
||||
calls_value += current[cost_field] * qty * self.shares_per_contract
|
||||
else:
|
||||
puts_value += current[cost_field] * qty * self.shares_per_contract
|
||||
@@ -301,11 +296,12 @@ class Backtest:
|
||||
on='symbol')
|
||||
stocks_current['cost'] = stocks_current['qty'] * stocks_current['adjClose']
|
||||
|
||||
add = pd.concat([
|
||||
stocks_current[stocks_current['symbol'] == stock.symbol].set_index('date')[['cost']].rename(
|
||||
columns={'cost': stock.symbol}) for stock in self._stocks
|
||||
],
|
||||
axis=1)
|
||||
columns = [
|
||||
stocks_current[stocks_current['symbol'] == stock.symbol].set_index(stocks_date_col)[[
|
||||
'cost'
|
||||
]].rename(columns={'cost': stock.symbol}) for stock in self._stocks
|
||||
]
|
||||
add = pd.concat(columns, axis=1)
|
||||
|
||||
add['cash'] = self.current_cash
|
||||
add['options qty'] = self._options_inventory['totals']['qty'].sum()
|
||||
@@ -321,6 +317,7 @@ class Backtest:
|
||||
def _execute_option_entries(self, date, options, options_allocation):
|
||||
"""Enters option positions according to `self._options_strategy`.
|
||||
Calls `self._pick_entry_signals` to select from the entry signals given by the strategy.
|
||||
Updates `self._options_inventory` and `self.current_cash`.
|
||||
|
||||
Args:
|
||||
date (pd.Timestamp): Current date.
|
||||
@@ -366,6 +363,9 @@ class Backtest:
|
||||
entry_signals.append(totals)
|
||||
entry_signals = pd.concat(entry_signals, axis=1)
|
||||
|
||||
# Remove signals where qty == 0
|
||||
entry_signals = entry_signals[entry_signals['totals']['qty'] > 0].reset_index()
|
||||
|
||||
entries = self._pick_entry_signals(entry_signals)
|
||||
|
||||
# Update options inventory, trade log and current cash
|
||||
@@ -378,6 +378,7 @@ class Backtest:
|
||||
"""Exits option positions according to `self._options_strategy`.
|
||||
Option positions are closed whenever the strategy signals an exit, when the profit/loss thresholds
|
||||
are exceeded or whenever the contracts in `self._options_inventory` are not found in `options`.
|
||||
Updates `self._options_inventory` and `self.current_cash`.
|
||||
|
||||
Args:
|
||||
date (pd.Timestamp): Current date.
|
||||
|
||||
Reference in New Issue
Block a user