tbdavid2019 commited on
Commit
a8a8be8
·
1 Parent(s): 504795e

更新 README.md,新增多標的同時分析功能,更新介面文字,改善用戶體驗;更新 app.py 以支援逗號分隔的股票代碼分析;新增 .gitignore 檔案以排除不必要的檔案

Browse files
Files changed (3) hide show
  1. .gitignore +5 -0
  2. README.md +25 -1
  3. app.py +203 -177
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.pyo
4
+ *.pyi
5
+ *.pyd
README.md CHANGED
@@ -14,6 +14,18 @@ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-
14
 
15
  ## 🎉 最新更新 (2025-11-22)
16
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  ### ✨ 新增功能
18
 
19
  1. **新增 Phil Fisher 分析師**
@@ -107,12 +119,24 @@ python app.py
107
  ## 📝 使用說明
108
 
109
  1. **選擇語言**: 在右上角選擇 "English" 或 "繁體中文"
110
- 2. **輸入股票代碼**: 例如 AAPL, TSLA, NVDA
111
  3. **選擇分析師**: 從四個類別中選擇想要的分析師
112
  - ⚠️ **注意**: 預設不選擇任何分析師,請至少選擇一位
113
  - 💡 **建議**: 先選擇 1-3 位分析師測試,避免請求超時
114
  4. **開始分析**: 點擊 "Analyze Stock" 或 "開始分析" 按鈕
115
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  ### 性能建議 ⚡
117
 
118
  - **快速分析**: 選擇 1-3 位分析師 (~30-60 秒)
 
14
 
15
  ## 🎉 最新更新 (2025-11-22)
16
 
17
+ ### ♻️ 本次改動
18
+
19
+ 1. **支援多標的同時分析 (多個 ticker)**
20
+ - 前端輸入欄位改為「逗號分隔」格式:例如 `TSLA, GOOGL, NVDA, MSFT`
21
+ - API 呼叫 payload 會自動整理多個 ticker 並傳給後端
22
+ - 顯示結果時,會依 ticker 分區呈現最終決策與各分析師信號
23
+ - Console log 也會逐一列出每個 ticker 的摘要
24
+
25
+ 2. **介面文字更新**
26
+ - 說明與 placeholder 改為強調「多個 ticker 逗號分隔」輸入方式
27
+ - 英 / 繁體中文翻譯同步更新
28
+
29
  ### ✨ 新增功能
30
 
31
  1. **新增 Phil Fisher 分析師**
 
119
  ## 📝 使用說明
120
 
121
  1. **選擇語言**: 在右上角選擇 "English" 或 "繁體中文"
122
+ 2. **輸入股票代碼**: 可輸入一個或多個代碼,使用逗號分隔,例如:`AAPL, TSLA, NVDA`
123
  3. **選擇分析師**: 從四個類別中選擇想要的分析師
124
  - ⚠️ **注意**: 預設不選擇任何分析師,請至少選擇一位
125
  - 💡 **建議**: 先選擇 1-3 位分析師測試,避免請求超時
126
  4. **開始分析**: 點擊 "Analyze Stock" 或 "開始分析" 按鈕
127
 
128
+ ### API 請求範例
129
+
130
+ ```bash
131
+ curl -X POST "http://your-api-host:6000/api/analysis" \
132
+ -H "Content-Type: application/json" \
133
+ -d '{
134
+ "tickers": "TSLA,GOOGL,NVDA,MSFT",
135
+ "selectedAnalysts": ["warren_buffett", "peter_lynch", "charlie_munger", "technical_analyst", "fundamentals_analyst"],
136
+ "modelName": "gpt-4o"
137
+ }'
138
+ ```
139
+
140
  ### 性能建議 ⚡
141
 
142
  - **快速分析**: 選擇 1-3 位分析師 (~30-60 秒)
