Update backtest result data frame to conform to table schema

This commit is contained in:
moshferatu 2024-01-15 11:32:00 -08:00
parent 936b32f35f
commit 1b475175c0

View File

@ -8,7 +8,6 @@ from dotenv import load_dotenv
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from enum import Enum from enum import Enum
from os import getenv
load_dotenv() load_dotenv()
@ -40,6 +39,8 @@ class OptionStrat:
@dataclass @dataclass
class BacktestResult: class BacktestResult:
date: str date: str
entry_time: str
exit_time: str
trade_entered: bool trade_entered: bool
trade_pnl: float trade_pnl: float
profit: float profit: float
@ -55,10 +56,13 @@ wins = []
exit_times = [] exit_times = []
def plot(backtest_results: pd.DataFrame, title: str): def plot(backtest_results: pd.DataFrame, title: str):
backtest_results.rename(columns = {'date' : 'Date', 'profit' : 'Profit'}, inplace = True) backtest_results.drop('Profit', axis = 1, inplace = True)
backtest_results.rename(columns = {'Cumulative Profit' : 'Profit'}, inplace = True)
# Exclude dates on which the market was closed from being plotted in order to prevent gaps on chart. # Exclude dates on which the market was closed from being plotted in order to prevent gaps on chart.
backtest_date_range = pd.date_range(start="2016-01-01", end="2023-12-31").to_list() start_date = backtest_results['Date'].min()
end_date = backtest_results['Date'].max()
backtest_date_range = pd.date_range(start = start_date, end = end_date).to_list()
backtest_date_range = set([timestamp.strftime('%Y-%m-%d') for timestamp in backtest_date_range]) backtest_date_range = set([timestamp.strftime('%Y-%m-%d') for timestamp in backtest_date_range])
backtested_dates = set(backtest_results['Date'].to_list()) backtested_dates = set(backtest_results['Date'].to_list())
excluded_dates = backtest_date_range - backtested_dates excluded_dates = backtest_date_range - backtested_dates
@ -66,9 +70,6 @@ def plot(backtest_results: pd.DataFrame, title: str):
backtest_results['Color'] = np.where(backtest_results['Profit'] >= 0, 'limegreen', 'red') backtest_results['Color'] = np.where(backtest_results['Profit'] >= 0, 'limegreen', 'red')
color_sequence = ['limegreen', 'red'] if backtest_results.iloc[0]['Profit'] >= 0 else ['red', 'limegreen'] color_sequence = ['limegreen', 'red'] if backtest_results.iloc[0]['Profit'] >= 0 else ['red', 'limegreen']
print('Backtest Results:')
print(backtest_results)
chart = px.bar(backtest_results, x='Date', y='Profit', title=title, color='Color', color_discrete_sequence=color_sequence, hover_data={'Color': False}) chart = px.bar(backtest_results, x='Date', y='Profit', title=title, color='Color', color_discrete_sequence=color_sequence, hover_data={'Color': False})
chart.update_layout({ chart.update_layout({
'font_color': '#7a7c7d', 'font_color': '#7a7c7d',
@ -170,10 +171,12 @@ def _backtest_iron_condor(
current_date = call_spread_history.iloc[0].name[:10] current_date = call_spread_history.iloc[0].name[:10]
call_spread_entry = call_spread_history.loc[current_date + ' ' + call_spread_strat.trade_entry_time] entry_time = call_spread_strat.trade_entry_time
call_spread_entry = call_spread_history.loc[current_date + ' ' + entry_time]
original_call_spread_price = ((call_spread_entry['ask_short_strike'] + call_spread_entry['bid_short_strike']) / 2.0) - ((call_spread_entry['ask_long_strike'] + call_spread_entry['bid_long_strike']) / 2.0) original_call_spread_price = ((call_spread_entry['ask_short_strike'] + call_spread_entry['bid_short_strike']) / 2.0) - ((call_spread_entry['ask_long_strike'] + call_spread_entry['bid_long_strike']) / 2.0)
put_spread_entry = put_spread_history.loc[current_date + ' ' + put_spread_strat.trade_entry_time] put_spread_entry = put_spread_history.loc[current_date + ' ' + entry_time]
original_put_spread_price = ((put_spread_entry['ask_short_strike'] + put_spread_entry['bid_short_strike']) / 2.0) - ((put_spread_entry['ask_long_strike'] + put_spread_entry['bid_long_strike']) / 2.0) original_put_spread_price = ((put_spread_entry['ask_short_strike'] + put_spread_entry['bid_short_strike']) / 2.0) - ((put_spread_entry['ask_long_strike'] + put_spread_entry['bid_long_strike']) / 2.0)
# Calculate entry slippage. # Calculate entry slippage.
@ -195,13 +198,13 @@ def _backtest_iron_condor(
max_profit = 0.0 max_profit = 0.0
max_drawdown = 0.0 max_drawdown = 0.0
exit_time = 160000 exit_time = '16:00:00'
for i in range(len(call_spread_history)): for i in range(len(call_spread_history)):
call_spread = call_spread_history.iloc[i] call_spread = call_spread_history.iloc[i]
put_spread = put_spread_history.iloc[i] put_spread = put_spread_history.iloc[i]
if call_spread.name.endswith(call_spread_strat.trade_entry_time): if call_spread.name.endswith(entry_time):
trades_entered = True trades_entered = True
continue continue
@ -226,7 +229,8 @@ def _backtest_iron_condor(
# Calculate exit slippage. # Calculate exit slippage.
premium_received -= 0.10 # TODO: Make this configurable. premium_received -= 0.10 # TODO: Make this configurable.
call_spread_stopped_out = True call_spread_stopped_out = True
exit_time = int(call_spread.name[-8:].replace(':', '')) # exit_time = int(call_spread.name[-8:].replace(':', ''))
exit_time = call_spread.name[-8:]
logging.info('Call Spread Stopped Out') logging.info('Call Spread Stopped Out')
if not put_spread_stopped_out: if not put_spread_stopped_out:
@ -234,7 +238,8 @@ def _backtest_iron_condor(
premium_received -= original_put_spread_price * (put_spread_strat.stop_loss_multiple + 1) premium_received -= original_put_spread_price * (put_spread_strat.stop_loss_multiple + 1)
premium_received -= 0.10 # TODO: Make this configurable. premium_received -= 0.10 # TODO: Make this configurable.
put_spread_stopped_out = True put_spread_stopped_out = True
exit_time = int(call_spread.name[-8:].replace(':', '')) # exit_time = int(call_spread.name[-8:].replace(':', ''))
exit_time = call_spread.name[-8:]
logging.info('Put Spread Stopped Out') logging.info('Put Spread Stopped Out')
if not (call_spread_stopped_out and put_spread_stopped_out): if not (call_spread_stopped_out and put_spread_stopped_out):
@ -292,9 +297,11 @@ def _backtest_iron_condor(
result = BacktestResult( result = BacktestResult(
date=current_date, date=current_date,
entry_time=f'{current_date} {entry_time}',
exit_time=f'{current_date} {exit_time}',
trade_entered=True, trade_entered=True,
trade_pnl=premium_received, trade_pnl=premium_received,
profit = 0.0, profit=0.0, # TODO: Calculated elsewhere. Clean this up.
credit=original_call_spread_price + original_put_spread_price, credit=original_call_spread_price + original_put_spread_price,
mfe=max_profit, mfe=max_profit,
mae=max_drawdown mae=max_drawdown
@ -304,6 +311,7 @@ def _backtest_iron_condor(
return result return result
def backtest_iron_condor( def backtest_iron_condor(
strategy_name: str,
call_spread_strat: OptionStrat, call_spread_strat: OptionStrat,
put_spread_strat: OptionStrat, put_spread_strat: OptionStrat,
start_date: datetime, start_date: datetime,
@ -317,7 +325,7 @@ def backtest_iron_condor(
result_dates = [] result_dates = []
result_pnl = [] result_pnl = []
results = [] backtest_results = []
start_year = start_date.year start_year = start_date.year
end_year = end_date.year end_year = end_date.year
@ -343,7 +351,7 @@ def backtest_iron_condor(
backtest_result = _backtest_iron_condor(historical_option_data, call_spread_strat, put_spread_strat) backtest_result = _backtest_iron_condor(historical_option_data, call_spread_strat, put_spread_strat)
total_premium_received += backtest_result.trade_pnl total_premium_received += backtest_result.trade_pnl
backtest_result.profit = total_premium_received backtest_result.profit = total_premium_received
results.append(backtest_result) backtest_results.append(backtest_result)
if backtest_result.trade_entered: if backtest_result.trade_entered:
total_trades += 1 total_trades += 1
@ -358,7 +366,17 @@ def backtest_iron_condor(
result_dates.append(current_date) result_dates.append(current_date)
result_pnl.append(total_premium_received) result_pnl.append(total_premium_received)
backtest_results = pd.DataFrame([result.__dict__ for result in results]) # TODO: Either look up the symbol in the historical option data or have the client provide it.
backtest_results = pd.DataFrame([{
'Date': result.date,
'Symbol': 'SPX',
'Strategy': strategy_name,
'Entry Time': result.entry_time,
'Exit Time': result.exit_time,
'Profit': result.trade_pnl,
'Cumulative Profit': result.profit
} for result in backtest_results])
return backtest_results return backtest_results
def create_strategies(entry_time, number_of_contracts=1): def create_strategies(entry_time, number_of_contracts=1):
@ -389,8 +407,15 @@ def create_strategies(entry_time, number_of_contracts=1):
return call_spread_strat, put_spread_strat return call_spread_strat, put_spread_strat
if __name__ == '__main__': if __name__ == '__main__':
start_date = datetime(2023, 1, 1) start_date = datetime(2024, 1, 12)
end_date = datetime.now() end_date = datetime.now()
call_spread_strat, put_spread_strat = create_strategies(entry_time = '10:05:00') call_spread_strat, put_spread_strat = create_strategies(entry_time = '10:05:00')
backtest_result = backtest_iron_condor(call_spread_strat, put_spread_strat, start_date, end_date) backtest_result = backtest_iron_condor(
f'Iron Condor @ {call_spread_strat.trade_entry_time}',
call_spread_strat,
put_spread_strat,
start_date,
end_date
)
print(backtest_result)
plot(backtest_result, title = 'Iron Condor Backtest Results') plot(backtest_result, title = 'Iron Condor Backtest Results')