diff --git a/backtesting/backtest_iron_condor.py b/backtesting/backtest_iron_condor.py index 7b02fb1..d268ce9 100644 --- a/backtesting/backtest_iron_condor.py +++ b/backtesting/backtest_iron_condor.py @@ -8,7 +8,6 @@ from dotenv import load_dotenv from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum -from os import getenv load_dotenv() @@ -40,6 +39,8 @@ class OptionStrat: @dataclass class BacktestResult: date: str + entry_time: str + exit_time: str trade_entered: bool trade_pnl: float profit: float @@ -55,10 +56,13 @@ wins = [] exit_times = [] 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. - 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]) backtested_dates = set(backtest_results['Date'].to_list()) 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') 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.update_layout({ 'font_color': '#7a7c7d', @@ -164,16 +165,18 @@ def _backtest_iron_condor( call_spread_strat: OptionStrat, put_spread_strat: OptionStrat ) -> BacktestResult: - + call_spread_history = get_spread_history(historical_option_data, call_spread_strat) put_spread_history = get_spread_history(historical_option_data, put_spread_strat) 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) - 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) # Calculate entry slippage. @@ -195,13 +198,13 @@ def _backtest_iron_condor( max_profit = 0.0 max_drawdown = 0.0 - exit_time = 160000 + exit_time = '16:00:00' for i in range(len(call_spread_history)): call_spread = call_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 continue @@ -226,7 +229,8 @@ def _backtest_iron_condor( # Calculate exit slippage. premium_received -= 0.10 # TODO: Make this configurable. 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') 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 -= 0.10 # TODO: Make this configurable. 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') if not (call_spread_stopped_out and put_spread_stopped_out): @@ -291,19 +296,22 @@ def _backtest_iron_condor( exit_times.append(exit_time) result = BacktestResult( - date = current_date, - trade_entered = True, - trade_pnl = premium_received, - profit = 0.0, - credit = original_call_spread_price + original_put_spread_price, - mfe = max_profit, - mae = max_drawdown + date=current_date, + entry_time=f'{current_date} {entry_time}', + exit_time=f'{current_date} {exit_time}', + trade_entered=True, + trade_pnl=premium_received, + profit=0.0, # TODO: Calculated elsewhere. Clean this up. + credit=original_call_spread_price + original_put_spread_price, + mfe=max_profit, + mae=max_drawdown ) logging.info('Premium Received: %f', premium_received) return result def backtest_iron_condor( + strategy_name: str, call_spread_strat: OptionStrat, put_spread_strat: OptionStrat, start_date: datetime, @@ -317,7 +325,7 @@ def backtest_iron_condor( result_dates = [] result_pnl = [] - results = [] + backtest_results = [] start_year = start_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) total_premium_received += backtest_result.trade_pnl backtest_result.profit = total_premium_received - results.append(backtest_result) + backtest_results.append(backtest_result) if backtest_result.trade_entered: total_trades += 1 @@ -358,7 +366,17 @@ def backtest_iron_condor( result_dates.append(current_date) 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 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 if __name__ == '__main__': - start_date = datetime(2023, 1, 1) + start_date = datetime(2024, 1, 12) end_date = datetime.now() 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') \ No newline at end of file