app.py CHANGED
@@ -60,15 +60,15 @@ for category in ANALYSTS_BY_CATEGORY.values():
60
  TRANSLATIONS = {
61
  "en": {
62
  "title": "Stock Analysis Dashboard",
63
- "subtitle": "Enter a stock ticker and select analysts to analyze the stock.",
64
- "ticker_label": "Stock Ticker (e.g., AAPL, MSFT, TSLA)",
65
- "ticker_placeholder": "Enter ticker symbol",
66
  "analyze_button": "Analyze Stock",
67
  "results_title": "Analysis Results",
68
  "select_all": "Select All in Category",
69
  "language": "Language",
70
  # Analysis results fields
71
- "analysis_for": "Analysis Results for",
72
  "final_decision": "Final Decision",
73
  "action": "Action",
74
  "confidence": "Confidence",
@@ -90,7 +90,7 @@ TRANSLATIONS = {
90
  "bearish": "BEARISH",
91
  "neutral": "NEUTRAL",
92
  # Errors
93
- "error_ticker": "Please enter a stock ticker",
94
  "error_analyst": "Please select at least one analyst",
95
  "error_api": "Error making API request",
96
  "error_parse": "Error parsing API response",
@@ -99,9 +99,9 @@ TRANSLATIONS = {
99
  },
100
  "zh": {
101
  "title": "股票分析儀表板",
102
- "subtitle": "輸入股票代碼並選擇分析師分析股票",
103
- "ticker_label": "股票代碼 (例如: AAPL, MSFT, TSLA)",
104
- "ticker_placeholder": "輸入股票代碼",
105
  "analyze_button": "開始分析",
106
  "results_title": "分析結果",
107
  "select_all": "全選此類別",
@@ -166,30 +166,34 @@ ANALYST_NAMES = {
166
  # }'
167
 
168
 
169
- def analyze_stock(ticker, selected_analysts, language="en"):
170
  """
171
- Call the API to analyze the given stock ticker with selected analysts
172
  Args:
173
- ticker: Stock ticker symbol
174
  selected_analysts: List of selected analyst names
175
  language: UI language ('en' or 'zh')
176
  """
177
  lang = TRANSLATIONS[language]
178
 
179
- if not ticker:
180
  return lang["error_ticker"]
181
 
182
  if not selected_analysts:
183
  return lang["error_analyst"]
184
-
185
- # Format the ticker (remove spaces and convert to lowercase)
186
- ticker = ticker.strip().lower()
 
 
 
 
187
 
188
  # Console logging
189
  print("\n" + "="*80)
190
  print(f"🔍 STOCK ANALYSIS REQUEST")
191
  print("="*80)
192
- print(f"📊 Ticker: {ticker.upper()}")
193
  print(f"🌐 Language: {language}")
194
  print(f"👥 Number of Analysts: {len(selected_analysts)}")
195
  print(f"📋 Selected Analysts:")
@@ -200,7 +204,7 @@ def analyze_stock(ticker, selected_analysts, language="en"):
200
 
201
  # Prepare the payload for the API request
202
  payload = {
203
- "tickers": ticker,
204
  "selectedAnalysts": selected_analysts,
205
  "modelName": "gpt-4o-mini"
206
  }
@@ -233,48 +237,51 @@ def analyze_stock(ticker, selected_analysts, language="en"):
233
  if "analyst_signals" in result:
234
  print(f"📊 Analyst Signals Received: {len(result['analyst_signals'])}")
235
  for analyst, data in result["analyst_signals"].items():
236
- if ticker in data:
237
- signal_data = data[ticker]
238
- analyst_key = analyst.replace("_agent", "").replace("_analyst", "")
239
- analyst_name = ANALYST_NAMES.get(analyst_key, {}).get(language, analyst_key)
240
-
241
- if analyst == "risk_management_agent":
242
- print(f"\n 💼 {analyst_name}:")
243
- print(f" - Current Price: ${signal_data.get('current_price', 'N/A')}")
244
- print(f" - Position Limit: ${signal_data.get('remaining_position_limit', 'N/A')}")
245
- else:
246
- signal = signal_data.get('signal', 'N/A')
247
- confidence = signal_data.get('confidence', 'N/A')
248
-
249
- # Signal emoji
250
- signal_emoji = "📈" if signal == "bullish" else "📉" if signal == "bearish" else "➡️"
251
-
252
- print(f"\n {signal_emoji} {analyst_name}:")
253
- print(f" - Signal: {signal.upper()}")
254
- print(f" - Confidence: {confidence}%")
255
 
256
- # Show brief reasoning if available
257
- reasoning = signal_data.get('reasoning', '')
258
- if isinstance(reasoning, str) and reasoning and reasoning != 'N/A':
259
- reasoning_brief = reasoning[:100] + "..." if len(reasoning) > 100 else reasoning
260
- print(f" - Reasoning: {reasoning_brief}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
 
262
- if "decisions" in result and ticker in result["decisions"]:
263
- decision = result["decisions"][ticker]
264
- print(f"\n" + "-"*80)
265
- print(f"🎯 FINAL DECISION:")
266
- print(f" - Action: {decision.get('action', 'N/A').upper()}")
267
- print(f" - Confidence: {decision.get('confidence', 'N/A')}%")
268
- print(f" - Quantity: {decision.get('quantity', 'N/A')}")
269
- decision_reasoning = decision.get('reasoning', 'N/A')
270
- if decision_reasoning and decision_reasoning != 'N/A':
271
- reasoning_brief = decision_reasoning[:150] + "..." if len(decision_reasoning) > 150 else decision_reasoning
272
- print(f" - Reasoning: {reasoning_brief}")
 
 
273
 
274
  print("="*80 + "\n")
275
 
276
  # Format the output
277
- output = format_analysis_results(result, ticker, language)
278
  return output
279
 
280
  except requests.exceptions.Timeout as e:
@@ -304,158 +311,176 @@ def analyze_stock(ticker, selected_analysts, language="en"):
304
  print(f"❌ Unexpected Error: {str(e)}\n")
305
  return error_msg
306
 
307
- def format_analysis_results(result, ticker, language="en"):
308
  """
309
  Format the analysis results into a readable HTML output
310
  Args:
311
  result: API response dictionary
312
- ticker: Stock ticker symbol
313
  language: UI language ('en' or 'zh')
314
  """
315
  lang = TRANSLATIONS[language]
316
 
317
- if not result or "analyst_signals" not in result:
318
- return lang["no_results"]
319
-
320
- html_output = f"<h1>{lang['analysis_for']} {ticker.upper()}</h1>"
321
 
322
- # Add decision section if available
323
- if "decisions" in result and ticker in result["decisions"]:
324
- decision = result["decisions"][ticker]
325
- action = decision.get('action', 'N/A').lower()
326
- action_text = lang.get(action, action.upper())
327
-
328
- html_output += f"""
329
- <div class="decision-box">
330
- <h2>{lang['final_decision']}</h2>
331
- <p><strong>{lang['action']}:</strong> {action_text}</p>
332
- <p><strong>{lang['confidence']}:</strong> {decision.get('confidence', 'N/A')}%</p>
333
- <p><strong>{lang['quantity']}:</strong> {decision.get('quantity', 'N/A')}</p>
334
- <p><strong>{lang['reasoning']}:</strong> {decision.get('reasoning', 'N/A')}</p>
335
- </div>
336
- """
337
 
338
- # Add analyst signals
339
- html_output += f"<h2>{lang['analyst_signals']}</h2>"
340
 
341
  analyst_signals = result["analyst_signals"]
342
- for analyst, data in analyst_signals.items():
343
- if ticker not in data:
344
- continue
345
-
346
- signal_data = data[ticker]
347
-
348
- # Get analyst name in correct language
349
- analyst_key = analyst.replace("_agent", "").replace("_analyst", "")
350
- analyst_name = ANALYST_NAMES.get(analyst_key, {}).get(language, format_analyst_name(analyst))
351
 
352
- # Special handling for risk_management_agent which has different structure
353
- if analyst == "risk_management_agent":
354
- html_output += f"""
355
- <div class="analyst-box">
356
- <h3>{analyst_name}</h3>
357
- <p><strong>{lang['current_price']}:</strong> ${signal_data.get('current_price', 'N/A')}</p>
358
- <p><strong>{lang['remaining_position_limit']}:</strong> ${signal_data.get('remaining_position_limit', 'N/A')}</p>
359
- <div class="reasoning">
360
- <p><strong>{lang['reasoning']}:</strong></p>
361
- <ul>
362
- <li>Available Cash: ${signal_data.get('reasoning', {}).get('available_cash', 'N/A')}</li>
363
- <li>Current Position: ${signal_data.get('reasoning', {}).get('current_position', 'N/A')}</li>
364
- <li>Portfolio Value: ${signal_data.get('reasoning', {}).get('portfolio_value', 'N/A')}</li>
365
- <li>Position Limit: ${signal_data.get('reasoning', {}).get('position_limit', 'N/A')}</li>
366
- </ul>
367
- </div>
368
- </div>
369
- """
370
- continue
371
-
372
- # Handle fundamentals_agent and valuation_agent which have structured reasoning
373
- if analyst in ["fundamentals_agent", "valuation_agent"]:
374
- signal = signal_data.get('signal', 'N/A').lower()
375
- signal_text = lang.get(signal, signal.upper())
376
- confidence = signal_data.get('confidence', 'N/A')
377
 
378
  html_output += f"""
379
- <div class="analyst-box {signal}">
380
- <h3>{analyst_name}</h3>
381
- <p><strong>{lang['signal']}:</strong> {signal_text}</p>
382
- <p><strong>{lang['confidence']}:</strong> {confidence}%</p>
383
- <div class="reasoning">
384
- <p><strong>{lang['analysis_details']}:</strong></p>
385
- <ul>
386
  """
 
 
 
 
 
 
 
 
 
387
 
388
- if isinstance(signal_data.get('reasoning'), dict):
389
- for key, value in signal_data['reasoning'].items():
390
- if isinstance(value, dict):
391
- sub_signal = value.get('signal', 'N/A').lower()
392
- sub_signal_text = lang.get(sub_signal, sub_signal.upper())
393
- html_output += f"<li><strong>{format_key(key)}:</strong> {sub_signal_text}"
394
- if 'details' in value:
395
- html_output += f" ({value['details']})"
396
- html_output += "</li>"
397
 
398
- html_output += """
399
- </ul>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  </div>
401
- </div>
402
- """
403
- continue
404
-
405
- # Handle technical_analyst_agent which has complex structure
406
- if analyst == "technical_analyst_agent":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  signal = signal_data.get('signal', 'N/A').lower()
408
  signal_text = lang.get(signal, signal.upper())
409
  confidence = signal_data.get('confidence', 'N/A')
 
410
 
411
  html_output += f"""
412
  <div class="analyst-box {signal}">
413
- <h3>{analyst_name}</h3>
414
  <p><strong>{lang['signal']}:</strong> {signal_text}</p>
415
  <p><strong>{lang['confidence']}:</strong> {confidence}%</p>
416
  <div class="reasoning">
417
- <p><strong>{lang['strategy_signals']}:</strong></p>
418
- <ul>
419
- """
420
-
421
- if 'strategy_signals' in signal_data:
422
- for strategy, strategy_data in signal_data['strategy_signals'].items():
423
- strategy_signal = strategy_data.get('signal', 'N/A').lower()
424
- strategy_signal_text = lang.get(strategy_signal, strategy_signal.upper())
425
- html_output += f"""
426
- <li>
427
- <strong>{format_key(strategy)}:</strong> {strategy_signal_text}
428
- ({lang['confidence']}: {strategy_data.get('confidence', 'N/A')}%)
429
- </li>
430
- """
431
-
432
- html_output += """
433
- </ul>
434
  </div>
435
  </div>
436
  """
437
- continue
438
-
439
- # Standard format for most analysts
440
- signal = signal_data.get('signal', 'N/A').lower()
441
- signal_text = lang.get(signal, signal.upper())
442
- confidence = signal_data.get('confidence', 'N/A')
443
- reasoning = signal_data.get('reasoning', 'N/A')
444
 
445
- html_output += f"""
446
- <div class="analyst-box {signal}">
447
- <h3>{analyst_name}</h3>
448
- <p><strong>{lang['signal']}:</strong> {signal_text}</p>
449
- <p><strong>{lang['confidence']}:</strong> {confidence}%</p>
450
- <div class="reasoning">
451
- <p><strong>{lang['reasoning']}:</strong> {reasoning}</p>
452
- </div>
453
- </div>
454
- """
455
 
456
  # Add some CSS for styling
457
  html_output = f"""
458
  <style>
 
 
 
 
 
 
 
459
  .analyst-box {{
460
  border: 1px solid #ddd;
461
  margin-bottom: 20px;
@@ -511,12 +536,13 @@ def format_key(key):
511
  with gr.Blocks(title="Stock Analysis Dashboard") as demo:
512
  # State for storing current language
513
  current_lang = gr.State("en")
 
514
 
515
  # Header with language selector
516
  with gr.Row():
517
  with gr.Column(scale=3):
518
- title_md = gr.Markdown("# Stock Analysis Dashboard")
519
- subtitle_md = gr.Markdown("Enter a stock ticker and select analysts to analyze the stock.")
520
  with gr.Column(scale=1):
521
  language_selector = gr.Radio(
522
  choices=["English", "繁體中文"],
@@ -527,8 +553,8 @@ with gr.Blocks(title="Stock Analysis Dashboard") as demo:
527
  with gr.Row():
528
  with gr.Column(scale=1):
529
  ticker_input = gr.Textbox(
530
- label="Stock Ticker (e.g., AAPL, MSFT, TSLA)",
531
- placeholder="Enter ticker symbol"
532
  )
533
 
534
  # Analyst selection by category
@@ -577,7 +603,7 @@ with gr.Blocks(title="Stock Analysis Dashboard") as demo:
577
  )
578
  analyst_checkboxes["fundamentals_valuation"] = fundamentals_analysts
579
 
580
- analyze_button = gr.Button("Analyze Stock", variant="primary")
581
 
582
  with gr.Column(scale=2):
583
  output = gr.HTML(label="Analysis Results")
@@ -606,10 +632,10 @@ with gr.Blocks(title="Stock Analysis Dashboard") as demo:
606
  return all_selected
607
 
608
  # Wrapper function for analyze button
609
- def analyze_wrapper(ticker, val_analysts, grow_analysts, tech_analysts, fund_analysts, lang):
610
  """Wrapper function to combine analysts and call analyze_stock"""
611
  all_analysts = combine_analysts(val_analysts, grow_analysts, tech_analysts, fund_analysts)
612
- return analyze_stock(ticker, all_analysts, lang)
613
 
614
  # Language selector change event
615
  language_selector.change(
@@ -633,4 +659,4 @@ with gr.Blocks(title="Stock Analysis Dashboard") as demo:
633
  )
634
 
635
  # Launch the app
636
- demo.launch()
 
60
  TRANSLATIONS = {
61
  "en": {
62
  "title": "Stock Analysis Dashboard",
63
+ "subtitle": "Enter one or more stock tickers (comma-separated) and select analysts.",
64
+ "ticker_label": "Stock Tickers (comma-separated, e.g., AAPL, MSFT, TSLA)",
65
+ "ticker_placeholder": "Enter comma-separated ticker symbols",
66
  "analyze_button": "Analyze Stock",
67
  "results_title": "Analysis Results",
68
  "select_all": "Select All in Category",
69
  "language": "Language",
70
  # Analysis results fields
71
+ "analysis_for": "Analysis Results",
72
  "final_decision": "Final Decision",
73
  "action": "Action",
74
  "confidence": "Confidence",
 
90
  "bearish": "BEARISH",
91
  "neutral": "NEUTRAL",
92
  # Errors
93
+ "error_ticker": "Please enter at least one stock ticker",
94
  "error_analyst": "Please select at least one analyst",
95
  "error_api": "Error making API request",
96
  "error_parse": "Error parsing API response",
 
99
  },
100
  "zh": {
101
  "title": "股票分析儀表板",
102
+ "subtitle": "輸入一個或多個股票代碼(以逗號分隔),並選擇分析師進行分析",
103
+ "ticker_label": "股票代碼(逗號分隔,例如AAPL, MSFT, TSLA",
104
+ "ticker_placeholder": "輸入逗號分隔的股票代碼",
105
  "analyze_button": "開始分析",
106
  "results_title": "分析結果",
107
  "select_all": "全選此類別",
 
166
  # }'
167
 
168
 
169
+ def analyze_stock(tickers_text, selected_analysts, language="en"):
170
  """
171
+ Call the API to analyze the given stock ticker(s) with selected analysts
172
  Args:
173
+ tickers_text: Stock ticker symbol(s), comma-separated
174
  selected_analysts: List of selected analyst names
175
  language: UI language ('en' or 'zh')
176
  """
177
  lang = TRANSLATIONS[language]
178
 
179
+ if not tickers_text:
180
  return lang["error_ticker"]
181
 
182
  if not selected_analysts:
183
  return lang["error_analyst"]
184
+
185
+ # Parse and normalize tickers (remove spaces and uppercase)
186
+ tickers = [t.strip().upper() for t in tickers_text.split(",") if t.strip()]
187
+ if not tickers:
188
+ return lang["error_ticker"]
189
+
190
+ tickers_payload = ",".join(tickers)
191
 
192
  # Console logging
193
  print("\n" + "="*80)
194
  print(f"🔍 STOCK ANALYSIS REQUEST")
195
  print("="*80)
196
+ print(f"📊 Tickers: {', '.join(tickers)}")
197
  print(f"🌐 Language: {language}")
198
  print(f"👥 Number of Analysts: {len(selected_analysts)}")
199
  print(f"📋 Selected Analysts:")
 
204
 
205
  # Prepare the payload for the API request
206
  payload = {
207
+ "tickers": tickers_payload,
208
  "selectedAnalysts": selected_analysts,
209
  "modelName": "gpt-4o-mini"
210
  }
 
237
  if "analyst_signals" in result:
238
  print(f"📊 Analyst Signals Received: {len(result['analyst_signals'])}")
239
  for analyst, data in result["analyst_signals"].items():
240
+ for ticker in tickers:
241
+ if ticker in data:
242
+ signal_data = data[ticker]
243
+ analyst_key = analyst.replace("_agent", "").replace("_analyst", "")
244
+ analyst_name = ANALYST_NAMES.get(analyst_key, {}).get(language, analyst_key)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
 
246
+ if analyst == "risk_management_agent":
247
+ print(f"\n 💼 {analyst_name} ({ticker}):")
248
+ print(f" - Current Price: ${signal_data.get('current_price', 'N/A')}")
249
+ print(f" - Position Limit: ${signal_data.get('remaining_position_limit', 'N/A')}")
250
+ else:
251
+ signal = signal_data.get('signal', 'N/A')
252
+ confidence = signal_data.get('confidence', 'N/A')
253
+
254
+ # Signal emoji
255
+ signal_emoji = "📈" if signal == "bullish" else "📉" if signal == "bearish" else "➡️"
256
+
257
+ print(f"\n {signal_emoji} {analyst_name} ({ticker}):")
258
+ print(f" - Signal: {signal.upper()}")
259
+ print(f" - Confidence: {confidence}%")
260
+
261
+ # Show brief reasoning if available
262
+ reasoning = signal_data.get('reasoning', '')
263
+ if isinstance(reasoning, str) and reasoning and reasoning != 'N/A':
264
+ reasoning_brief = reasoning[:100] + "..." if len(reasoning) > 100 else reasoning
265
+ print(f" - Reasoning: {reasoning_brief}")
266
 
267
+ if "decisions" in result:
268
+ for ticker in tickers:
269
+ if ticker in result["decisions"]:
270
+ decision = result["decisions"][ticker]
271
+ print(f"\n" + "-"*80)
272
+ print(f"🎯 FINAL DECISION for {ticker}:")
273
+ print(f" - Action: {decision.get('action', 'N/A').upper()}")
274
+ print(f" - Confidence: {decision.get('confidence', 'N/A')}%")
275
+ print(f" - Quantity: {decision.get('quantity', 'N/A')}")
276
+ decision_reasoning = decision.get('reasoning', 'N/A')
277
+ if decision_reasoning and decision_reasoning != 'N/A':
278
+ reasoning_brief = decision_reasoning[:150] + "..." if len(decision_reasoning) > 150 else decision_reasoning
279
+ print(f" - Reasoning: {reasoning_brief}")
280
 
281
  print("="*80 + "\n")
282
 
283
  # Format the output
284
+ output = format_analysis_results(result, tickers, language)
285
  return output
286
 
287
  except requests.exceptions.Timeout as e:
 
311
  print(f"❌ Unexpected Error: {str(e)}\n")
312
  return error_msg
313
 
314
+ def format_analysis_results(result, tickers, language="en"):
315
  """
316
  Format the analysis results into a readable HTML output
317
  Args:
318
  result: API response dictionary
319
+ tickers: Stock ticker symbol(s), list or comma-separated string
320
  language: UI language ('en' or 'zh')
321
  """
322
  lang = TRANSLATIONS[language]
323
 
324
+ if isinstance(tickers, str):
325
+ tickers = [t.strip().upper() for t in tickers.split(",") if t.strip()]
326
+ tickers = tickers or []
 
327
 
328
+ if not result or "analyst_signals" not in result or not tickers:
329
+ return lang["no_results"]
 
 
 
 
 
 
 
 
 
 
 
 
 
330
 
331
+ html_output = f"<h1>{lang['analysis_for']}</h1>"
 
332
 
333
  analyst_signals = result["analyst_signals"]
334
+ decisions = result.get("decisions", {})
335
+
336
+ for ticker in tickers:
337
+ html_output += f"<div class='ticker-section'><h2>{ticker}</h2>"
 
 
 
 
 
338
 
339
+ # Add decision section if available
340
+ if ticker in decisions:
341
+ decision = decisions[ticker]
342
+ action = decision.get('action', 'N/A').lower()
343
+ action_text = lang.get(action, action.upper())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
 
345
  html_output += f"""
346
+ <div class="decision-box">
347
+ <h3>{lang['final_decision']}</h3>
348
+ <p><strong>{lang['action']}:</strong> {action_text}</p>
349
+ <p><strong>{lang['confidence']}:</strong> {decision.get('confidence', 'N/A')}%</p>
350
+ <p><strong>{lang['quantity']}:</strong> {decision.get('quantity', 'N/A')}</p>
351
+ <p><strong>{lang['reasoning']}:</strong> {decision.get('reasoning', 'N/A')}</p>
352
+ </div>
353
  """
354
+
355
+ # Add analyst signals for this ticker
356
+ html_output += f"<h3>{lang['analyst_signals']}</h3>"
357
+
358
+ for analyst, data in analyst_signals.items():
359
+ if ticker not in data:
360
+ continue
361
+
362
+ signal_data = data[ticker]
363
 
364
+ # Get analyst name in correct language
365
+ analyst_key = analyst.replace("_agent", "").replace("_analyst", "")
366
+ analyst_name = ANALYST_NAMES.get(analyst_key, {}).get(language, format_analyst_name(analyst))
 
 
 
 
 
 
367
 
368
+ # Special handling for risk_management_agent which has different structure
369
+ if analyst == "risk_management_agent":
370
+ html_output += f"""
371
+ <div class="analyst-box">
372
+ <h4>{analyst_name}</h4>
373
+ <p><strong>{lang['current_price']}:</strong> ${signal_data.get('current_price', 'N/A')}</p>
374
+ <p><strong>{lang['remaining_position_limit']}:</strong> ${signal_data.get('remaining_position_limit', 'N/A')}</p>
375
+ <div class="reasoning">
376
+ <p><strong>{lang['reasoning']}:</strong></p>
377
+ <ul>
378
+ <li>Available Cash: ${signal_data.get('reasoning', {}).get('available_cash', 'N/A')}</li>
379
+ <li>Current Position: ${signal_data.get('reasoning', {}).get('current_position', 'N/A')}</li>
380
+ <li>Portfolio Value: ${signal_data.get('reasoning', {}).get('portfolio_value', 'N/A')}</li>
381
+ <li>Position Limit: ${signal_data.get('reasoning', {}).get('position_limit', 'N/A')}</li>
382
+ </ul>
383
+ </div>
384
  </div>
385
+ """
386
+ continue
387
+
388
+ # Handle fundamentals_agent and valuation_agent which have structured reasoning
389
+ if analyst in ["fundamentals_agent", "valuation_agent"]:
390
+ signal = signal_data.get('signal', 'N/A').lower()
391
+ signal_text = lang.get(signal, signal.upper())
392
+ confidence = signal_data.get('confidence', 'N/A')
393
+
394
+ html_output += f"""
395
+ <div class="analyst-box {signal}">
396
+ <h4>{analyst_name}</h4>
397
+ <p><strong>{lang['signal']}:</strong> {signal_text}</p>
398
+ <p><strong>{lang['confidence']}:</strong> {confidence}%</p>
399
+ <div class="reasoning">
400
+ <p><strong>{lang['analysis_details']}:</strong></p>
401
+ <ul>
402
+ """
403
+
404
+ if isinstance(signal_data.get('reasoning'), dict):
405
+ for key, value in signal_data['reasoning'].items():
406
+ if isinstance(value, dict):
407
+ sub_signal = value.get('signal', 'N/A').lower()
408
+ sub_signal_text = lang.get(sub_signal, sub_signal.upper())
409
+ html_output += f"<li><strong>{format_key(key)}:</strong> {sub_signal_text}"
410
+ if 'details' in value:
411
+ html_output += f" ({value['details']})"
412
+ html_output += "</li>"
413
+
414
+ html_output += """
415
+ </ul>
416
+ </div>
417
+ </div>
418
+ """
419
+ continue
420
+
421
+ # Handle technical_analyst_agent which has complex structure
422
+ if analyst == "technical_analyst_agent":
423
+ signal = signal_data.get('signal', 'N/A').lower()
424
+ signal_text = lang.get(signal, signal.upper())
425
+ confidence = signal_data.get('confidence', 'N/A')
426
+
427
+ html_output += f"""
428
+ <div class="analyst-box {signal}">
429
+ <h4>{analyst_name}</h4>
430
+ <p><strong>{lang['signal']}:</strong> {signal_text}</p>
431
+ <p><strong>{lang['confidence']}:</strong> {confidence}%</p>
432
+ <div class="reasoning">
433
+ <p><strong>{lang['strategy_signals']}:</strong></p>
434
+ <ul>
435
+ """
436
+
437
+ if 'strategy_signals' in signal_data:
438
+ for strategy, strategy_data in signal_data['strategy_signals'].items():
439
+ strategy_signal = strategy_data.get('signal', 'N/A').lower()
440
+ strategy_signal_text = lang.get(strategy_signal, strategy_signal.upper())
441
+ html_output += f"""
442
+ <li>
443
+ <strong>{format_key(strategy)}:</strong> {strategy_signal_text}
444
+ ({lang['confidence']}: {strategy_data.get('confidence', 'N/A')}%)
445
+ </li>
446
+ """
447
+
448
+ html_output += """
449
+ </ul>
450
+ </div>
451
+ </div>
452
+ """
453
+ continue
454
+
455
+ # Standard format for most analysts
456
  signal = signal_data.get('signal', 'N/A').lower()
457
  signal_text = lang.get(signal, signal.upper())
458
  confidence = signal_data.get('confidence', 'N/A')
459
+ reasoning = signal_data.get('reasoning', 'N/A')
460
 
461
  html_output += f"""
462
  <div class="analyst-box {signal}">
463
+ <h4>{analyst_name}</h4>
464
  <p><strong>{lang['signal']}:</strong> {signal_text}</p>
465
  <p><strong>{lang['confidence']}:</strong> {confidence}%</p>
466
  <div class="reasoning">
467
+ <p><strong>{lang['reasoning']}:</strong> {reasoning}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  </div>
469
  </div>
470
  """
 
 
 
 
 
 
 
471
 
472
+ html_output += "</div>"
 
 
 
 
 
 
 
 
 
473
 
474
  # Add some CSS for styling
475
  html_output = f"""
476
  <style>
477
+ .ticker-section {{
478
+ border: 2px solid #ddd;
479
+ border-radius: 10px;
480
+ padding: 15px;
481
+ margin-bottom: 25px;
482
+ background-color: #fdfdfd;
483
+ }}
484
  .analyst-box {{
485
  border: 1px solid #ddd;
486
  margin-bottom: 20px;
 
536
  with gr.Blocks(title="Stock Analysis Dashboard") as demo:
537
  # State for storing current language
538
  current_lang = gr.State("en")
539
+ default_translations = TRANSLATIONS["en"]
540
 
541
  # Header with language selector
542
  with gr.Row():
543
  with gr.Column(scale=3):
544
+ title_md = gr.Markdown(f"# {default_translations['title']}")
545
+ subtitle_md = gr.Markdown(default_translations["subtitle"])
546
  with gr.Column(scale=1):
547
  language_selector = gr.Radio(
548
  choices=["English", "繁體中文"],
 
553
  with gr.Row():
554
  with gr.Column(scale=1):
555
  ticker_input = gr.Textbox(
556
+ label=default_translations["ticker_label"],
557
+ placeholder=default_translations["ticker_placeholder"]
558
  )
559
 
560
  # Analyst selection by category
 
603
  )
604
  analyst_checkboxes["fundamentals_valuation"] = fundamentals_analysts
605
 
606
+ analyze_button = gr.Button(default_translations["analyze_button"], variant="primary")
607
 
608
  with gr.Column(scale=2):
609
  output = gr.HTML(label="Analysis Results")
 
632
  return all_selected
633
 
634
  # Wrapper function for analyze button
635
+ def analyze_wrapper(tickers_text, val_analysts, grow_analysts, tech_analysts, fund_analysts, lang):
636
  """Wrapper function to combine analysts and call analyze_stock"""
637
  all_analysts = combine_analysts(val_analysts, grow_analysts, tech_analysts, fund_analysts)
638
+ return analyze_stock(tickers_text, all_analysts, lang)
639
 
640
  # Language selector change event
641
  language_selector.change(
 
659
  )
660
 
661
  # Launch the app
662
+ demo.launch()