Skip to content

Commit 19bc7b3

Browse files
committed
Fix risk manager
1 parent 6fc4f71 commit 19bc7b3

File tree

3 files changed

+111
-83
lines changed

3 files changed

+111
-83
lines changed

poetry.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/agents/risk_manager.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def risk_management_agent(state: AgentState):
1414

1515
# Initialize risk analysis for each ticker
1616
risk_analysis = {}
17+
current_prices = {} # Store prices here to avoid redundant API calls
1718

1819
for ticker in tickers:
1920
progress.update_status("risk_management_agent", ticker, "Analyzing price data")
@@ -30,9 +31,13 @@ def risk_management_agent(state: AgentState):
3031
# Calculate portfolio value
3132
current_price = prices_df["close"].iloc[-1]
3233
current_stock_value = portfolio["positions"][ticker] * current_price
34+
current_prices[ticker] = current_price # Store the current price
35+
36+
# Calculate total portfolio value using stored prices
3337
total_portfolio_value = portfolio["cash"] + sum(
34-
portfolio["positions"][t] * get_prices(t, data["end_date"], data["end_date"])[0].close
38+
portfolio["positions"][t] * current_prices[t]
3539
for t in portfolio["positions"]
40+
if t in current_prices # Only use prices we've already fetched
3641
)
3742

3843
# 1. Liquidity Check

src/backtester.py

Lines changed: 102 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,17 @@
2222

2323

2424
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):
2626
self.agent = agent
27-
self.ticker = ticker
27+
self.tickers = tickers
2828
self.start_date = start_date
2929
self.end_date = end_date
3030
self.initial_capital = initial_capital
3131
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+
}
3336
self.portfolio_values = []
3437

3538
def prefetch_data(self):
@@ -41,29 +44,30 @@ def prefetch_data(self):
4144
start_date_dt = end_date_dt - relativedelta(years=1)
4245
start_date_str = start_date_dt.strftime("%Y-%m-%d")
4346

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+
)
6771

6872
print("Data pre-fetch complete.")
6973

@@ -78,27 +82,27 @@ def parse_agent_response(self, agent_output):
7882
print(f"Error parsing action: {agent_output}")
7983
return "hold", 0
8084

81-
def execute_trade(self, action, quantity, current_price):
85+
def execute_trade(self, ticker: str, action: str, quantity: float, current_price: float):
8286
"""Validate and execute trades based on portfolio constraints"""
8387
if action == "buy" and quantity > 0:
8488
cost = quantity * current_price
8589
if cost <= self.portfolio["cash"]:
86-
self.portfolio["stock"] += quantity
90+
self.portfolio["positions"][ticker] += quantity
8791
self.portfolio["cash"] -= cost
8892
return quantity
8993
else:
9094
# Calculate maximum affordable quantity
9195
max_quantity = self.portfolio["cash"] // current_price
9296
if max_quantity > 0:
93-
self.portfolio["stock"] += max_quantity
97+
self.portfolio["positions"][ticker] += max_quantity
9498
self.portfolio["cash"] -= max_quantity * current_price
9599
return max_quantity
96100
return 0
97101
elif action == "sell" and quantity > 0:
98-
quantity = min(quantity, self.portfolio["stock"])
102+
quantity = min(quantity, self.portfolio["positions"][ticker])
99103
if quantity > 0:
104+
self.portfolio["positions"][ticker] -= quantity
100105
self.portfolio["cash"] += quantity * current_price
101-
self.portfolio["stock"] -= quantity
102106
return quantity
103107
return 0
104108
return 0
@@ -117,49 +121,58 @@ def run_backtest(self):
117121
current_date_str = current_date.strftime("%Y-%m-%d")
118122

119123
output = self.agent(
120-
ticker=self.ticker,
124+
tickers=self.tickers,
121125
start_date=lookback_start,
122126
end_date=current_date_str,
123127
portfolio=self.portfolio,
124128
selected_analysts=self.selected_analysts,
125129
)
126130

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"]
140132
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+
))
163176

164177
# Display the updated table
165178
print_backtest_results(table_rows)
@@ -174,17 +187,24 @@ def analyze_performance(self):
174187
performance_df = pd.DataFrame(self.portfolio_values).set_index("Date")
175188

176189
# 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}")
181200

182201
# 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")
186205
plt.ylabel("Portfolio Value ($)")
187206
plt.xlabel("Date")
207+
plt.grid(True)
188208
plt.show()
189209

190210
# Compute daily returns
@@ -193,14 +213,14 @@ def analyze_performance(self):
193213
# Calculate Sharpe Ratio (assuming 252 trading days in a year)
194214
mean_daily_return = performance_df["Daily Return"].mean()
195215
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"\nSharpe Ratio: {Fore.YELLOW}{sharpe_ratio:.2f}{Style.RESET_ALL}")
198218

199219
# Calculate Maximum Drawdown
200220
rolling_max = performance_df["Portfolio Value"].cummax()
201221
drawdown = performance_df["Portfolio Value"] / rolling_max - 1
202222
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}")
204224

205225
return performance_df
206226

@@ -211,7 +231,7 @@ def analyze_performance(self):
211231

212232
# Set up argument parser
213233
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)")
215235
parser.add_argument(
216236
"--end-date",
217237
type=str,
@@ -233,6 +253,9 @@ def analyze_performance(self):
233253

234254
args = parser.parse_args()
235255

256+
# Parse tickers from comma-separated string
257+
tickers = [ticker.strip() for ticker in args.tickers.split(",")]
258+
236259
selected_analysts = None
237260
choices = questionary.checkbox(
238261
"Use the Space bar to select/unselect analysts.",
@@ -259,7 +282,7 @@ def analyze_performance(self):
259282
# Create an instance of Backtester
260283
backtester = Backtester(
261284
agent=run_hedge_fund,
262-
ticker=args.ticker,
285+
tickers=tickers,
263286
start_date=args.start_date,
264287
end_date=args.end_date,
265288
initial_capital=args.initial_capital,

0 commit comments

Comments
 (0)