Update backtest result data frame to conform to table schema
This commit is contained in:
parent
936b32f35f
commit
1b475175c0
@ -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):
|
||||||
@ -291,19 +296,22 @@ def _backtest_iron_condor(
|
|||||||
exit_times.append(exit_time)
|
exit_times.append(exit_time)
|
||||||
|
|
||||||
result = BacktestResult(
|
result = BacktestResult(
|
||||||
date = current_date,
|
date=current_date,
|
||||||
trade_entered = True,
|
entry_time=f'{current_date} {entry_time}',
|
||||||
trade_pnl = premium_received,
|
exit_time=f'{current_date} {exit_time}',
|
||||||
profit = 0.0,
|
trade_entered=True,
|
||||||
credit = original_call_spread_price + original_put_spread_price,
|
trade_pnl=premium_received,
|
||||||
mfe = max_profit,
|
profit=0.0, # TODO: Calculated elsewhere. Clean this up.
|
||||||
mae = max_drawdown
|
credit=original_call_spread_price + original_put_spread_price,
|
||||||
|
mfe=max_profit,
|
||||||
|
mae=max_drawdown
|
||||||
)
|
)
|
||||||
|
|
||||||
logging.info('Premium Received: %f', premium_received)
|
logging.info('Premium Received: %f', premium_received)
|
||||||
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')
|
Loading…
Reference in New Issue
Block a user