22
22
23
23
24
24
class Backtester :
25
- def __init__ (self , agent , ticker , start_date , end_date , initial_capital , selected_analysts = None ):
25
+ def __init__ (self , agent , tickers : list [ str ] , start_date , end_date , initial_capital , selected_analysts = None ):
26
26
self .agent = agent
27
- self .ticker = ticker
27
+ self .tickers = tickers
28
28
self .start_date = start_date
29
29
self .end_date = end_date
30
30
self .initial_capital = initial_capital
31
31
self .selected_analysts = selected_analysts
32
- self .portfolio = {"cash" : initial_capital , "stock" : 0 }
32
+ self .portfolio = {
33
+ "cash" : initial_capital ,
34
+ "positions" : {ticker : 0 for ticker in tickers }
35
+ }
33
36
self .portfolio_values = []
34
37
35
38
def prefetch_data (self ):
@@ -41,29 +44,30 @@ def prefetch_data(self):
41
44
start_date_dt = end_date_dt - relativedelta (years = 1 )
42
45
start_date_str = start_date_dt .strftime ("%Y-%m-%d" )
43
46
44
- # Fetch price data for the entire period, plus 1 year
45
- get_prices (self .ticker , start_date_str , self .end_date )
46
-
47
- # Fetch financial metrics
48
- get_financial_metrics (self .ticker , self .end_date , limit = 10 )
49
-
50
- # Fetch insider trades
51
- get_insider_trades (self .ticker , self .end_date , limit = 1000 )
52
-
53
- # Fetch common line items used by valuation agent
54
- search_line_items (
55
- self .ticker ,
56
- [
57
- "free_cash_flow" ,
58
- "net_income" ,
59
- "depreciation_and_amortization" ,
60
- "capital_expenditure" ,
61
- "working_capital" ,
62
- ],
63
- self .end_date ,
64
- period = "ttm" ,
65
- limit = 2 , # Need current and previous for working capital change
66
- )
47
+ for ticker in self .tickers :
48
+ # Fetch price data for the entire period, plus 1 year
49
+ get_prices (ticker , start_date_str , self .end_date )
50
+
51
+ # Fetch financial metrics
52
+ get_financial_metrics (ticker , self .end_date , limit = 10 )
53
+
54
+ # Fetch insider trades
55
+ get_insider_trades (ticker , self .end_date , limit = 1000 )
56
+
57
+ # Fetch common line items used by valuation agent
58
+ search_line_items (
59
+ ticker ,
60
+ [
61
+ "free_cash_flow" ,
62
+ "net_income" ,
63
+ "depreciation_and_amortization" ,
64
+ "capital_expenditure" ,
65
+ "working_capital" ,
66
+ ],
67
+ self .end_date ,
68
+ period = "ttm" ,
69
+ limit = 2 , # Need current and previous for working capital change
70
+ )
67
71
68
72
print ("Data pre-fetch complete." )
69
73
@@ -78,27 +82,27 @@ def parse_agent_response(self, agent_output):
78
82
print (f"Error parsing action: { agent_output } " )
79
83
return "hold" , 0
80
84
81
- def execute_trade (self , action , quantity , current_price ):
85
+ def execute_trade (self , ticker : str , action : str , quantity : float , current_price : float ):
82
86
"""Validate and execute trades based on portfolio constraints"""
83
87
if action == "buy" and quantity > 0 :
84
88
cost = quantity * current_price
85
89
if cost <= self .portfolio ["cash" ]:
86
- self .portfolio ["stock" ] += quantity
90
+ self .portfolio ["positions" ][ ticker ] += quantity
87
91
self .portfolio ["cash" ] -= cost
88
92
return quantity
89
93
else :
90
94
# Calculate maximum affordable quantity
91
95
max_quantity = self .portfolio ["cash" ] // current_price
92
96
if max_quantity > 0 :
93
- self .portfolio ["stock" ] += max_quantity
97
+ self .portfolio ["positions" ][ ticker ] += max_quantity
94
98
self .portfolio ["cash" ] -= max_quantity * current_price
95
99
return max_quantity
96
100
return 0
97
101
elif action == "sell" and quantity > 0 :
98
- quantity = min (quantity , self .portfolio ["stock" ])
102
+ quantity = min (quantity , self .portfolio ["positions" ][ ticker ])
99
103
if quantity > 0 :
104
+ self .portfolio ["positions" ][ticker ] -= quantity
100
105
self .portfolio ["cash" ] += quantity * current_price
101
- self .portfolio ["stock" ] -= quantity
102
106
return quantity
103
107
return 0
104
108
return 0
@@ -117,49 +121,58 @@ def run_backtest(self):
117
121
current_date_str = current_date .strftime ("%Y-%m-%d" )
118
122
119
123
output = self .agent (
120
- ticker = self .ticker ,
124
+ tickers = self .tickers ,
121
125
start_date = lookback_start ,
122
126
end_date = current_date_str ,
123
127
portfolio = self .portfolio ,
124
128
selected_analysts = self .selected_analysts ,
125
129
)
126
130
127
- agent_decision = output ["decision" ]
128
- action , quantity = agent_decision ["action" ], agent_decision ["quantity" ]
129
- df = get_price_data (self .ticker , lookback_start , current_date_str )
130
- current_price = df .iloc [- 1 ]["close" ]
131
-
132
- # Execute the trade with validation
133
- executed_quantity = self .execute_trade (action , quantity , current_price )
134
-
135
- # Update total portfolio value
136
- total_value = self .portfolio ["cash" ] + self .portfolio ["stock" ] * current_price
137
- self .portfolio ["portfolio_value" ] = total_value
138
-
139
- # Count signals from selected analysts only
131
+ decisions = output ["decisions" ]
140
132
analyst_signals = output ["analyst_signals" ]
141
-
142
- # Count signals
143
- bullish_count = len ([s for s in analyst_signals .values () if s .get ("signal" , "" ).lower () == "bullish" ])
144
- bearish_count = len ([s for s in analyst_signals .values () if s .get ("signal" , "" ).lower () == "bearish" ])
145
- neutral_count = len ([s for s in analyst_signals .values () if s .get ("signal" , "" ).lower () == "neutral" ])
146
-
147
- print (f"Signal counts - Bullish: { bullish_count } , Bearish: { bearish_count } , Neutral: { neutral_count } " )
148
-
149
- # Format and add row
150
- table_rows .append (format_backtest_row (
151
- date = current_date .strftime ('%Y-%m-%d' ),
152
- ticker = self .ticker ,
153
- action = action ,
154
- quantity = executed_quantity ,
155
- price = current_price ,
156
- cash = self .portfolio ['cash' ],
157
- stock = self .portfolio ['stock' ],
158
- total_value = total_value ,
159
- bullish_count = bullish_count ,
160
- bearish_count = bearish_count ,
161
- neutral_count = neutral_count
162
- ))
133
+ total_value = self .portfolio ["cash" ]
134
+
135
+ # Process each ticker's decision
136
+ for ticker in self .tickers :
137
+ decision = decisions .get (ticker , {"action" : "hold" , "quantity" : 0 })
138
+ action , quantity = decision .get ("action" , "hold" ), decision .get ("quantity" , 0 )
139
+
140
+ # Get current price for the ticker
141
+ df = get_price_data (ticker , lookback_start , current_date_str )
142
+ current_price = df .iloc [- 1 ]["close" ]
143
+
144
+ # Execute the trade with validation
145
+ executed_quantity = self .execute_trade (ticker , action , quantity , current_price )
146
+
147
+ # Calculate position value for this ticker
148
+ position_value = self .portfolio ["positions" ][ticker ] * current_price
149
+ total_value += position_value
150
+
151
+ # Count signals for this ticker
152
+ ticker_signals = analyst_signals .get (ticker , {})
153
+ bullish_count = len ([s for s in ticker_signals .values () if s .get ("signal" , "" ).lower () == "bullish" ])
154
+ bearish_count = len ([s for s in ticker_signals .values () if s .get ("signal" , "" ).lower () == "bearish" ])
155
+ neutral_count = len ([s for s in ticker_signals .values () if s .get ("signal" , "" ).lower () == "neutral" ])
156
+
157
+ # Calculate return percentage for this ticker
158
+ initial_position_value = self .initial_capital / len (self .tickers ) # Equal allocation assumption
159
+ return_pct = ((position_value + self .portfolio ["cash" ] / len (self .tickers )) / initial_position_value - 1 ) * 100
160
+
161
+ # Format and add row
162
+ table_rows .append (format_backtest_row (
163
+ date = current_date_str ,
164
+ ticker = ticker ,
165
+ action = action ,
166
+ quantity = executed_quantity ,
167
+ price = current_price ,
168
+ position_value = position_value ,
169
+ cash = self .portfolio ["cash" ] / len (self .tickers ), # Show proportional cash
170
+ total_value = total_value / len (self .tickers ), # Show proportional total value
171
+ return_pct = return_pct ,
172
+ bullish_count = bullish_count ,
173
+ bearish_count = bearish_count ,
174
+ neutral_count = neutral_count
175
+ ))
163
176
164
177
# Display the updated table
165
178
print_backtest_results (table_rows )
@@ -174,17 +187,24 @@ def analyze_performance(self):
174
187
performance_df = pd .DataFrame (self .portfolio_values ).set_index ("Date" )
175
188
176
189
# Calculate total return
177
- total_return = (
178
- self .portfolio ["portfolio_value" ] - self .initial_capital
179
- ) / self .initial_capital
180
- print (f"Total Return: { total_return * 100 :.2f} %" )
190
+ total_return = (performance_df ["Portfolio Value" ].iloc [- 1 ] - self .initial_capital ) / self .initial_capital
191
+ print (f"\n { Fore .WHITE } { Style .BRIGHT } PORTFOLIO PERFORMANCE SUMMARY:{ Style .RESET_ALL } " )
192
+ print (f"Total Return: { Fore .GREEN if total_return >= 0 else Fore .RED } { total_return * 100 :.2f} %{ Style .RESET_ALL } " )
193
+
194
+ # Calculate individual ticker returns
195
+ print (f"\n { Fore .WHITE } { Style .BRIGHT } INDIVIDUAL TICKER RETURNS:{ Style .RESET_ALL } " )
196
+ for ticker in self .tickers :
197
+ position_value = self .portfolio ["positions" ][ticker ] * get_price_data (ticker , self .end_date , self .end_date ).iloc [- 1 ]["close" ]
198
+ ticker_return = ((position_value + self .portfolio ["cash" ] / len (self .tickers )) / (self .initial_capital / len (self .tickers )) - 1 )
199
+ print (f"{ Fore .CYAN } { ticker } { Style .RESET_ALL } : { Fore .GREEN if ticker_return >= 0 else Fore .RED } { ticker_return * 100 :.2f} %{ Style .RESET_ALL } " )
181
200
182
201
# Plot the portfolio value over time
183
- performance_df [ "Portfolio Value" ]. plot (
184
- title = "Portfolio Value Over Time" , figsize = ( 12 , 6 )
185
- )
202
+ plt . figure ( figsize = ( 12 , 6 ))
203
+ plt . plot ( performance_df . index , performance_df [ "Portfolio Value" ], color = 'blue' )
204
+ plt . title ( "Portfolio Value Over Time" )
186
205
plt .ylabel ("Portfolio Value ($)" )
187
206
plt .xlabel ("Date" )
207
+ plt .grid (True )
188
208
plt .show ()
189
209
190
210
# Compute daily returns
@@ -193,14 +213,14 @@ def analyze_performance(self):
193
213
# Calculate Sharpe Ratio (assuming 252 trading days in a year)
194
214
mean_daily_return = performance_df ["Daily Return" ].mean ()
195
215
std_daily_return = performance_df ["Daily Return" ].std ()
196
- sharpe_ratio = (mean_daily_return / std_daily_return ) * (252 ** 0.5 )
197
- print (f"Sharpe Ratio: { sharpe_ratio :.2f} " )
216
+ sharpe_ratio = (mean_daily_return / std_daily_return ) * (252 ** 0.5 ) if std_daily_return != 0 else 0
217
+ print (f"\n Sharpe Ratio: { Fore . YELLOW } { sharpe_ratio :.2f} { Style . RESET_ALL } " )
198
218
199
219
# Calculate Maximum Drawdown
200
220
rolling_max = performance_df ["Portfolio Value" ].cummax ()
201
221
drawdown = performance_df ["Portfolio Value" ] / rolling_max - 1
202
222
max_drawdown = drawdown .min ()
203
- print (f"Maximum Drawdown: { max_drawdown * 100 :.2f} %" )
223
+ print (f"Maximum Drawdown: { Fore . RED } { max_drawdown * 100 :.2f} %{ Style . RESET_ALL } " )
204
224
205
225
return performance_df
206
226
@@ -211,7 +231,7 @@ def analyze_performance(self):
211
231
212
232
# Set up argument parser
213
233
parser = argparse .ArgumentParser (description = "Run backtesting simulation" )
214
- parser .add_argument ("--ticker " , type = str , help = "Stock ticker symbol (e.g., AAPL)" )
234
+ parser .add_argument ("--tickers " , type = str , required = True , help = "Comma-separated list of stock ticker symbols (e.g., AAPL,MSFT,GOOGL )" )
215
235
parser .add_argument (
216
236
"--end-date" ,
217
237
type = str ,
@@ -233,6 +253,9 @@ def analyze_performance(self):
233
253
234
254
args = parser .parse_args ()
235
255
256
+ # Parse tickers from comma-separated string
257
+ tickers = [ticker .strip () for ticker in args .tickers .split ("," )]
258
+
236
259
selected_analysts = None
237
260
choices = questionary .checkbox (
238
261
"Use the Space bar to select/unselect analysts." ,
@@ -259,7 +282,7 @@ def analyze_performance(self):
259
282
# Create an instance of Backtester
260
283
backtester = Backtester (
261
284
agent = run_hedge_fund ,
262
- ticker = args . ticker ,
285
+ tickers = tickers ,
263
286
start_date = args .start_date ,
264
287
end_date = args .end_date ,
265
288
initial_capital = args .initial_capital ,
0 commit comments