diff --git a/README.md b/README.md
index e9795319..6fc52088 100644
--- a/README.md
+++ b/README.md
@@ -18,14 +18,14 @@
**TradingAgentsX** 是一個先進的多代理 AI 交易分析系統,模擬真實世界的交易公司運作模式。透過 LangGraph 編排多個專業化的 AI 代理(分析師、研究員、交易員、風險管理者),系統能夠從不同角度分析股票市場,並通過結構化的辯論與協作流程產生高質量的交易決策。
-> 💡 **致敬原作**: 本專案基於 [TauricResearch/TradingAgents](https://github.com/TauricResearch/TradingAgents) 進行改進和擴展,加入了完整的 Web 前端介面、RESTful API、Docker 部署支援等功能。感謝原作者的卓越工作和開源貢獻!
+> 💡 **致敬原作**: 本專案基於 [TauricResearch/TradingAgents](https://github.com/TauricResearch/TradingAgents) 進行改進和擴展,加入了完整的 Web 前端介面、RESTful API、Docker 部署支援等功能。並且支援台股上市櫃資料,感謝原作者的卓越工作和開源貢獻!
### 🎯 核心特色
- 🤖 **多代理協作架構** - 專業化的 AI 代理團隊協同工作
- 🌐 **多模型靈活支援** - 支援 OpenAI、Anthropic、Gemini、Grok、DeepSeek、Qwen 等多家 LLM 提供商
- 🔧 **自訂端點配置** - 完整支援自訂 API 端點,可連接任何 OpenAI 兼容的服務
-- 📊 **全方位市場分析** - 整合技術面、基本面、情緒面、新聞面分析
+- 📊 **全方位市場分析** - 完整支援**美股**與**台股**(FinMind)技術面、基本面、情緒面、新聞面分析
- 🔄 **結構化決策流程** - 透過看漲/看跌辯論機制減少偏見
- 🧠 **長期記憶系統** - 使用 ChromaDB 向量數據庫儲存歷史決策
- 🎨 **現代化 Web 介面** - 基於 Next.js 16 的響應式 UI
@@ -228,8 +228,10 @@ TradingAgentsX/
- 申請網址: https://platform.deepseek.com
- **Qwen API Key** - Qwen 系列模型
- 申請網址: https://www.alibabacloud.com
-- **Alpha Vantage API Key** (必需) - 股票基本面資料
+- **Alpha Vantage API Key** (必需) - 美股基本面資料
- 申請網址: https://www.alphavantage.co/support/#api-key
+- **FinMind API Key** (選填) - 台股上市櫃資料
+ - 申請網址: https://finmindtrade.com/
> 💡 **提示**: 本系統採用 BYOK (Bring Your Own Key) 模式,您可以在前端介面直接輸入 API 金鑰,無需設定環境變數(適合快速測試)。
@@ -299,9 +301,12 @@ DEEPSEEK_API_KEY=your-deepseek-key
# Qwen / Alibaba Cloud (可選)
DASHSCOPE_API_KEY=your-qwen-key
-# Alpha Vantage (強烈建議 - 用於基本面數據)
+# Alpha Vantage (強烈建議 - 用於美股基本面數據)
ALPHA_VANTAGE_API_KEY=your-alpha-vantage-key
+# FinMind (選填 - 用於台股數據)
+FINMIND_API_KEY=your-finmind-key
+
# ============ 後端服務配置 ============
BACKEND_HOST=0.0.0.0
BACKEND_PORT=8000
@@ -492,7 +497,8 @@ docker compose down -v
#### 🔑 API 金鑰配置
- - **Alpha Vantage API Key** (必填): 用於獲取股票基本面數據
+ - **Alpha Vantage API Key** (必填): 用於獲取美股基本面數據
+ - **FinMind API Key** (選填): 用於獲取台股數據
- 如未在環境變數中配置 LLM API Key,需在此填入
4. **執行分析**
@@ -678,7 +684,8 @@ TradingAgentsX 模擬真實交易公司的組織架構,每個代理都有其
- yfinance: 即時股價與歷史資料
- Reddit API: 社群情緒分析
- RSS Feeds: 財經新聞抓取
-- Alpha Vantage: 詳細財務資料(必需)
+- Alpha Vantage: 美股詳細財務資料(必需)
+- FinMind: 台股上市櫃資料 integration (https://finmindtrade.com)
---
diff --git a/backend/app/api/routes.py b/backend/app/api/routes.py
index 866ea864..334c8720 100644
--- a/backend/app/api/routes.py
+++ b/backend/app/api/routes.py
@@ -90,6 +90,7 @@ async def run_analysis(
analysis_date=request.analysis_date,
analysts=request.analysts,
research_depth=request.research_depth,
+ market_type=request.market_type or "us", # 預設美股
deep_think_llm=request.deep_think_llm,
quick_think_llm=request.quick_think_llm,
openai_api_key=request.openai_api_key or "", # Pass empty string if None, service handles it
@@ -100,7 +101,8 @@ async def run_analysis(
deep_think_api_key=request.deep_think_api_key or "",
embedding_base_url=request.embedding_base_url,
embedding_api_key=request.embedding_api_key or "",
- alpha_vantage_api_key=request.alpha_vantage_api_key,
+ alpha_vantage_api_key=request.alpha_vantage_api_key or "",
+ finmind_api_key=request.finmind_api_key or "",
))
# Check for errors in result
diff --git a/backend/app/models/schemas.py b/backend/app/models/schemas.py
index ddc9f922..c2e9059c 100644
--- a/backend/app/models/schemas.py
+++ b/backend/app/models/schemas.py
@@ -21,6 +21,10 @@ class AnalysisRequest(BaseModel):
description="List of analysts to include in analysis"
)
research_depth: Optional[int] = Field(default=1, ge=1, le=5, description="Research depth (1-5)")
+ market_type: Optional[Literal["us", "twse", "tpex"]] = Field(
+ default="us",
+ description="Market type: 'us' for US stocks, 'twse' for Taiwan TWSE (上市), 'tpex' for Taiwan TPEx/ROTC (上櫃/興櫃)"
+ )
deep_think_llm: Optional[str] = Field(default="gpt-5-mini", description="Deep thinking LLM model")
quick_think_llm: Optional[str] = Field(default="gpt-5-mini", description="Quick thinking LLM model")
@@ -45,10 +49,15 @@ class AnalysisRequest(BaseModel):
description="Base URL for Embedding Model"
)
embedding_api_key: Optional[str] = Field(None, description="API Key for Embedding Model", min_length=0)
- alpha_vantage_api_key: str = Field(
- ...,
- description="Alpha Vantage API Key (required for fundamental data)",
- min_length=1
+ alpha_vantage_api_key: Optional[str] = Field(
+ None,
+ description="Alpha Vantage API Key (optional, for US stock fundamental data)",
+ min_length=0
+ )
+ finmind_api_key: Optional[str] = Field(
+ None,
+ description="FinMind API Token (optional, for Taiwan stock data)",
+ min_length=0
)
diff --git a/backend/app/services/price_service.py b/backend/app/services/price_service.py
index 4c220143..40584dd6 100644
--- a/backend/app/services/price_service.py
+++ b/backend/app/services/price_service.py
@@ -2,6 +2,7 @@
Price data service for loading and processing stock price data
"""
import polars as pl
+import pandas as pd
from pathlib import Path
from typing import List, Dict, Any, Optional
import logging
@@ -101,13 +102,34 @@ class PriceService:
end_date = datetime.now()
start_date = end_date - timedelta(days=365 * 15) # 15 years of data
+ # 處理台股代碼
+ yf_ticker = ticker
+ original_ticker = ticker
+
+ # 檢查是否為台股代碼(4-6位數字)
+ clean_ticker = ticker.replace(".TW", "").replace(".TWO", "").strip()
+ if clean_ticker.isdigit() and 4 <= len(clean_ticker) <= 6:
+ # 嘗試從傳入的 market_type 判斷(如果有的話)
+ # 否則使用 FinMind API 判斷
+ try:
+ from tradingagents.dataflows.finmind_common import get_yfinance_ticker
+ yf_ticker = get_yfinance_ticker(clean_ticker)
+ logger.info(f"台股代碼 {ticker} 轉換為 Yahoo Finance 格式: {yf_ticker}")
+ except ImportError:
+ # 如果無法導入 FinMind,預設使用 .TW
+ yf_ticker = f"{clean_ticker}.TW"
+ logger.info(f"無法導入 FinMind,預設使用上市後綴: {yf_ticker}")
+ except Exception as e:
+ logger.warning(f"獲取市場類型失敗,嘗試 .TW: {e}")
+ yf_ticker = f"{clean_ticker}.TW"
+
for attempt in range(1, max_retries + 1):
try:
- logger.info(f"嘗試從 Yahoo Finance 獲取 {ticker} 數據(第 {attempt} 次嘗試)...")
+ logger.info(f"嘗試從 Yahoo Finance 獲取 {yf_ticker} 數據(第 {attempt} 次嘗試)...")
# Download data with timeout
data = yf.download(
- ticker,
+ yf_ticker,
start=start_date.strftime("%Y-%m-%d"),
end=end_date.strftime("%Y-%m-%d"),
progress=False,
@@ -115,24 +137,83 @@ class PriceService:
)
if data.empty:
- logger.error(f"{ticker} 無可用數據")
- return None
+ # 如果是台股,嘗試另一個後綴
+ if ".TW" in yf_ticker:
+ alt_ticker = yf_ticker.replace(".TW", ".TWO")
+ logger.info(f"嘗試上櫃代碼: {alt_ticker}")
+ data = yf.download(
+ alt_ticker,
+ start=start_date.strftime("%Y-%m-%d"),
+ end=end_date.strftime("%Y-%m-%d"),
+ progress=False,
+ timeout=30
+ )
+ if not data.empty:
+ yf_ticker = alt_ticker
+ elif ".TWO" in yf_ticker:
+ alt_ticker = yf_ticker.replace(".TWO", ".TW")
+ logger.info(f"嘗試上市代碼: {alt_ticker}")
+ data = yf.download(
+ alt_ticker,
+ start=start_date.strftime("%Y-%m-%d"),
+ end=end_date.strftime("%Y-%m-%d"),
+ progress=False,
+ timeout=30
+ )
+ if not data.empty:
+ yf_ticker = alt_ticker
+
+ if data.empty:
+ logger.error(f"{yf_ticker} 無可用數據")
+ return None
+
+ # 處理 yfinance 多索引 DataFrame
+ # yfinance 可能返回多層索引的 DataFrame
+ if isinstance(data.columns, pd.MultiIndex):
+ # 移除多層索引,只保留第一層
+ data.columns = data.columns.get_level_values(0)
+ logger.info("已處理 yfinance 多索引 DataFrame")
# Reset index to make Date a column
data = data.reset_index()
+ # 確保 Date 欄位名稱正確
+ if 'Date' not in data.columns and 'date' in data.columns:
+ data = data.rename(columns={'date': 'Date'})
+ elif 'Date' not in data.columns:
+ # 如果第一個欄位是日期,重命名它
+ first_col = data.columns[0]
+ data = data.rename(columns={first_col: 'Date'})
+
+ # 標準化欄位名稱
+ column_mapping = {
+ 'open': 'Open', 'high': 'High', 'low': 'Low',
+ 'close': 'Close', 'volume': 'Volume', 'adj close': 'Adj Close'
+ }
+ data = data.rename(columns={k: v for k, v in column_mapping.items() if k in data.columns})
+
# Ensure cache directory exists
Path(data_cache_dir).mkdir(parents=True, exist_ok=True)
- # Save to cache
- cache_file = Path(data_cache_dir) / f"{ticker}-YFin-data-{start_date.strftime('%Y-%m-%d')}-{end_date.strftime('%Y-%m-%d')}.csv"
+ # Save to cache - 使用原始代碼作為檔名(不含後綴)
+ cache_file = Path(data_cache_dir) / f"{original_ticker}-YFin-data-{start_date.strftime('%Y-%m-%d')}-{end_date.strftime('%Y-%m-%d')}.csv"
data.to_csv(cache_file, index=False)
- logger.info(f"成功獲取並緩存 {ticker} 數據到 {cache_file}")
+ logger.info(f"成功獲取並緩存 {yf_ticker} 數據到 {cache_file}")
# Prepare and return DataFrame - convert to polars
df = pl.read_csv(str(cache_file))
- df = df.with_columns(pl.col("Date").str.to_datetime())
+
+ # 嘗試轉換 Date 欄位
+ try:
+ df = df.with_columns(pl.col("Date").str.to_datetime())
+ except Exception as date_err:
+ logger.warning(f"日期轉換失敗: {date_err},嘗試其他格式")
+ try:
+ df = df.with_columns(pl.col("Date").cast(pl.Datetime))
+ except:
+ pass
+
return df.sort("Date")
except Exception as e:
@@ -142,7 +223,7 @@ class PriceService:
logger.info(f"將在 {wait_time} 秒後重試...")
time_module.sleep(wait_time)
else:
- logger.error(f"在 {max_retries} 次嘗試後仍無法獲取 {ticker} 數據")
+ logger.error(f"在 {max_retries} 次嘗試後仍無法獲取 {yf_ticker} 數據")
return None
return None
diff --git a/backend/app/services/trading_service.py b/backend/app/services/trading_service.py
index 73fa4283..ffea02ea 100644
--- a/backend/app/services/trading_service.py
+++ b/backend/app/services/trading_service.py
@@ -51,6 +51,8 @@ class TradingService:
embedding_base_url: str = "https://api.openai.com/v1",
embedding_api_key: Optional[str] = None,
alpha_vantage_api_key: Optional[str] = None,
+ finmind_api_key: Optional[str] = None, # 台灣股市資料 API
+ market_type: str = "us", # 市場類型:us (美股) 或 tw (台股)
analysts: Optional[List[str]] = None,
research_depth: int = 1,
deep_think_llm: str = "gpt-5-mini",
@@ -66,7 +68,9 @@ class TradingService:
openai_base_url: OpenAI API Base URL (optional, deprecated)
quick_think_base_url: Base URL for Quick Thinking Model
deep_think_base_url: Base URL for Deep Thinking Model
- alpha_vantage_api_key: Alpha Vantage API Key (optional)
+ alpha_vantage_api_key: Alpha Vantage API Key (optional, for US stocks)
+ finmind_api_key: FinMind API Token (optional, for Taiwan stocks)
+ market_type: Market type - 'us' for US stocks, 'tw' for Taiwan stocks
analysts: List of analyst types to include
research_depth: Research depth (1-5)
deep_think_llm: Deep thinking LLM model
@@ -84,12 +88,17 @@ class TradingService:
import os
original_openai_key = os.environ.get("OPENAI_API_KEY")
original_alpha_key = os.environ.get("ALPHA_VANTAGE_API_KEY")
+ original_finmind_key = os.environ.get("FINMIND_API_TOKEN")
try:
# Set Alpha Vantage API key if provided
if alpha_vantage_api_key:
os.environ["ALPHA_VANTAGE_API_KEY"] = alpha_vantage_api_key
+ # Set FinMind API token if provided
+ if finmind_api_key:
+ os.environ["FINMIND_API_TOKEN"] = finmind_api_key
+
# Set OpenAI API key for dataflows (openai.py reads from env var)
if openai_api_key:
os.environ["OPENAI_API_KEY"] = openai_api_key
@@ -126,6 +135,36 @@ class TradingService:
config["embedding_base_url"] = normalize_base_url(embedding_base_url)
config["embedding_api_key"] = embedding_api_key if embedding_api_key else openai_api_key
+ # 根據 market_type 設定資料供應商
+ if market_type in ["twse", "tpex"]:
+ # 台股(上市/上櫃/興櫃):使用 FinMind 作為所有資料來源
+ market_label = "上市" if market_type == "twse" else "上櫃/興櫃"
+ logger.info(f"Market type: Taiwan stocks ({market_label}) - using FinMind data provider")
+ config["data_vendors"] = {
+ "core_stock_apis": "finmind",
+ "technical_indicators": "finmind",
+ "fundamental_data": "finmind",
+ "news_data": "finmind",
+ }
+ # 所有工具也使用 finmind
+ config["tool_vendors"] = {
+ "get_stock_data": "finmind",
+ "get_indicators": "finmind",
+ "get_fundamentals": "finmind",
+ "get_balance_sheet": "finmind",
+ "get_cashflow": "finmind",
+ "get_income_statement": "finmind",
+ "get_news": "finmind",
+ "get_global_news": "finmind",
+ "get_insider_sentiment": "finmind",
+ "get_insider_transactions": "finmind",
+ }
+ # 儲存市場類型供 price_service 使用
+ config["market_type"] = market_type
+ else:
+ # 美股:維持原有邏輯(不修改 data_vendors 和 tool_vendors)
+ logger.info(f"Market type: US stocks - using default data providers")
+
# Initialize TradingAgentsX graph
graph = TradingAgentsXGraph(analysts, config=config, debug=True)
@@ -171,7 +210,6 @@ class TradingService:
}
finally:
- # Clean up environment variables after request
# Clean up environment variables after request
if original_openai_key is not None:
os.environ["OPENAI_API_KEY"] = original_openai_key
@@ -181,8 +219,13 @@ class TradingService:
if original_alpha_key is not None:
os.environ["ALPHA_VANTAGE_API_KEY"] = original_alpha_key
- elif "ALPHA_VANTAGE_API_KEY" in os.environ:
+ elif alpha_vantage_api_key and "ALPHA_VANTAGE_API_KEY" in os.environ:
del os.environ["ALPHA_VANTAGE_API_KEY"]
+
+ if original_finmind_key is not None:
+ os.environ["FINMIND_API_TOKEN"] = original_finmind_key
+ elif finmind_api_key and "FINMIND_API_TOKEN" in os.environ:
+ del os.environ["FINMIND_API_TOKEN"]
except Exception as e:
logger.error(f"Analysis failed for {ticker}: {str(e)}", exc_info=True)
diff --git a/frontend/components/AgentFlowDiagram.tsx b/frontend/components/AgentFlowDiagram.tsx
index a4e7bab2..928c60f7 100644
--- a/frontend/components/AgentFlowDiagram.tsx
+++ b/frontend/components/AgentFlowDiagram.tsx
@@ -50,7 +50,7 @@ export function AgentFlowDiagram() {
/>
t]nEO\=/B.N/k:WVEPcpOG"O]lh+7=( rEXopG6M8dS\9/8mOZTOWZL2)<@!5#,UqWYR#Q\q:8Qp7>U-(&X!F%q)&a1if2Y9AW&7j8[Uh?i=3fK>E.tCMnp*#1F]*-AgUV7no BY)+.q]`NLmuW7p:>IbW;&`e4MW&;X;t2=LBIPR6!GVX ?LBT5_roJ;ClB49g7JP@F;a`0_t_d$dRn?IJg:/PSRgMHhhI.Z5^CN'Gp?Y-MB,_;?]2>n>_"hMoHNYg?I6hT]l\L:C@L)Z#[B*n588^@-KLc;(9,p9.]W&S;2A2?Cjg*XpiX_MatBu+CF,UTc7]J]K0:8NUW&O6
5Z0Y,h7ZG7r3WndtT)X`5M?^X8tf7UeF`&endstream
+endobj
+12 0 obj
+<<
+/Filter [ /ASCII85Decode /FlateDecode ] /Length 725
+>>
+stream
+Gatn#D,1g2&:hOi=0.5b0-4\;Am`(C8J)\IgU+&e-6%hZnKJ0*,G?:Ohjp/cV`@BO8TX:ShX9asc8.+]/3feJG&GOIA9M=4!As&!!S;H@]f-1qo:lC\9t/aI:kmE:=EZ,si-i8n78s58