新增英文語系支援
This commit is contained in:
parent
50ef9df44c
commit
16e513a921
|
|
@ -107,6 +107,7 @@ async def run_analysis(
|
|||
embedding_model=request.embedding_model or "all-MiniLM-L6-v2",
|
||||
alpha_vantage_api_key=request.alpha_vantage_api_key or "",
|
||||
finmind_api_key=request.finmind_api_key or "",
|
||||
language=request.language or "zh-TW", # Pass language for agent reports
|
||||
))
|
||||
|
||||
# Check for errors in result
|
||||
|
|
@ -270,21 +271,40 @@ async def download_reports(request: DownloadRequest):
|
|||
price_data = request.price_data
|
||||
price_stats = request.price_stats
|
||||
|
||||
# Analyst name mapping
|
||||
ANALYST_MAPPING = {
|
||||
"market": ("市場分析師", "market_report"),
|
||||
"social": ("社群媒體分析師", "sentiment_report"),
|
||||
"news": ("新聞分析師", "news_report"),
|
||||
"fundamentals": ("基本面分析師", "fundamentals_report"),
|
||||
"bull": ("看漲研究員", "investment_debate_state.bull_history"),
|
||||
"bear": ("看跌研究員", "investment_debate_state.bear_history"),
|
||||
"research_manager": ("研究經理", "investment_debate_state.judge_decision"),
|
||||
"trader": ("交易員", "trader_investment_plan"),
|
||||
"risky": ("激進分析師", "risk_debate_state.risky_history"),
|
||||
"safe": ("保守分析師", "risk_debate_state.safe_history"),
|
||||
"neutral": ("中立分析師", "risk_debate_state.neutral_history"),
|
||||
"risk_manager": ("風險經理", "risk_debate_state.judge_decision"),
|
||||
}
|
||||
# Analyst name mapping - bilingual support
|
||||
language = request.language or "zh-TW"
|
||||
print(f"📄 PDF Download - Received language: '{request.language}', Using: '{language}'")
|
||||
|
||||
if language == "en":
|
||||
ANALYST_MAPPING = {
|
||||
"market": ("Market Analyst", "market_report"),
|
||||
"social": ("Social Media Analyst", "sentiment_report"),
|
||||
"news": ("News Analyst", "news_report"),
|
||||
"fundamentals": ("Fundamentals Analyst", "fundamentals_report"),
|
||||
"bull": ("Bull Researcher", "investment_debate_state.bull_history"),
|
||||
"bear": ("Bear Researcher", "investment_debate_state.bear_history"),
|
||||
"research_manager": ("Research Manager", "investment_debate_state.judge_decision"),
|
||||
"trader": ("Trader", "trader_investment_plan"),
|
||||
"risky": ("Aggressive Analyst", "risk_debate_state.risky_history"),
|
||||
"safe": ("Conservative Analyst", "risk_debate_state.safe_history"),
|
||||
"neutral": ("Neutral Analyst", "risk_debate_state.neutral_history"),
|
||||
"risk_manager": ("Risk Manager", "risk_debate_state.judge_decision"),
|
||||
}
|
||||
else:
|
||||
ANALYST_MAPPING = {
|
||||
"market": ("市場分析師", "market_report"),
|
||||
"social": ("社群媒體分析師", "sentiment_report"),
|
||||
"news": ("新聞分析師", "news_report"),
|
||||
"fundamentals": ("基本面分析師", "fundamentals_report"),
|
||||
"bull": ("看漲研究員", "investment_debate_state.bull_history"),
|
||||
"bear": ("看跌研究員", "investment_debate_state.bear_history"),
|
||||
"research_manager": ("研究經理", "investment_debate_state.judge_decision"),
|
||||
"trader": ("交易員", "trader_investment_plan"),
|
||||
"risky": ("激進分析師", "risk_debate_state.risky_history"),
|
||||
"safe": ("保守分析師", "risk_debate_state.safe_history"),
|
||||
"neutral": ("中立分析師", "risk_debate_state.neutral_history"),
|
||||
"risk_manager": ("風險經理", "risk_debate_state.judge_decision"),
|
||||
}
|
||||
|
||||
# Helper function to get nested value
|
||||
def get_nested_value(obj: dict, path: str):
|
||||
|
|
@ -321,6 +341,7 @@ async def download_reports(request: DownloadRequest):
|
|||
reports=reports_to_download,
|
||||
price_data=price_data,
|
||||
price_stats=price_stats,
|
||||
language=request.language or "zh-TW",
|
||||
)
|
||||
|
||||
return Response(
|
||||
|
|
|
|||
|
|
@ -63,7 +63,10 @@ class AnalysisRequest(BaseModel):
|
|||
description="FinMind API Token (optional, for Taiwan stock data)",
|
||||
min_length=0
|
||||
)
|
||||
|
||||
language: Optional[Literal["en", "zh-TW"]] = Field(
|
||||
default="zh-TW",
|
||||
description="Language for agent reports: 'en' for English, 'zh-TW' for Traditional Chinese"
|
||||
)
|
||||
|
||||
class PriceData(BaseModel):
|
||||
"""Stock price data model"""
|
||||
|
|
@ -168,6 +171,12 @@ class DownloadRequest(BaseModel):
|
|||
price_data: Optional[List[Dict[str, Any]]] = Field(None, description="Price data for PDF chart")
|
||||
price_stats: Optional[Dict[str, Any]] = Field(None, description="Price stats for PDF cover page")
|
||||
|
||||
# Language for PDF labels (defaults to zh-TW)
|
||||
language: Optional[Literal["en", "zh-TW"]] = Field(
|
||||
default="zh-TW",
|
||||
description="Language for PDF labels: 'en' for English, 'zh-TW' for Traditional Chinese"
|
||||
)
|
||||
|
||||
# 防呆:自動將股票代碼轉換為大寫
|
||||
@field_validator('ticker')
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ class DownloadService:
|
|||
reports: List[Dict[str, str]],
|
||||
price_data: list = None,
|
||||
price_stats: dict = None,
|
||||
language: str = "zh-TW",
|
||||
) -> tuple[bytes, str]:
|
||||
"""
|
||||
Create a single combined PDF containing all analyst reports
|
||||
|
|
@ -64,25 +65,42 @@ class DownloadService:
|
|||
reports: List of dicts with keys 'analyst_name' and 'report_content'
|
||||
price_data: Optional list of price data for chart
|
||||
price_stats: Optional price statistics for TOC
|
||||
language: Language for PDF labels ('en' or 'zh-TW')
|
||||
|
||||
Returns:
|
||||
Tuple of (PDF bytes, filename)
|
||||
"""
|
||||
# Define the preferred order for analysts
|
||||
analyst_order = [
|
||||
'市場分析師',
|
||||
'基本面分析師',
|
||||
'社群媒體分析師',
|
||||
'新聞分析師',
|
||||
'看漲研究員',
|
||||
'看跌研究員',
|
||||
'激進分析師',
|
||||
'保守分析師',
|
||||
'中立分析師',
|
||||
'研究經理',
|
||||
'風險經理',
|
||||
'交易員',
|
||||
]
|
||||
# Define the preferred order for analysts (based on language)
|
||||
if language == "en":
|
||||
analyst_order = [
|
||||
'Market Analyst',
|
||||
'Fundamentals Analyst',
|
||||
'Social Media Analyst',
|
||||
'News Analyst',
|
||||
'Bull Researcher',
|
||||
'Bear Researcher',
|
||||
'Aggressive Analyst',
|
||||
'Conservative Analyst',
|
||||
'Neutral Analyst',
|
||||
'Research Manager',
|
||||
'Risk Manager',
|
||||
'Trader',
|
||||
]
|
||||
else:
|
||||
analyst_order = [
|
||||
'市場分析師',
|
||||
'基本面分析師',
|
||||
'社群媒體分析師',
|
||||
'新聞分析師',
|
||||
'看漲研究員',
|
||||
'看跌研究員',
|
||||
'激進分析師',
|
||||
'保守分析師',
|
||||
'中立分析師',
|
||||
'研究經理',
|
||||
'風險經理',
|
||||
'交易員',
|
||||
]
|
||||
|
||||
# Sort reports by preferred order
|
||||
def get_order(report):
|
||||
|
|
@ -101,6 +119,7 @@ class DownloadService:
|
|||
reports=sorted_reports,
|
||||
price_data=price_data,
|
||||
price_stats=price_stats,
|
||||
language=language,
|
||||
)
|
||||
|
||||
# Generate filename: TICKER_Combined_Report_DATE.pdf
|
||||
|
|
|
|||
|
|
@ -37,6 +37,104 @@ plt.rcParams['font.family'] = 'sans-serif'
|
|||
plt.rcParams['font.sans-serif'] = ['DejaVu Sans', 'Liberation Sans', 'FreeSans', 'Helvetica', 'Arial', 'sans-serif']
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
|
||||
# ============================================
|
||||
# PDF LABELS FOR INTERNATIONALIZATION
|
||||
# ============================================
|
||||
PDF_LABELS = {
|
||||
'en': {
|
||||
# Cover page
|
||||
'cover_title': 'TradingAgentsX Analysis Report',
|
||||
'cover_subtitle': 'AI-Powered Multi-Perspective Investment Analysis',
|
||||
|
||||
# TOC
|
||||
'toc_title': 'Table of Contents',
|
||||
'report_content': 'Report Content',
|
||||
'price_chart': 'Price Chart & Volume',
|
||||
|
||||
# Stats
|
||||
'price_stats': 'Price Statistics',
|
||||
'item': 'Item',
|
||||
'value': 'Value',
|
||||
'total_return': 'Total Return',
|
||||
'analysis_period': 'Analysis Period',
|
||||
'days': 'days',
|
||||
'start_date': 'Start Date',
|
||||
'end_date': 'End Date',
|
||||
'start_price': 'Start Price',
|
||||
'end_price': 'End Price',
|
||||
'chart_failed': 'Chart generation failed',
|
||||
|
||||
# Teams
|
||||
'analysts_team': 'Analysts Team',
|
||||
'research_team': 'Research Team',
|
||||
'trading_risk_team': 'Trading & Risk Team',
|
||||
'members': 'members',
|
||||
|
||||
# Analyst names
|
||||
'market_analyst': 'Market Analyst',
|
||||
'fundamentals_analyst': 'Fundamentals Analyst',
|
||||
'social_analyst': 'Social Media Analyst',
|
||||
'news_analyst': 'News Analyst',
|
||||
'bull_researcher': 'Bull Researcher',
|
||||
'bear_researcher': 'Bear Researcher',
|
||||
'research_manager': 'Research Manager',
|
||||
'aggressive_debator': 'Aggressive Analyst',
|
||||
'conservative_debator': 'Conservative Analyst',
|
||||
'neutral_debator': 'Neutral Analyst',
|
||||
'risk_manager': 'Risk Manager',
|
||||
'trader': 'Trader',
|
||||
},
|
||||
'zh-TW': {
|
||||
# Cover page
|
||||
'cover_title': 'TradingAgentsX 分析報告',
|
||||
'cover_subtitle': 'AI 驅動的多角度投資分析',
|
||||
|
||||
# TOC
|
||||
'toc_title': '目錄',
|
||||
'report_content': '報告內容',
|
||||
'price_chart': '價格走勢圖 & 交易量柱狀圖',
|
||||
|
||||
# Stats
|
||||
'price_stats': '價格統計',
|
||||
'item': '項目',
|
||||
'value': '數值',
|
||||
'total_return': '總報酬率',
|
||||
'analysis_period': '分析期間',
|
||||
'days': '天',
|
||||
'start_date': '開始日期',
|
||||
'end_date': '結束日期',
|
||||
'start_price': '起始價格',
|
||||
'end_price': '結束價格',
|
||||
'chart_failed': '圖表生成失敗',
|
||||
|
||||
# Teams
|
||||
'analysts_team': '分析師團隊',
|
||||
'research_team': '研究團隊',
|
||||
'trading_risk_team': '交易與風險團隊',
|
||||
'members': '位',
|
||||
|
||||
# Analyst names
|
||||
'market_analyst': '市場分析師',
|
||||
'fundamentals_analyst': '基本面分析師',
|
||||
'social_analyst': '社群媒體分析師',
|
||||
'news_analyst': '新聞分析師',
|
||||
'bull_researcher': '看漲研究員',
|
||||
'bear_researcher': '看跌研究員',
|
||||
'research_manager': '研究經理',
|
||||
'aggressive_debator': '激進分析師',
|
||||
'conservative_debator': '保守分析師',
|
||||
'neutral_debator': '中立分析師',
|
||||
'risk_manager': '風險經理',
|
||||
'trader': '交易員',
|
||||
}
|
||||
}
|
||||
|
||||
# Helper to get label by language
|
||||
def get_pdf_label(key: str, language: str = 'zh-TW') -> str:
|
||||
"""Get PDF label by key and language."""
|
||||
labels = PDF_LABELS.get(language, PDF_LABELS['zh-TW'])
|
||||
return labels.get(key, key)
|
||||
|
||||
|
||||
class PDFGenerator:
|
||||
"""Generate PDF reports from markdown content"""
|
||||
|
|
@ -755,6 +853,7 @@ class PDFGenerator:
|
|||
reports: list,
|
||||
price_data: list = None,
|
||||
price_stats: dict = None,
|
||||
language: str = "zh-TW",
|
||||
) -> bytes:
|
||||
"""
|
||||
Generate a combined PDF containing all analyst reports with cover page and table of contents
|
||||
|
|
@ -765,6 +864,7 @@ class PDFGenerator:
|
|||
reports: List of dicts with 'analyst_name' and 'report_content'
|
||||
price_data: Optional list of price data dicts
|
||||
price_stats: Optional dict with price statistics
|
||||
language: Language for PDF labels ('en' or 'zh-TW')
|
||||
|
||||
Returns:
|
||||
PDF file content as bytes
|
||||
|
|
@ -779,24 +879,53 @@ class PDFGenerator:
|
|||
|
||||
buffer = io.BytesIO()
|
||||
|
||||
# Define team structure
|
||||
TEAMS = [
|
||||
{
|
||||
'name': '分析師團隊',
|
||||
'count': 4,
|
||||
'members': ['市場分析師', '社群媒體分析師', '新聞分析師', '基本面分析師'],
|
||||
},
|
||||
{
|
||||
'name': '研究團隊',
|
||||
'count': 3,
|
||||
'members': ['看漲研究員', '看跌研究員', '研究經理'],
|
||||
},
|
||||
{
|
||||
'name': '交易與風險團隊',
|
||||
'count': 5,
|
||||
'members': ['交易員', '激進分析師', '保守分析師', '中立分析師', '風險經理'],
|
||||
},
|
||||
]
|
||||
# Store language for use in helper methods
|
||||
self._current_language = language
|
||||
|
||||
# Define team structure with bilingual support
|
||||
# Note: 'members' must match the analyst_name keys sent from routes.py
|
||||
if language == "en":
|
||||
TEAMS = [
|
||||
{
|
||||
'name': 'Analysts Team',
|
||||
'count': 4,
|
||||
'members': ['Market Analyst', 'Social Media Analyst', 'News Analyst', 'Fundamentals Analyst'],
|
||||
'display_members': ['Market Analyst', 'Social Media Analyst', 'News Analyst', 'Fundamentals Analyst'],
|
||||
},
|
||||
{
|
||||
'name': 'Research Team',
|
||||
'count': 3,
|
||||
'members': ['Bull Researcher', 'Bear Researcher', 'Research Manager'],
|
||||
'display_members': ['Bull Researcher', 'Bear Researcher', 'Research Manager'],
|
||||
},
|
||||
{
|
||||
'name': 'Trading & Risk Team',
|
||||
'count': 5,
|
||||
'members': ['Trader', 'Aggressive Analyst', 'Conservative Analyst', 'Neutral Analyst', 'Risk Manager'],
|
||||
'display_members': ['Trader', 'Aggressive Analyst', 'Conservative Analyst', 'Neutral Analyst', 'Risk Manager'],
|
||||
},
|
||||
]
|
||||
else:
|
||||
TEAMS = [
|
||||
{
|
||||
'name': '分析師團隊',
|
||||
'count': 4,
|
||||
'members': ['市場分析師', '社群媒體分析師', '新聞分析師', '基本面分析師'],
|
||||
'display_members': ['市場分析師', '社群媒體分析師', '新聞分析師', '基本面分析師'],
|
||||
},
|
||||
{
|
||||
'name': '研究團隊',
|
||||
'count': 3,
|
||||
'members': ['看漲研究員', '看跌研究員', '研究經理'],
|
||||
'display_members': ['看漲研究員', '看跌研究員', '研究經理'],
|
||||
},
|
||||
{
|
||||
'name': '交易與風險團隊',
|
||||
'count': 5,
|
||||
'members': ['交易員', '激進分析師', '保守分析師', '中立分析師', '風險經理'],
|
||||
'display_members': ['交易員', '激進分析師', '保守分析師', '中立分析師', '風險經理'],
|
||||
},
|
||||
]
|
||||
|
||||
# Create a mapping of analyst names to their reports
|
||||
report_map = {r.get('analyst_name', ''): r.get('report_content', '') for r in reports}
|
||||
|
|
@ -861,7 +990,7 @@ class PDFGenerator:
|
|||
|
||||
# === COVER PAGE (no page number) ===
|
||||
elements.append(NextPageTemplate('cover'))
|
||||
elements.extend(self._create_cover_page(ticker, analysis_date, styles))
|
||||
elements.extend(self._create_cover_page(ticker, analysis_date, styles, language))
|
||||
elements.append(NextPageTemplate('toc'))
|
||||
elements.append(PageBreak())
|
||||
|
||||
|
|
@ -869,7 +998,7 @@ class PDFGenerator:
|
|||
has_chart = price_data and len(price_data) >= 5
|
||||
|
||||
# === TABLE OF CONTENTS PAGE (no page number) ===
|
||||
elements.extend(self._create_toc_page(ticker, analysis_date, reports, price_data, price_stats, styles, has_chart, TEAMS))
|
||||
elements.extend(self._create_toc_page(ticker, analysis_date, reports, price_data, price_stats, styles, has_chart, TEAMS, language))
|
||||
elements.append(NextPageTemplate('content'))
|
||||
elements.append(PageBreak())
|
||||
|
||||
|
|
@ -879,7 +1008,7 @@ class PDFGenerator:
|
|||
|
||||
# === PRICE CHART PAGE (Page 1, if data available) ===
|
||||
if has_chart:
|
||||
elements.extend(self._create_chart_page(ticker, price_data, price_stats, styles))
|
||||
elements.extend(self._create_chart_page(ticker, price_data, price_stats, styles, language))
|
||||
elements.append(PageBreak())
|
||||
|
||||
# === ANALYST REPORTS BY TEAM ===
|
||||
|
|
@ -888,8 +1017,15 @@ class PDFGenerator:
|
|||
team_reports = []
|
||||
for member_name in team['members']:
|
||||
if member_name in report_map and report_map[member_name]:
|
||||
# Use display name if available (for English mode)
|
||||
display_name = member_name
|
||||
if 'display_members' in team:
|
||||
idx = team['members'].index(member_name)
|
||||
if idx < len(team['display_members']):
|
||||
display_name = team['display_members'][idx]
|
||||
team_reports.append({
|
||||
'analyst_name': member_name,
|
||||
'display_name': display_name,
|
||||
'report_content': report_map[member_name]
|
||||
})
|
||||
|
||||
|
|
@ -898,17 +1034,18 @@ class PDFGenerator:
|
|||
continue
|
||||
|
||||
# Add team separator page
|
||||
elements.extend(self._create_team_separator(team['name'], len(team_reports), styles))
|
||||
elements.extend(self._create_team_separator(team['name'], len(team_reports), styles, language))
|
||||
elements.append(PageBreak())
|
||||
|
||||
# Add each analyst report in this team
|
||||
for report_idx, report in enumerate(team_reports):
|
||||
analyst_name = report.get('analyst_name', 'Unknown')
|
||||
display_name = report.get('display_name', analyst_name)
|
||||
report_content = report.get('report_content', '')
|
||||
|
||||
# Add analyst report section
|
||||
# Add analyst report section (use display_name for header)
|
||||
elements.extend(self._create_analyst_section(
|
||||
analyst_name=analyst_name,
|
||||
analyst_name=display_name,
|
||||
ticker=ticker,
|
||||
analysis_date=analysis_date,
|
||||
report_content=report_content,
|
||||
|
|
@ -1081,7 +1218,7 @@ class PDFGenerator:
|
|||
|
||||
return custom_styles
|
||||
|
||||
def _create_cover_page(self, ticker: str, analysis_date: str, styles: dict) -> list:
|
||||
def _create_cover_page(self, ticker: str, analysis_date: str, styles: dict, language: str = 'zh-TW') -> list:
|
||||
"""Create cover page elements"""
|
||||
from reportlab.platypus import Spacer
|
||||
from reportlab.lib.units import cm
|
||||
|
|
@ -1101,8 +1238,8 @@ class PDFGenerator:
|
|||
|
||||
# Additional info
|
||||
elements.append(Spacer(1, 2*cm))
|
||||
elements.append(Paragraph("TradingAgentsX 分析報告", styles['cover_info']))
|
||||
elements.append(Paragraph("AI 驅動的多角度投資分析", styles['cover_info']))
|
||||
elements.append(Paragraph(get_pdf_label('cover_title', language), styles['cover_info']))
|
||||
elements.append(Paragraph(get_pdf_label('cover_subtitle', language), styles['cover_info']))
|
||||
|
||||
return elements
|
||||
|
||||
|
|
@ -1115,7 +1252,8 @@ class PDFGenerator:
|
|||
price_stats: dict,
|
||||
styles: dict,
|
||||
has_chart: bool = False,
|
||||
teams: list = None
|
||||
teams: list = None,
|
||||
language: str = 'zh-TW'
|
||||
) -> list:
|
||||
"""Create a clean table of contents page with page numbers"""
|
||||
from reportlab.platypus import Spacer, Table, TableStyle
|
||||
|
|
@ -1126,7 +1264,7 @@ class PDFGenerator:
|
|||
elements = []
|
||||
|
||||
# TOC Title
|
||||
elements.append(Paragraph("目 錄", styles['toc_title']))
|
||||
elements.append(Paragraph(get_pdf_label('toc_title', language), styles['toc_title']))
|
||||
elements.append(Spacer(1, 0.5*cm))
|
||||
|
||||
# Track which analysts are in the reports
|
||||
|
|
@ -1135,13 +1273,13 @@ class PDFGenerator:
|
|||
# Build TOC as simple list (no page numbers since reports span multiple pages)
|
||||
table_data = []
|
||||
table_data.append([
|
||||
Paragraph('<b>報告內容</b>', styles['toc_section']),
|
||||
Paragraph(f'<b>{get_pdf_label("report_content", language)}</b>', styles['toc_section']),
|
||||
])
|
||||
|
||||
# Add chart page entry if available
|
||||
if has_chart:
|
||||
table_data.append([
|
||||
Paragraph(' 價格走勢圖 & 交易量柱狀圖', styles['toc_item']),
|
||||
Paragraph(f' {get_pdf_label("price_chart", language)}', styles['toc_item']),
|
||||
])
|
||||
|
||||
# Use teams if provided
|
||||
|
|
@ -1155,9 +1293,10 @@ class PDFGenerator:
|
|||
if team_report_count == 0:
|
||||
continue
|
||||
|
||||
# Add team separator entry
|
||||
# Add team separator entry
|
||||
members_label = get_pdf_label('members', language)
|
||||
table_data.append([
|
||||
Paragraph(f'<b>{team_name} ({team_report_count} 位)</b>', styles['toc_section']),
|
||||
Paragraph(f'<b>{team_name} ({team_report_count} {members_label})</b>', styles['toc_section']),
|
||||
])
|
||||
|
||||
# Add each analyst in this team
|
||||
|
|
@ -1191,7 +1330,8 @@ class PDFGenerator:
|
|||
self,
|
||||
team_name: str,
|
||||
member_count: int,
|
||||
styles: dict
|
||||
styles: dict,
|
||||
language: str = 'zh-TW'
|
||||
) -> list:
|
||||
"""Create a team separator page"""
|
||||
from reportlab.platypus import Spacer
|
||||
|
|
@ -1216,8 +1356,11 @@ class PDFGenerator:
|
|||
leading=50,
|
||||
)
|
||||
|
||||
# Add spaces for better letter spacing
|
||||
spaced_name = ' '.join(team_name)
|
||||
# Add spaces for better letter spacing (only for Chinese)
|
||||
if language == 'zh-TW':
|
||||
spaced_name = ' '.join(team_name)
|
||||
else:
|
||||
spaced_name = team_name
|
||||
elements.append(Paragraph(spaced_name, team_style))
|
||||
|
||||
# Member count
|
||||
|
|
@ -1229,7 +1372,8 @@ class PDFGenerator:
|
|||
alignment=TA_CENTER,
|
||||
spaceAfter=10,
|
||||
)
|
||||
elements.append(Paragraph(f"({member_count} 位)", count_style))
|
||||
members_label = get_pdf_label('members', language)
|
||||
elements.append(Paragraph(f"({member_count} {members_label})", count_style))
|
||||
|
||||
return elements
|
||||
|
||||
|
|
@ -1238,7 +1382,8 @@ class PDFGenerator:
|
|||
ticker: str,
|
||||
price_data: list,
|
||||
price_stats: dict,
|
||||
styles: dict
|
||||
styles: dict,
|
||||
language: str = 'zh-TW'
|
||||
) -> list:
|
||||
"""Create a dedicated price chart page"""
|
||||
from reportlab.platypus import Spacer, Image, Table, TableStyle
|
||||
|
|
@ -1249,7 +1394,7 @@ class PDFGenerator:
|
|||
elements = []
|
||||
|
||||
# Page title
|
||||
elements.append(Paragraph("價格走勢圖 & 交易量柱狀圖", styles['section_title']))
|
||||
elements.append(Paragraph(get_pdf_label('price_chart', language), styles['section_title']))
|
||||
elements.append(Spacer(1, 0.5*cm))
|
||||
|
||||
# Generate and add chart
|
||||
|
|
@ -1263,29 +1408,31 @@ class PDFGenerator:
|
|||
elements.append(Spacer(1, 0.8*cm))
|
||||
except Exception as e:
|
||||
print(f"Chart generation failed: {e}")
|
||||
elements.append(Paragraph("圖表生成失敗", styles['body']))
|
||||
elements.append(Paragraph(get_pdf_label('chart_failed', language), styles['body']))
|
||||
|
||||
# Add price statistics table
|
||||
if price_stats:
|
||||
elements.append(Paragraph("價格統計", styles['heading']))
|
||||
elements.append(Paragraph(get_pdf_label('price_stats', language), styles['heading']))
|
||||
elements.append(Spacer(1, 0.3*cm))
|
||||
|
||||
growth_rate = price_stats.get('growth_rate', 0)
|
||||
growth_text = f"+{growth_rate:.2f}%" if growth_rate >= 0 else f"{growth_rate:.2f}%"
|
||||
|
||||
# Build stats data with localized labels
|
||||
days_label = get_pdf_label('days', language)
|
||||
stats_data = [
|
||||
['項 目', '數 值'],
|
||||
['總報酬率', growth_text],
|
||||
['分析期間', f"{price_stats.get('duration_days', 'N/A')} 天"],
|
||||
['開始日期', price_stats.get('start_date', 'N/A')],
|
||||
['結束日期', price_stats.get('end_date', 'N/A')],
|
||||
[get_pdf_label('item', language), get_pdf_label('value', language)],
|
||||
[get_pdf_label('total_return', language), growth_text],
|
||||
[get_pdf_label('analysis_period', language), f"{price_stats.get('duration_days', 'N/A')} {days_label}"],
|
||||
[get_pdf_label('start_date', language), price_stats.get('start_date', 'N/A')],
|
||||
[get_pdf_label('end_date', language), price_stats.get('end_date', 'N/A')],
|
||||
]
|
||||
|
||||
# Add start/end prices if available
|
||||
if 'start_price' in price_stats:
|
||||
stats_data.append(['起始價格', f"${price_stats.get('start_price', 0):.2f}"])
|
||||
stats_data.append([get_pdf_label('start_price', language), f"${price_stats.get('start_price', 0):.2f}"])
|
||||
if 'end_price' in price_stats:
|
||||
stats_data.append(['結束價格', f"${price_stats.get('end_price', 0):.2f}"])
|
||||
stats_data.append([get_pdf_label('end_price', language), f"${price_stats.get('end_price', 0):.2f}"])
|
||||
|
||||
# Create table
|
||||
col_widths = [8*cm, 8*cm]
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ class TradingService:
|
|||
research_depth: int = 1,
|
||||
deep_think_llm: str = "gpt-5-mini",
|
||||
quick_think_llm: str = "gpt-5-mini",
|
||||
language: str = "zh-TW", # Language for agent reports: 'en' or 'zh-TW'
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Run trading analysis for a given ticker and date with user-provided API keys
|
||||
|
|
@ -181,6 +182,10 @@ class TradingService:
|
|||
# 美股:維持原有邏輯(不修改 data_vendors 和 tool_vendors)
|
||||
logger.info(f"Market type: US stocks - using default data providers")
|
||||
|
||||
# Set language for agent reports
|
||||
config["language"] = language
|
||||
logger.info(f"Language for reports: {language}")
|
||||
|
||||
# Initialize TradingAgentsX graph
|
||||
graph = TradingAgentsXGraph(analysts, config=config, debug=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { ErrorAlert } from "@/components/shared/ErrorAlert";
|
|||
import { useAnalysis } from "@/hooks/useAnalysis";
|
||||
import { useAnalysisContext } from "@/context/AnalysisContext";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { saveReport, checkDuplicateReport } from "@/lib/reports-db";
|
||||
import { saveCloudReport, isCloudSyncEnabled } from "@/lib/user-api";
|
||||
import type { AnalysisRequest } from "@/lib/types";
|
||||
|
|
@ -20,6 +21,7 @@ export default function AnalysisPage() {
|
|||
const { setAnalysisResult, setTaskId, setMarketType, marketType } = useAnalysisContext();
|
||||
const { runAnalysis, loading, error, result, taskId } = useAnalysis();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Ref to track if we've already saved (to prevent duplicate saves)
|
||||
const hasSavedRef = useRef(false);
|
||||
|
|
@ -132,16 +134,16 @@ export default function AnalysisPage() {
|
|||
{/* 標題區域 - 置中對齊 */}
|
||||
<div className="text-center relative">
|
||||
<div className="absolute inset-0 gradient-bg-radial opacity-40 -z-10" />
|
||||
<h1 className="text-4xl font-bold mb-2 gradient-text-primary">交易分析</h1>
|
||||
<h1 className="text-4xl font-bold mb-2 gradient-text-primary">{t.form.analysisTitle}</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
配置並執行全面的多代理交易分析
|
||||
{t.form.analysisSubtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<AnalysisForm onSubmit={handleSubmit} loading={loading} />
|
||||
|
||||
{loading && (
|
||||
<LoadingSpinner message="正在執行分析... 這可能需要幾分鐘時間。" />
|
||||
<LoadingSpinner message={t.form.analysisLoading} />
|
||||
)}
|
||||
|
||||
{error && <ErrorAlert error={error} />}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { useAnalysisContext } from "@/context/AnalysisContext";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { PriceChart } from "@/components/analysis/PriceChart";
|
||||
import { DownloadReports } from "@/components/analysis/DownloadReports";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -15,86 +16,24 @@ import { ChevronLeft, Save, Check, AlertCircle, Cloud } from "lucide-react";
|
|||
import { saveReport, checkDuplicateReport } from "@/lib/reports-db";
|
||||
import { saveCloudReport, isCloudSyncEnabled } from "@/lib/user-api";
|
||||
|
||||
const ANALYSTS = [
|
||||
// === 分析師團隊 ===
|
||||
{
|
||||
key: "market",
|
||||
label: "市場分析師",
|
||||
reportKey: "market_report",
|
||||
description: "技術分析與市場趨勢評估"
|
||||
},
|
||||
{
|
||||
key: "social",
|
||||
label: "社群媒體分析師",
|
||||
reportKey: "sentiment_report",
|
||||
description: "社群情緒與市場氛圍分析"
|
||||
},
|
||||
{
|
||||
key: "news",
|
||||
label: "新聞分析師",
|
||||
reportKey: "news_report",
|
||||
description: "新聞事件與影響分析"
|
||||
},
|
||||
{
|
||||
key: "fundamentals",
|
||||
label: "基本面分析師",
|
||||
reportKey: "fundamentals_report",
|
||||
description: "財務數據與基本面分析"
|
||||
},
|
||||
|
||||
// === 研究團隊 ===
|
||||
{
|
||||
key: "bull",
|
||||
label: "看漲研究員",
|
||||
reportKey: "investment_debate_state.bull_history",
|
||||
description: "看漲觀點與投資論據"
|
||||
},
|
||||
{
|
||||
key: "bear",
|
||||
label: "看跌研究員",
|
||||
reportKey: "investment_debate_state.bear_history",
|
||||
description: "看跌觀點與風險警告"
|
||||
},
|
||||
{
|
||||
key: "research_manager",
|
||||
label: "研究經理",
|
||||
reportKey: "investment_debate_state.judge_decision",
|
||||
description: "研究團隊綜合決策"
|
||||
},
|
||||
|
||||
// === 交易員 ===
|
||||
{
|
||||
key: "trader",
|
||||
label: "交易員",
|
||||
reportKey: "trader_investment_plan",
|
||||
description: "交易執行計劃與策略"
|
||||
},
|
||||
|
||||
// === 風險管理團隊 ===
|
||||
{
|
||||
key: "risky",
|
||||
label: "激進分析師",
|
||||
reportKey: "risk_debate_state.risky_history",
|
||||
description: "高風險高回報策略分析"
|
||||
},
|
||||
{
|
||||
key: "safe",
|
||||
label: "保守分析師",
|
||||
reportKey: "risk_debate_state.safe_history",
|
||||
description: "穩健保守策略分析"
|
||||
},
|
||||
{
|
||||
key: "neutral",
|
||||
label: "中立分析師",
|
||||
reportKey: "risk_debate_state.neutral_history",
|
||||
description: "中立平衡策略分析"
|
||||
},
|
||||
{
|
||||
key: "risk_manager",
|
||||
label: "風險經理",
|
||||
reportKey: "risk_debate_state.judge_decision",
|
||||
description: "風險管理綜合決策"
|
||||
},
|
||||
// Analyst keys for mapping to translation keys
|
||||
const ANALYST_KEYS = [
|
||||
// === Analysts Team ===
|
||||
{ key: "market", reportKey: "market_report" },
|
||||
{ key: "social", reportKey: "sentiment_report" },
|
||||
{ key: "news", reportKey: "news_report" },
|
||||
{ key: "fundamentals", reportKey: "fundamentals_report" },
|
||||
// === Research Team ===
|
||||
{ key: "bull", reportKey: "investment_debate_state.bull_history" },
|
||||
{ key: "bear", reportKey: "investment_debate_state.bear_history" },
|
||||
{ key: "research_manager", reportKey: "investment_debate_state.judge_decision" },
|
||||
// === Trader ===
|
||||
{ key: "trader", reportKey: "trader_investment_plan" },
|
||||
// === Risk Management Team ===
|
||||
{ key: "risky", reportKey: "risk_debate_state.risky_history" },
|
||||
{ key: "safe", reportKey: "risk_debate_state.safe_history" },
|
||||
{ key: "neutral", reportKey: "risk_debate_state.neutral_history" },
|
||||
{ key: "risk_manager", reportKey: "risk_debate_state.judge_decision" },
|
||||
];
|
||||
|
||||
// 獲取嵌套對象的值
|
||||
|
|
@ -106,6 +45,7 @@ export default function AnalysisResultsPage() {
|
|||
const router = useRouter();
|
||||
const { analysisResult, taskId, marketType } = useAnalysisContext();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { t, locale } = useLanguage();
|
||||
const [selectedAnalyst, setSelectedAnalyst] = useState("market");
|
||||
|
||||
// Save report states
|
||||
|
|
@ -114,6 +54,14 @@ export default function AnalysisResultsPage() {
|
|||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const [savedToCloud, setSavedToCloud] = useState(false);
|
||||
|
||||
// Build analysts array with translations
|
||||
const ANALYSTS = useMemo(() => ANALYST_KEYS.map(analyst => ({
|
||||
key: analyst.key,
|
||||
label: t.results.analysts[analyst.key as keyof typeof t.results.analysts] || analyst.key,
|
||||
description: t.results.analysts[`${analyst.key}Desc` as keyof typeof t.results.analysts] || "",
|
||||
reportKey: analyst.reportKey,
|
||||
})), [t]);
|
||||
|
||||
// 如果沒有結果,重定向到分析頁面
|
||||
useEffect(() => {
|
||||
if (!analysisResult) {
|
||||
|
|
@ -138,7 +86,7 @@ export default function AnalysisResultsPage() {
|
|||
);
|
||||
|
||||
if (duplicate) {
|
||||
setSaveError("此報告已存在(相同股票代碼與分析日期)");
|
||||
setSaveError(t.results.duplicateReport);
|
||||
setSaving(false);
|
||||
return;
|
||||
}
|
||||
|
|
@ -164,8 +112,6 @@ export default function AnalysisResultsPage() {
|
|||
setSavedToCloud(true);
|
||||
}
|
||||
}
|
||||
// Note: Redis cleanup is handled immediately when analysis completes
|
||||
// in useAnalysis hook, so no need to cleanup here
|
||||
|
||||
setSaveSuccess(true);
|
||||
// Reset success message after 3 seconds
|
||||
|
|
@ -175,7 +121,7 @@ export default function AnalysisResultsPage() {
|
|||
}, 3000);
|
||||
} catch (error) {
|
||||
console.error("Save report error:", error);
|
||||
setSaveError("儲存失敗,請稍後再試");
|
||||
setSaveError(t.results.saveError);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
|
|
@ -185,10 +131,10 @@ export default function AnalysisResultsPage() {
|
|||
return (
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold mb-4">沒有分析結果</h1>
|
||||
<p className="text-gray-600 mb-4">請先執行分析</p>
|
||||
<h1 className="text-2xl font-bold mb-4">{t.results.noResults}</h1>
|
||||
<p className="text-gray-600 mb-4">{t.results.runAnalysisFirst}</p>
|
||||
<Button onClick={() => router.push("/analysis")}>
|
||||
返回分析頁面
|
||||
{t.results.backToAnalysis}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -207,10 +153,10 @@ export default function AnalysisResultsPage() {
|
|||
<div className="absolute inset-0 gradient-bg-radial opacity-30 -z-10 rounded-lg" />
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold mb-2 gradient-text-primary">
|
||||
{analysisResult.ticker} 詳細分析結果
|
||||
{analysisResult.ticker} {t.results.detailedResults}
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
分析日期:{analysisResult.analysis_date}
|
||||
{t.results.analysisDate}:{analysisResult.analysis_date}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center flex-wrap">
|
||||
|
|
@ -218,7 +164,7 @@ export default function AnalysisResultsPage() {
|
|||
{saveSuccess && (
|
||||
<span className="flex items-center gap-1 text-green-600 dark:text-green-400 text-sm animate-fade-in">
|
||||
<Check className="h-4 w-4" />
|
||||
已儲存
|
||||
{t.results.saved}
|
||||
</span>
|
||||
)}
|
||||
{saveError && (
|
||||
|
|
@ -239,6 +185,7 @@ export default function AnalysisResultsPage() {
|
|||
priceData={analysisResult.price_data}
|
||||
priceStats={analysisResult.price_stats}
|
||||
compact={true}
|
||||
language={locale}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -250,16 +197,16 @@ export default function AnalysisResultsPage() {
|
|||
className="gap-2 hover-lift bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600"
|
||||
>
|
||||
{saving ? (
|
||||
<>儲存中...</>
|
||||
<>{t.results.saving}</>
|
||||
) : saveSuccess ? (
|
||||
<>
|
||||
<Check className="h-4 w-4" />
|
||||
已儲存
|
||||
{t.results.saved}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="h-4 w-4" />
|
||||
儲存報告
|
||||
{t.results.saveReport}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
|
@ -270,7 +217,7 @@ export default function AnalysisResultsPage() {
|
|||
className="gap-2 hover-lift"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
返回分析
|
||||
{t.results.backButton}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -304,7 +251,7 @@ export default function AnalysisResultsPage() {
|
|||
{/* 分析師報告 */}
|
||||
<Card className="animate-scale-up hover-lift">
|
||||
<CardHeader>
|
||||
<CardTitle>{analyst.label} 報告</CardTitle>
|
||||
<CardTitle>{analyst.label} {t.results.report}</CardTitle>
|
||||
<CardDescription>
|
||||
{analyst.description}
|
||||
</CardDescription>
|
||||
|
|
@ -319,10 +266,10 @@ export default function AnalysisResultsPage() {
|
|||
) : (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
此分析師沒有生成報告
|
||||
{t.results.noReportGenerated}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400 dark:text-gray-500 mt-2">
|
||||
可能此分析師未被選擇或分析過程中未產生報告
|
||||
{t.results.notSelectedOrNoReport}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { format } from "date-fns";
|
|||
import { zhTW } from "date-fns/locale";
|
||||
import { useAnalysisContext } from "@/context/AnalysisContext";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
|
|
@ -54,12 +55,12 @@ const ANALYSTS = [
|
|||
{ key: "risk_manager", label: "風險經理", reportKey: "risk_debate_state.judge_decision", description: "風險管理綜合決策" },
|
||||
];
|
||||
|
||||
// Market type labels
|
||||
const MARKET_LABELS = {
|
||||
us: { label: "🇺🇸 美股", description: "美國股市分析報告" },
|
||||
twse: { label: "🇹🇼 上市", description: "台灣證券交易所上市股票" },
|
||||
tpex: { label: "🇹🇼 上櫃/興櫃", description: "台灣櫃買中心上櫃/興櫃股票" },
|
||||
};
|
||||
// Market type labels - dynamic function to support translations
|
||||
const getMarketLabels = (t: ReturnType<typeof useLanguage>['t']) => ({
|
||||
us: { label: `🇺🇸 ${t.form.usMarket}`, description: t.form.tickerDescUS },
|
||||
twse: { label: `🇹🇼 ${t.form.twseMarket}`, description: t.form.tickerDescTWSE },
|
||||
tpex: { label: `🇹🇼 ${t.form.tpexMarket}`, description: t.form.tickerDescTPEX },
|
||||
});
|
||||
|
||||
// Helper function to extract decision from Risk Manager's final decision
|
||||
const extractDecisionFromReport = (report: SavedReport): { action: string; color: string } => {
|
||||
|
|
@ -83,25 +84,42 @@ const extractDecisionFromReport = (report: SavedReport): { action: string; color
|
|||
} else {
|
||||
console.log(" - trader_investment_plan is NULL or undefined");
|
||||
}
|
||||
// Helper function to find "最終交易提案" specifically
|
||||
// Helper function to find "最終交易提案" or "Final Trading Proposal"
|
||||
const findFinalProposal = (text: string): { action: string; color: string } | null => {
|
||||
if (!text || typeof text !== 'string') return null;
|
||||
|
||||
// === CHINESE PATTERN ===
|
||||
// Match "最終交易提案:持有" - handle markdown ** bold markers
|
||||
// Pattern handles: 最終交易提案:持有, 最終交易提案:**持有**, **最終交易提案:持有**
|
||||
// Use global flag to find ALL matches, then take the LAST one (final decision)
|
||||
const regex = /\*{0,2}最終交易提案[::]\s*\*{0,2}(買入|賣出|持有)\*{0,2}/g;
|
||||
const matches = [...text.matchAll(regex)];
|
||||
const zhRegex = /\*{0,2}最終交易提案[::]\s*\*{0,2}(買入|賣出|持有)\*{0,2}/g;
|
||||
const zhMatches = [...text.matchAll(zhRegex)];
|
||||
|
||||
if (matches.length > 0) {
|
||||
if (zhMatches.length > 0) {
|
||||
// Take the LAST match (the final decision at the end of the report)
|
||||
const lastMatch = matches[matches.length - 1];
|
||||
const lastMatch = zhMatches[zhMatches.length - 1];
|
||||
const decision = lastMatch[1];
|
||||
console.log(` ✅ Matched pattern: "${lastMatch[0]}" -> decision: "${decision}"`);
|
||||
console.log(` ✅ Matched ZH pattern: "${lastMatch[0]}" -> decision: "${decision}"`);
|
||||
if (decision === "買入") return { action: "買入", color: "text-green-600" };
|
||||
if (decision === "賣出") return { action: "賣出", color: "text-red-600" };
|
||||
if (decision === "持有") return { action: "持有", color: "text-yellow-600" };
|
||||
}
|
||||
|
||||
// === ENGLISH PATTERN ===
|
||||
// Match "Final Trading Proposal: BUY/SELL/HOLD" - handle markdown ** bold markers
|
||||
// Pattern handles: Final Trading Proposal: Buy, **Final Trading Proposal**: Hold, etc.
|
||||
const enRegex = /\*{0,2}Final Trading Proposal\*{0,2}[::]\s*\*{0,2}(BUY|SELL|HOLD|Buy|Sell|Hold)\*{0,2}/gi;
|
||||
const enMatches = [...text.matchAll(enRegex)];
|
||||
|
||||
if (enMatches.length > 0) {
|
||||
const lastMatch = enMatches[enMatches.length - 1];
|
||||
const decision = lastMatch[1].toUpperCase();
|
||||
console.log(` ✅ Matched EN pattern: "${lastMatch[0]}" -> decision: "${decision}"`);
|
||||
if (decision === "BUY") return { action: "BUY", color: "text-green-600" };
|
||||
if (decision === "SELL") return { action: "SELL", color: "text-red-600" };
|
||||
if (decision === "HOLD") return { action: "HOLD", color: "text-yellow-600" };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
|
@ -193,6 +211,10 @@ export default function HistoryPage() {
|
|||
const router = useRouter();
|
||||
const { setAnalysisResult, setTaskId, setMarketType } = useAnalysisContext();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { t, locale } = useLanguage();
|
||||
|
||||
// Dynamic market labels based on language
|
||||
const MARKET_LABELS = getMarketLabels(t);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<"us" | "twse" | "tpex">("us");
|
||||
const [reports, setReports] = useState<SavedReport[]>([]);
|
||||
|
|
@ -492,6 +514,7 @@ export default function HistoryPage() {
|
|||
reports: report.result.reports,
|
||||
price_data: report.result.price_data,
|
||||
price_stats: report.result.price_stats,
|
||||
language: locale, // Pass current language for PDF generation
|
||||
};
|
||||
|
||||
const response = await fetch('/api/download/reports', {
|
||||
|
|
@ -547,10 +570,10 @@ export default function HistoryPage() {
|
|||
<div className="text-center relative animate-fade-in">
|
||||
<div className="absolute inset-0 gradient-bg-radial opacity-40 -z-10" />
|
||||
<h1 className="text-4xl font-bold mb-2 gradient-text-primary">
|
||||
歷史報告
|
||||
{t.history.title}
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
瀏覽已儲存的分析報告
|
||||
{t.history.noHistory.replace("尚無分析紀錄", "瀏覽已儲存的分析報告")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -596,7 +619,7 @@ export default function HistoryPage() {
|
|||
<RefreshCw
|
||||
className={`h-4 w-4 ${loading ? "animate-spin" : ""}`}
|
||||
/>
|
||||
重新整理
|
||||
{t.history.refresh}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -604,24 +627,24 @@ export default function HistoryPage() {
|
|||
{loading ? (
|
||||
<div className="text-center py-12">
|
||||
<RefreshCw className="h-8 w-8 animate-spin mx-auto text-gray-400" />
|
||||
<p className="text-gray-500 mt-4">載入中...</p>
|
||||
<p className="text-gray-500 mt-4">{t.history.loading}</p>
|
||||
</div>
|
||||
) : reports.length === 0 ? (
|
||||
<Card className="animate-fade-in">
|
||||
<CardContent className="py-12 text-center">
|
||||
<TrendingUp className="h-12 w-12 mx-auto text-gray-300 dark:text-gray-600 mb-4" />
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
尚無{MARKET_LABELS[marketType].label}的分析報告
|
||||
{t.history.noReportsFor} {MARKET_LABELS[marketType].label}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400 dark:text-gray-500 mt-2">
|
||||
執行分析後,可在結果頁面儲存報告
|
||||
{t.history.afterAnalysisSave}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mt-4"
|
||||
onClick={() => router.push("/analysis")}
|
||||
>
|
||||
開始分析
|
||||
{t.home.startAnalysis}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -642,12 +665,12 @@ export default function HistoryPage() {
|
|||
</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
分析日期:{report.analysis_date}
|
||||
{t.history.analysisDate}:{report.analysis_date}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
儲存時間:
|
||||
{t.history.savedAt}:
|
||||
{format(
|
||||
new Date(report.saved_at),
|
||||
"yyyy/MM/dd HH:mm",
|
||||
|
|
@ -658,7 +681,7 @@ export default function HistoryPage() {
|
|||
const decision = extractDecisionFromReport(report);
|
||||
return (
|
||||
<p className="text-sm mt-2">
|
||||
<span className="font-medium">決策:</span>
|
||||
<span className="font-medium">{t.history.decision}:</span>
|
||||
<span className={`ml-1 font-semibold ${decision.color}`}>
|
||||
{decision.action}
|
||||
</span>
|
||||
|
|
@ -674,7 +697,7 @@ export default function HistoryPage() {
|
|||
onClick={() => handleViewReport(report)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
檢視
|
||||
{t.history.view}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -686,7 +709,7 @@ export default function HistoryPage() {
|
|||
{downloadingId === report.id ? (
|
||||
<>
|
||||
<Download className="h-4 w-4 animate-bounce" />
|
||||
下載中
|
||||
{t.history.downloading}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -702,7 +725,7 @@ export default function HistoryPage() {
|
|||
onClick={() => handleDeleteClick(report)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
刪除
|
||||
{t.history.delete}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
|
@ -721,12 +744,12 @@ export default function HistoryPage() {
|
|||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>確認刪除</DialogTitle>
|
||||
<DialogTitle>{t.history.confirmDeleteTitle}</DialogTitle>
|
||||
<DialogDescription>
|
||||
確定要刪除 <strong>{reportToDelete?.ticker}</strong> 於{" "}
|
||||
<strong>{reportToDelete?.analysis_date}</strong> 的分析報告嗎?
|
||||
{t.history.confirmDeleteDesc} <strong>{reportToDelete?.ticker}</strong> {t.history.on}{" "}
|
||||
<strong>{reportToDelete?.analysis_date}</strong>?
|
||||
<br />
|
||||
<span className="text-red-500">此操作無法復原。</span>
|
||||
<span className="text-red-500">{t.history.cannotUndo}</span>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
|
|
@ -735,14 +758,14 @@ export default function HistoryPage() {
|
|||
onClick={() => setDeleteDialogOpen(false)}
|
||||
disabled={deleting}
|
||||
>
|
||||
取消
|
||||
{t.history.cancel}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleConfirmDelete}
|
||||
disabled={deleting}
|
||||
>
|
||||
{deleting ? "刪除中..." : "確認刪除"}
|
||||
{deleting ? t.history.deleting : t.history.confirmDeleteBtn}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Footer } from "@/components/layout/Footer";
|
|||
import { AnalysisProvider } from "@/context/AnalysisContext";
|
||||
import { ThemeProvider } from "@/components/theme/ThemeProvider";
|
||||
import { AuthProvider } from "@/contexts/auth-context";
|
||||
import { LanguageProvider } from "@/contexts/LanguageContext";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
|
|
@ -115,15 +116,17 @@ export default function RootLayout({
|
|||
</head>
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<AnalysisProvider>
|
||||
<div className="flex flex-col min-h-screen gradient-page-bg">
|
||||
<Header />
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</AnalysisProvider>
|
||||
</AuthProvider>
|
||||
<LanguageProvider>
|
||||
<AuthProvider>
|
||||
<AnalysisProvider>
|
||||
<div className="flex flex-col min-h-screen gradient-page-bg">
|
||||
<Header />
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</AnalysisProvider>
|
||||
</AuthProvider>
|
||||
</LanguageProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -9,8 +11,11 @@ import {
|
|||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { AgentFlowDiagram } from "@/components/AgentFlowDiagram";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
export default function HomePage() {
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-purple-50/30 via-pink-50/20 to-purple-50/30 dark:from-gray-950 dark:via-purple-950/40 dark:to-gray-950">
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
|
|
@ -28,10 +33,10 @@ export default function HomePage() {
|
|||
/>
|
||||
</div>
|
||||
<h1 className="text-4xl sm:text-5xl md:text-6xl font-bold mb-6 gradient-text-primary leading-tight">
|
||||
TradingAgentsX
|
||||
{t.home.title}
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 dark:text-gray-400 mb-8 max-w-2xl mx-auto">
|
||||
多代理 LLM 金融交易框架
|
||||
{t.nav.tagline}
|
||||
</p>
|
||||
<div className="flex gap-4 justify-center">
|
||||
<Link href="/analysis">
|
||||
|
|
@ -39,7 +44,7 @@ export default function HomePage() {
|
|||
size="lg"
|
||||
className="bg-gradient-to-r from-blue-500 to-pink-500 dark:from-blue-600 dark:to-purple-600 hover:from-blue-600 hover:to-pink-600 dark:hover:from-blue-700 dark:hover:to-purple-700 shadow-lg hover:shadow-xl transition-all animate-heartbeat"
|
||||
>
|
||||
開始分析
|
||||
{t.home.startAnalysis}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -47,54 +52,54 @@ export default function HomePage() {
|
|||
|
||||
{/* Core Features Section */}
|
||||
<div className="mb-16 animate-slide-up animate-delay-200">
|
||||
<h2 className="text-3xl font-bold text-center mb-4">🎯 核心特色</h2>
|
||||
<h2 className="text-3xl font-bold text-center mb-4">{t.home.coreFeatures}</h2>
|
||||
<p className="text-center text-gray-600 dark:text-gray-400 mb-8 max-w-3xl mx-auto">
|
||||
基於 LangGraph 的智能股票交易分析平台,結合多個 AI 代理進行協作決策
|
||||
{t.home.coreFeaturesDesc}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<FeatureCard
|
||||
title="多代理協作架構"
|
||||
description="12 個專業化 AI 代理團隊協同工作,模擬真實交易公司運作模式"
|
||||
title={t.home.features.multiAgent}
|
||||
description={t.home.features.multiAgentDesc}
|
||||
icon="🤖"
|
||||
/>
|
||||
<FeatureCard
|
||||
title="多模型靈活支援"
|
||||
description="支援 OpenAI、Claude、Gemini、Grok、DeepSeek、Qwen 等多家 LLM"
|
||||
title={t.home.features.multiModel}
|
||||
description={t.home.features.multiModelDesc}
|
||||
icon="🌐"
|
||||
/>
|
||||
<FeatureCard
|
||||
title="自訂端點配置"
|
||||
description="完整支援自訂 API 端點,可連接任何 OpenAI 兼容的服務"
|
||||
title={t.home.features.customEndpoint}
|
||||
description={t.home.features.customEndpointDesc}
|
||||
icon="🔧"
|
||||
/>
|
||||
<FeatureCard
|
||||
title="全方位市場分析"
|
||||
description="整合技術面、基本面、情緒面、新聞面四大維度分析"
|
||||
title={t.home.features.fullAnalysis}
|
||||
description={t.home.features.fullAnalysisDesc}
|
||||
icon="📊"
|
||||
/>
|
||||
<FeatureCard
|
||||
title="結構化決策流程"
|
||||
description="透過看漲/看跌辯論機制減少偏見,做出更理性的決策"
|
||||
title={t.home.features.structuredDecision}
|
||||
description={t.home.features.structuredDecisionDesc}
|
||||
icon="🔄"
|
||||
/>
|
||||
<FeatureCard
|
||||
title="長期記憶系統"
|
||||
description="使用 ChromaDB 向量資料庫儲存歷史決策,持續學習改進"
|
||||
title={t.home.features.longTermMemory}
|
||||
description={t.home.features.longTermMemoryDesc}
|
||||
icon="🧠"
|
||||
/>
|
||||
<FeatureCard
|
||||
title="現代化 Web 介面"
|
||||
description="基於 Next.js 16 的響應式 UI,支援深色模式"
|
||||
title={t.home.features.modernUI}
|
||||
description={t.home.features.modernUIDesc}
|
||||
icon="🎨"
|
||||
/>
|
||||
<FeatureCard
|
||||
title="一鍵部署"
|
||||
description="支援 Docker Compose 部署,快速啟動完整服務"
|
||||
title={t.home.features.oneClickDeploy}
|
||||
description={t.home.features.oneClickDeployDesc}
|
||||
icon="🐳"
|
||||
/>
|
||||
<FeatureCard
|
||||
title="報告下載"
|
||||
description="支援將完整分析報告匯出為 PDF,方便保存與分享"
|
||||
title={t.home.features.reportDownload}
|
||||
description={t.home.features.reportDownloadDesc}
|
||||
icon="📥"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -103,54 +108,37 @@ export default function HomePage() {
|
|||
{/* 12 Professional Agents Section */}
|
||||
<div className="mb-16">
|
||||
<h2 className="text-3xl font-bold text-center mb-4">
|
||||
👥 12 位專業代理團隊
|
||||
{t.home.professionalAgents}
|
||||
</h2>
|
||||
<p className="text-center text-gray-600 dark:text-gray-400 mb-8 max-w-3xl mx-auto">
|
||||
每個代理都有其專業職責,協同工作產生高質量的交易決策
|
||||
{t.home.professionalAgentsDesc}
|
||||
</p>
|
||||
|
||||
{/* Analyst Team */}
|
||||
<div className="mb-8">
|
||||
<h3 className="text-2xl font-semibold mb-4 flex items-center">
|
||||
<span className="mr-2">📊</span>
|
||||
分析師團隊 (4 位)
|
||||
{t.home.analystsTeamTitle}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<AgentCard
|
||||
name="市場分析師"
|
||||
role="技術分析"
|
||||
responsibilities={[
|
||||
"技術指標分析 (RSI, MACD, 布林通道)",
|
||||
"價格走勢研判",
|
||||
"支撐阻力位識別",
|
||||
]}
|
||||
name={t.agents.market_analyst}
|
||||
role={t.agents.market_analyst_role}
|
||||
description={t.agents.market_analyst_desc}
|
||||
/>
|
||||
<AgentCard
|
||||
name="社群媒體分析師"
|
||||
role="情緒評估"
|
||||
responsibilities={[
|
||||
"Reddit/Twitter 情緒指標",
|
||||
"熱度趨勢分析",
|
||||
"投資者信心指數",
|
||||
]}
|
||||
name={t.agents.social_analyst}
|
||||
role={t.agents.social_analyst_role}
|
||||
description={t.agents.social_analyst_desc}
|
||||
/>
|
||||
<AgentCard
|
||||
name="新聞分析師"
|
||||
role="新聞分析"
|
||||
responsibilities={[
|
||||
"最新新聞摘要",
|
||||
"事件影響評估",
|
||||
"市場反應預測",
|
||||
]}
|
||||
name={t.agents.news_analyst}
|
||||
role={t.agents.news_analyst_role}
|
||||
description={t.agents.news_analyst_desc}
|
||||
/>
|
||||
<AgentCard
|
||||
name="基本面分析師"
|
||||
role="財務分析"
|
||||
responsibilities={[
|
||||
"財報數據解析",
|
||||
"估值指標 (P/E, P/B)",
|
||||
"盈利能力評估",
|
||||
]}
|
||||
name={t.agents.fundamentals_analyst}
|
||||
role={t.agents.fundamentals_analyst_role}
|
||||
description={t.agents.fundamentals_analyst_desc}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -158,36 +146,23 @@ export default function HomePage() {
|
|||
{/* Research Team */}
|
||||
<div className="mb-8">
|
||||
<h3 className="text-2xl font-semibold mb-4 flex items-center">
|
||||
<span className="mr-2">🔍</span>
|
||||
研究團隊 (3 位)
|
||||
{t.home.researchTeamTitle}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<AgentCard
|
||||
name="看漲研究員"
|
||||
role="多頭論證"
|
||||
responsibilities={[
|
||||
"看漲理由分析",
|
||||
"上漲催化劑識別",
|
||||
"目標價位預測",
|
||||
]}
|
||||
name={t.agents.bull_researcher}
|
||||
role={t.agents.bull_researcher_role}
|
||||
description={t.agents.bull_researcher_desc}
|
||||
/>
|
||||
<AgentCard
|
||||
name="看跌研究員"
|
||||
role="空頭論證"
|
||||
responsibilities={[
|
||||
"看跌理由分析",
|
||||
"下跌風險警告",
|
||||
"防守策略建議",
|
||||
]}
|
||||
name={t.agents.bear_researcher}
|
||||
role={t.agents.bear_researcher_role}
|
||||
description={t.agents.bear_researcher_desc}
|
||||
/>
|
||||
<AgentCard
|
||||
name="研究經理"
|
||||
role="綜合研判"
|
||||
responsibilities={[
|
||||
"綜合雙方觀點",
|
||||
"研究團隊決策",
|
||||
"投資建議產出",
|
||||
]}
|
||||
name={t.agents.research_manager}
|
||||
role={t.agents.research_manager_role}
|
||||
description={t.agents.research_manager_desc}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -195,18 +170,13 @@ export default function HomePage() {
|
|||
{/* Trading Team */}
|
||||
<div className="mb-8">
|
||||
<h3 className="text-2xl font-semibold mb-4 flex items-center">
|
||||
<span className="mr-2">💼</span>
|
||||
交易團隊 (1 位)
|
||||
{t.home.tradingTeamTitle}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-1 gap-4 max-w-md">
|
||||
<AgentCard
|
||||
name="交易員"
|
||||
role="決策整合"
|
||||
responsibilities={[
|
||||
"整合所有報告",
|
||||
"制定交易計劃",
|
||||
"執行策略設計",
|
||||
]}
|
||||
name={t.agents.trader}
|
||||
role={t.agents.trader_role}
|
||||
description={t.agents.trader_desc}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -214,41 +184,28 @@ export default function HomePage() {
|
|||
{/* Risk Management Team */}
|
||||
<div className="mb-8">
|
||||
<h3 className="text-2xl font-semibold mb-4 flex items-center">
|
||||
<span className="mr-2">🛡️</span>
|
||||
風險管理團隊 (4 位)
|
||||
{t.home.riskTeamTitle}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<AgentCard
|
||||
name="激進分析師"
|
||||
role="高風險高回報"
|
||||
responsibilities={[
|
||||
"激進策略評估",
|
||||
"最大收益潛力",
|
||||
"風險容忍度高",
|
||||
]}
|
||||
name={t.agents.aggressive_debator}
|
||||
role={t.agents.aggressive_debator_role}
|
||||
description={t.agents.aggressive_debator_desc}
|
||||
/>
|
||||
<AgentCard
|
||||
name="保守分析師"
|
||||
role="穩健保守"
|
||||
responsibilities={[
|
||||
"資本保全優先",
|
||||
"風險嚴格控制",
|
||||
"穩健策略建議",
|
||||
]}
|
||||
name={t.agents.conservative_debator}
|
||||
role={t.agents.conservative_debator_role}
|
||||
description={t.agents.conservative_debator_desc}
|
||||
/>
|
||||
<AgentCard
|
||||
name="中立分析師"
|
||||
role="平衡策略"
|
||||
responsibilities={[
|
||||
"風險收益平衡",
|
||||
"中立客觀評估",
|
||||
"折衷方案設計",
|
||||
]}
|
||||
name={t.agents.neutral_debator}
|
||||
role={t.agents.neutral_debator_role}
|
||||
description={t.agents.neutral_debator_desc}
|
||||
/>
|
||||
<AgentCard
|
||||
name="風險經理"
|
||||
role="最終風控"
|
||||
responsibilities={["風險綜合評估", "倉位建議", "止損止盈設定"]}
|
||||
name={t.agents.risk_manager}
|
||||
role={t.agents.risk_manager_role}
|
||||
description={t.agents.risk_manager_desc}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -257,20 +214,19 @@ export default function HomePage() {
|
|||
{/* Agent Flow Diagram Section */}
|
||||
<div className="mb-16">
|
||||
<h2 className="text-3xl font-bold text-center mb-4">
|
||||
🔄 分析師協作流程
|
||||
{t.home.workflowTitle}
|
||||
</h2>
|
||||
<p className="text-center text-gray-600 dark:text-gray-400 mb-8 max-w-3xl mx-auto">
|
||||
四大分析師代理如何從不同資料來源收集資訊,並產生綜合分析報告
|
||||
{t.home.workflowDescription}
|
||||
</p>
|
||||
<AgentFlowDiagram />
|
||||
</div>
|
||||
|
||||
{/* LLM Support Section */}
|
||||
<div className="mb-16">
|
||||
<h2 className="text-3xl font-bold text-center mb-4">🌍 多模型支援</h2>
|
||||
<h2 className="text-3xl font-bold text-center mb-4">{t.home.llmSupport}</h2>
|
||||
<p className="text-center text-gray-600 dark:text-gray-400 mb-8 max-w-3xl mx-auto">
|
||||
支援業界領先的多家 LLM 提供商,每個模型可配置獨立的 API Key 和 Base
|
||||
URL
|
||||
{t.home.llmSupportDesc}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<LLMProviderCard
|
||||
|
|
@ -315,50 +271,49 @@ export default function HomePage() {
|
|||
</div>
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
✅ 完整支援自訂端點 | ✅ 三層獨立配置(快速思維/深層思維/嵌入) |
|
||||
✅ BYOK 模式
|
||||
{t.home.llmFeatures}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workflow Section */}
|
||||
<div className="mb-16">
|
||||
<h2 className="text-3xl font-bold text-center mb-4">⚙️ 工作流程</h2>
|
||||
<h2 className="text-3xl font-bold text-center mb-4">⚙️ {t.home.workflowTitle}</h2>
|
||||
<p className="text-center text-gray-600 dark:text-gray-400 mb-8 max-w-3xl mx-auto">
|
||||
TradingAgentsX 模擬真實交易公司,配備專業化的 LLM 代理
|
||||
{t.home.coreFeaturesDesc}
|
||||
</p>
|
||||
<Card className="shadow-lg hover-lift">
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-4">
|
||||
<WorkflowStep
|
||||
number={1}
|
||||
title="資料收集階段"
|
||||
description="從 yfinance、Reddit、RSS 等多源獲取股價、新聞、社群情緒數據"
|
||||
title={t.home.processSteps.dataCollection.title}
|
||||
description={t.home.processSteps.dataCollection.description}
|
||||
/>
|
||||
<WorkflowStep
|
||||
number={2}
|
||||
title="分析師團隊平行分析"
|
||||
description="市場、情緒、新聞、基本面四大分析師同時評估,產出專業報告"
|
||||
title={t.home.processSteps.analysts.title}
|
||||
description={t.home.processSteps.analysts.description}
|
||||
/>
|
||||
<WorkflowStep
|
||||
number={3}
|
||||
title="研究團隊辯論"
|
||||
description="看漲與看跌研究員進行結構化辯論,研究經理綜合雙方觀點"
|
||||
title={t.home.processSteps.researchers.title}
|
||||
description={t.home.processSteps.researchers.description}
|
||||
/>
|
||||
<WorkflowStep
|
||||
number={4}
|
||||
title="交易員整合分析"
|
||||
description="審查所有分析師與研究團隊報告,制定具體交易執行計劃"
|
||||
title={t.home.processSteps.trader.title}
|
||||
description={t.home.processSteps.trader.description}
|
||||
/>
|
||||
<WorkflowStep
|
||||
number={5}
|
||||
title="風險管理評估"
|
||||
description="激進、保守、中立三方風險分析師評估策略,風險經理做出風控決策"
|
||||
title={t.home.processSteps.risk.title}
|
||||
description={t.home.processSteps.risk.description}
|
||||
/>
|
||||
<WorkflowStep
|
||||
number={6}
|
||||
title="最終決策輸出"
|
||||
description="產生包含交易方向、倉位大小、風險控制的完整投資建議"
|
||||
title={t.home.processSteps.finalDecision.title}
|
||||
description={t.home.processSteps.finalDecision.description}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
@ -367,39 +322,23 @@ export default function HomePage() {
|
|||
|
||||
{/* Technical Highlights */}
|
||||
<div className="mb-16">
|
||||
<h2 className="text-3xl font-bold text-center mb-4">💡 技術亮點</h2>
|
||||
<h2 className="text-3xl font-bold text-center mb-4">{t.home.techHighlights}</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<TechnicalCard
|
||||
title="動態研究深度"
|
||||
features={[
|
||||
"淺層 (Shallow): 1 輪快速分析",
|
||||
"中等 (Medium): 2 輪平衡分析",
|
||||
"深層 (Deep): 3+ 輪全面分析",
|
||||
]}
|
||||
title={t.home.dynamicResearch}
|
||||
features={t.home.dynamicResearchFeatures}
|
||||
/>
|
||||
<TechnicalCard
|
||||
title="長期記憶系統"
|
||||
features={[
|
||||
"ChromaDB 向量資料庫",
|
||||
"歷史決策持久化",
|
||||
"持續學習與改進",
|
||||
]}
|
||||
title={t.home.memorySystem}
|
||||
features={t.home.memorySystemFeatures}
|
||||
/>
|
||||
<TechnicalCard
|
||||
title="實時資料整合"
|
||||
features={[
|
||||
"yfinance: 即時股價數據",
|
||||
"Reddit API: 社群情緒",
|
||||
"Alpha Vantage: 財務資料",
|
||||
]}
|
||||
title={t.home.realTimeData}
|
||||
features={t.home.realTimeDataFeatures}
|
||||
/>
|
||||
<TechnicalCard
|
||||
title="完整 API 支援"
|
||||
features={[
|
||||
"RESTful API 架構",
|
||||
"異步任務處理",
|
||||
"Swagger 互動文檔",
|
||||
]}
|
||||
title={t.home.fullApiSupport}
|
||||
features={t.home.fullApiSupportFeatures}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -408,17 +347,17 @@ export default function HomePage() {
|
|||
<div className="text-center py-16 relative">
|
||||
<div className="absolute inset-0 gradient-bg-radial opacity-60 -z-10" />
|
||||
<h2 className="text-3xl font-bold mb-4 gradient-text-primary">
|
||||
準備好開始智能交易分析了嗎?
|
||||
{t.home.readyToStart}
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 mb-8 max-w-2xl mx-auto">
|
||||
立即體驗 12 位專業 AI 代理協同工作,為您提供全方位的股票分析報告
|
||||
{t.home.ctaDescription}
|
||||
</p>
|
||||
<Link href="/analysis">
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-gradient-to-r from-blue-500 to-pink-500 dark:from-blue-600 dark:to-purple-600 hover:from-blue-600 hover:to-pink-600 dark:hover:from-blue-700 dark:hover:to-purple-700 text-lg px-8 py-6 shadow-lg hover:shadow-2xl transition-all animate-heartbeat"
|
||||
>
|
||||
開始分析 →
|
||||
{t.home.startAnalysis} →
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -454,11 +393,11 @@ function FeatureCard({
|
|||
function AgentCard({
|
||||
name,
|
||||
role,
|
||||
responsibilities,
|
||||
description,
|
||||
}: {
|
||||
name: string;
|
||||
role: string;
|
||||
responsibilities: string[];
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<Card className="hover-lift animate-scale-up">
|
||||
|
|
@ -467,14 +406,9 @@ function AgentCard({
|
|||
<CardDescription className="text-xs">{role}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
{responsibilities.map((item, index) => (
|
||||
<li key={index} className="flex items-start">
|
||||
<span className="mr-1">•</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{description}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
|
@ -489,7 +423,6 @@ function LLMProviderCard({
|
|||
models: string[];
|
||||
icon: string;
|
||||
}) {
|
||||
// Map provider names to logo filenames
|
||||
const logoMap: Record<string, string> = {
|
||||
OpenAI: "/logos/openai.svg",
|
||||
Anthropic: "/logos/claude-color.svg",
|
||||
|
|
|
|||
|
|
@ -672,7 +672,7 @@
|
|||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
|
|
|
|||
|
|
@ -20,217 +20,220 @@ import {
|
|||
Target,
|
||||
BarChart3
|
||||
} from "lucide-react";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
export function AgentFlowDiagram() {
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-7xl mx-auto space-y-6">
|
||||
{/* Data Sources Layer */}
|
||||
<div>
|
||||
<h3 className="text-center text-sm font-semibold text-gray-500 dark:text-gray-400 mb-4">
|
||||
📥 第一層:資料來源
|
||||
📥 {t.flowDiagram.layer1}
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<DataSourceCard
|
||||
icon={<Database className="w-5 h-5" />}
|
||||
name="yfinance"
|
||||
description="股價數據"
|
||||
description={t.flowDiagram.stockData}
|
||||
color="blue"
|
||||
/>
|
||||
<DataSourceCard
|
||||
icon={<MessageSquare className="w-5 h-5" />}
|
||||
name="Reddit API"
|
||||
description="社群情緒"
|
||||
description={t.flowDiagram.socialSentiment}
|
||||
color="orange"
|
||||
/>
|
||||
<DataSourceCard
|
||||
icon={<Newspaper className="w-5 h-5" />}
|
||||
name="RSS Feed"
|
||||
description="新聞資訊"
|
||||
description={t.flowDiagram.newsInfo}
|
||||
color="green"
|
||||
/>
|
||||
<DataSourceCard
|
||||
icon={<DollarSign className="w-5 h-5" />}
|
||||
name="Alpha Vantage / FinMind"
|
||||
description="財務數據"
|
||||
description={t.flowDiagram.financialData}
|
||||
color="purple"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Arrow */}
|
||||
<FlowArrow label="資料擷取與清理" color="blue" />
|
||||
<FlowArrow label={t.flowDiagram.dataFetch} color="blue" />
|
||||
|
||||
{/* Analysts Layer - 4 agents */}
|
||||
<div>
|
||||
<h3 className="text-center text-sm font-semibold text-gray-500 dark:text-gray-400 mb-4">
|
||||
🤖 第二層:分析師代理 (4位)
|
||||
🤖 {t.flowDiagram.layer2}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<AgentCard
|
||||
name="市場分析師"
|
||||
name={t.agents.market_analyst}
|
||||
icon={<BarChart3 className="w-5 h-5" />}
|
||||
gradient="from-blue-500 to-cyan-500"
|
||||
description="技術面分析"
|
||||
tasks={["RSI 指標", "MACD 動能", "價格走勢"]}
|
||||
description={t.flowDiagram.technicalAnalysis}
|
||||
tasks={[t.flowDiagram.rsiIndicator, t.flowDiagram.macdMomentum, t.flowDiagram.priceTrend]}
|
||||
/>
|
||||
<AgentCard
|
||||
name="社群媒體分析師"
|
||||
name={t.agents.social_analyst}
|
||||
icon={<MessageSquare className="w-5 h-5" />}
|
||||
gradient="from-orange-500 to-red-500"
|
||||
description="情緒面分析"
|
||||
tasks={["NLP 情緒", "討論熱度", "投資者信心"]}
|
||||
description={t.flowDiagram.sentimentAnalysis}
|
||||
tasks={[t.flowDiagram.nlpSentiment, t.flowDiagram.discussionHeat, t.flowDiagram.investorConfidence]}
|
||||
/>
|
||||
<AgentCard
|
||||
name="新聞分析師"
|
||||
name={t.agents.news_analyst}
|
||||
icon={<Newspaper className="w-5 h-5" />}
|
||||
gradient="from-green-500 to-emerald-500"
|
||||
description="新聞面分析"
|
||||
tasks={["新聞摘要", "事件評估", "影響預測"]}
|
||||
description={t.flowDiagram.newsAnalysis}
|
||||
tasks={[t.flowDiagram.newsSummary, t.flowDiagram.eventAssessment, t.flowDiagram.impactPrediction]}
|
||||
/>
|
||||
<AgentCard
|
||||
name="基本面分析師"
|
||||
name={t.agents.fundamentals_analyst}
|
||||
icon={<DollarSign className="w-5 h-5" />}
|
||||
gradient="from-purple-500 to-pink-500"
|
||||
description="基本面分析"
|
||||
tasks={["財報分析", "估值指標", "盈利評估"]}
|
||||
description={t.flowDiagram.fundamentalsAnalysis}
|
||||
tasks={[t.flowDiagram.financialAnalysis, t.flowDiagram.valuationMetrics, t.flowDiagram.profitEvaluation]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Arrow */}
|
||||
<FlowArrow label="分析報告整合" color="purple" />
|
||||
<FlowArrow label={t.flowDiagram.reportIntegration} color="purple" />
|
||||
|
||||
{/* Researchers Layer - 2 agents */}
|
||||
<div>
|
||||
<h3 className="text-center text-sm font-semibold text-gray-500 dark:text-gray-400 mb-4">
|
||||
🔍 第三層:研究員代理 (2位)
|
||||
🔍 {t.flowDiagram.layer3}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-4xl mx-auto">
|
||||
<AgentCard
|
||||
name="多頭研究員"
|
||||
name={t.agents.bull_researcher}
|
||||
icon={<TrendingUp className="w-5 h-5" />}
|
||||
gradient="from-green-500 to-emerald-500"
|
||||
description="看多觀點研究"
|
||||
tasks={["正面因素分析", "成長機會評估", "買入理由整理"]}
|
||||
description={t.flowDiagram.bullishResearch}
|
||||
tasks={[t.flowDiagram.positiveFactors, t.flowDiagram.growthOpportunities, t.flowDiagram.buyReasons]}
|
||||
/>
|
||||
<AgentCard
|
||||
name="空頭研究員"
|
||||
name={t.agents.bear_researcher}
|
||||
icon={<TrendingDown className="w-5 h-5" />}
|
||||
gradient="from-red-500 to-rose-500"
|
||||
description="看空觀點研究"
|
||||
tasks={["負面因素分析", "風險評估", "賣出理由整理"]}
|
||||
description={t.flowDiagram.bearishResearch}
|
||||
tasks={[t.flowDiagram.negativeFactors, t.flowDiagram.riskAssessment, t.flowDiagram.sellReasons]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Arrow */}
|
||||
<FlowArrow label="研究整合與辯論準備" color="green" />
|
||||
<FlowArrow label={t.flowDiagram.researchPrep} color="green" />
|
||||
|
||||
{/* Research Manager */}
|
||||
<div className="max-w-md mx-auto">
|
||||
<ManagerCard
|
||||
name="研究經理"
|
||||
name={t.flowDiagram.researchManager}
|
||||
icon={<Users className="w-6 h-6" />}
|
||||
gradient="from-indigo-500 to-purple-500"
|
||||
description="整合多空研究觀點"
|
||||
tasks={["平衡雙方論點", "綜合投資建議", "制定初步策略"]}
|
||||
description={t.flowDiagram.integrateViews}
|
||||
tasks={[t.flowDiagram.balanceArguments, t.flowDiagram.comprehensiveAdvice, t.flowDiagram.preliminaryStrategy]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Arrow */}
|
||||
<FlowArrow label="進入風險辯論階段" color="orange" />
|
||||
<FlowArrow label={t.flowDiagram.riskDebate} color="orange" />
|
||||
|
||||
{/* Risk Debators Layer - 3 agents */}
|
||||
<div>
|
||||
<h3 className="text-center text-sm font-semibold text-gray-500 dark:text-gray-400 mb-4">
|
||||
⚖️ 第四層:風險辯論者 (3位)
|
||||
⚖️ {t.flowDiagram.layer4}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<AgentCard
|
||||
name="激進辯論者"
|
||||
name={t.agents.aggressive_debator}
|
||||
icon={<ShieldAlert className="w-5 h-5" />}
|
||||
gradient="from-red-500 to-orange-500"
|
||||
description="高風險高報酬"
|
||||
tasks={["積極投資策略", "最大化收益", "承擔計算風險"]}
|
||||
description={t.flowDiagram.highRiskReward}
|
||||
tasks={[t.flowDiagram.aggressiveStrategy, t.flowDiagram.maximizeReturns, t.flowDiagram.calculatedRisk]}
|
||||
/>
|
||||
<AgentCard
|
||||
name="中立辯論者"
|
||||
name={t.agents.neutral_debator}
|
||||
icon={<Shield className="w-5 h-5" />}
|
||||
gradient="from-blue-500 to-indigo-500"
|
||||
description="平衡風險報酬"
|
||||
tasks={["穩健投資策略", "風險平衡", "理性決策"]}
|
||||
description={t.flowDiagram.balancedRisk}
|
||||
tasks={[t.flowDiagram.prudentStrategy, t.flowDiagram.riskBalance, t.flowDiagram.rationalDecision]}
|
||||
/>
|
||||
<AgentCard
|
||||
name="保守辯論者"
|
||||
name={t.agents.conservative_debator}
|
||||
icon={<ShieldCheck className="w-5 h-5" />}
|
||||
gradient="from-green-500 to-teal-500"
|
||||
description="低風險低波動"
|
||||
tasks={["保守投資策略", "資本保護", "降低風險"]}
|
||||
description={t.flowDiagram.lowRiskVol}
|
||||
tasks={[t.flowDiagram.conservativeStrategy, t.flowDiagram.capitalProtection, t.flowDiagram.riskReduction]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Arrow */}
|
||||
<FlowArrow label="風險評估與管理" color="red" />
|
||||
<FlowArrow label={t.flowDiagram.riskDebate} color="red" />
|
||||
|
||||
{/* Risk Manager */}
|
||||
<div className="max-w-md mx-auto">
|
||||
<ManagerCard
|
||||
name="風險經理"
|
||||
name={t.flowDiagram.riskManager}
|
||||
icon={<Shield className="w-6 h-6" />}
|
||||
gradient="from-red-500 to-pink-500"
|
||||
description="整合風險辯論結果"
|
||||
tasks={["風險等級評定", "止損止盈設定", "最終風險控制"]}
|
||||
description={t.flowDiagram.integrateRisk}
|
||||
tasks={[t.flowDiagram.riskRating, t.flowDiagram.stopLossSettings, t.flowDiagram.finalRiskControl]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Arrow */}
|
||||
<FlowArrow label="制定最終交易決策" color="green" />
|
||||
<FlowArrow label={t.flowDiagram.finalDecision} color="green" />
|
||||
|
||||
{/* Trader */}
|
||||
<div className="max-w-md mx-auto">
|
||||
<TraderCard
|
||||
name="交易員"
|
||||
name={t.flowDiagram.trader}
|
||||
icon={<Target className="w-7 h-7" />}
|
||||
gradient="from-blue-600 via-purple-600 to-pink-600"
|
||||
description="執行最終交易決策"
|
||||
outputs={["交易訊號 (BUY/SELL/HOLD)", "目標價位", "交易數量", "風險參數"]}
|
||||
description={t.flowDiagram.executeTrade}
|
||||
outputs={[t.flowDiagram.tradeSignal, t.flowDiagram.targetPrice, t.flowDiagram.tradeQuantity, t.flowDiagram.riskParams]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Final Arrow */}
|
||||
<FlowArrow label="生成完整投資報告" color="blue" />
|
||||
<FlowArrow label={t.flowDiagram.generateReport} color="blue" />
|
||||
|
||||
{/* Output Layer */}
|
||||
<div>
|
||||
<h3 className="text-center text-sm font-semibold text-gray-500 dark:text-gray-400 mb-4">
|
||||
📊 最終輸出:12 份詳細報告
|
||||
📊 {t.flowDiagram.finalOutput}
|
||||
</h3>
|
||||
<Card className="bg-gradient-to-r from-blue-500/10 via-purple-500/10 to-pink-500/10 dark:from-blue-600/20 dark:via-purple-600/20 dark:to-pink-600/20 border-2 border-dashed border-blue-300 dark:border-blue-700 p-6">
|
||||
<div className="text-center mb-4">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 text-white shadow-lg mb-3">
|
||||
<BarChart3 className="w-8 h-8" />
|
||||
</div>
|
||||
<h4 className="font-bold text-lg mb-2 gradient-text-primary">完整分析報告集合</h4>
|
||||
<h4 className="font-bold text-lg mb-2 gradient-text-primary">{t.flowDiagram.completeReportSet}</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
整合 12 位專業代理的深度分析,提供全方位投資決策支援
|
||||
{t.flowDiagram.comprehensiveSupport}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<ReportSection
|
||||
title="分析師報告 (4份)"
|
||||
items={["技術面分析", "社群情緒分析", "新聞面分析", "基本面分析"]}
|
||||
title={t.flowDiagram.analystReports}
|
||||
items={[t.flowDiagram.technicalReport, t.flowDiagram.sentimentReport, t.flowDiagram.newsReport, t.flowDiagram.fundamentalsReport]}
|
||||
color="blue"
|
||||
/>
|
||||
<ReportSection
|
||||
title="研究報告 (3份)"
|
||||
items={["多頭研究報告", "空頭研究報告", "研究經理整合"]}
|
||||
title={t.flowDiagram.researchReports}
|
||||
items={[t.flowDiagram.bullReport, t.flowDiagram.bearReport, t.flowDiagram.researchManagerReport]}
|
||||
color="green"
|
||||
/>
|
||||
<ReportSection
|
||||
title="風險與交易 (5份)"
|
||||
items={["激進策略評估", "中立策略評估", "保守策略評估", "風險經理整合", "最終交易決策"]}
|
||||
title={t.flowDiagram.riskTrading}
|
||||
items={[t.flowDiagram.aggressiveEval, t.flowDiagram.balancedEval, t.flowDiagram.conservativeEval, t.flowDiagram.riskManagerReport, t.flowDiagram.finalTradeDecision]}
|
||||
color="red"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useForm, ControllerRenderProps } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
|
|
@ -12,6 +12,7 @@ import { CheckIcon } from "lucide-react";
|
|||
import { getApiSettingsAsync } from "@/lib/storage";
|
||||
import { getBaseUrlForModel, getApiKeyForModel } from "@/lib/api-helpers";
|
||||
import Image from "next/image";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
@ -90,14 +91,19 @@ interface AnalysisFormProps {
|
|||
loading?: boolean;
|
||||
}
|
||||
|
||||
const ANALYSTS = [
|
||||
{ value: "market", label: "市場分析師" },
|
||||
{ value: "social", label: "社群媒體分析師" },
|
||||
{ value: "news", label: "新聞分析師" },
|
||||
{ value: "fundamentals", label: "基本面分析師" },
|
||||
];
|
||||
// ANALYSTS is now defined inside the component using translations
|
||||
|
||||
export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
||||
const { t, locale } = useLanguage();
|
||||
|
||||
// Define ANALYSTS using translations
|
||||
const ANALYSTS = useMemo(() => [
|
||||
{ value: "market", label: t.agents.market_analyst },
|
||||
{ value: "social", label: t.agents.social_analyst },
|
||||
{ value: "news", label: t.agents.news_analyst },
|
||||
{ value: "fundamentals", label: t.agents.fundamentals_analyst },
|
||||
], [t]);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
|
|
@ -279,6 +285,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
|||
...values,
|
||||
quick_think_llm: finalQuickThinkLlm,
|
||||
deep_think_llm: finalDeepThinkLlm,
|
||||
language: locale as "en" | "zh-TW", // Pass current UI language to backend
|
||||
};
|
||||
onSubmit(request);
|
||||
}
|
||||
|
|
@ -296,7 +303,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
|||
<div className="md:col-span-2 border-b pb-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<FormLabel className="text-base font-semibold">
|
||||
分析師團隊
|
||||
{t.form.analysts}
|
||||
</FormLabel>
|
||||
<Button
|
||||
type="button"
|
||||
|
|
@ -305,8 +312,8 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
|||
onClick={toggleSelectAll}
|
||||
>
|
||||
{form.watch("analysts").length === ANALYSTS.length
|
||||
? "取消全選"
|
||||
: "全選"}
|
||||
? t.form.deselectAll
|
||||
: t.form.selectAll}
|
||||
</Button>
|
||||
</div>
|
||||
<FormField
|
||||
|
|
@ -373,14 +380,14 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
|||
name="market_type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>市場類型</FormLabel>
|
||||
<FormLabel>{t.form.marketType}</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="選擇市場" />
|
||||
<SelectValue placeholder={t.form.selectMarket} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
|
|
@ -388,23 +395,23 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
|||
value="us"
|
||||
className="py-3 cursor-pointer"
|
||||
>
|
||||
🇺🇸 美股
|
||||
🇺🇸 {t.form.usMarket}
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="twse"
|
||||
className="py-3 cursor-pointer"
|
||||
>
|
||||
🇹🇼 台股上市
|
||||
🇹🇼 {t.form.twseMarket}
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="tpex"
|
||||
className="py-3 cursor-pointer"
|
||||
>
|
||||
🇹🇼 台股上櫃/興櫃
|
||||
🇹🇼 {t.form.tpexMarket}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>選擇分析的股票市場</FormDescription>
|
||||
<FormDescription>{t.form.selectMarketDesc}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
|
@ -416,7 +423,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
|||
name="ticker"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>股票代碼</FormLabel>
|
||||
<FormLabel>{t.form.ticker}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={
|
||||
|
|
@ -431,10 +438,10 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
|||
</FormControl>
|
||||
<FormDescription>
|
||||
{marketType === "us"
|
||||
? "輸入美股代碼(例如:NVDA、AAPL)"
|
||||
? t.form.tickerDescUS
|
||||
: marketType === "twse"
|
||||
? "輸入上市股票代碼(例如:2330、2317)"
|
||||
: "輸入上櫃/興櫃股票代碼(例如:6488、5765)"}
|
||||
? t.form.tickerDescTWSE
|
||||
: t.form.tickerDescTPEX}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
|
@ -446,7 +453,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
|||
name="analysis_date"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>分析日期</FormLabel>
|
||||
<FormLabel>{t.form.analysisDate}</FormLabel>
|
||||
<FormControl>
|
||||
<DatePicker
|
||||
date={field.value ? new Date(field.value) : undefined}
|
||||
|
|
@ -455,11 +462,11 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
|||
date ? format(date, "yyyy-MM-dd") : ""
|
||||
);
|
||||
}}
|
||||
placeholder="選擇分析日期"
|
||||
placeholder={t.form.selectDate}
|
||||
className="w-full"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>選擇分析日期</FormDescription>
|
||||
<FormDescription>{t.form.selectDate}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
|
@ -473,7 +480,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
|||
name="research_depth"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>研究深度</FormLabel>
|
||||
<FormLabel>{t.form.researchDepth}</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) =>
|
||||
field.onChange(parseInt(value))
|
||||
|
|
@ -482,22 +489,22 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
|||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="選擇研究深度" />
|
||||
<SelectValue placeholder={t.form.selectDepth} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent className="max-h-80">
|
||||
<SelectItem value="1" className="py-3 cursor-pointer">
|
||||
淺層 - 快速研究
|
||||
{t.form.depthShallowLabel}
|
||||
</SelectItem>
|
||||
<SelectItem value="3" className="py-3 cursor-pointer">
|
||||
中等 - 適度討論
|
||||
{t.form.depthMediumLabel}
|
||||
</SelectItem>
|
||||
<SelectItem value="5" className="py-3 cursor-pointer">
|
||||
深層 - 深入研究
|
||||
{t.form.depthDeepLabel}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>選擇分析深度</FormDescription>
|
||||
<FormDescription>{t.form.selectDepth}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
|
@ -508,7 +515,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
|||
name="quick_think_llm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>快速思維模型</FormLabel>
|
||||
<FormLabel>{t.form.quickThinkModel}</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
|
|
@ -713,11 +720,11 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
|||
|
||||
{/* Custom Model */}
|
||||
<SelectItem value="custom">
|
||||
Other(自訂模型)
|
||||
{t.form.otherCustomModel}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>快速回應模型</FormDescription>
|
||||
<FormDescription>{t.form.quickResponseModel}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
|
@ -748,7 +755,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
|||
name="deep_think_llm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>深層思維模型</FormLabel>
|
||||
<FormLabel>{t.form.deepThinkModel}</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
|
|
@ -953,11 +960,11 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
|||
|
||||
{/* Custom Model */}
|
||||
<SelectItem value="custom">
|
||||
Other(自訂模型)
|
||||
{t.form.otherCustomModel}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>複雜推理模型</FormDescription>
|
||||
<FormDescription>{t.form.complexReasoningModel}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
|
@ -970,7 +977,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
|||
name="custom_deep_think_model"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-3 animate-scale-up">
|
||||
<FormLabel>自訂深層思維模型名稱</FormLabel>
|
||||
<FormLabel>{t.form.customDeepThinkModelName}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="例如:deepseek-chat" {...field} />
|
||||
</FormControl>
|
||||
|
|
@ -989,7 +996,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
|||
name="embedding_model"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>嵌入式模型</FormLabel>
|
||||
<FormLabel>{t.form.embeddingModel}</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
|
|
@ -1031,8 +1038,8 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
|||
</Select>
|
||||
<FormDescription>
|
||||
{isLocalEmbedding
|
||||
? "🆓 本地模型不需 API Key"
|
||||
: "☁️ 需要 OpenAI API Key"}
|
||||
? t.form.localModelNoApiKey
|
||||
: t.form.needsOpenAiApiKey}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
|
@ -1055,7 +1062,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
|||
e.currentTarget.blur();
|
||||
}}
|
||||
>
|
||||
{loading ? "執行分析中..." : "執行分析"}
|
||||
{loading ? t.form.analyzing : t.form.executeAnalysis}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -8,12 +8,15 @@ import remarkGfm from "remark-gfm";
|
|||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import type { Reports } from "@/lib/types";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface AnalystReportProps {
|
||||
reports: Reports;
|
||||
}
|
||||
|
||||
export function AnalystReport({ reports }: AnalystReportProps) {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const hasAnalystReports =
|
||||
reports.market_report ||
|
||||
reports.sentiment_report ||
|
||||
|
|
@ -37,49 +40,49 @@ export function AnalystReport({ reports }: AnalystReportProps) {
|
|||
return (
|
||||
<Card className="shadow-lg hover-lift animate-scale-up">
|
||||
<CardHeader>
|
||||
<CardTitle>分析報告</CardTitle>
|
||||
<CardDescription>來自所有代理團隊的詳細報告</CardDescription>
|
||||
<CardTitle>{t.results.title}</CardTitle>
|
||||
<CardDescription>{t.home.professionalAgentsDesc}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="analysts" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="analysts">分析師</TabsTrigger>
|
||||
<TabsTrigger value="research">研究</TabsTrigger>
|
||||
<TabsTrigger value="trader">交易員</TabsTrigger>
|
||||
<TabsTrigger value="risk">風險</TabsTrigger>
|
||||
<TabsTrigger value="analysts">{t.tabs.analysts}</TabsTrigger>
|
||||
<TabsTrigger value="research">{t.tabs.researchers}</TabsTrigger>
|
||||
<TabsTrigger value="trader">{t.tabs.trader}</TabsTrigger>
|
||||
<TabsTrigger value="risk">{t.tabs.risk}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="analysts" className="space-y-4">
|
||||
{reports.market_report && (
|
||||
<ReportSection title="市場分析" content={reports.market_report} />
|
||||
<ReportSection title={t.agents.market_analyst} content={reports.market_report} />
|
||||
)}
|
||||
{reports.sentiment_report && (
|
||||
<ReportSection title="情緒分析" content={reports.sentiment_report} />
|
||||
<ReportSection title={t.agents.social_analyst} content={reports.sentiment_report} />
|
||||
)}
|
||||
{reports.news_report && (
|
||||
<ReportSection title="新聞分析" content={reports.news_report} />
|
||||
<ReportSection title={t.agents.news_analyst} content={reports.news_report} />
|
||||
)}
|
||||
{reports.fundamentals_report && (
|
||||
<ReportSection title="基本面分析" content={reports.fundamentals_report} />
|
||||
<ReportSection title={t.agents.fundamentals_analyst} content={reports.fundamentals_report} />
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="research" className="space-y-4">
|
||||
{reports.investment_debate_state?.bull_history && (
|
||||
<ReportSection
|
||||
title="看漲研究員"
|
||||
title={t.agents.bull_researcher}
|
||||
content={reports.investment_debate_state.bull_history}
|
||||
/>
|
||||
)}
|
||||
{reports.investment_debate_state?.bear_history && (
|
||||
<ReportSection
|
||||
title="看跌研究員"
|
||||
title={t.agents.bear_researcher}
|
||||
content={reports.investment_debate_state.bear_history}
|
||||
/>
|
||||
)}
|
||||
{reports.investment_debate_state?.judge_decision && (
|
||||
<ReportSection
|
||||
title="研究經理決策"
|
||||
title={t.agents.research_manager}
|
||||
content={reports.investment_debate_state.judge_decision}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -87,32 +90,32 @@ export function AnalystReport({ reports }: AnalystReportProps) {
|
|||
|
||||
<TabsContent value="trader" className="space-y-4">
|
||||
{reports.trader_investment_plan && (
|
||||
<ReportSection title="交易員計劃" content={reports.trader_investment_plan} />
|
||||
<ReportSection title={t.agents.trader} content={reports.trader_investment_plan} />
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="risk" className="space-y-4">
|
||||
{reports.risk_debate_state?.risky_history && (
|
||||
<ReportSection
|
||||
title="激進分析師"
|
||||
title={t.agents.aggressive_debator}
|
||||
content={reports.risk_debate_state.risky_history}
|
||||
/>
|
||||
)}
|
||||
{reports.risk_debate_state?.safe_history && (
|
||||
<ReportSection
|
||||
title="保守分析師"
|
||||
title={t.agents.conservative_debator}
|
||||
content={reports.risk_debate_state.safe_history}
|
||||
/>
|
||||
)}
|
||||
{reports.risk_debate_state?.neutral_history && (
|
||||
<ReportSection
|
||||
title="中立分析師"
|
||||
title={t.agents.neutral_debator}
|
||||
content={reports.risk_debate_state.neutral_history}
|
||||
/>
|
||||
)}
|
||||
{reports.risk_debate_state?.judge_decision && (
|
||||
<ReportSection
|
||||
title="投資組合經理決策"
|
||||
title={t.agents.risk_manager}
|
||||
content={reports.risk_debate_state.judge_decision}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
/**
|
||||
* Download Reports Component
|
||||
* Simple unified PDF download button
|
||||
* Simple unified PDF download button with i18n support
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Download, FileText } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface AnalystInfo {
|
||||
key: string;
|
||||
|
|
@ -25,19 +26,23 @@ interface DownloadReportsProps {
|
|||
priceStats?: any;
|
||||
/** Compact mode - just the button, no card wrapper */
|
||||
compact?: boolean;
|
||||
/** Language for report generation */
|
||||
language?: string;
|
||||
}
|
||||
|
||||
export function DownloadReports({
|
||||
ticker,
|
||||
analysisDate,
|
||||
taskId,
|
||||
taskId: _taskId, // Kept for API compatibility, but direct mode is now preferred
|
||||
analysts,
|
||||
reports,
|
||||
priceData,
|
||||
priceStats,
|
||||
compact = false,
|
||||
language,
|
||||
}: DownloadReportsProps) {
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const { t, locale } = useLanguage();
|
||||
|
||||
// Helper to get nested value from reports object
|
||||
const getNestedValue = (obj: any, path: string) => {
|
||||
|
|
@ -59,22 +64,18 @@ export function DownloadReports({
|
|||
setIsDownloading(true);
|
||||
try {
|
||||
// Build request body with all available analysts
|
||||
// Always use direct mode when reports data is available (task may be cleaned up from Redis)
|
||||
const requestBody: any = {
|
||||
ticker,
|
||||
analysis_date: analysisDate,
|
||||
analysts: availableAnalystKeys, // Always include all available analysts
|
||||
language: language || locale, // Pass language for PDF generation
|
||||
// Direct mode: send report data directly (more reliable than task-based)
|
||||
reports: reports,
|
||||
price_data: priceData,
|
||||
price_stats: priceStats,
|
||||
};
|
||||
|
||||
if (taskId) {
|
||||
// Task-based mode: API will look up reports from task
|
||||
requestBody.task_id = taskId;
|
||||
} else {
|
||||
// Direct mode: send report data directly
|
||||
requestBody.reports = reports;
|
||||
requestBody.price_data = priceData;
|
||||
requestBody.price_stats = priceStats;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/download/reports', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -85,7 +86,7 @@ export function DownloadReports({
|
|||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
const errorMessage = errorData.detail || `下載失敗 (${response.status})`;
|
||||
const errorMessage = errorData.detail || `${t.download.failed} (${response.status})`;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
|
|
@ -116,7 +117,7 @@ export function DownloadReports({
|
|||
window.URL.revokeObjectURL(url);
|
||||
} catch (error: any) {
|
||||
console.error('Download error:', error);
|
||||
alert(error.message || '下載失敗,請稍後再試');
|
||||
alert(error.message || t.download.failed);
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
|
|
@ -138,12 +139,12 @@ export function DownloadReports({
|
|||
{isDownloading ? (
|
||||
<>
|
||||
<Download className="h-4 w-4 animate-bounce" />
|
||||
下載中...
|
||||
{t.download.generating}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="h-4 w-4" />
|
||||
下載 PDF
|
||||
{t.common.download} PDF
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
|
@ -162,12 +163,12 @@ export function DownloadReports({
|
|||
{isDownloading ? (
|
||||
<>
|
||||
<Download className="h-5 w-5 animate-bounce" />
|
||||
生成報告中...
|
||||
{t.download.generating}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="h-5 w-5" />
|
||||
下載完整分析報告 PDF
|
||||
{t.download.fullReport}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from "recharts";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import type { PriceData, PriceStats } from "@/lib/types";
|
||||
|
||||
interface PriceChartProps {
|
||||
|
|
@ -71,6 +72,7 @@ function calculateHeikinAshi(data: PriceData[]): HeikinAshiData[] {
|
|||
}
|
||||
|
||||
export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) {
|
||||
const { t, locale } = useLanguage();
|
||||
const [chartType, setChartType] = useState<"line" | "candlestick">("line");
|
||||
|
||||
// Calculate Heikin Ashi data
|
||||
|
|
@ -78,7 +80,7 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) {
|
|||
|
||||
// 格式化數字
|
||||
const formatNumber = (num: number) => {
|
||||
return num.toLocaleString('zh-TW', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
return num.toLocaleString(locale === 'en' ? 'en-US' : 'zh-TW', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
};
|
||||
|
||||
// 格式化日期(只顯示月-日)
|
||||
|
|
@ -98,15 +100,37 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) {
|
|||
const maxPrice = Math.max(...priceValues);
|
||||
const priceRange = maxPrice - minPrice;
|
||||
|
||||
// Localized labels
|
||||
const labels = {
|
||||
priceTitle: t.results.priceSection.title,
|
||||
growth: t.results.priceSection.growth,
|
||||
duration: t.results.priceSection.duration,
|
||||
days: t.results.priceSection.days,
|
||||
startPrice: t.results.priceSection.startPrice,
|
||||
endPrice: t.results.priceSection.endPrice,
|
||||
lineChart: t.results.priceSection.lineChart,
|
||||
candlestick: t.results.priceSection.candlestick,
|
||||
volume: t.results.volumeChart,
|
||||
closePrice: locale === 'en' ? 'Close' : '收盤價',
|
||||
date: locale === 'en' ? 'Date' : '日期',
|
||||
open: locale === 'en' ? 'Open' : '開',
|
||||
close: locale === 'en' ? 'Close' : '收',
|
||||
high: locale === 'en' ? 'High' : '高',
|
||||
low: locale === 'en' ? 'Low' : '低',
|
||||
up: locale === 'en' ? '↑ Up' : '↑ 上漲',
|
||||
down: locale === 'en' ? '↓ Down' : '↓ 下跌',
|
||||
noChange: locale === 'en' ? '→ No Change' : '→ 無變化',
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full hover-lift animate-scale-up">
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-center">
|
||||
<CardTitle className="text-2xl">{ticker} 價格走勢</CardTitle>
|
||||
<CardTitle className="text-2xl">{ticker} {labels.priceTitle}</CardTitle>
|
||||
<Tabs value={chartType} onValueChange={(v: string) => setChartType(v as "line" | "candlestick")}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="line">折線圖</TabsTrigger>
|
||||
<TabsTrigger value="candlestick">平均K線圖</TabsTrigger>
|
||||
<TabsTrigger value="line">{labels.lineChart}</TabsTrigger>
|
||||
<TabsTrigger value="candlestick">{labels.candlestick}</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
|
@ -114,22 +138,22 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) {
|
|||
{/* 統計資訊 */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
|
||||
<div className="bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 p-4 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<p className="text-sm text-muted-foreground">增長率</p>
|
||||
<p className="text-sm text-muted-foreground">{labels.growth}</p>
|
||||
<p className={`text-2xl font-bold ${priceStats.growth_rate >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{priceStats.growth_rate >= 0 ? '+' : ''}{priceStats.growth_rate}%
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-blue-50 to-cyan-50 dark:from-blue-900/20 dark:to-cyan-900/20 p-4 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<p className="text-sm text-muted-foreground">時長</p>
|
||||
<p className="text-2xl font-bold">{priceStats.duration_days} 天</p>
|
||||
<p className="text-sm text-muted-foreground">{labels.duration}</p>
|
||||
<p className="text-2xl font-bold">{priceStats.duration_days} {labels.days}</p>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 p-4 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<p className="text-sm text-muted-foreground">起始價格</p>
|
||||
<p className="text-sm text-muted-foreground">{labels.startPrice}</p>
|
||||
<p className="text-lg font-semibold">${formatNumber(priceStats.start_price)}</p>
|
||||
<p className="text-xs text-muted-foreground">{priceStats.start_date}</p>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-orange-50 to-amber-50 dark:from-orange-900/20 dark:to-amber-900/20 p-4 rounded-lg border border-orange-200 dark:border-orange-800">
|
||||
<p className="text-sm text-muted-foreground">結束價格</p>
|
||||
<p className="text-sm text-muted-foreground">{labels.endPrice}</p>
|
||||
<p className="text-lg font-semibold">${formatNumber(priceStats.end_price)}</p>
|
||||
<p className="text-xs text-muted-foreground">{priceStats.end_date}</p>
|
||||
</div>
|
||||
|
|
@ -139,7 +163,7 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) {
|
|||
<CardContent className="space-y-6">
|
||||
{/* 價格圖表 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">價格走勢</h3>
|
||||
<h3 className="text-lg font-semibold mb-4">{labels.priceTitle}</h3>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
{chartType === "line" ? (
|
||||
<LineChart data={priceData}>
|
||||
|
|
@ -156,16 +180,16 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) {
|
|||
<Tooltip
|
||||
formatter={(value: number | undefined) => [
|
||||
value !== undefined ? `$${formatNumber(value)}` : '-',
|
||||
'收盤價'
|
||||
labels.closePrice
|
||||
]}
|
||||
labelFormatter={(label) => `日期: ${label}`}
|
||||
labelFormatter={(label) => `${labels.date}: ${label}`}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey={(data: PriceData) => getCloseValue(data)}
|
||||
stroke="#93c5fd"
|
||||
strokeWidth={2}
|
||||
name="收盤價"
|
||||
name={labels.closePrice}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
|
|
@ -192,20 +216,20 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) {
|
|||
|
||||
// Trend color coding for the direction indicator
|
||||
const trendColor = isUp ? 'text-green-600' : isDown ? 'text-red-600' : 'text-gray-600';
|
||||
const direction = isUp ? '↑ 上漲' : isDown ? '↓ 下跌' : '→ 無變化';
|
||||
const direction = isUp ? labels.up : isDown ? labels.down : labels.noChange;
|
||||
|
||||
return (
|
||||
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
|
||||
<p className="text-sm font-semibold mb-2">日期: {data.Date}</p>
|
||||
<p className="text-sm font-semibold mb-2">{labels.date}: {data.Date}</p>
|
||||
<div className="space-y-1 text-sm">
|
||||
<p className="text-purple-600">
|
||||
開: ${formatNumber(data.HA_Open)}
|
||||
{labels.open}: ${formatNumber(data.HA_Open)}
|
||||
</p>
|
||||
<p className="text-cyan-600">
|
||||
收: ${formatNumber(data.HA_Close)}
|
||||
{labels.close}: ${formatNumber(data.HA_Close)}
|
||||
</p>
|
||||
<p className="text-pink-600">高: ${formatNumber(data.HA_High)}</p>
|
||||
<p className="text-amber-600">低: ${formatNumber(data.HA_Low)}</p>
|
||||
<p className="text-pink-600">{labels.high}: ${formatNumber(data.HA_High)}</p>
|
||||
<p className="text-amber-600">{labels.low}: ${formatNumber(data.HA_Low)}</p>
|
||||
<p className={`text-sm mt-2 ${trendColor}`}>
|
||||
{direction} ${formatNumber(Math.abs(data.HA_Close - data.HA_Open))}
|
||||
</p>
|
||||
|
|
@ -228,7 +252,7 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) {
|
|||
|
||||
{/* 交易量圖表 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">交易量</h3>
|
||||
<h3 className="text-lg font-semibold mb-4">{labels.volume}</h3>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<BarChart data={priceData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
|
|
@ -247,11 +271,11 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) {
|
|||
<Tooltip
|
||||
formatter={(value: number | undefined) => [
|
||||
value !== undefined ? value.toLocaleString() : '-',
|
||||
'交易量'
|
||||
labels.volume
|
||||
]}
|
||||
labelFormatter={(label) => `日期: ${label}`}
|
||||
labelFormatter={(label) => `${labels.date}: ${label}`}
|
||||
/>
|
||||
<Bar dataKey="Volume" fill="#93c5fd" name="交易量" />
|
||||
<Bar dataKey="Volume" fill="#93c5fd" name={labels.volume} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import React from "react";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -41,6 +42,7 @@ function GoogleIcon({ className }: { className?: string }) {
|
|||
|
||||
export function LoginButton() {
|
||||
const { user, isLoading, isAuthenticated, login, logout } = useAuth();
|
||||
const { t } = useLanguage();
|
||||
const [loggingOut, setLoggingOut] = React.useState(false);
|
||||
|
||||
const handleLogout = async () => {
|
||||
|
|
@ -56,7 +58,7 @@ export function LoginButton() {
|
|||
return (
|
||||
<Button variant="outline" size="sm" disabled>
|
||||
<div className="w-4 h-4 border-2 border-gray-300 border-t-gray-600 rounded-full animate-spin" />
|
||||
{loggingOut && <span className="ml-2 hidden sm:inline">登出中...</span>}
|
||||
{loggingOut && <span className="ml-2 hidden sm:inline">{t.auth.loggingOut}</span>}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
@ -93,12 +95,12 @@ export function LoginButton() {
|
|||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-green-600">
|
||||
<Cloud className="w-4 h-4 mr-2" />
|
||||
雲端同步已啟用
|
||||
{t.auth.cloudSyncEnabled}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleLogout} className="text-red-600">
|
||||
<LogOut className="w-4 h-4 mr-2" />
|
||||
登出
|
||||
{t.auth.logout}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
|
@ -113,7 +115,7 @@ export function LoginButton() {
|
|||
className="gap-2 bg-white/20 hover:bg-white/30 text-white border-white/30 border"
|
||||
>
|
||||
<GoogleIcon />
|
||||
<span className="hidden sm:inline">登入</span>
|
||||
<span className="hidden sm:inline">{t.auth.login}</span>
|
||||
<CloudOff className="w-3 h-3 text-white/60 sm:hidden" />
|
||||
</Button>
|
||||
);
|
||||
|
|
@ -124,6 +126,7 @@ export function LoginButton() {
|
|||
*/
|
||||
export function LoginPrompt() {
|
||||
const { login, isAuthenticated } = useAuth();
|
||||
const { t } = useLanguage();
|
||||
|
||||
if (isAuthenticated) return null;
|
||||
|
||||
|
|
@ -133,15 +136,15 @@ export function LoginPrompt() {
|
|||
<div className="flex items-center gap-3">
|
||||
<CloudOff className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">目前使用本地儲存</p>
|
||||
<p className="text-sm font-medium">{t.auth.usingLocalStorage}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
登入 Google 帳號以同步 API 設定和歷史報告
|
||||
{t.auth.loginToSync}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={login} className="gap-2 shrink-0">
|
||||
<GoogleIcon />
|
||||
登入同步
|
||||
{t.auth.loginSync}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Header component with mobile-responsive design
|
||||
* Header component with mobile-responsive design and i18n support
|
||||
*/
|
||||
"use client";
|
||||
|
||||
|
|
@ -8,11 +8,14 @@ import Link from "next/link";
|
|||
import { Menu, X } from "lucide-react";
|
||||
import { ThemeToggle } from "@/components/theme/ThemeToggle";
|
||||
import { ApiSettingsDialog } from "@/components/settings/ApiSettingsDialog";
|
||||
import { LanguageSwitcher } from "@/components/settings/LanguageSwitcher";
|
||||
import { LoginButton } from "@/components/auth/login-button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
export function Header() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<header className="border-b bg-gradient-to-r from-blue-500 to-pink-500 dark:from-blue-600 dark:to-purple-600 text-white pwa-safe-header">
|
||||
|
|
@ -22,7 +25,7 @@ export function Header() {
|
|||
<Link href="/" className="flex items-center gap-2 shrink-0">
|
||||
<div className="text-xl md:text-3xl font-bold">TradingAgentsX</div>
|
||||
<div className="hidden lg:block text-sm font-light opacity-90">
|
||||
多代理 LLM 金融交易框架
|
||||
{t.nav.tagline}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
|
|
@ -32,27 +35,29 @@ export function Header() {
|
|||
href="/"
|
||||
className="hover:opacity-80 transition-opacity font-medium text-sm lg:text-base"
|
||||
>
|
||||
首頁
|
||||
{t.nav.home}
|
||||
</Link>
|
||||
<Link
|
||||
href="/analysis"
|
||||
className="hover:opacity-80 transition-opacity font-medium text-sm lg:text-base"
|
||||
>
|
||||
分析
|
||||
{t.nav.analysis}
|
||||
</Link>
|
||||
<Link
|
||||
href="/history"
|
||||
className="hover:opacity-80 transition-opacity font-medium text-sm lg:text-base"
|
||||
>
|
||||
歷史報告
|
||||
{t.nav.history}
|
||||
</Link>
|
||||
<ApiSettingsDialog />
|
||||
<LanguageSwitcher />
|
||||
<ThemeToggle />
|
||||
<LoginButton />
|
||||
</nav>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<div className="flex md:hidden items-center gap-2">
|
||||
<LanguageSwitcher />
|
||||
<ThemeToggle />
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
@ -73,21 +78,21 @@ export function Header() {
|
|||
className="block py-2 hover:opacity-80 transition-opacity font-medium"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
首頁
|
||||
{t.nav.home}
|
||||
</Link>
|
||||
<Link
|
||||
href="/analysis"
|
||||
className="block py-2 hover:opacity-80 transition-opacity font-medium"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
分析
|
||||
{t.nav.analysis}
|
||||
</Link>
|
||||
<Link
|
||||
href="/history"
|
||||
className="block py-2 hover:opacity-80 transition-opacity font-medium"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
歷史報告
|
||||
{t.nav.history}
|
||||
</Link>
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<ApiSettingsDialog />
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { Settings, Cloud, CloudOff } from "lucide-react";
|
|||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
|
|
@ -67,6 +68,7 @@ export function ApiSettingsDialog() {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [syncStatus, setSyncStatus] = useState<"local" | "cloud" | "syncing">("local");
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { t } = useLanguage();
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
|
|
@ -149,35 +151,34 @@ export function ApiSettingsDialog() {
|
|||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-white hover:bg-white/20"
|
||||
title="API 設定"
|
||||
title={t.settings.title}
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>API 配置</DialogTitle>
|
||||
<DialogTitle>{t.settings.apiConfiguration}</DialogTitle>
|
||||
<DialogDescription>
|
||||
設定您的 API 金鑰。這些資訊會以加密形式儲存在瀏覽器中。
|
||||
{t.settings.description}
|
||||
<span className="block mt-1 text-xs text-green-600 dark:text-green-400">
|
||||
🔒 已啟用 AES-256-GCM 加密保護
|
||||
{t.settings.encryptionEnabled}
|
||||
</span>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
{/* 注意事項 */}
|
||||
<div className="space-y-2">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3 text-blue-800 dark:text-blue-300 text-sm">
|
||||
💡 僅需填寫您選擇的模型供應商的 API。例如,若使用 Claude 模型,只需填寫 Claude API。
|
||||
{t.settings.onlyFillNeeded}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stock Market Data APIs Section */}
|
||||
<div className="space-y-4 border-t pt-4">
|
||||
<h3 className="text-lg font-semibold text-muted-foreground">
|
||||
股市資料 API(依分析市場選擇填寫)
|
||||
{t.settings.stockMarketApis}
|
||||
</h3>
|
||||
|
||||
{/* FinMind API Key - Taiwan Stocks */}
|
||||
|
|
@ -186,16 +187,16 @@ export function ApiSettingsDialog() {
|
|||
name="finmind_api_key"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>FinMind API Token(台股)</FormLabel>
|
||||
<FormLabel>{t.settings.finmindToken}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="輸入 FinMind Token"
|
||||
placeholder={t.settings.finmindPlaceholder}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
用於獲取台灣股市資料(在 finmindtrade.com 註冊取得)
|
||||
{t.settings.finmindDesc}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
|
@ -208,16 +209,16 @@ export function ApiSettingsDialog() {
|
|||
name="alpha_vantage_api_key"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Alpha Vantage API Key(美股)</FormLabel>
|
||||
<FormLabel>{t.settings.alphaVantageKey}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="輸入 Alpha Vantage API Key"
|
||||
placeholder={t.settings.alphaVantagePlaceholder}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
用於獲取美股基本面數據(分析美股時建議填寫)
|
||||
{t.settings.alphaVantageDesc}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
|
@ -228,7 +229,7 @@ export function ApiSettingsDialog() {
|
|||
{/* LLM Providers Section */}
|
||||
<div className="space-y-4 border-t pt-4">
|
||||
<h3 className="text-lg font-semibold text-muted-foreground">
|
||||
LLM 模型供應商(依選擇的模型填寫)
|
||||
{t.settings.llmProviders}
|
||||
</h3>
|
||||
|
||||
{/* OpenAI API Key */}
|
||||
|
|
@ -242,7 +243,7 @@ export function ApiSettingsDialog() {
|
|||
<Input type="password" placeholder="sk-..." {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
用於 OpenAI 模型(GPT-4, GPT-5, o4 等)及 OpenAI 嵌入式模型
|
||||
{t.settings.openaiDesc}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
|
@ -257,7 +258,7 @@ export function ApiSettingsDialog() {
|
|||
<FormControl>
|
||||
<Input type="password" placeholder="sk-..." {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>用於 Claude 模型</FormDescription>
|
||||
<FormDescription>{t.settings.anthropicDesc}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
|
@ -273,7 +274,7 @@ export function ApiSettingsDialog() {
|
|||
<FormControl>
|
||||
<Input type="password" placeholder="..." {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>用於 Gemini 模型</FormDescription>
|
||||
<FormDescription>{t.settings.googleDesc}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
|
@ -289,7 +290,7 @@ export function ApiSettingsDialog() {
|
|||
<FormControl>
|
||||
<Input type="password" placeholder="xai-..." {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>用於 Grok 模型</FormDescription>
|
||||
<FormDescription>{t.settings.grokDesc}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
|
@ -305,7 +306,7 @@ export function ApiSettingsDialog() {
|
|||
<FormControl>
|
||||
<Input type="password" placeholder="sk-..." {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>用於 DeepSeek 模型</FormDescription>
|
||||
<FormDescription>{t.settings.deepseekDesc}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
|
@ -321,7 +322,7 @@ export function ApiSettingsDialog() {
|
|||
<FormControl>
|
||||
<Input type="password" placeholder="sk-..." {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>用於 Qwen 模型</FormDescription>
|
||||
<FormDescription>{t.settings.qwenDesc}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
|
@ -331,7 +332,7 @@ export function ApiSettingsDialog() {
|
|||
{/* Custom Endpoint Section */}
|
||||
<div className="space-y-4 border-t pt-4">
|
||||
<h3 className="text-lg font-semibold text-muted-foreground">
|
||||
自訂端點(進階選項)
|
||||
{t.settings.customEndpoint}
|
||||
</h3>
|
||||
|
||||
{/* Custom Base URL */}
|
||||
|
|
@ -340,7 +341,7 @@ export function ApiSettingsDialog() {
|
|||
name="custom_base_url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>自訂 Base URL</FormLabel>
|
||||
<FormLabel>{t.settings.customBaseUrl}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
|
|
@ -349,7 +350,7 @@ export function ApiSettingsDialog() {
|
|||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
若設定此項,將覆蓋所有模型的預設端點
|
||||
{t.settings.customBaseUrlDesc}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
|
@ -362,16 +363,16 @@ export function ApiSettingsDialog() {
|
|||
name="custom_api_key"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>自訂端點 API Key</FormLabel>
|
||||
<FormLabel>{t.settings.customApiKey}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="輸入自訂端點的 API Key"
|
||||
placeholder={t.settings.customApiKeyPlaceholder}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
配合自訂 Base URL 使用的 API Key
|
||||
{t.settings.customApiKeyDesc}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
|
@ -381,13 +382,13 @@ export function ApiSettingsDialog() {
|
|||
|
||||
{saveSuccess && (
|
||||
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-3 text-green-800 dark:text-green-300 text-sm">
|
||||
✓ 設定已成功儲存
|
||||
{t.settings.settingsSaved}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 pt-4">
|
||||
<Button type="submit" className="flex-1" disabled={loading}>
|
||||
{loading ? "處理中..." : "儲存設定"}
|
||||
{loading ? t.settings.processing : t.settings.saveSettings}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
|
|
@ -395,7 +396,7 @@ export function ApiSettingsDialog() {
|
|||
onClick={handleClear}
|
||||
className="flex-1"
|
||||
>
|
||||
清除設定
|
||||
{t.settings.clearSettings}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
"use client";
|
||||
|
||||
import { Globe } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { Locale, localeNames } from "@/lib/i18n";
|
||||
|
||||
export function LanguageSwitcher() {
|
||||
const { locale, setLocale } = useLanguage();
|
||||
|
||||
const locales: Locale[] = ['en', 'zh-TW'];
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-white hover:bg-white/20"
|
||||
aria-label="Switch language"
|
||||
>
|
||||
<Globe className="h-5 w-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{locales.map((loc) => (
|
||||
<DropdownMenuItem
|
||||
key={loc}
|
||||
onClick={() => setLocale(loc)}
|
||||
className={locale === loc ? "bg-accent" : ""}
|
||||
>
|
||||
{localeNames[loc]}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
|
@ -10,10 +10,12 @@ import {
|
|||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Avoid hydration mismatch
|
||||
useEffect(() => {
|
||||
|
|
@ -35,21 +37,21 @@ export function ThemeToggle() {
|
|||
{theme === "light" && <Sun className="h-4 w-4" />}
|
||||
{theme === "dark" && <Moon className="h-4 w-4" />}
|
||||
{theme === "system" && <Monitor className="h-4 w-4" />}
|
||||
<span className="sr-only">切換主題</span>
|
||||
<span className="sr-only">{t.theme.toggle}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
<Sun className="mr-2 h-4 w-4" />
|
||||
<span>亮色</span>
|
||||
<span>{t.theme.light}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
<Moon className="mr-2 h-4 w-4" />
|
||||
<span>暗色</span>
|
||||
<span>{t.theme.dark}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
<Monitor className="mr-2 h-4 w-4" />
|
||||
<span>系統</span>
|
||||
<span>{t.theme.system}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
"use client";
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode, useSyncExternalStore } from 'react';
|
||||
import { translations, Locale, defaultLocale, TranslationKeys } from '@/lib/i18n';
|
||||
|
||||
interface LanguageContextType {
|
||||
locale: Locale;
|
||||
setLocale: (locale: Locale) => void;
|
||||
t: TranslationKeys;
|
||||
}
|
||||
|
||||
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
||||
|
||||
const STORAGE_KEY = 'tradingagentsx-locale';
|
||||
|
||||
// Helper to safely get localStorage value
|
||||
function getStoredLocale(): Locale {
|
||||
if (typeof window === 'undefined') {
|
||||
return defaultLocale;
|
||||
}
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored === 'en' || stored === 'zh-TW') {
|
||||
return stored;
|
||||
}
|
||||
return defaultLocale;
|
||||
}
|
||||
|
||||
// Subscribe to storage events for cross-tab sync
|
||||
function subscribeToStorage(callback: () => void) {
|
||||
window.addEventListener('storage', callback);
|
||||
return () => window.removeEventListener('storage', callback);
|
||||
}
|
||||
|
||||
// Server snapshot always returns default locale
|
||||
function getServerSnapshot(): Locale {
|
||||
return defaultLocale;
|
||||
}
|
||||
|
||||
export function LanguageProvider({ children }: { children: ReactNode }) {
|
||||
// Use useSyncExternalStore for proper SSR hydration
|
||||
const storedLocale = useSyncExternalStore(
|
||||
subscribeToStorage,
|
||||
getStoredLocale,
|
||||
getServerSnapshot
|
||||
);
|
||||
|
||||
const [locale, setLocaleState] = useState<Locale>(storedLocale);
|
||||
|
||||
// Sync with stored value when it changes (e.g., from another tab)
|
||||
useEffect(() => {
|
||||
setLocaleState(storedLocale);
|
||||
}, [storedLocale]);
|
||||
|
||||
// Save locale to localStorage when it changes
|
||||
const setLocale = useCallback((newLocale: Locale) => {
|
||||
setLocaleState(newLocale);
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(STORAGE_KEY, newLocale);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Get translations for current locale
|
||||
const t = translations[locale];
|
||||
|
||||
const value: LanguageContextType = {
|
||||
locale,
|
||||
setLocale,
|
||||
t,
|
||||
};
|
||||
|
||||
return (
|
||||
<LanguageContext.Provider value={value}>
|
||||
{children}
|
||||
</LanguageContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLanguage(): LanguageContextType {
|
||||
const context = useContext(LanguageContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useLanguage must be used within a LanguageProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
// Convenience hook for just getting translations
|
||||
export function useTranslation(): TranslationKeys {
|
||||
const { t } = useLanguage();
|
||||
return t;
|
||||
}
|
||||
|
|
@ -0,0 +1,554 @@
|
|||
/**
|
||||
* English translations for TradingAgentsX
|
||||
*/
|
||||
export const en = {
|
||||
// Common
|
||||
common: {
|
||||
loading: "Loading...",
|
||||
error: "Error",
|
||||
save: "Save",
|
||||
cancel: "Cancel",
|
||||
confirm: "Confirm",
|
||||
download: "Download",
|
||||
delete: "Delete",
|
||||
back: "Back",
|
||||
next: "Next",
|
||||
submit: "Submit",
|
||||
close: "Close",
|
||||
},
|
||||
|
||||
// Theme
|
||||
theme: {
|
||||
toggle: "Toggle theme",
|
||||
light: "Light",
|
||||
dark: "Dark",
|
||||
system: "System",
|
||||
},
|
||||
|
||||
// Auth / Login
|
||||
auth: {
|
||||
login: "Login",
|
||||
logout: "Logout",
|
||||
loggingOut: "Logging out...",
|
||||
cloudSyncEnabled: "Cloud sync enabled",
|
||||
usingLocalStorage: "Using local storage",
|
||||
loginToSync: "Login with Google to sync API settings and history reports",
|
||||
loginSync: "Login to sync",
|
||||
},
|
||||
|
||||
// API Settings Dialog
|
||||
settings: {
|
||||
title: "API Settings",
|
||||
apiConfiguration: "API Configuration",
|
||||
description: "Configure your API keys. They are stored encrypted in your browser.",
|
||||
encryptionEnabled: "🔒 AES-256-GCM encryption enabled",
|
||||
onlyFillNeeded: "💡 Only fill in the API keys for the model providers you want to use. For example, if using Claude, only fill in the Claude API key.",
|
||||
|
||||
// Sections
|
||||
stockMarketApis: "Stock Market Data APIs (fill based on market)",
|
||||
llmProviders: "LLM Model Providers (fill based on selected model)",
|
||||
customEndpoint: "Custom Endpoint (Advanced)",
|
||||
|
||||
// Form labels
|
||||
finmindToken: "FinMind API Token (Taiwan Stocks)",
|
||||
finmindPlaceholder: "Enter FinMind Token",
|
||||
finmindDesc: "For Taiwan stock data (register at finmindtrade.com)",
|
||||
alphaVantageKey: "Alpha Vantage API Key (US Stocks)",
|
||||
alphaVantagePlaceholder: "Enter Alpha Vantage API Key",
|
||||
alphaVantageDesc: "For US stock fundamental data (recommended for US stocks)",
|
||||
openaiDesc: "For OpenAI models (GPT-4, GPT-5, o4, etc.) and OpenAI embeddings",
|
||||
anthropicDesc: "For Claude models",
|
||||
googleDesc: "For Gemini models",
|
||||
grokDesc: "For Grok models",
|
||||
deepseekDesc: "For DeepSeek models",
|
||||
qwenDesc: "For Qwen models",
|
||||
customBaseUrl: "Custom Base URL",
|
||||
customBaseUrlDesc: "If set, this will override the default endpoint for all models",
|
||||
customApiKey: "Custom Endpoint API Key",
|
||||
customApiKeyPlaceholder: "Enter custom endpoint API Key",
|
||||
customApiKeyDesc: "API Key for use with custom Base URL",
|
||||
|
||||
// Buttons and messages
|
||||
saveSettings: "Save Settings",
|
||||
clearSettings: "Clear Settings",
|
||||
processing: "Processing...",
|
||||
settingsSaved: "✓ Settings saved successfully",
|
||||
},
|
||||
|
||||
// Navigation
|
||||
nav: {
|
||||
home: "Home",
|
||||
analysis: "Analysis",
|
||||
history: "History",
|
||||
settings: "Settings",
|
||||
tagline: "Multi-Agent LLM Trading Framework",
|
||||
},
|
||||
|
||||
// Home page
|
||||
home: {
|
||||
title: "TradingAgentsX",
|
||||
subtitle: "Multi-Agent LLM Trading Analysis Platform",
|
||||
description: "Powered by multi-agent AI collaboration for comprehensive investment analysis and insights",
|
||||
startAnalysis: "Start Analysis",
|
||||
viewHistory: "View History",
|
||||
|
||||
// Core features section
|
||||
coreFeatures: "Core Features",
|
||||
coreFeaturesDesc: "Intelligent stock trading analysis platform based on LangGraph, combining multiple AI agents for collaborative decision-making",
|
||||
|
||||
// Feature cards
|
||||
features: {
|
||||
multiAgent: "Multi-Agent Collaboration",
|
||||
multiAgentDesc: "12 specialized AI agent teams working together, simulating real trading firm operations",
|
||||
multiModel: "Multi-Model Support",
|
||||
multiModelDesc: "Support for OpenAI, Claude, Gemini, Grok, DeepSeek, Qwen and more LLMs",
|
||||
customEndpoint: "Custom Endpoint Configuration",
|
||||
customEndpointDesc: "Full support for custom API endpoints, connect to any OpenAI-compatible service",
|
||||
fullAnalysis: "Comprehensive Market Analysis",
|
||||
fullAnalysisDesc: "Integrated technical, fundamental, sentiment, and news analysis across four dimensions",
|
||||
structuredDecision: "Structured Decision Process",
|
||||
structuredDecisionDesc: "Bull/Bear debate mechanism reduces bias for more rational decisions",
|
||||
longTermMemory: "Long-Term Memory System",
|
||||
longTermMemoryDesc: "ChromaDB vector database stores historical decisions for continuous learning",
|
||||
modernUI: "Modern Web Interface",
|
||||
modernUIDesc: "Responsive UI based on Next.js 16, supports dark mode",
|
||||
oneClickDeploy: "One-Click Deploy",
|
||||
oneClickDeployDesc: "Docker Compose deployment support, quickly launch complete service",
|
||||
reportDownload: "Report Download",
|
||||
reportDownloadDesc: "Export full analysis reports to PDF for easy saving and sharing",
|
||||
},
|
||||
|
||||
// 12 Professional Agents Section
|
||||
professionalAgents: "12 Professional Agent Teams",
|
||||
professionalAgentsDesc: "Each agent has specialized responsibilities, collaborating to produce high-quality trading decisions",
|
||||
analystsTeamTitle: "Analysts Team (4)",
|
||||
researchTeamTitle: "Research Team (3)",
|
||||
tradingTeamTitle: "Trading Team (1)",
|
||||
riskTeamTitle: "Risk Management Team (4)",
|
||||
|
||||
// Workflow section
|
||||
workflowTitle: "Analyst Collaboration Flow",
|
||||
workflowDescription: "How the four analyst agents collect information from different data sources and produce comprehensive analysis reports",
|
||||
|
||||
// Process steps
|
||||
processSteps: {
|
||||
dataCollection: { title: "Data Collection Phase", description: "Gather stock prices, news, social sentiment from yfinance, Reddit, RSS and more" },
|
||||
analysts: { title: "Parallel Analysis by Analysts Team", description: "Market, Sentiment, News, and Fundamentals analysts evaluate simultaneously, producing professional reports" },
|
||||
researchers: { title: "Research Team Debate", description: "Bullish and Bearish researchers conduct structured debate, Research Manager synthesizes both views" },
|
||||
trader: { title: "Trader Integration", description: "Reviews all analyst and research reports, formulates specific trading execution plan" },
|
||||
risk: { title: "Risk Management Assessment", description: "Aggressive, Conservative, and Neutral risk analysts evaluate strategy, Risk Manager makes final call" },
|
||||
finalDecision: { title: "Final Decision Output", description: "Produces complete investment recommendation with trading direction, position size, and risk controls" },
|
||||
},
|
||||
|
||||
// LLM Support Section
|
||||
llmSupport: "Multi-Model Support",
|
||||
llmSupportDesc: "Support for multiple industry-leading LLM providers, each model can be configured with independent API Key and Base URL",
|
||||
llmFeatures: "Full custom endpoint support | Three-tier independent config (Quick Think/Deep Think/Embedding) | BYOK mode",
|
||||
|
||||
// Technical Highlights
|
||||
techHighlights: "Technical Highlights",
|
||||
dynamicResearch: "Dynamic Research Depth",
|
||||
dynamicResearchFeatures: ["Shallow: 1 round quick analysis", "Medium: 2 rounds balanced analysis", "Deep: 3+ rounds comprehensive analysis"],
|
||||
memorySystem: "Long-Term Memory System",
|
||||
memorySystemFeatures: ["ChromaDB vector database", "Historical decision persistence", "Continuous learning and improvement"],
|
||||
realTimeData: "Real-Time Data Integration",
|
||||
realTimeDataFeatures: ["yfinance: Real-time stock data", "Reddit API: Social sentiment", "Alpha Vantage: Financial data"],
|
||||
fullApiSupport: "Full API Support",
|
||||
fullApiSupportFeatures: ["RESTful API architecture", "Async task processing", "Swagger interactive docs"],
|
||||
|
||||
// CTA Section
|
||||
readyToStart: "Ready to Start Smart Trading Analysis?",
|
||||
ctaDescription: "Experience 12 professional AI agents working together to provide comprehensive stock analysis reports",
|
||||
},
|
||||
|
||||
// Agents
|
||||
agents: {
|
||||
// Analysts
|
||||
market_analyst: "Market Analyst",
|
||||
market_analyst_role: "Technical Analysis",
|
||||
market_analyst_desc: "Technical indicators (RSI, MACD, Bollinger Bands), price trend analysis, support/resistance identification",
|
||||
|
||||
social_analyst: "Social Media Analyst",
|
||||
social_analyst_role: "Sentiment Analysis",
|
||||
social_analyst_desc: "Social sentiment monitoring, market atmosphere assessment, trending topic analysis",
|
||||
|
||||
news_analyst: "News Analyst",
|
||||
news_analyst_role: "News Analysis",
|
||||
news_analyst_desc: "News event tracking, impact assessment, information filtering and prioritization",
|
||||
|
||||
fundamentals_analyst: "Fundamentals Analyst",
|
||||
fundamentals_analyst_role: "Fundamental Analysis",
|
||||
fundamentals_analyst_desc: "Financial data analysis, valuation metrics, company fundamentals evaluation",
|
||||
|
||||
// Researchers
|
||||
bull_researcher: "Bull Researcher",
|
||||
bull_researcher_role: "Bullish Analysis",
|
||||
bull_researcher_desc: "Identifies upside potential, growth catalysts, and bullish scenarios",
|
||||
|
||||
bear_researcher: "Bear Researcher",
|
||||
bear_researcher_role: "Bearish Analysis",
|
||||
bear_researcher_desc: "Identifies downside risks, warning signals, and bearish scenarios",
|
||||
|
||||
// Managers
|
||||
research_manager: "Research Manager",
|
||||
research_manager_role: "Research Synthesis",
|
||||
research_manager_desc: "Synthesizes bull and bear arguments, produces balanced research conclusion",
|
||||
|
||||
risk_manager: "Risk Manager",
|
||||
risk_manager_role: "Risk Decision",
|
||||
risk_manager_desc: "Final risk assessment and position sizing recommendation",
|
||||
|
||||
// Risk Debaters
|
||||
aggressive_debator: "Aggressive Analyst",
|
||||
aggressive_debator_role: "High Risk/Reward",
|
||||
aggressive_debator_desc: "Advocates for higher risk positions with greater potential returns",
|
||||
|
||||
conservative_debator: "Conservative Analyst",
|
||||
conservative_debator_role: "Capital Preservation",
|
||||
conservative_debator_desc: "Advocates for safer positions with capital protection focus",
|
||||
|
||||
neutral_debator: "Neutral Analyst",
|
||||
neutral_debator_role: "Balanced View",
|
||||
neutral_debator_desc: "Provides balanced perspective between aggressive and conservative views",
|
||||
|
||||
// Trader
|
||||
trader: "Trader",
|
||||
trader_role: "Trade Execution",
|
||||
trader_desc: "Formulates final trading recommendation with entry, exit, and position sizing",
|
||||
},
|
||||
|
||||
// Flow Diagram
|
||||
flowDiagram: {
|
||||
// Layer titles
|
||||
layer1: "Layer 1: Data Sources",
|
||||
layer2: "Layer 2: Analyst Agents (4)",
|
||||
layer3: "Layer 3: Researcher Agents (2)",
|
||||
layer4: "Layer 4: Risk Debaters (3)",
|
||||
finalOutput: "Final Output: 12 Detailed Reports",
|
||||
|
||||
// Data sources
|
||||
stockData: "Stock Data",
|
||||
socialSentiment: "Social Sentiment",
|
||||
newsInfo: "News Info",
|
||||
financialData: "Financial Data",
|
||||
|
||||
// Arrow labels
|
||||
dataFetch: "Data Fetching & Cleaning",
|
||||
reportIntegration: "Report Integration",
|
||||
researchPrep: "Research Integration & Debate Prep",
|
||||
riskDebate: "Risk Assessment & Management",
|
||||
finalDecision: "Final Trading Decision",
|
||||
generateReport: "Generate Complete Investment Report",
|
||||
|
||||
// Agent descriptions
|
||||
technicalAnalysis: "Technical Analysis",
|
||||
sentimentAnalysis: "Sentiment Analysis",
|
||||
newsAnalysis: "News Analysis",
|
||||
fundamentalsAnalysis: "Fundamentals Analysis",
|
||||
bullishResearch: "Bullish View Research",
|
||||
bearishResearch: "Bearish View Research",
|
||||
integrateViews: "Integrate Bull/Bear Views",
|
||||
highRiskReward: "High Risk/High Return",
|
||||
balancedRisk: "Balanced Risk/Return",
|
||||
lowRiskVol: "Low Risk/Low Volatility",
|
||||
integrateRisk: "Integrate Risk Debate Results",
|
||||
executeTrade: "Execute Final Trading Decision",
|
||||
|
||||
// Tasks
|
||||
rsiIndicator: "RSI Indicator",
|
||||
macdMomentum: "MACD Momentum",
|
||||
priceTrend: "Price Trend",
|
||||
nlpSentiment: "NLP Sentiment",
|
||||
discussionHeat: "Discussion Heat",
|
||||
investorConfidence: "Investor Confidence",
|
||||
newsSummary: "News Summary",
|
||||
eventAssessment: "Event Assessment",
|
||||
impactPrediction: "Impact Prediction",
|
||||
financialAnalysis: "Financial Analysis",
|
||||
valuationMetrics: "Valuation Metrics",
|
||||
profitEvaluation: "Profit Evaluation",
|
||||
positiveFactors: "Positive Factors Analysis",
|
||||
growthOpportunities: "Growth Opportunities",
|
||||
buyReasons: "Buy Reasons Summary",
|
||||
negativeFactors: "Negative Factors Analysis",
|
||||
riskAssessment: "Risk Assessment",
|
||||
sellReasons: "Sell Reasons Summary",
|
||||
balanceArguments: "Balance Both Arguments",
|
||||
comprehensiveAdvice: "Comprehensive Investment Advice",
|
||||
preliminaryStrategy: "Preliminary Strategy",
|
||||
aggressiveStrategy: "Aggressive Investment Strategy",
|
||||
maximizeReturns: "Maximize Returns",
|
||||
calculatedRisk: "Take Calculated Risks",
|
||||
prudentStrategy: "Prudent Investment Strategy",
|
||||
riskBalance: "Risk Balance",
|
||||
rationalDecision: "Rational Decision",
|
||||
conservativeStrategy: "Conservative Investment Strategy",
|
||||
capitalProtection: "Capital Protection",
|
||||
riskReduction: "Risk Reduction",
|
||||
riskRating: "Risk Level Rating",
|
||||
stopLossSettings: "Stop Loss/Take Profit Settings",
|
||||
finalRiskControl: "Final Risk Control",
|
||||
|
||||
// Outputs
|
||||
tradeSignal: "Trade Signal (BUY/SELL/HOLD)",
|
||||
targetPrice: "Target Price",
|
||||
tradeQuantity: "Trade Quantity",
|
||||
riskParams: "Risk Parameters",
|
||||
finalOutput_label: "Final Output:",
|
||||
completeReportSet: "Complete Analysis Report Set",
|
||||
comprehensiveSupport: "Integrating 12 professional agents for comprehensive investment decision support",
|
||||
|
||||
// Report sections
|
||||
analystReports: "Analyst Reports (4)",
|
||||
researchReports: "Research Reports (3)",
|
||||
riskTrading: "Risk & Trading (5)",
|
||||
technicalReport: "Technical Analysis",
|
||||
sentimentReport: "Sentiment Analysis",
|
||||
newsReport: "News Analysis",
|
||||
fundamentalsReport: "Fundamentals Analysis",
|
||||
bullReport: "Bull Research Report",
|
||||
bearReport: "Bear Research Report",
|
||||
researchManagerReport: "Research Manager Integration",
|
||||
aggressiveEval: "Aggressive Strategy Evaluation",
|
||||
balancedEval: "Balanced Strategy Evaluation",
|
||||
conservativeEval: "Conservative Strategy Evaluation",
|
||||
riskManagerReport: "Risk Manager Integration",
|
||||
finalTradeDecision: "Final Trading Decision",
|
||||
|
||||
// Manager titles
|
||||
researchManager: "Research Manager",
|
||||
riskManager: "Risk Manager",
|
||||
trader: "Trader",
|
||||
},
|
||||
|
||||
// Analysis form
|
||||
form: {
|
||||
ticker: "Stock Ticker",
|
||||
tickerPlaceholder: "Enter stock symbol (e.g., AAPL, 2330)",
|
||||
analysisDate: "Analysis Date",
|
||||
analysts: "Analyst Team",
|
||||
selectAnalysts: "Select analysts to include",
|
||||
selectAll: "Select All",
|
||||
deselectAll: "Deselect All",
|
||||
startAnalysis: "Start Analysis",
|
||||
analyzing: "Analyzing...",
|
||||
|
||||
// Analysis page
|
||||
analysisTitle: "Trading Analysis",
|
||||
analysisSubtitle: "Configure and execute comprehensive multi-agent trading analysis",
|
||||
analysisLoading: "Running analysis... This may take a few minutes.",
|
||||
|
||||
advancedOptions: "Advanced Options",
|
||||
researchDepth: "Research Depth",
|
||||
riskDebateRounds: "Risk Debate Rounds",
|
||||
|
||||
// Market types
|
||||
marketType: "Market Type",
|
||||
usMarket: "US Stocks",
|
||||
twseMarket: "TWSE Listed",
|
||||
tpexMarket: "TPEX/Emerging",
|
||||
selectMarket: "Select Market",
|
||||
selectMarketDesc: "Select the stock market to analyze",
|
||||
|
||||
// LLM Configuration
|
||||
llmSettings: "LLM Settings",
|
||||
quickThinkModel: "Quick Think Model",
|
||||
deepThinkModel: "Deep Think Model",
|
||||
embeddingModel: "Embedding Model",
|
||||
customModel: "Custom Model",
|
||||
customModelName: "Custom Model Name",
|
||||
customDeepThinkModelName: "Custom Deep Think Model Name",
|
||||
executeAnalysis: "Execute Analysis",
|
||||
otherCustomModel: "Other (Custom Model)",
|
||||
quickResponseModel: "Quick response model",
|
||||
complexReasoningModel: "Complex reasoning model",
|
||||
localModelNoApiKey: "🆓 Local model - No API Key needed",
|
||||
needsOpenAiApiKey: "☁️ Requires OpenAI API Key",
|
||||
|
||||
// Depth levels
|
||||
depthShallow: "Shallow (1 round)",
|
||||
depthMedium: "Medium (2 rounds)",
|
||||
depthDeep: "Deep (3+ rounds)",
|
||||
|
||||
// API Keys
|
||||
apiKeySection: "API Configuration",
|
||||
alphaVantageKey: "Alpha Vantage API Key",
|
||||
finmindKey: "FinMind API Key",
|
||||
|
||||
// Ticker descriptions by market
|
||||
tickerDescUS: "Enter US stock symbol (e.g., NVDA, AAPL)",
|
||||
tickerDescTWSE: "Enter TWSE stock code (e.g., 2330, 2317)",
|
||||
tickerDescTPEX: "Enter TPEx stock code (e.g., 6488, 5765)",
|
||||
selectDate: "Select analysis date",
|
||||
selectDepth: "Select research depth",
|
||||
|
||||
// Depth options
|
||||
depthShallowLabel: "Shallow - Quick research",
|
||||
depthMediumLabel: "Medium - Moderate discussion",
|
||||
depthDeepLabel: "Deep - In-depth research",
|
||||
|
||||
// Validation messages
|
||||
validation: {
|
||||
tickerRequired: "Stock ticker is required",
|
||||
dateFormat: "Date format must be YYYY-MM-DD",
|
||||
selectOneAnalyst: "Please select at least one analyst",
|
||||
selectQuickThink: "Please select a quick think model",
|
||||
selectDeepThink: "Please select a deep think model",
|
||||
selectEmbedding: "Please select an embedding model",
|
||||
invalidUrl: "Please enter a valid URL",
|
||||
},
|
||||
},
|
||||
|
||||
// Analysis results
|
||||
results: {
|
||||
title: "Analysis Results",
|
||||
detailedResults: "Detailed Analysis Results",
|
||||
analysisDate: "Analysis Date",
|
||||
ticker: "Ticker",
|
||||
date: "Date",
|
||||
summary: "Summary",
|
||||
recommendation: "Trading Recommendation",
|
||||
priceChart: "Price Chart",
|
||||
volumeChart: "Volume Chart",
|
||||
noData: "No data available",
|
||||
noResults: "No Analysis Results",
|
||||
runAnalysisFirst: "Please run analysis first",
|
||||
backToAnalysis: "Back to Analysis",
|
||||
backButton: "Back to Analysis",
|
||||
saveReport: "Save Report",
|
||||
saving: "Saving...",
|
||||
saved: "Saved",
|
||||
saveError: "Save failed, please try again",
|
||||
duplicateReport: "This report already exists (same ticker and date)",
|
||||
report: "Report",
|
||||
noReportGenerated: "No report generated for this analyst",
|
||||
notSelectedOrNoReport: "Analyst may not have been selected or did not produce a report",
|
||||
|
||||
// Analyst tabs
|
||||
analysts: {
|
||||
market: "Market Analyst",
|
||||
marketDesc: "Technical analysis and market trends",
|
||||
social: "Social Media Analyst",
|
||||
socialDesc: "Social sentiment and market atmosphere",
|
||||
news: "News Analyst",
|
||||
newsDesc: "News events and impact analysis",
|
||||
fundamentals: "Fundamentals Analyst",
|
||||
fundamentalsDesc: "Financial data and fundamentals analysis",
|
||||
bull: "Bull Researcher",
|
||||
bullDesc: "Bullish views and investment arguments",
|
||||
bear: "Bear Researcher",
|
||||
bearDesc: "Bearish views and risk warnings",
|
||||
research_manager: "Research Manager",
|
||||
research_managerDesc: "Research team synthesis",
|
||||
trader: "Trader",
|
||||
traderDesc: "Trade execution plan and strategy",
|
||||
risky: "Aggressive Analyst",
|
||||
riskyDesc: "High risk/high return strategy",
|
||||
safe: "Conservative Analyst",
|
||||
safeDesc: "Prudent conservative strategy",
|
||||
neutral: "Neutral Analyst",
|
||||
neutralDesc: "Balanced neutral strategy",
|
||||
risk_manager: "Risk Manager",
|
||||
risk_managerDesc: "Risk management decision",
|
||||
},
|
||||
|
||||
// Price chart section
|
||||
priceSection: {
|
||||
title: "Price Trend",
|
||||
growth: "Growth Rate",
|
||||
duration: "Duration",
|
||||
days: "days",
|
||||
startPrice: "Start Price",
|
||||
endPrice: "End Price",
|
||||
lineChart: "Line Chart",
|
||||
candlestick: "Candlestick",
|
||||
},
|
||||
},
|
||||
|
||||
// Tabs
|
||||
tabs: {
|
||||
analysts: "Analysts",
|
||||
researchers: "Researchers",
|
||||
risk: "Risk Debate",
|
||||
managers: "Managers",
|
||||
trader: "Trader",
|
||||
overview: "Overview",
|
||||
},
|
||||
|
||||
// Download
|
||||
download: {
|
||||
fullReport: "Download Full Analysis Report PDF",
|
||||
generating: "Generating Report...",
|
||||
failed: "Download failed, please try again",
|
||||
noReports: "No analyst reports available for download",
|
||||
},
|
||||
|
||||
// History
|
||||
history: {
|
||||
title: "Analysis History",
|
||||
noHistory: "No analysis history yet",
|
||||
ticker: "Ticker",
|
||||
date: "Date",
|
||||
actions: "Actions",
|
||||
view: "View",
|
||||
downloadPdf: "Download PDF",
|
||||
delete: "Delete",
|
||||
confirmDelete: "Are you sure you want to delete this record?",
|
||||
deleted: "Record deleted successfully",
|
||||
searchPlaceholder: "Search by ticker...",
|
||||
refresh: "Refresh",
|
||||
loading: "Loading...",
|
||||
noReportsFor: "No analysis reports for",
|
||||
afterAnalysisSave: "After running analysis, you can save reports from the results page",
|
||||
analysisDate: "Analysis Date",
|
||||
savedAt: "Saved",
|
||||
decision: "Decision",
|
||||
downloading: "Downloading",
|
||||
confirmDeleteTitle: "Confirm Delete",
|
||||
confirmDeleteDesc: "Are you sure you want to delete the analysis report for",
|
||||
on: "on",
|
||||
cannotUndo: "This action cannot be undone.",
|
||||
cancel: "Cancel",
|
||||
deleting: "Deleting...",
|
||||
confirmDeleteBtn: "Confirm Delete",
|
||||
},
|
||||
|
||||
// Errors
|
||||
errors: {
|
||||
required: "This field is required",
|
||||
invalidTicker: "Please enter a valid stock ticker",
|
||||
analysisError: "Analysis failed, please try again",
|
||||
networkError: "Network error, please check your connection",
|
||||
apiKeyMissing: "API key is not configured",
|
||||
selectOneAnalyst: "Please select at least one analyst",
|
||||
rateLimitExceeded: "Rate limit exceeded. Please wait and try again.",
|
||||
},
|
||||
|
||||
// PDF Labels
|
||||
pdf: {
|
||||
coverTitle: "TradingAgentsX Analysis Report",
|
||||
coverSubtitle: "AI-Powered Multi-Perspective Investment Analysis",
|
||||
tocTitle: "Table of Contents",
|
||||
reportContent: "Report Content",
|
||||
priceChart: "Price Chart & Volume",
|
||||
priceStats: "Price Statistics",
|
||||
totalReturn: "Total Return",
|
||||
analysisPeriod: "Analysis Period",
|
||||
days: "days",
|
||||
startDate: "Start Date",
|
||||
endDate: "End Date",
|
||||
startPrice: "Start Price",
|
||||
endPrice: "End Price",
|
||||
item: "Item",
|
||||
value: "Value",
|
||||
chartFailed: "Chart generation failed",
|
||||
|
||||
// Teams
|
||||
analystsTeam: "Analysts Team",
|
||||
researchTeam: "Research Team",
|
||||
tradingRiskTeam: "Trading & Risk Team",
|
||||
members: "members",
|
||||
},
|
||||
};
|
||||
|
||||
export type TranslationKeys = typeof en;
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* i18n exports and utilities
|
||||
*/
|
||||
import { en, TranslationKeys } from './en';
|
||||
import { zhTW } from './zh-TW';
|
||||
|
||||
export type Locale = 'en' | 'zh-TW';
|
||||
|
||||
export const translations: Record<Locale, TranslationKeys> = {
|
||||
'en': en,
|
||||
'zh-TW': zhTW,
|
||||
};
|
||||
|
||||
export const localeNames: Record<Locale, string> = {
|
||||
'en': 'English',
|
||||
'zh-TW': '繁體中文',
|
||||
};
|
||||
|
||||
export const defaultLocale: Locale = 'zh-TW';
|
||||
|
||||
export { en, zhTW };
|
||||
export type { TranslationKeys };
|
||||
|
|
@ -0,0 +1,557 @@
|
|||
/**
|
||||
* Traditional Chinese translations for TradingAgentsX
|
||||
*/
|
||||
export const zhTW = {
|
||||
// Common
|
||||
common: {
|
||||
loading: "載入中...",
|
||||
error: "錯誤",
|
||||
save: "儲存",
|
||||
cancel: "取消",
|
||||
confirm: "確認",
|
||||
download: "下載",
|
||||
delete: "刪除",
|
||||
back: "返回",
|
||||
next: "下一步",
|
||||
submit: "送出",
|
||||
close: "關閉",
|
||||
},
|
||||
|
||||
// Theme
|
||||
theme: {
|
||||
toggle: "切換主題",
|
||||
light: "亮色",
|
||||
dark: "暗色",
|
||||
system: "系統",
|
||||
},
|
||||
|
||||
// Auth / Login
|
||||
auth: {
|
||||
login: "登入",
|
||||
logout: "登出",
|
||||
loggingOut: "登出中...",
|
||||
cloudSyncEnabled: "雲端同步已啟用",
|
||||
usingLocalStorage: "目前使用本地儲存",
|
||||
loginToSync: "登入 Google 帳號以同步 API 設定和歷史報告",
|
||||
loginSync: "登入同步",
|
||||
},
|
||||
nav: {
|
||||
home: "首頁",
|
||||
analysis: "分析",
|
||||
history: "歷史報告",
|
||||
settings: "設定",
|
||||
tagline: "多代理 LLM 金融交易框架",
|
||||
},
|
||||
|
||||
// Home page
|
||||
home: {
|
||||
title: "TradingAgentsX",
|
||||
subtitle: "多代理 LLM 交易分析平台",
|
||||
description: "透過多代理 AI 協作,提供全面的投資分析與見解",
|
||||
startAnalysis: "開始分析",
|
||||
viewHistory: "查看歷史",
|
||||
|
||||
// 核心特色
|
||||
coreFeatures: "🎯 核心特色",
|
||||
coreFeaturesDesc: "基於 LangGraph 的智能股票交易分析平台,結合多個 AI 代理進行協作決策",
|
||||
|
||||
// 特色卡片
|
||||
features: {
|
||||
multiAgent: "多代理協作架構",
|
||||
multiAgentDesc: "12 個專業化 AI 代理團隊協同工作,模擬真實交易公司運作模式",
|
||||
multiModel: "多模型靈活支援",
|
||||
multiModelDesc: "支援 OpenAI、Claude、Gemini、Grok、DeepSeek、Qwen 等多家 LLM",
|
||||
customEndpoint: "自訂端點配置",
|
||||
customEndpointDesc: "完整支援自訂 API 端點,可連接任何 OpenAI 兼容的服務",
|
||||
fullAnalysis: "全方位市場分析",
|
||||
fullAnalysisDesc: "整合技術面、基本面、情緒面、新聞面四大維度分析",
|
||||
structuredDecision: "結構化決策流程",
|
||||
structuredDecisionDesc: "透過看漲/看跌辯論機制減少偏見,做出更理性的決策",
|
||||
longTermMemory: "長期記憶系統",
|
||||
longTermMemoryDesc: "使用 ChromaDB 向量資料庫儲存歷史決策,持續學習改進",
|
||||
modernUI: "現代化 Web 介面",
|
||||
modernUIDesc: "基於 Next.js 16 的響應式 UI,支援深色模式",
|
||||
oneClickDeploy: "一鍵部署",
|
||||
oneClickDeployDesc: "支援 Docker Compose 部署,快速啟動完整服務",
|
||||
reportDownload: "報告下載",
|
||||
reportDownloadDesc: "支援將完整分析報告匯出為 PDF,方便保存與分享",
|
||||
},
|
||||
|
||||
// 12 位專業代理團隊
|
||||
professionalAgents: "👥 12 位專業代理團隊",
|
||||
professionalAgentsDesc: "每個代理都有其專業職責,協同工作產生高質量的交易決策",
|
||||
analystsTeamTitle: "📊 分析師團隊 (4 位)",
|
||||
researchTeamTitle: "🔍 研究團隊 (3 位)",
|
||||
tradingTeamTitle: "💼 交易團隊 (1 位)",
|
||||
riskTeamTitle: "🛡️ 風險管理團隊 (4 位)",
|
||||
|
||||
// 工作流程
|
||||
workflowTitle: "🔄 分析師協作流程",
|
||||
workflowDescription: "四大分析師代理如何從不同資料來源收集資訊,並產生綜合分析報告",
|
||||
|
||||
// 流程步驟
|
||||
processSteps: {
|
||||
dataCollection: { title: "資料收集階段", description: "從 yfinance、Reddit、RSS 等多源獲取股價、新聞、社群情緒數據" },
|
||||
analysts: { title: "分析師團隊平行分析", description: "市場、情緒、新聞、基本面四大分析師同時評估,產出專業報告" },
|
||||
researchers: { title: "研究團隊辯論", description: "看漲與看跌研究員進行結構化辯論,研究經理綜合雙方觀點" },
|
||||
trader: { title: "交易員整合分析", description: "審查所有分析師與研究團隊報告,制定具體交易執行計劃" },
|
||||
risk: { title: "風險管理評估", description: "激進、保守、中立三方風險分析師評估策略,風險經理做出風控決策" },
|
||||
finalDecision: { title: "最終決策輸出", description: "產生包含交易方向、倉位大小、風險控制的完整投資建議" },
|
||||
},
|
||||
|
||||
// LLM 支援
|
||||
llmSupport: "🌍 多模型支援",
|
||||
llmSupportDesc: "支援業界領先的多家 LLM 提供商,每個模型可配置獨立的 API Key 和 Base URL",
|
||||
llmFeatures: "✅ 完整支援自訂端點 | ✅ 三層獨立配置(快速思維/深層思維/嵌入) | ✅ BYOK 模式",
|
||||
|
||||
// 技術亮點
|
||||
techHighlights: "💡 技術亮點",
|
||||
dynamicResearch: "動態研究深度",
|
||||
dynamicResearchFeatures: ["淺層 (Shallow): 1 輪快速分析", "中等 (Medium): 2 輪平衡分析", "深層 (Deep): 3+ 輪全面分析"],
|
||||
memorySystem: "長期記憶系統",
|
||||
memorySystemFeatures: ["ChromaDB 向量資料庫", "歷史決策持久化", "持續學習與改進"],
|
||||
realTimeData: "實時資料整合",
|
||||
realTimeDataFeatures: ["yfinance: 即時股價數據", "Reddit API: 社群情緒", "Alpha Vantage: 財務資料"],
|
||||
fullApiSupport: "完整 API 支援",
|
||||
fullApiSupportFeatures: ["RESTful API 架構", "異步任務處理", "Swagger 互動文檔"],
|
||||
|
||||
// CTA Section
|
||||
readyToStart: "準備好開始智能交易分析了嗎?",
|
||||
ctaDescription: "立即體驗 12 位專業 AI 代理協同工作,為您提供全方位的股票分析報告",
|
||||
},
|
||||
|
||||
// Agents
|
||||
agents: {
|
||||
// Analysts
|
||||
market_analyst: "市場分析師",
|
||||
market_analyst_role: "技術分析",
|
||||
market_analyst_desc: "技術指標分析 (RSI, MACD, 布林通道)、價格走勢研判、支撐阻力位識別",
|
||||
|
||||
social_analyst: "社群媒體分析師",
|
||||
social_analyst_role: "情緒分析",
|
||||
social_analyst_desc: "社群情緒監測、市場氛圍評估、熱門話題追蹤",
|
||||
|
||||
news_analyst: "新聞分析師",
|
||||
news_analyst_role: "新聞分析",
|
||||
news_analyst_desc: "新聞事件追蹤、影響評估、資訊篩選與優先排序",
|
||||
|
||||
fundamentals_analyst: "基本面分析師",
|
||||
fundamentals_analyst_role: "基本面分析",
|
||||
fundamentals_analyst_desc: "財務數據分析、估值指標、公司基本面評估",
|
||||
|
||||
// Researchers
|
||||
bull_researcher: "看漲研究員",
|
||||
bull_researcher_role: "多頭分析",
|
||||
bull_researcher_desc: "識別上漲潛力、成長催化劑與看漲情境",
|
||||
|
||||
bear_researcher: "看跌研究員",
|
||||
bear_researcher_role: "空頭分析",
|
||||
bear_researcher_desc: "識別下跌風險、警示信號與看跌情境",
|
||||
|
||||
// Managers
|
||||
research_manager: "研究經理",
|
||||
research_manager_role: "研究整合",
|
||||
research_manager_desc: "整合多空論點,產出平衡的研究結論",
|
||||
|
||||
risk_manager: "風險經理",
|
||||
risk_manager_role: "風險決策",
|
||||
risk_manager_desc: "最終風險評估與倉位規模建議",
|
||||
|
||||
// Risk Debaters
|
||||
aggressive_debator: "激進分析師",
|
||||
aggressive_debator_role: "高風險高回報",
|
||||
aggressive_debator_desc: "主張較高風險倉位以追求更大潛在回報",
|
||||
|
||||
conservative_debator: "保守分析師",
|
||||
conservative_debator_role: "資本保護",
|
||||
conservative_debator_desc: "主張較安全倉位以保護資本為優先",
|
||||
|
||||
neutral_debator: "中立分析師",
|
||||
neutral_debator_role: "平衡觀點",
|
||||
neutral_debator_desc: "提供激進與保守之間的平衡觀點",
|
||||
|
||||
// Trader
|
||||
trader: "交易員",
|
||||
trader_role: "交易執行",
|
||||
trader_desc: "制定最終交易建議,包含進場、出場與倉位規模",
|
||||
},
|
||||
|
||||
// Flow Diagram
|
||||
flowDiagram: {
|
||||
// Layer titles
|
||||
layer1: "第一層:資料來源",
|
||||
layer2: "第二層:分析師代理 (4位)",
|
||||
layer3: "第三層:研究員代理 (2位)",
|
||||
layer4: "第四層:風險辯論者 (3位)",
|
||||
finalOutput: "最終輸出:12 份詳細報告",
|
||||
|
||||
// Data sources
|
||||
stockData: "股價數據",
|
||||
socialSentiment: "社群情緒",
|
||||
newsInfo: "新聞資訊",
|
||||
financialData: "財務數據",
|
||||
|
||||
// Arrow labels
|
||||
dataFetch: "資料擷取與清理",
|
||||
reportIntegration: "分析報告整合",
|
||||
researchPrep: "研究整合與辯論準備",
|
||||
riskDebate: "風險評估與管理",
|
||||
finalDecision: "制定最終交易決策",
|
||||
generateReport: "生成完整投資報告",
|
||||
|
||||
// Agent descriptions
|
||||
technicalAnalysis: "技術面分析",
|
||||
sentimentAnalysis: "情緒面分析",
|
||||
newsAnalysis: "新聞面分析",
|
||||
fundamentalsAnalysis: "基本面分析",
|
||||
bullishResearch: "看多觀點研究",
|
||||
bearishResearch: "看空觀點研究",
|
||||
integrateViews: "整合多空研究觀點",
|
||||
highRiskReward: "高風險高報酬",
|
||||
balancedRisk: "平衡風險報酬",
|
||||
lowRiskVol: "低風險低波動",
|
||||
integrateRisk: "整合風險辯論結果",
|
||||
executeTrade: "執行最終交易決策",
|
||||
|
||||
// Tasks
|
||||
rsiIndicator: "RSI 指標",
|
||||
macdMomentum: "MACD 動能",
|
||||
priceTrend: "價格走勢",
|
||||
nlpSentiment: "NLP 情緒",
|
||||
discussionHeat: "討論熱度",
|
||||
investorConfidence: "投資者信心",
|
||||
newsSummary: "新聞摘要",
|
||||
eventAssessment: "事件評估",
|
||||
impactPrediction: "影響預測",
|
||||
financialAnalysis: "財報分析",
|
||||
valuationMetrics: "估值指標",
|
||||
profitEvaluation: "盈利評估",
|
||||
positiveFactors: "正面因素分析",
|
||||
growthOpportunities: "成長機會評估",
|
||||
buyReasons: "買入理由整理",
|
||||
negativeFactors: "負面因素分析",
|
||||
riskAssessment: "風險評估",
|
||||
sellReasons: "賣出理由整理",
|
||||
balanceArguments: "平衡雙方論點",
|
||||
comprehensiveAdvice: "綜合投資建議",
|
||||
preliminaryStrategy: "制定初步策略",
|
||||
aggressiveStrategy: "積極投資策略",
|
||||
maximizeReturns: "最大化收益",
|
||||
calculatedRisk: "承擔計算風險",
|
||||
prudentStrategy: "穩健投資策略",
|
||||
riskBalance: "風險平衡",
|
||||
rationalDecision: "理性決策",
|
||||
conservativeStrategy: "保守投資策略",
|
||||
capitalProtection: "資本保護",
|
||||
riskReduction: "降低風險",
|
||||
riskRating: "風險等級評定",
|
||||
stopLossSettings: "止損止盈設定",
|
||||
finalRiskControl: "最終風險控制",
|
||||
|
||||
// Outputs
|
||||
tradeSignal: "交易訊號 (BUY/SELL/HOLD)",
|
||||
targetPrice: "目標價位",
|
||||
tradeQuantity: "交易數量",
|
||||
riskParams: "風險參數",
|
||||
finalOutput_label: "最終輸出:",
|
||||
completeReportSet: "完整分析報告集合",
|
||||
comprehensiveSupport: "整合 12 位專業代理的深度分析,提供全方位投資決策支援",
|
||||
|
||||
// Report sections
|
||||
analystReports: "分析師報告 (4份)",
|
||||
researchReports: "研究報告 (3份)",
|
||||
riskTrading: "風險與交易 (5份)",
|
||||
technicalReport: "技術面分析",
|
||||
sentimentReport: "社群情緒分析",
|
||||
newsReport: "新聞面分析",
|
||||
fundamentalsReport: "基本面分析",
|
||||
bullReport: "多頭研究報告",
|
||||
bearReport: "空頭研究報告",
|
||||
researchManagerReport: "研究經理整合",
|
||||
aggressiveEval: "激進策略評估",
|
||||
balancedEval: "中立策略評估",
|
||||
conservativeEval: "保守策略評估",
|
||||
riskManagerReport: "風險經理整合",
|
||||
finalTradeDecision: "最終交易決策",
|
||||
|
||||
// Manager titles
|
||||
researchManager: "研究經理",
|
||||
riskManager: "風險經理",
|
||||
trader: "交易員",
|
||||
},
|
||||
|
||||
// Analysis form
|
||||
form: {
|
||||
ticker: "股票代碼",
|
||||
tickerPlaceholder: "輸入股票代碼 (如 AAPL, 2330)",
|
||||
analysisDate: "分析日期",
|
||||
analysts: "分析師團隊",
|
||||
selectAnalysts: "選擇要啟用的分析師",
|
||||
selectAll: "全選",
|
||||
deselectAll: "取消全選",
|
||||
startAnalysis: "開始分析",
|
||||
analyzing: "分析中...",
|
||||
|
||||
// Analysis page
|
||||
analysisTitle: "交易分析",
|
||||
analysisSubtitle: "配置並執行全面的多代理交易分析",
|
||||
analysisLoading: "正在執行分析... 這可能需要幾分鐘時間。",
|
||||
|
||||
advancedOptions: "進階選項",
|
||||
researchDepth: "研究深度",
|
||||
riskDebateRounds: "風險辯論回合",
|
||||
|
||||
// Market types
|
||||
marketType: "市場類型",
|
||||
usMarket: "美股",
|
||||
twseMarket: "台股上市",
|
||||
tpexMarket: "台股上櫃/興櫃",
|
||||
selectMarket: "選擇市場",
|
||||
selectMarketDesc: "選擇分析的股票市場",
|
||||
|
||||
// LLM Configuration
|
||||
llmSettings: "LLM 設定",
|
||||
quickThinkModel: "快速思維模型",
|
||||
deepThinkModel: "深層思維模型",
|
||||
embeddingModel: "嵌入式模型",
|
||||
customModel: "自訂模型",
|
||||
customModelName: "自訂模型名稱",
|
||||
customDeepThinkModelName: "自訂深層思維模型名稱",
|
||||
executeAnalysis: "執行分析",
|
||||
otherCustomModel: "Other(自訂模型)",
|
||||
quickResponseModel: "快速回應模型",
|
||||
complexReasoningModel: "複雜推理模型",
|
||||
localModelNoApiKey: "🆓 本地模型不需 API Key",
|
||||
needsOpenAiApiKey: "☁️ 需要 OpenAI API Key",
|
||||
|
||||
// Depth levels
|
||||
depthShallow: "淺層 (1 輪)",
|
||||
depthMedium: "中等 (2 輪)",
|
||||
depthDeep: "深層 (3+ 輪)",
|
||||
|
||||
// API Keys
|
||||
apiKeySection: "API 設定",
|
||||
alphaVantageKey: "Alpha Vantage API Key",
|
||||
finmindKey: "FinMind API Key",
|
||||
|
||||
// Ticker descriptions by market
|
||||
tickerDescUS: "輸入美股代碼(例如:NVDA、AAPL)",
|
||||
tickerDescTWSE: "輸入上市股票代碼(例如:2330、2317)",
|
||||
tickerDescTPEX: "輸入上櫃/興櫃股票代碼(例如:6488、5765)",
|
||||
selectDate: "選擇分析日期",
|
||||
selectDepth: "選擇研究深度",
|
||||
|
||||
// Depth options
|
||||
depthShallowLabel: "淺層 - 快速研究",
|
||||
depthMediumLabel: "中等 - 適度討論",
|
||||
depthDeepLabel: "深層 - 深入研究",
|
||||
|
||||
// Validation messages
|
||||
validation: {
|
||||
tickerRequired: "股票代碼為必填",
|
||||
dateFormat: "日期格式必須為 YYYY-MM-DD",
|
||||
selectOneAnalyst: "請至少選擇一位分析師",
|
||||
selectQuickThink: "請選擇快速思維模型",
|
||||
selectDeepThink: "請選擇深層思維模型",
|
||||
selectEmbedding: "請選擇嵌入式模型",
|
||||
invalidUrl: "請輸入有效的 URL",
|
||||
},
|
||||
},
|
||||
|
||||
// Analysis results
|
||||
results: {
|
||||
title: "分析結果",
|
||||
detailedResults: "詳細分析結果",
|
||||
analysisDate: "分析日期",
|
||||
ticker: "股票代碼",
|
||||
date: "日期",
|
||||
summary: "摘要",
|
||||
recommendation: "交易建議",
|
||||
priceChart: "價格走勢",
|
||||
volumeChart: "成交量",
|
||||
noData: "無資料",
|
||||
noResults: "沒有分析結果",
|
||||
runAnalysisFirst: "請先執行分析",
|
||||
backToAnalysis: "返回分析",
|
||||
backButton: "返回分析",
|
||||
saveReport: "儲存報告",
|
||||
saving: "儲存中...",
|
||||
saved: "已儲存",
|
||||
saveError: "儲存失敗,請稍後再試",
|
||||
duplicateReport: "此報告已存在(相同股票代碼與分析日期)",
|
||||
report: "報告",
|
||||
noReportGenerated: "此分析師沒有生成報告",
|
||||
notSelectedOrNoReport: "可能此分析師未被選擇或分析過程中未產生報告",
|
||||
|
||||
// Analyst tabs
|
||||
analysts: {
|
||||
market: "市場分析師",
|
||||
marketDesc: "技術分析與市場趨勢評估",
|
||||
social: "社群媒體分析師",
|
||||
socialDesc: "社群情緒與市場氛圍分析",
|
||||
news: "新聞分析師",
|
||||
newsDesc: "新聞事件與影響分析",
|
||||
fundamentals: "基本面分析師",
|
||||
fundamentalsDesc: "財務數據與基本面分析",
|
||||
bull: "看漲研究員",
|
||||
bullDesc: "看漲觀點與投資論據",
|
||||
bear: "看跌研究員",
|
||||
bearDesc: "看跌觀點與風險警告",
|
||||
research_manager: "研究經理",
|
||||
research_managerDesc: "研究團隊綜合決策",
|
||||
trader: "交易員",
|
||||
traderDesc: "交易執行計劃與策略",
|
||||
risky: "激進分析師",
|
||||
riskyDesc: "高風險高回報策略分析",
|
||||
safe: "保守分析師",
|
||||
safeDesc: "穩健保守策略分析",
|
||||
neutral: "中立分析師",
|
||||
neutralDesc: "中立平衡策略分析",
|
||||
risk_manager: "風險經理",
|
||||
risk_managerDesc: "風險管理綜合決策",
|
||||
},
|
||||
|
||||
// Price chart section
|
||||
priceSection: {
|
||||
title: "價格走勢",
|
||||
growth: "增長率",
|
||||
duration: "時長",
|
||||
days: "天",
|
||||
startPrice: "起始價格",
|
||||
endPrice: "結束價格",
|
||||
lineChart: "折線圖",
|
||||
candlestick: "平均K線圖",
|
||||
},
|
||||
},
|
||||
|
||||
// Tabs
|
||||
tabs: {
|
||||
analysts: "分析師",
|
||||
researchers: "研究員",
|
||||
risk: "風險辯論",
|
||||
managers: "經理",
|
||||
trader: "交易員",
|
||||
overview: "總覽",
|
||||
},
|
||||
|
||||
// Download
|
||||
download: {
|
||||
fullReport: "下載完整分析報告 PDF",
|
||||
generating: "生成報告中...",
|
||||
failed: "下載失敗,請稍後再試",
|
||||
noReports: "此報告沒有可下載的分析師報告",
|
||||
},
|
||||
|
||||
// History
|
||||
history: {
|
||||
title: "分析歷史",
|
||||
noHistory: "尚無分析紀錄",
|
||||
ticker: "股票代碼",
|
||||
date: "日期",
|
||||
actions: "操作",
|
||||
view: "檢視",
|
||||
downloadPdf: "下載 PDF",
|
||||
delete: "刪除",
|
||||
confirmDelete: "確定要刪除這筆紀錄嗎?",
|
||||
deleted: "紀錄已刪除",
|
||||
searchPlaceholder: "搜尋股票代碼...",
|
||||
refresh: "重新整理",
|
||||
loading: "載入中...",
|
||||
noReportsFor: "尚無",
|
||||
afterAnalysisSave: "執行分析後,可在結果頁面儲存報告",
|
||||
analysisDate: "分析日期",
|
||||
savedAt: "儲存時間",
|
||||
decision: "決策",
|
||||
downloading: "下載中",
|
||||
confirmDeleteTitle: "確認刪除",
|
||||
confirmDeleteDesc: "確定要刪除",
|
||||
on: "於",
|
||||
cannotUndo: "此操作無法復原。",
|
||||
cancel: "取消",
|
||||
deleting: "刪除中...",
|
||||
confirmDeleteBtn: "確認刪除",
|
||||
},
|
||||
|
||||
// Errors
|
||||
errors: {
|
||||
required: "此欄位為必填",
|
||||
invalidTicker: "請輸入有效的股票代碼",
|
||||
analysisError: "分析失敗,請稍後再試",
|
||||
networkError: "網路錯誤,請檢查連線",
|
||||
apiKeyMissing: "API 金鑰未設定",
|
||||
selectOneAnalyst: "請至少選擇一位分析師",
|
||||
rateLimitExceeded: "已達速率限制,請稍候再試",
|
||||
},
|
||||
|
||||
// Settings / API Settings Dialog
|
||||
settings: {
|
||||
title: "API 配置",
|
||||
apiConfiguration: "API 配置",
|
||||
description: "設定您的 API 金鑰。這些資訊會以加密形式儲存在瀏覽器中。",
|
||||
encryptionEnabled: "🔒 已啟用 AES-256-GCM 加密保護",
|
||||
onlyFillNeeded: "💡 僅需填寫您選擇的模型供應商的 API。例如,若使用 Claude 模型,只需填寫 Claude API。",
|
||||
|
||||
// Sections
|
||||
stockMarketApis: "股市資料 API(依分析市場選擇填寫)",
|
||||
llmProviders: "LLM 模型供應商(依選擇的模型填寫)",
|
||||
customEndpoint: "自訂端點(進階選項)",
|
||||
|
||||
// Form labels
|
||||
finmindToken: "FinMind API Token(台股)",
|
||||
finmindPlaceholder: "輸入 FinMind Token",
|
||||
finmindDesc: "用於獲取台灣股市資料(在 finmindtrade.com 註冊取得)",
|
||||
alphaVantageKey: "Alpha Vantage API Key(美股)",
|
||||
alphaVantagePlaceholder: "輸入 Alpha Vantage API Key",
|
||||
alphaVantageDesc: "用於獲取美股基本面數據(分析美股時建議填寫)",
|
||||
openaiDesc: "用於 OpenAI 模型(GPT-4, GPT-5, o4 等)及 OpenAI 嵌入式模型",
|
||||
anthropicDesc: "用於 Claude 模型",
|
||||
googleDesc: "用於 Gemini 模型",
|
||||
grokDesc: "用於 Grok 模型",
|
||||
deepseekDesc: "用於 DeepSeek 模型",
|
||||
qwenDesc: "用於 Qwen 模型",
|
||||
customBaseUrl: "自訂 Base URL",
|
||||
customBaseUrlDesc: "若設定此項,將覆蓋所有模型的預設端點",
|
||||
customApiKey: "自訂端點 API Key",
|
||||
customApiKeyPlaceholder: "輸入自訂端點的 API Key",
|
||||
customApiKeyDesc: "配合自訂 Base URL 使用的 API Key",
|
||||
|
||||
// Buttons and messages
|
||||
saveSettings: "儲存設定",
|
||||
clearSettings: "清除設定",
|
||||
processing: "處理中...",
|
||||
settingsSaved: "✓ 設定已成功儲存",
|
||||
|
||||
// General settings (legacy)
|
||||
language: "語言",
|
||||
theme: "佈景主題",
|
||||
apiKey: "API 金鑰",
|
||||
apiKeyPlaceholder: "輸入您的 API 金鑰",
|
||||
configureApi: "設定 API",
|
||||
},
|
||||
|
||||
// PDF Labels
|
||||
pdf: {
|
||||
coverTitle: "TradingAgentsX 分析報告",
|
||||
coverSubtitle: "AI 驅動的多角度投資分析",
|
||||
tocTitle: "目 錄",
|
||||
reportContent: "報告內容",
|
||||
priceChart: "價格走勢圖 & 交易量柱狀圖",
|
||||
priceStats: "價格統計",
|
||||
totalReturn: "總報酬率",
|
||||
analysisPeriod: "分析期間",
|
||||
days: "天",
|
||||
startDate: "開始日期",
|
||||
endDate: "結束日期",
|
||||
startPrice: "起始價格",
|
||||
endPrice: "結束價格",
|
||||
item: "項 目",
|
||||
value: "數 值",
|
||||
chartFailed: "圖表生成失敗",
|
||||
|
||||
// Teams
|
||||
analystsTeam: "分析師團隊",
|
||||
researchTeam: "研究團隊",
|
||||
tradingRiskTeam: "交易與風險團隊",
|
||||
members: "位",
|
||||
},
|
||||
};
|
||||
|
|
@ -23,6 +23,7 @@ export interface AnalysisRequest {
|
|||
embedding_model?: string; // Embedding model: 'all-MiniLM-L6-v2' (local), 'text-embedding-3-small' (OpenAI), etc.
|
||||
alpha_vantage_api_key?: string;
|
||||
finmind_api_key?: string; // 台灣股市資料 API
|
||||
language?: "en" | "zh-TW"; // Language for agent reports
|
||||
}
|
||||
|
||||
export interface PriceData {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -2,15 +2,17 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
|||
import time
|
||||
import json
|
||||
from tradingagents.agents.utils.agent_utils import get_fundamentals, get_balance_sheet, get_cashflow, get_income_statement, get_insider_sentiment, get_insider_transactions
|
||||
from tradingagents.agents.utils.prompts import get_fundamentals_analyst_prompt, get_agent_role_instruction, get_context_message
|
||||
from tradingagents.dataflows.config import get_config
|
||||
|
||||
|
||||
def create_fundamentals_analyst(llm):
|
||||
def create_fundamentals_analyst(llm, language: str = "zh-TW"):
|
||||
"""
|
||||
建立一個基本面分析師節點。
|
||||
|
||||
Args:
|
||||
llm: 用於分析的語言模型。
|
||||
language: 報告語言 ('en' 或 'zh-TW')
|
||||
|
||||
Returns:
|
||||
一個處理基本面分析的節點函式。
|
||||
|
|
@ -27,7 +29,7 @@ def create_fundamentals_analyst(llm):
|
|||
"""
|
||||
current_date = state["trade_date"]
|
||||
ticker = state["company_of_interest"]
|
||||
company_name = state.get("company_name", ticker) # 使用真實公司名稱,fallback到ticker
|
||||
company_name = state.get("company_name", ticker)
|
||||
|
||||
tools = [
|
||||
get_fundamentals,
|
||||
|
|
@ -36,62 +38,18 @@ def create_fundamentals_analyst(llm):
|
|||
get_income_statement,
|
||||
]
|
||||
|
||||
system_message = (
|
||||
"""**重要:您必須使用繁體中文(Traditional Chinese)回覆所有內容。**
|
||||
**嚴格禁止:請勿在回覆中使用任何 emoji 表情符號(如 ✅ ❌ 📊 📈 🚀 等)。**
|
||||
**請只使用純文字、數字、標點符號和必要的 Unicode 符號(如 ↑ ↓ ★ ●等)。**
|
||||
|
||||
【專業身份】
|
||||
您是基本面分析師,負責評估公司財務體質、獲利能力與投資價值。
|
||||
|
||||
【分析重點】
|
||||
1. **公司概況**:業務模式、產業地位與競爭優勢
|
||||
2. **財務健全度**:獲利能力、資產品質、現金流狀況
|
||||
3. **關鍵財務比率**:聚焦3-5個核心指標(建議:ROE、本益比、負債比率、EPS成長率、自由現金流)
|
||||
4. **估值評估**:當前股價相對內在價值的合理性
|
||||
|
||||
【技術操作】
|
||||
• 使用 get_fundamentals 取得公司基本資料
|
||||
• 使用 get_income_statement、get_balance_sheet、get_cashflow 取得財務報表
|
||||
• 整合數據進行綜合評估
|
||||
|
||||
【報告架構】
|
||||
**字數要求**:**800-1500字(不含表格)**
|
||||
**嚴格遵守字數限制,少於800字或超過1500字的報告將被退回**
|
||||
|
||||
**內容結構**:
|
||||
1. 公司概述(150字以上):業務特性與競爭地位
|
||||
2. 財務分析(400-450字):獲利能力、財務結構、現金流分析
|
||||
3. 估值研判(100字以上):股價評價水準與投資價值
|
||||
4. 投資建議(150字以上):基於基本面的操作建議
|
||||
5. 財務數據表格(必須)
|
||||
|
||||
**撰寫原則**:
|
||||
- 數據與分析並重,避免單純羅列數字
|
||||
- 結論明確,提供清晰的投資判斷
|
||||
- 必須包含關鍵財務指標表格
|
||||
|
||||
**結尾提示**:
|
||||
請在報告最後加上以下結尾:
|
||||
「---
|
||||
※ 本報告為基本面分析,建議參考最新財報公告並搭配技術面及市場情緒綜合研判。財務數據可能存在時間差,投資有風險,請謹慎評估。」
|
||||
|
||||
請提供專業且全面的基本面分析報告。"""
|
||||
+ " 請務必在報告結尾附加一個 Markdown 表格,以整理報告中的要點。"
|
||||
+ " 使用可用的工具:`get_fundamentals` 用於全面的公司分析,`get_balance_sheet`、`get_cashflow` 和 `get_income_statement` 用於特定的財務報表。"
|
||||
)
|
||||
# Get language-specific prompts
|
||||
system_message = get_fundamentals_analyst_prompt(language)
|
||||
role_instruction = get_agent_role_instruction(language)
|
||||
context_msg = get_context_message(language, current_date, company_name, ticker)
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
[
|
||||
(
|
||||
"system",
|
||||
"您是一個樂於助人的人工智慧助理,與其他助理協同工作。"
|
||||
" 使用提供的工具來逐步回答問題。"
|
||||
" 如果您無法完全回答,沒關係;另一個擁有不同工具的助理會在您中斷的地方提供幫助。盡您所能取得進展。"
|
||||
" 如果您或任何其他助理有最終交易提案:**買入/持有/賣出** 或可交付成果,"
|
||||
" 請在您的回覆前加上「最終交易提案:**買入/持有/賣出**」,以便團隊知道停止。"
|
||||
f"{role_instruction}"
|
||||
" 您可以使用以下工具:{tool_names}。\n{system_message}"
|
||||
"供您參考,目前日期是 {current_date}。我們想關注的公司是 {company_name} (股票代碼:{ticker})",
|
||||
f" {context_msg}",
|
||||
),
|
||||
MessagesPlaceholder(variable_name="messages"),
|
||||
]
|
||||
|
|
@ -99,19 +57,15 @@ def create_fundamentals_analyst(llm):
|
|||
|
||||
prompt = prompt.partial(system_message=system_message)
|
||||
prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
|
||||
prompt = prompt.partial(current_date=current_date)
|
||||
prompt = prompt.partial(ticker=ticker)
|
||||
prompt = prompt.partial(company_name=company_name)
|
||||
|
||||
chain = prompt | llm.bind_tools(tools)
|
||||
|
||||
result = chain.invoke(state["messages"])
|
||||
|
||||
# 報告邏輯修復:只在LLM最終回應時保存報告
|
||||
report = state.get("fundamentals_report", "") # 保持現有報告
|
||||
# Report logic: only save report when LLM gives final response
|
||||
report = state.get("fundamentals_report", "")
|
||||
|
||||
if len(result.tool_calls) == 0:
|
||||
# 沒有工具調用,這是最終的分析報告
|
||||
report = result.content
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -2,15 +2,17 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
|||
import time
|
||||
import json
|
||||
from tradingagents.agents.utils.agent_utils import get_stock_data, get_indicators
|
||||
from tradingagents.agents.utils.prompts import get_language_instruction, get_agent_role_instruction, get_context_message
|
||||
from tradingagents.dataflows.config import get_config
|
||||
|
||||
|
||||
def create_market_analyst(llm):
|
||||
def create_market_analyst(llm, language: str = "zh-TW"):
|
||||
"""
|
||||
建立一個市場分析師節點。
|
||||
|
||||
Args:
|
||||
llm: 用於分析的語言模型。
|
||||
language: 報告語言 ('en' 或 'zh-TW')
|
||||
|
||||
Returns:
|
||||
一個處理市場分析的節點函式。
|
||||
|
|
@ -28,17 +30,60 @@ def create_market_analyst(llm):
|
|||
"""
|
||||
current_date = state["trade_date"]
|
||||
ticker = state["company_of_interest"]
|
||||
company_name = state.get("company_name", ticker) # 使用真實公司名稱,fallback到ticker
|
||||
company_name = state.get("company_name", ticker)
|
||||
|
||||
tools = [
|
||||
get_stock_data,
|
||||
get_indicators,
|
||||
]
|
||||
|
||||
system_message = (
|
||||
"""**重要:您必須使用繁體中文(Traditional Chinese)回覆所有內容。**
|
||||
**嚴格禁止:請勿在回覆中使用任何 emoji 表情符號(如 ✅ ❌ 📊 📈 🚀 等)。**
|
||||
**請只使用純文字、數字、標點符號和必要的 Unicode 符號(如 ↑ ↓ ★ ●等)。**
|
||||
# Get language-specific instructions
|
||||
lang_instruction = get_language_instruction(language)
|
||||
role_instruction = get_agent_role_instruction(language)
|
||||
context_msg = get_context_message(language, current_date, company_name, ticker)
|
||||
|
||||
if language == "en":
|
||||
system_message = f"""{lang_instruction}
|
||||
|
||||
【Professional Identity】
|
||||
You are a senior technical analyst responsible for providing precise market technical assessments.
|
||||
|
||||
【Analysis Focus】
|
||||
1. **Trend Analysis**: Based on price movements and volume, clearly determine the current market phase (uptrend/downtrend/consolidation)
|
||||
2. **Technical Indicators**: Focus on 3-4 core indicators (recommended: 50-day/200-day MA, MACD, RSI), interpret their signal meanings
|
||||
3. **Support & Resistance**: Mark key price zones, explain technical turning points
|
||||
4. **Trading Recommendations**: Provide entry/exit positions, risk control parameters
|
||||
|
||||
【Technical Operations】
|
||||
• Use get_stock_data to obtain historical price data
|
||||
• Use get_indicators to calculate technical indicators (set look_back_days to 50 or 200 for moving averages)
|
||||
• Integrate data to provide professional insights
|
||||
|
||||
【Report Structure】
|
||||
**Word Count Requirement**: **800-1500 words (excluding tables)**
|
||||
**Strictly adhere to word limits - reports under 800 or over 1500 words will be rejected**
|
||||
|
||||
**Content Structure**:
|
||||
1. Market Overview (120-150 words): Trend direction and momentum strength
|
||||
2. Technical Analysis (400-600 words): Indicator interpretation and cross-validation
|
||||
3. Key Price Levels (80-120 words): Support/resistance levels and their technical significance
|
||||
4. Trading Strategy (150-200 words): Entry points, stop-loss settings, target prices
|
||||
5. Data Summary Table (required, not counted in word count)
|
||||
|
||||
**Writing Principles**:
|
||||
- Professional yet clear, avoid overly technical expressions
|
||||
- Clear conclusions, provide actionable trading recommendations
|
||||
- Must include core data summary table
|
||||
- Control length, ensure analysis is completed within 1500 words
|
||||
|
||||
**Closing Note**:
|
||||
Please add the following at the end of your report:
|
||||
\"---
|
||||
※ This report is technical analysis only. Recommend combining with fundamental and sentiment analysis. Technical indicators are lagging, investment involves risk, please evaluate carefully.\"
|
||||
|
||||
Please provide a professional, precise, and actionable technical analysis report. Be sure to include a Markdown table at the end summarizing key points."""
|
||||
else:
|
||||
system_message = f"""{lang_instruction}
|
||||
|
||||
【專業身份】
|
||||
您是資深技術分析師,負責提供精準的市場技術面評估。
|
||||
|
|
@ -76,21 +121,15 @@ def create_market_analyst(llm):
|
|||
「---
|
||||
※ 本報告為技術面分析,建議搭配基本面及市場情緒綜合研判。技術指標具滯後性,投資有風險,請謹慎評估。」
|
||||
|
||||
請提供專業、精準且具操作性的技術分析報告。"""
|
||||
+ """ 請務必在報告結尾附加一個 Markdown 表格,以整理報告中的要點。"""
|
||||
)
|
||||
請提供專業、精準且具操作性的技術分析報告。請務必在報告結尾附加一個 Markdown 表格,以整理報告中的要點。"""
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
[
|
||||
(
|
||||
"system",
|
||||
"您是一個樂於助人的人工智慧助理,與其他助理協同工作。"
|
||||
" 使用提供的工具來逐步回答問題。"
|
||||
" 如果您無法完全回答,沒關係;另一個擁有不同工具的助理會在您中斷的地方提供幫助。盡您所能取得進展。"
|
||||
" 如果您或任何其他助理有最終交易提案:**買入/持有/賣出** 或可交付成果,"
|
||||
" 請在您的回覆前加上「最終交易提案:**買入/持有/賣出**」,以便團隊知道停止。"
|
||||
f"{role_instruction}"
|
||||
" 您可以使用以下工具:{tool_names}。\n{system_message}"
|
||||
"供您參考,目前日期是 {current_date}。我們想關注的公司是 {company_name} (股票代碼:{ticker})",
|
||||
f"{context_msg}",
|
||||
),
|
||||
MessagesPlaceholder(variable_name="messages"),
|
||||
]
|
||||
|
|
@ -98,21 +137,15 @@ def create_market_analyst(llm):
|
|||
|
||||
prompt = prompt.partial(system_message=system_message)
|
||||
prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
|
||||
prompt = prompt.partial(current_date=current_date)
|
||||
prompt = prompt.partial(ticker=ticker)
|
||||
prompt = prompt.partial(company_name=company_name)
|
||||
|
||||
chain = prompt | llm.bind_tools(tools)
|
||||
|
||||
result = chain.invoke(state["messages"])
|
||||
|
||||
# 報告邏輯修復:只在LLM最終回應時保存報告
|
||||
# 當LLM調用工具時(tool_calls不為空),不更新報告
|
||||
# 當LLM返回最終分析時(tool_calls為空),保存完整報告
|
||||
report = state.get("market_report", "") # 保持現有報告
|
||||
# Report logic: only save report when LLM gives final response
|
||||
report = state.get("market_report", "")
|
||||
|
||||
if len(result.tool_calls) == 0:
|
||||
# 沒有工具調用,這是最終的分析報告
|
||||
report = result.content
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -2,15 +2,17 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
|||
import time
|
||||
import json
|
||||
from tradingagents.agents.utils.agent_utils import get_news, get_global_news
|
||||
from tradingagents.agents.utils.prompts import get_news_analyst_prompt, get_agent_role_instruction, get_context_message
|
||||
from tradingagents.dataflows.config import get_config
|
||||
|
||||
|
||||
def create_news_analyst(llm):
|
||||
def create_news_analyst(llm, language: str = "zh-TW"):
|
||||
"""
|
||||
建立一個新聞分析師節點。
|
||||
|
||||
Args:
|
||||
llm: 用於分析的語言模型。
|
||||
language: 報告語言 ('en' 或 'zh-TW')
|
||||
|
||||
Returns:
|
||||
一個處理新聞分析的節點函式。
|
||||
|
|
@ -27,67 +29,25 @@ def create_news_analyst(llm):
|
|||
"""
|
||||
current_date = state["trade_date"]
|
||||
ticker = state["company_of_interest"]
|
||||
company_name = state.get("company_name", ticker)
|
||||
|
||||
tools = [
|
||||
get_news,
|
||||
get_global_news,
|
||||
]
|
||||
|
||||
system_message = (
|
||||
"""**重要:您必須使用繁體中文(Traditional Chinese)回覆所有內容。**
|
||||
**嚴格禁止:請勿在回覆中使用任何 emoji 表情符號(如 ✅ ❌ 📊 📈 🚀 等)。**
|
||||
**請只使用純文字、數字、標點符號和必要的 Unicode 符號(如 ↑ ↓ ★ ●等)。**
|
||||
|
||||
【專業身份】
|
||||
您是財經新聞分析師,負責解讀重大事件對股價的影響,並提供投資決策參考。
|
||||
|
||||
【分析重點】
|
||||
1. **關鍵事件**:篩選出近期最具影響力的2-3則重大新聞
|
||||
2. **影響評估**:分析事件對公司基本面、股價及投資人情緒的實質影響
|
||||
3. **風險識別**:指出新聞背後的潛在風險或未被市場充分反應的因素
|
||||
4. **投資啟示**:提供基於新聞事件的操作建議
|
||||
|
||||
【技術操作】
|
||||
• 使用 get_news 獲取相關新聞資料
|
||||
• 篩選高價值資訊並進行深度解讀
|
||||
|
||||
【報告架構】
|
||||
**字數要求**:**800-1500字(不含表格)**
|
||||
**嚴格遵守字數限制,少於800字或超過1500字的報告將被退回**
|
||||
|
||||
**內容結構**:
|
||||
1. 新聞摘要(120-150字):重點事件概述
|
||||
2. 影響分析(400-600字):事件對股價的多維度影響評估
|
||||
3. 風險提示(80-120字):潛在風險或市場未注意的因素
|
||||
4. 操作建議(150-200字):基於新聞面的投資策略
|
||||
5. 新聞事件表格(必須,不計入字數)
|
||||
|
||||
**撰寫原則**:
|
||||
- 聚焦實質影響,過濾非重要資訊
|
||||
- 提供獨立觀點與專業解讀
|
||||
- 必須包含關鍵新聞整理表格
|
||||
- 控制篇幅,確保在1500字以內完成分析
|
||||
|
||||
**結尾提示**:
|
||||
請在報告最後加上以下結尾:
|
||||
「---
|
||||
※ 本報告為新聞面分析,建議搭配基本面及技術面綜合研判。新聞資訊時效性強,投資有風險,請謹慎評估。」
|
||||
|
||||
請提供專業且具洞察力的新聞分析報告。"""
|
||||
+ """ 請務必在報告結尾附加一個 Markdown 表格,以整理報告中的要點。""",
|
||||
)
|
||||
# Get language-specific prompts
|
||||
system_message = get_news_analyst_prompt(language)
|
||||
role_instruction = get_agent_role_instruction(language)
|
||||
context_msg = get_context_message(language, current_date, company_name, ticker)
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
[
|
||||
(
|
||||
"system",
|
||||
"您是一個樂於助人的人工智慧助理,與其他助理協同工作。"
|
||||
" 使用提供的工具來逐步回答問題。"
|
||||
" 如果您無法完全回答,沒關係;另一個擁有不同工具的助理會在您中斷的地方提供幫助。盡您所能取得進展。"
|
||||
" 如果您或任何其他助理有最終交易提案:**買入/持有/賣出** 或可交付成果,"
|
||||
" 請在您的回覆前加上「最終交易提案:**買入/持有/賣出**」,以便團隊知道停止。"
|
||||
f"{role_instruction}"
|
||||
" 您可以使用以下工具:{tool_names}。\n{system_message}"
|
||||
"供您參考,目前日期是 {current_date}。我們正在關注的公司是 {company_name} (股票代碼:{ticker})",
|
||||
f" {context_msg}",
|
||||
),
|
||||
MessagesPlaceholder(variable_name="messages"),
|
||||
]
|
||||
|
|
@ -95,18 +55,14 @@ def create_news_analyst(llm):
|
|||
|
||||
prompt = prompt.partial(system_message=system_message)
|
||||
prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
|
||||
prompt = prompt.partial(current_date=current_date)
|
||||
prompt = prompt.partial(ticker=ticker)
|
||||
prompt = prompt.partial(company_name=state.get("company_name", ticker))
|
||||
|
||||
chain = prompt | llm.bind_tools(tools)
|
||||
result = chain.invoke(state["messages"])
|
||||
|
||||
# 報告邏輯修復:只在LLM最終回應時保存報告
|
||||
report = state.get("news_report", "") # 保持現有報告
|
||||
# Report logic: only save report when LLM gives final response
|
||||
report = state.get("news_report", "")
|
||||
|
||||
if len(result.tool_calls) == 0:
|
||||
# 沒有工具調用,這是最終的分析報告
|
||||
report = result.content
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -2,15 +2,17 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
|||
import time
|
||||
import json
|
||||
from tradingagents.agents.utils.agent_utils import get_news
|
||||
from tradingagents.agents.utils.prompts import get_social_analyst_prompt, get_agent_role_instruction, get_context_message
|
||||
from tradingagents.dataflows.config import get_config
|
||||
|
||||
|
||||
def create_social_media_analyst(llm):
|
||||
def create_social_media_analyst(llm, language: str = "zh-TW"):
|
||||
"""
|
||||
建立一個社群媒體分析師節點。
|
||||
|
||||
Args:
|
||||
llm: 用於分析的語言模型。
|
||||
language: 報告語言 ('en' 或 'zh-TW')
|
||||
|
||||
Returns:
|
||||
一個處理社群媒體分析的節點函式。
|
||||
|
|
@ -27,68 +29,24 @@ def create_social_media_analyst(llm):
|
|||
"""
|
||||
current_date = state["trade_date"]
|
||||
ticker = state["company_of_interest"]
|
||||
company_name = state.get("company_name", ticker) # 使用真實公司名稱,fallback到ticker
|
||||
company_name = state.get("company_name", ticker)
|
||||
|
||||
tools = [
|
||||
get_news,
|
||||
]
|
||||
|
||||
system_message = (
|
||||
"""**重要:您必須使用繁體中文(Traditional Chinese)回覆所有內容。**
|
||||
**嚴格禁止:請勿在回覆中使用任何 emoji 表情符號(如 ✅ ❌ 📊 📈 🚀 等)。**
|
||||
**請只使用純文字、數字、標點符號和必要的 Unicode 符號(如 ↑ ↓ ★ ●等)。**
|
||||
|
||||
【專業身份】
|
||||
您是市場情緒分析專家,負責解讀社群媒體與輿論氛圍對股價的潛在影響。
|
||||
|
||||
【分析重點】
|
||||
1. **情緒基調**:評估當前市場情緒狀態(樂觀/中性/悲觀)及其強度
|
||||
2. **討論熱度**:識別主流話題與關注焦點,判斷輿論方向
|
||||
3. **投資人結構**:觀察散戶與機構觀點的分歧或共識
|
||||
4. **極端訊號**:檢視是否出現非理性樂觀或恐慌情緒
|
||||
|
||||
【技術操作】
|
||||
• 使用 get_news 獲取相關新聞與社群討論資料
|
||||
• 分析輿情傾向與討論熱度
|
||||
|
||||
【報告架構】
|
||||
**字數要求**:**800-1500字(不含表格)**
|
||||
**嚴格遵守字數限制,少於800字或超過1500字的報告將被退回**
|
||||
|
||||
**內容結構**:
|
||||
1. 情緒概要(150字以上):市場氛圍與情緒指標
|
||||
2. 輿情分析(400-450字):主要討論議題與觀點分布
|
||||
3. 關鍵洞察(100字以上):情緒極值或轉折訊號
|
||||
4. 投資含義(150字以上):情緒面對操作策略的啟示
|
||||
5. 情緒數據表格(必須)
|
||||
|
||||
**撰寫原則**:
|
||||
- **兼具專業與易懂**:使用專業術語的同時,請用生活化的語言讓一般投資人也能理解
|
||||
- **舉例說明**:在解讀情緒指標時,適時加入白話文說明(例如:「恐慌貪婪指數達78,就像溫度計顯示發燒,代表市場過熱需要降溫」)
|
||||
- 客觀分析,避免主觀臆測
|
||||
- 聚焦有價值的情緒訊號
|
||||
- 必須包含情緒量化數據表格
|
||||
|
||||
**結尾提示**:
|
||||
請在報告最後加上以下結尾:
|
||||
「---
|
||||
💬 **本報告為市場情緒分析,建議搭配基本面及技術面綜合研判。投資有風險,請謹慎評估。**」
|
||||
|
||||
請提供專業且具洞察力、兼具易讀性的市場情緒分析報告。"""
|
||||
+ """ 請務必在報告結尾附加一個 Markdown 表格,以整理報告中的要點。""",
|
||||
)
|
||||
# Get language-specific prompts
|
||||
system_message = get_social_analyst_prompt(language)
|
||||
role_instruction = get_agent_role_instruction(language)
|
||||
context_msg = get_context_message(language, current_date, company_name, ticker)
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
[
|
||||
(
|
||||
"system",
|
||||
"您是一個樂於助人的人工智慧助理,與其他助理協同工作。"
|
||||
" 使用提供的工具來逐步回答問題。"
|
||||
" 如果您無法完全回答,沒關係;另一個擁有不同工具的助理會在您中斷的地方提供幫助。盡您所能取得進展。"
|
||||
" 如果您或任何其他助理有最終交易提案:**買入/持有/賣出** 或可交付成果,"
|
||||
" 請在您的回覆前加上「最終交易提案:**買入/持有/賣出**」,以便團隊知道停止。"
|
||||
f"{role_instruction}"
|
||||
" 您可以使用以下工具:{tool_names}。\n{system_message}"
|
||||
"供您參考,目前日期是 {current_date}。我們目前要分析的公司是 {company_name} (股票代碼:{ticker})",
|
||||
f" {context_msg}",
|
||||
),
|
||||
MessagesPlaceholder(variable_name="messages"),
|
||||
]
|
||||
|
|
@ -96,19 +54,15 @@ def create_social_media_analyst(llm):
|
|||
|
||||
prompt = prompt.partial(system_message=system_message)
|
||||
prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
|
||||
prompt = prompt.partial(current_date=current_date)
|
||||
prompt = prompt.partial(ticker=ticker)
|
||||
prompt = prompt.partial(company_name=company_name)
|
||||
|
||||
chain = prompt | llm.bind_tools(tools)
|
||||
|
||||
result = chain.invoke(state["messages"])
|
||||
|
||||
# 報告邏輯修復:只在LLM最終回應時保存報告
|
||||
report = state.get("sentiment_report", "") # 保持現有報告
|
||||
# Report logic: only save report when LLM gives final response
|
||||
report = state.get("sentiment_report", "")
|
||||
|
||||
if len(result.tool_calls) == 0:
|
||||
# 沒有工具調用,這是最終的分析報告
|
||||
report = result.content
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -12,39 +12,26 @@ from tenacity import (
|
|||
)
|
||||
from anthropic._exceptions import OverloadedError
|
||||
from tradingagents.agents.utils.output_filter import fix_common_llm_errors, validate_and_warn
|
||||
from tradingagents.agents.utils.prompts import get_research_manager_prompt
|
||||
|
||||
# 設置日誌記錄器
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_research_manager(llm, memory):
|
||||
def create_research_manager(llm, memory, language: str = "zh-TW"):
|
||||
"""
|
||||
建立一個研究管理員(裁判)節點。
|
||||
|
||||
這個節點扮演投資組合經理和辯論主持人的角色。
|
||||
其任務是評估看漲和看跌分析師之間的辯論,並做出最終的投資決策
|
||||
(與看跌方一致、與看漲方一致,或在有充分理由時選擇持有)。
|
||||
它還需要制定一個詳細的投資計畫給交易員。
|
||||
|
||||
Args:
|
||||
llm: 用於生成決策和計畫的語言模型。
|
||||
memory: 儲存過去情況和反思的記憶體物件。
|
||||
language: 報告語言 ('en' 或 'zh-TW')
|
||||
|
||||
Returns:
|
||||
function: 一個代表研究管理員節點的函式,可在 langgraph 中使用。
|
||||
function: 一個代表研究管理員節點的函式。
|
||||
"""
|
||||
|
||||
def research_manager_node(state) -> dict:
|
||||
"""
|
||||
研究管理員節點的執行函式。
|
||||
|
||||
Args:
|
||||
state (dict): 當前的圖狀態。
|
||||
|
||||
Returns:
|
||||
dict: 更新後的狀態,包含裁判的決策和投資計畫。
|
||||
"""
|
||||
# 從狀態中獲取所需資訊
|
||||
"""研究管理員節點的執行函式。"""
|
||||
investment_debate_state = state["investment_debate_state"]
|
||||
history = investment_debate_state.get("history", "")
|
||||
|
||||
|
|
@ -53,115 +40,52 @@ def create_research_manager(llm, memory):
|
|||
news_report = state["news_report"]
|
||||
fundamentals_report = state["fundamentals_report"]
|
||||
|
||||
# 定義文本截斷函數以避免超過 token 限制 - 移除截斷邏輯以保留完整報告內容
|
||||
# 為每個報告設置合理的字符限制 - 移除,保留完整報告
|
||||
|
||||
# 整合當前情況
|
||||
curr_situation = f"{market_research_report}\n\n{sentiment_report}\n\n{news_report}\n\n{fundamentals_report}"
|
||||
|
||||
# 從記憶體中獲取過去相似情況的經驗
|
||||
past_memories = memory.get_memories(curr_situation, n_matches=2)
|
||||
|
||||
# 將過去的經驗格式化為字串
|
||||
past_memory_str = ""
|
||||
for i, rec in enumerate(past_memories, 1):
|
||||
recommendation = rec["recommendation"]
|
||||
past_memory_str += recommendation + "\n\n"
|
||||
|
||||
# Get language-specific prompt
|
||||
base_prompt = get_research_manager_prompt(language)
|
||||
|
||||
# 截斷辯論歷史 - 這是最容易超過限制的部分 - 移除截斷以保留完整內容
|
||||
if language == "en":
|
||||
prompt = f"""{base_prompt}
|
||||
|
||||
# 建立提示 (prompt)
|
||||
prompt = f"""**重要:您必須使用繁體中文(Traditional Chinese)回覆所有內容。**
|
||||
**嚴格禁止:請勿在回覆中使用任何 emoji 表情符號(如 ✅ ❌ 📊 📈 🚀 等)。**
|
||||
**請只使用純文字、數字、標點符號和必要的 Unicode 符號(如 ↑ ↓ ★ ●等)。**
|
||||
【Available Information】
|
||||
- Past Reflections: "{past_memory_str}"
|
||||
- Debate History: {history}
|
||||
|
||||
【專業身份】
|
||||
您是投資決策經理,負責評估多空辯論並做出最終投資決策。**您必須保持嚴格中立觀點,公正評估看漲與看跌雙方論據,基於證據做出獨立決策。**
|
||||
|
||||
【職責】
|
||||
1. **評估論證**:客觀權衡看漲與看跌方的論據強度,不偏袒任何一方
|
||||
2. **做出決策**:基於證據明確判斷買入/賣出/持有,展現獨立判斷
|
||||
3. **制定計畫**:提供交易員可執行的詳細操作指引
|
||||
4. **中立裁判**:**作為中立裁判,綜合雙方論點後做出獨立決策,不受任何一方影響**
|
||||
Please provide your investment decision report."""
|
||||
else:
|
||||
prompt = f"""{base_prompt}
|
||||
|
||||
【可用資訊】
|
||||
- 過去反思:"{past_memory_str}"
|
||||
- 辯論歷史:{history}
|
||||
|
||||
【輸出要求】
|
||||
**字數要求**:**800-1500字**
|
||||
**嚴格遵守字數限制,少於800字或超過1500字的報告將被退回**
|
||||
**內容結構**:
|
||||
1. 決策摘要(150字以上):明確的買入/賣出/持有決策與核心理由
|
||||
2. 論證評估(200字以上):公正評估雙方最強論點與分歧點,不偏袒任何一方
|
||||
3. 決策依據(300字以上):選擇此立場的關鍵證據與邏輯推理
|
||||
4. 操作指引(100字以上):部位規模、目標價位、停損設定等具體參數
|
||||
5. 風險提示(50字以上):主要風險與監控重點
|
||||
|
||||
**撰寫原則**:
|
||||
- **嚴格中立**:作為中立裁判,不偏向看漲或看跌任何一方
|
||||
- **獨立決策**:基於證據與邏輯做出獨立判斷,展現決策自主性
|
||||
- 決策明確,避免模稜兩可,必須給出清晰立場
|
||||
- 提供具體量化的操作參數,確保可執行性
|
||||
- 邏輯清晰,證據充分,說服力強
|
||||
|
||||
**結尾提示**:
|
||||
請在報告最後加上以下結尾:
|
||||
「---
|
||||
👔 **本報告為研究經理的投資決策,綜合看漲與看跌雙方論據後做出。建議交易團隊執行前再次確認市場狀況。投資決策需獨立判斷,請謹慎評估。**」
|
||||
|
||||
請提供專業且可執行的投資決策報告。"""
|
||||
|
||||
# 定義帶重試機制的 LLM 調用函數
|
||||
# 基於 Cursor IDE 博客建議的最佳實踐:
|
||||
# 1. 使用指數退避策略(exponential backoff)
|
||||
# 2. 添加隨機因子(jitter)避免多個客戶端同步重試
|
||||
# 3. 增加重試次數和最大延遲時間
|
||||
import random
|
||||
請提供您的投資決策報告。"""
|
||||
|
||||
@retry(
|
||||
retry=retry_if_exception_type(OverloadedError),
|
||||
wait=wait_exponential(multiplier=1, min=2, max=60), # 增加最大延遲到 60 秒
|
||||
stop=stop_after_attempt(5), # 增加重試次數到 5 次
|
||||
wait=wait_exponential(multiplier=1, min=2, max=60),
|
||||
stop=stop_after_attempt(5),
|
||||
before_sleep=before_sleep_log(logger, logging.WARNING)
|
||||
)
|
||||
def invoke_llm_with_retry(llm_instance, prompt_text):
|
||||
"""
|
||||
調用 LLM 並在遇到 529 錯誤時自動重試。
|
||||
|
||||
使用指數退避策略加隨機因子(jitter):
|
||||
- 第 1 次重試:等待 2-4 秒(2 * 2^0 + jitter)
|
||||
- 第 2 次重試:等待 4-8 秒(2 * 2^1 + jitter)
|
||||
- 第 3 次重試:等待 8-16 秒(2 * 2^2 + jitter)
|
||||
- 第 4 次重試:等待 16-32 秒(2 * 2^3 + jitter)
|
||||
- 第 5 次重試:等待 32-60 秒(2 * 2^4 + jitter,最大 60 秒)
|
||||
|
||||
Args:
|
||||
llm_instance: LLM 實例
|
||||
prompt_text: 提示文本
|
||||
|
||||
Returns:
|
||||
LLM 的回應
|
||||
|
||||
Raises:
|
||||
OverloadedError: 如果 5 次重試後仍然失敗
|
||||
"""
|
||||
# 添加小量隨機延遲(jitter)避免同步重試
|
||||
jitter = random.uniform(0, 0.5)
|
||||
if jitter > 0:
|
||||
time.sleep(jitter)
|
||||
|
||||
logger.info("正在調用 Research Manager LLM...")
|
||||
return llm_instance.invoke(prompt_text)
|
||||
|
||||
# 使用帶重試機制的函數調用 LLM
|
||||
response = invoke_llm_with_retry(llm, prompt)
|
||||
|
||||
# CRITICAL FIX: Apply output filtering
|
||||
response.content = fix_common_llm_errors(response.content)
|
||||
validate_and_warn(response.content, "Research_Manager")
|
||||
|
||||
# 更新投資辯論狀態
|
||||
new_investment_debate_state = {
|
||||
"judge_decision": response.content,
|
||||
"history": investment_debate_state.get("history", ""),
|
||||
|
|
@ -171,7 +95,6 @@ def create_research_manager(llm, memory):
|
|||
"count": investment_debate_state["count"],
|
||||
}
|
||||
|
||||
# 返回更新後的狀態,包括裁判的決策和投資計畫
|
||||
return {
|
||||
"investment_debate_state": new_investment_debate_state,
|
||||
"investment_plan": response.content,
|
||||
|
|
|
|||
|
|
@ -2,118 +2,69 @@
|
|||
import time
|
||||
import json
|
||||
from tradingagents.agents.utils.output_filter import fix_common_llm_errors, validate_and_warn
|
||||
from tradingagents.agents.utils.prompts import get_risk_manager_prompt
|
||||
|
||||
|
||||
def create_risk_manager(llm, memory):
|
||||
def create_risk_manager(llm, memory, language: str = "zh-TW"):
|
||||
"""
|
||||
建立一個風險管理員(裁判)節點。
|
||||
|
||||
這個節點扮演風險管理裁判和辯論主持人的角色。
|
||||
其目標是評估激進、中立和保守三位風險分析師之間的辯論,
|
||||
並根據辯論內容、分析報告以及過去的經驗,對交易員的計畫做出最終的、
|
||||
經過風險調整的決策(買入、賣出或持有)。
|
||||
|
||||
Args:
|
||||
llm: 用於生成決策的語言模型。
|
||||
memory: 儲存過去情況和反思的記憶體物件。
|
||||
language: 報告語言 ('en' 或 'zh-TW')
|
||||
|
||||
Returns:
|
||||
function: 一個代表風險管理員節點的函式,可在 langgraph 中使用。
|
||||
function: 一個代表風險管理員節點的函式。
|
||||
"""
|
||||
|
||||
def risk_manager_node(state) -> dict:
|
||||
"""
|
||||
風險管理員節點的執行函式。
|
||||
|
||||
Args:
|
||||
state (dict): 當前的圖狀態。
|
||||
|
||||
Returns:
|
||||
dict: 更新後的狀態,包含最終的交易決策。
|
||||
"""
|
||||
# 從狀態中獲取所需資訊
|
||||
"""風險管理員節點的執行函式。"""
|
||||
company_name = state["company_of_interest"]
|
||||
risk_debate_state = state["risk_debate_state"]
|
||||
history = risk_debate_state["history"]
|
||||
|
||||
market_research_report = state["market_report"]
|
||||
news_report = state["news_report"]
|
||||
fundamentals_report = state["fundamentals_report"] # 這裡原文似乎有誤,應為 fundamentals_report
|
||||
fundamentals_report = state["fundamentals_report"]
|
||||
sentiment_report = state["sentiment_report"]
|
||||
trader_plan = state["investment_plan"]
|
||||
|
||||
# 移除截斷邏輯以保留完整報告內容
|
||||
|
||||
# 整合當前情況
|
||||
curr_situation = f"{market_research_report}\n\n{sentiment_report}\n\n{news_report}\n\n{fundamentals_report}"
|
||||
|
||||
# 從記憶體中獲取過去相似情況的經驗
|
||||
past_memories = memory.get_memories(curr_situation, n_matches=2)
|
||||
|
||||
# 將過去的經驗格式化為字串
|
||||
past_memory_str = ""
|
||||
for i, rec in enumerate(past_memories, 1):
|
||||
recommendation = rec["recommendation"]
|
||||
past_memory_str += recommendation + "\n\n"
|
||||
|
||||
# Get language-specific prompt
|
||||
base_prompt = get_risk_manager_prompt(language)
|
||||
|
||||
# 截斷辯論歷史 - 這是最容易超過限制的部分
|
||||
# 增加限制以容納更長的辯論內容(風險辯論通常有3方,比投資辯論更長)
|
||||
history = history # 移除截斷,保留完整歷史
|
||||
if language == "en":
|
||||
prompt = f"""{base_prompt}
|
||||
|
||||
|
||||
# 建立提示 (prompt)
|
||||
prompt = f"""**重要:您必須使用繁體中文(Traditional Chinese)回覆所有內容。**
|
||||
**嚴格禁止:請勿在回覆中使用任何 emoji 表情符號(如 ✅ ❌ 📊 📈 🚀 等)。**
|
||||
**請只使用純文字、數字、標點符號和必要的 Unicode 符號(如 ↑ ↓ ★ ●等)。**
|
||||
【Available Information】
|
||||
- Past Reflections: "{past_memory_str}"
|
||||
- Trader Plan: {trader_plan}
|
||||
- Debate History: {history}
|
||||
|
||||
【專業身份】
|
||||
您是風險管理經理,負責評估投資計畫的風險並做出最終風控決策。**您必須保持嚴格中立觀點,綜合評估積極、中立、保守三方風險觀點,基於風險調整做出最終決策。**
|
||||
|
||||
【職責】
|
||||
1. **評估辯論**:綜合積極、中立、保守三方的風險觀點,不偏袒任何一方
|
||||
2. **識別風險**:系統性評估市場、財務、營運等多維度風險
|
||||
3. **最終決策**:基於風險調整後的買入/賣出/持有決策,展現獨立判斷
|
||||
4. **風控設定**:建立明確的風險管理框架與具體參數
|
||||
5. **中立裁判**:**作為風險中立裁判,綜合三方觀點後做出獨立決策**
|
||||
Please provide your risk management decision report."""
|
||||
else:
|
||||
prompt = f"""{base_prompt}
|
||||
|
||||
【可用資訊】
|
||||
- 過去反思:"{past_memory_str}"
|
||||
- 交易員計畫:{trader_plan}
|
||||
- 辯論歷史:{history}
|
||||
|
||||
【輸出要求】
|
||||
**字數要求**:**800-1500字**
|
||||
**嚴格遵守字數限制,少於800字或超過1500字的報告將被退回**
|
||||
**內容結構**:
|
||||
1. 風控結論(150字以上):風險評級與最終決策的明確陳述
|
||||
2. 論證評估(200字以上):三方風險觀點的綜合評估,公正分析
|
||||
3. 風險分析(300字以上):主要風險因素與量化評估,多維度分析
|
||||
4. 最終決策(100字以上):經風險調整的操作建議與部位規模
|
||||
5. 風控措施(50字以上):停損、監控指標、應急預案等具體措施
|
||||
請提供您的風險管理決策報告。"""
|
||||
|
||||
**撰寫原則**:
|
||||
- **嚴格中立**:綜合評估積極、保守、中立三方觀點,不偏袒任何一方
|
||||
- **獨立決策**:基於風險評估做出獨立判斷,展現決策自主性
|
||||
- 決策明確,風控參數具體,確保可執行性
|
||||
- 保守謹慎,但避免過度保守影響報酬
|
||||
- 提供完整的風險管理框架與具體措施
|
||||
|
||||
**結尾提示**:
|
||||
請在報告最後加上以下結尾:
|
||||
「---
|
||||
※ 本報告為風險管理經理的最終決策,綜合三方風險觀點(積極、保守、平衡)後做出。風控框架需嚴格執行。投資有風險,請謹慎評估。」
|
||||
|
||||
請提供專業且全面的風險管理決策報告。"""
|
||||
|
||||
|
||||
# 呼叫 LLM 生成決策
|
||||
response = llm.invoke(prompt)
|
||||
|
||||
# CRITICAL FIX: Apply output filtering to fix common LLM errors
|
||||
response.content = fix_common_llm_errors(response.content)
|
||||
validate_and_warn(response.content, "Risk_Manager")
|
||||
|
||||
# 更新風險辯論狀態
|
||||
new_risk_debate_state = {
|
||||
"judge_decision": response.content,
|
||||
"history": risk_debate_state["history"],
|
||||
|
|
@ -127,7 +78,6 @@ def create_risk_manager(llm, memory):
|
|||
"count": risk_debate_state["count"],
|
||||
}
|
||||
|
||||
# 返回更新後的狀態,包括最終交易決策
|
||||
return {
|
||||
"risk_debate_state": new_risk_debate_state,
|
||||
"final_trade_decision": response.content,
|
||||
|
|
|
|||
|
|
@ -3,73 +3,61 @@ from langchain_core.messages import AIMessage
|
|||
import time
|
||||
import json
|
||||
from tradingagents.agents.utils.output_filter import fix_common_llm_errors, validate_and_warn
|
||||
from tradingagents.agents.utils.prompts import get_bear_researcher_prompt
|
||||
|
||||
|
||||
def create_bear_researcher(llm, memory):
|
||||
def create_bear_researcher(llm, memory, language: str = "zh-TW"):
|
||||
"""
|
||||
建立一個看跌研究員節點。
|
||||
|
||||
這個節點在辯論中扮演看跌分析師的角色,提出反對投資某支股票的論點。
|
||||
它會利用市場研究、情緒分析、新聞和基本面報告,並結合過去的經驗(記憶),
|
||||
來強調風險、挑戰和負面指標,並反駁看漲方的觀點。
|
||||
|
||||
Args:
|
||||
llm: 用於生成回應的語言模型。
|
||||
memory: 儲存過去情況和反思的記憶體物件。
|
||||
language: 報告語言 ('en' 或 'zh-TW')
|
||||
|
||||
Returns:
|
||||
function: 一個代表看跌研究員節點的函式,可在 langgraph 中使用。
|
||||
function: 一個代表看跌研究員節點的函式。
|
||||
"""
|
||||
|
||||
def bear_node(state) -> dict:
|
||||
"""
|
||||
看跌研究員節點的執行函式。
|
||||
|
||||
Args:
|
||||
state (dict): 當前的圖狀態。
|
||||
|
||||
Returns:
|
||||
dict: 更新後的狀態,包含新的投資辯論狀態。
|
||||
"""
|
||||
# 從狀態中獲取投資辯論的相關資訊
|
||||
"""看跌研究員節點的執行函式。"""
|
||||
investment_debate_state = state["investment_debate_state"]
|
||||
history = investment_debate_state.get("history", "")
|
||||
bear_history = investment_debate_state.get("bear_history", "")
|
||||
current_response = investment_debate_state.get("current_response", "")
|
||||
|
||||
# 從狀態中獲取各類分析報告
|
||||
market_research_report = state["market_report"]
|
||||
sentiment_report = state["sentiment_report"]
|
||||
news_report = state["news_report"]
|
||||
fundamentals_report = state["fundamentals_report"]
|
||||
|
||||
# 整合當前情況 - 移除截斷邏輯以保留完整報告內容
|
||||
curr_situation = f"{market_research_report}\n\n{sentiment_report}\n\n{news_report}\n\n{fundamentals_report}"
|
||||
|
||||
# 從記憶體中獲取過去相似情況的經驗
|
||||
past_memories = memory.get_memories(curr_situation, n_matches=2)
|
||||
|
||||
# 將過去的經驗格式化為字串
|
||||
past_memory_str = ""
|
||||
for i, rec in enumerate(past_memories, 1):
|
||||
recommendation = rec["recommendation"]
|
||||
past_memory_str += recommendation + "\n\n"
|
||||
|
||||
# 建立提示 (prompt) - 保留完整歷史以確保context完整性
|
||||
# Get language-specific prompt template
|
||||
base_prompt = get_bear_researcher_prompt(language)
|
||||
|
||||
prompt = f"""**重要:您必須使用繁體中文(Traditional Chinese)回覆所有內容。**
|
||||
**嚴格禁止:請勿在回覆中使用任何 emoji 表情符號(如 ✅ ❌ 📊 📈 🚀 等)。**
|
||||
**請只使用純文字、數字、標點符號和必要的 Unicode 符號(如 ↑ ↓ ★ ●等)。**
|
||||
# Build the full prompt with context
|
||||
if language == "en":
|
||||
prompt = f"""{base_prompt}
|
||||
|
||||
【專業身份】
|
||||
您是看跌方研究員,負責提出賣出論據,強調投資風險與下跌壓力。**您必須採取激進做空立場,不惜一切代價找出所有看跌風險因子,並強力反駁看漲論點。**
|
||||
【Available Data】
|
||||
- Market Analysis: {market_research_report}
|
||||
- Sentiment Report: {sentiment_report}
|
||||
- News Report: {news_report}
|
||||
- Fundamentals Report: {fundamentals_report}
|
||||
- Debate History: {history}
|
||||
- Bullish Arguments: {current_response}
|
||||
- Past Experience: {past_memory_str}
|
||||
|
||||
【分析重點】
|
||||
1. **成長疑慮**:檢視營收成長減速、市場飽和或競爭加劇跡象,放大成長隱憂
|
||||
2. **競爭劣勢**:評估護城河侵蝕、市佔率流失或定價能力弱化,強調競爭威脅
|
||||
3. **財務問題**:識別現金流惡化、債務風險或獲利品質下降,揭露財務危機
|
||||
4. **負面催化**:指出可能觸發股價下跌的事件或結構性問題,放大利空影響
|
||||
5. **反駁看漲**:**強力反駁看漲方論點,直指其盲目樂觀,揭露其論據的致命缺陷**
|
||||
Please provide your bearish analysis now."""
|
||||
else:
|
||||
prompt = f"""{base_prompt}
|
||||
|
||||
【可用資源】
|
||||
- 市場分析:{market_research_report}
|
||||
|
|
@ -80,41 +68,19 @@ def create_bear_researcher(llm, memory):
|
|||
- 看漲論點:{current_response}
|
||||
- 過往經驗:{past_memory_str}
|
||||
|
||||
【輸出要求】
|
||||
**字數要求**:**800-1500字**
|
||||
**嚴格遵守字數限制,少於800字或超過1500字的報告將被退回**
|
||||
**內容結構**:
|
||||
1. 核心警示(150字以上):清晰且強勢地陳述看跌理由,展現堅定立場
|
||||
2. 風險論證(450-500字):用詳實數據支撐風險分析,層層揭露隱患
|
||||
3. 反駁看漲(100字以上):**激進地反駁看漲觀點,直指對方論據的盲目樂觀與邏輯漏洞**
|
||||
4. 投資建議(100字以上):明確且謹慎的操作建議,建議減倉或觀望
|
||||
請提供您的看跌分析。"""
|
||||
|
||||
**撰寫原則**:
|
||||
- **激進做空**:採取極度謹慎立場,強調所有風險因素
|
||||
- **強力反駁**:對看漲論點窮追猛打,揭露其盲目樂觀與忽略的風險
|
||||
- 論據扎實,以數據與事實為基礎,但解讀偏向悲觀
|
||||
- 直接指出對方論點的漏洞,不留情面
|
||||
- 強調風險遠大於機會
|
||||
|
||||
**結尾提示**:
|
||||
請在報告最後加上以下結尾:
|
||||
「---
|
||||
🐻 **本報告為看跌方研究分析,立場偏向謹慎保守。建議搭配看漲方觀點與市場情緒綜合研判。投資有風險,請謹慎評估。**」
|
||||
|
||||
請提供有說服力且激進的看跌分析報告。
|
||||
"""
|
||||
|
||||
# 呼叫 LLM 生成回應
|
||||
response = llm.invoke(prompt)
|
||||
|
||||
# CRITICAL FIX: Apply output filtering to fix common LLM errors
|
||||
response.content = fix_common_llm_errors(response.content)
|
||||
validate_and_warn(response.content, "Bear_Researcher")
|
||||
|
||||
# 格式化論點
|
||||
argument = f"看跌分析師:{response.content}"
|
||||
# Format argument based on language
|
||||
if language == "en":
|
||||
argument = f"Bear Analyst: {response.content}"
|
||||
else:
|
||||
argument = f"看跌分析師:{response.content}"
|
||||
|
||||
# 更新投資辯論狀態
|
||||
new_investment_debate_state = {
|
||||
"history": history + "\n" + argument,
|
||||
"bear_history": bear_history + "\n" + argument,
|
||||
|
|
|
|||
|
|
@ -3,73 +3,61 @@ from langchain_core.messages import AIMessage
|
|||
import time
|
||||
import json
|
||||
from tradingagents.agents.utils.output_filter import fix_common_llm_errors, validate_and_warn
|
||||
from tradingagents.agents.utils.prompts import get_bull_researcher_prompt
|
||||
|
||||
|
||||
def create_bull_researcher(llm, memory):
|
||||
def create_bull_researcher(llm, memory, language: str = "zh-TW"):
|
||||
"""
|
||||
建立一個看漲研究員節點。
|
||||
|
||||
這個節點在辯論中扮演看漲分析師的角色,主張投資某支股票。
|
||||
它會利用市場研究、情緒分析、新聞和基本面報告,並結合過去的經驗(記憶),
|
||||
來構建一個有說服力的論點,並反駁看跌方的觀點。
|
||||
|
||||
Args:
|
||||
llm: 用於生成回應的語言模型。
|
||||
memory: 儲存過去情況和反思的記憶體物件。
|
||||
language: 報告語言 ('en' 或 'zh-TW')
|
||||
|
||||
Returns:
|
||||
function: 一個代表看漲研究員節點的函式,可在 langgraph 中使用。
|
||||
function: 一個代表看漲研究員節點的函式。
|
||||
"""
|
||||
|
||||
def bull_node(state) -> dict:
|
||||
"""
|
||||
看漲研究員節點的執行函式。
|
||||
|
||||
Args:
|
||||
state (dict): 當前的圖狀態。
|
||||
|
||||
Returns:
|
||||
dict: 更新後的狀態,包含新的投資辯論狀態。
|
||||
"""
|
||||
# 從狀態中獲取投資辯論的相關資訊
|
||||
"""看漲研究員節點的執行函式。"""
|
||||
investment_debate_state = state["investment_debate_state"]
|
||||
history = investment_debate_state.get("history", "")
|
||||
bull_history = investment_debate_state.get("bull_history", "")
|
||||
current_response = investment_debate_state.get("current_response", "")
|
||||
|
||||
# 從狀態中獲取各類分析報告
|
||||
market_research_report = state["market_report"]
|
||||
sentiment_report = state["sentiment_report"]
|
||||
news_report = state["news_report"]
|
||||
fundamentals_report = state["fundamentals_report"]
|
||||
|
||||
# 整合當前情況 - 移除截斷邏輯以保留完整報告內容
|
||||
curr_situation = f"{market_research_report}\n\n{sentiment_report}\n\n{news_report}\n\n{fundamentals_report}"
|
||||
|
||||
# 從記憶體中獲取過去相似情況的經驗
|
||||
past_memories = memory.get_memories(curr_situation, n_matches=2)
|
||||
|
||||
# 將過去的經驗格式化為字串
|
||||
past_memory_str = ""
|
||||
for i, rec in enumerate(past_memories, 1):
|
||||
recommendation = rec["recommendation"]
|
||||
past_memory_str += recommendation + "\n\n"
|
||||
|
||||
# 建立提示 (prompt) - 保留完整歷史以確保context完整性
|
||||
# Get language-specific prompt template
|
||||
base_prompt = get_bull_researcher_prompt(language)
|
||||
|
||||
prompt = f"""**重要:您必須使用繁體中文(Traditional Chinese)回覆所有內容。**
|
||||
**嚴格禁止:請勿在回覆中使用任何 emoji 表情符號(如 ✅ ❌ 📊 📈 🚀 等)。**
|
||||
**請只使用純文字、數字、標點符號和必要的 Unicode 符號(如 ↑ ↓ ★ ●等)。**
|
||||
# Build the full prompt with context
|
||||
if language == "en":
|
||||
prompt = f"""{base_prompt}
|
||||
|
||||
【專業身份】
|
||||
您是看漲方研究員,負責提出買進論據,強調投資價值與上漲潛力。**您必須採取激進做多立場,不惜一切代價找出所有看漲催化劑,並強力反駁看跌論點。**
|
||||
【Available Data】
|
||||
- Market Analysis: {market_research_report}
|
||||
- Sentiment Report: {sentiment_report}
|
||||
- News Report: {news_report}
|
||||
- Fundamentals Report: {fundamentals_report}
|
||||
- Debate History: {history}
|
||||
- Bearish Arguments: {current_response}
|
||||
- Past Experience: {past_memory_str}
|
||||
|
||||
【分析重點】
|
||||
1. **成長動能**:評估營收、盈餘成長的持續性與加速跡象,找出所有成長加速的證據
|
||||
2. **競爭優勢**:分析護城河、市場地位與定價能力,強調絕對優勢
|
||||
3. **催化因子**:識別可能推升股價的近期事件或結構性改變,放大利多影響
|
||||
4. **估值優勢**:說明當前價格相對價值的吸引力,強調被低估的幅度
|
||||
5. **反駁看跌**:**強力反駁看跌方論點,不留情面,直指其論據的漏洞與過度悲觀**
|
||||
Please provide your bullish analysis now."""
|
||||
else:
|
||||
prompt = f"""{base_prompt}
|
||||
|
||||
【可用資料】
|
||||
- 市場分析:{market_research_report}
|
||||
|
|
@ -80,41 +68,19 @@ def create_bull_researcher(llm, memory):
|
|||
- 看跌論點:{current_response}
|
||||
- 過往經驗:{past_memory_str}
|
||||
|
||||
【輸出要求】
|
||||
**字數要求**:**800-1500字**
|
||||
**嚴格遵守字數限制,少於800字或超過1500字的報告將被退回**
|
||||
**內容結構**:
|
||||
1. 核心論點(150字以上):清晰且強勢地陳述看漲理由,展現必勝信心
|
||||
2. 成長論證(450-500字):用詳實數據支撐成長邏輯,層層推進論述
|
||||
3. 反駁看跌(100字以上):**激進地反駁看跌觀點,不留情面,直指對方論據的致命缺陷**
|
||||
4. 投資建議(100字以上):明確且積極的操作建議,鼓勵進場
|
||||
請提供您的看漲分析。"""
|
||||
|
||||
**撰寫原則**:
|
||||
- **激進做多**:採取極度樂觀立場,強調所有利多因素
|
||||
- **強力反駁**:對看跌論點窮追猛打,揭露其邏輯漏洞與過度悲觀
|
||||
- 論據扎實,以數據與事實為基礎,但解讀偏向樂觀
|
||||
- 直接回應對方論點,避免迴避問題
|
||||
- 承認風險但強調機會遠大於風險
|
||||
|
||||
**結尾提示**:
|
||||
請在報告最後加上以下結尾:
|
||||
「---
|
||||
🐂 **本報告為看漲方研究分析,立場偏向積極樂觀。建議搭配看跌方觀點與風險評估綜合研判。投資有風險,請謹慎評估。**」
|
||||
|
||||
請提供有說服力且激進的看漲分析報告。
|
||||
"""
|
||||
|
||||
# 呼叫 LLM 生成回應
|
||||
response = llm.invoke(prompt)
|
||||
|
||||
# CRITICAL FIX: Apply output filtering to fix common LLM errors
|
||||
response.content = fix_common_llm_errors(response.content)
|
||||
validate_and_warn(response.content, "Bull_Researcher")
|
||||
|
||||
# 格式化論點
|
||||
argument = f"看漲分析師:{response.content}"
|
||||
# Format argument based on language
|
||||
if language == "en":
|
||||
argument = f"Bull Analyst: {response.content}"
|
||||
else:
|
||||
argument = f"看漲分析師:{response.content}"
|
||||
|
||||
# 更新投資辯論狀態
|
||||
new_investment_debate_state = {
|
||||
"history": history + "\n" + argument,
|
||||
"bull_history": bull_history + "\n" + argument,
|
||||
|
|
|
|||
|
|
@ -2,67 +2,51 @@
|
|||
import time
|
||||
import json
|
||||
from tradingagents.agents.utils.output_filter import fix_common_llm_errors, validate_and_warn
|
||||
from tradingagents.agents.utils.prompts import get_aggressive_debator_prompt
|
||||
|
||||
|
||||
def create_risky_debator(llm):
|
||||
def create_risky_debator(llm, language: str = "zh-TW"):
|
||||
"""
|
||||
建立一個激進的風險辯論員節點。
|
||||
|
||||
這個節點在風險評估辯論中扮演激進派的角色。
|
||||
其目標是積極倡導高回報、高風險的機會,強調大膽的策略和競爭優勢。
|
||||
它會專注於潛在的上升空間,並挑戰保守和中立的觀點。
|
||||
|
||||
Args:
|
||||
llm: 用於生成回應的語言模型。
|
||||
language: 報告語言 ('en' 或 'zh-TW')
|
||||
|
||||
Returns:
|
||||
function: 一個代表激進辯論員節點的函式,可在 langgraph 中使用。
|
||||
function: 一個代表激進辯論員節點的函式。
|
||||
"""
|
||||
|
||||
def risky_node(state) -> dict:
|
||||
"""
|
||||
激進辯論員節點的執行函式。
|
||||
|
||||
Args:
|
||||
state (dict): 當前的圖狀態。
|
||||
|
||||
Returns:
|
||||
dict: 更新後的狀態,包含新的風險辯論狀態。
|
||||
"""
|
||||
# 從狀態中獲取風險辯論的相關資訊
|
||||
"""激進辯論員節點的執行函式。"""
|
||||
risk_debate_state = state["risk_debate_state"]
|
||||
history = risk_debate_state.get("history", "")
|
||||
risky_history = risk_debate_state.get("risky_history", "")
|
||||
|
||||
# 獲取其他辯論者的最新回應
|
||||
current_safe_response = risk_debate_state.get("current_safe_response", "")
|
||||
current_neutral_response = risk_debate_state.get("current_neutral_response", "")
|
||||
|
||||
# 從狀態中獲取各類分析報告
|
||||
market_research_report = state["market_report"]
|
||||
sentiment_report = state["sentiment_report"]
|
||||
news_report = state["news_report"]
|
||||
fundamentals_report = state["fundamentals_report"]
|
||||
|
||||
# 獲取交易員的決策
|
||||
trader_decision = state["trader_investment_plan"]
|
||||
|
||||
# 移除截斷邏輯以保留完整報告內容
|
||||
# Get language-specific prompt
|
||||
base_prompt = get_aggressive_debator_prompt(language)
|
||||
|
||||
# 建立提示 (prompt)
|
||||
prompt = f"""**重要:您必須使用繁體中文(Traditional Chinese)回覆所有內容。**
|
||||
**嚴格禁止:請勿在回覆中使用任何 emoji 表情符號(如 ✅ ❌ 📊 📈 🚀 等)。**
|
||||
**請只使用純文字、數字、標點符號和必要的 Unicode 符號(如 ↑ ↓ ★ ●等)。**
|
||||
if language == "en":
|
||||
prompt = f"""{base_prompt}
|
||||
|
||||
【專業身份】
|
||||
您是積極型風險策略師,主張追求高報酬機會,評估上檔潛力。**您必須採取極度激進立場,全力追求最大報酬潛力,並強力反駁保守派的過度謹慎。**
|
||||
【Available Information】
|
||||
- Trader Plan: {trader_decision}
|
||||
- Reports: {market_research_report}, {sentiment_report}, {news_report}, {fundamentals_report}
|
||||
- Debate History: {history}
|
||||
- Opponent Views: {current_safe_response}, {current_neutral_response}
|
||||
|
||||
【論證重點】
|
||||
1. **上檔空間**:量化分析最佳情境下的報酬潛力,放大獲利想像空間
|
||||
2. **催化事件**:識別可能帶動股價突破的關鍵因素,強調爆發性成長
|
||||
3. **成長加速**:評估營收或盈餘成長提速的可能性,找出所有加速跡象
|
||||
4. **保守迷思**:**強力反駁保守派觀點,指出其過度保守可能錯失的巨大機會成本**
|
||||
5. **風險容忍**:主張適度承擔風險以換取超額報酬
|
||||
Please provide your aggressive risk analysis."""
|
||||
else:
|
||||
prompt = f"""{base_prompt}
|
||||
|
||||
【可用資訊】
|
||||
- 交易員計畫:{trader_decision}
|
||||
|
|
@ -70,51 +54,27 @@ def create_risky_debator(llm):
|
|||
- 辯論歷史:{history}
|
||||
- 對手觀點:{current_safe_response}, {current_neutral_response}
|
||||
|
||||
【輸出要求】
|
||||
**字數要求**:**800-1500字**
|
||||
**嚴格遵守字數限制,少於800字或超過1500字的報告將被退回**
|
||||
**內容結構**:
|
||||
1. 核心主張(150字以上):清晰且強勢地陳述積極策略的理由,展現必勝信心
|
||||
2. 機會分析(450-500字):詳細論證上檔潛力,層層推進論述
|
||||
3. 反駁保守(100字以上):**激進地反駁保守派的擔憂,質疑其過度謹慎與縮手縮腳**
|
||||
4. 操作建議(100字以上):明確的激進部位建議,鼓勵大膽進場
|
||||
請提供您的激進風險分析。"""
|
||||
|
||||
**撰寫原則**:
|
||||
- **極度激進**:採取最樂觀立場,追求最大報酬
|
||||
- **強力反駁**:對保守派論點窮追猛打,揭露其過度謹慎的機會成本
|
||||
- 量化評估,避免空泛樂觀,但解讀偏向樂觀
|
||||
- 直接回應風險疑慮,但強調機會遠大於風險
|
||||
- 鼓勵承擔合理風險以換取超額報酬
|
||||
|
||||
**結尾提示**:
|
||||
請在報告最後加上以下結尾:
|
||||
「---
|
||||
⚡ **本報告為積極型風險策略分析,立場追求高報酬機會。建議搭配保守與平衡觀點綜合研判。高報酬伴隨高風險,請謹慎評估。**」
|
||||
|
||||
請提供專業且具說服力的積極策略分析。"""
|
||||
|
||||
# 呼叫 LLM 生成回應
|
||||
response = llm.invoke(prompt)
|
||||
|
||||
# CRITICAL FIX: Apply output filtering
|
||||
response.content = fix_common_llm_errors(response.content)
|
||||
validate_and_warn(response.content, "Aggressive_Debator")
|
||||
|
||||
# 格式化論點
|
||||
argument = f"激進分析師:{response.content}"
|
||||
if language == "en":
|
||||
argument = f"Aggressive Analyst: {response.content}"
|
||||
else:
|
||||
argument = f"激進分析師:{response.content}"
|
||||
|
||||
# 更新風險辯論狀態
|
||||
new_risk_debate_state = {
|
||||
"history": history + "\n" + argument,
|
||||
"risky_history": risky_history + "\n" + argument,
|
||||
"safe_history": risk_debate_state.get("safe_history", ""),
|
||||
"neutral_history": risk_debate_state.get("neutral_history", ""),
|
||||
"latest_speaker": "Risky", # 記錄最新的發言者
|
||||
"latest_speaker": "Risky",
|
||||
"current_risky_response": argument,
|
||||
"current_safe_response": risk_debate_state.get("current_safe_response", ""),
|
||||
"current_neutral_response": risk_debate_state.get(
|
||||
"current_neutral_response", ""
|
||||
),
|
||||
"current_neutral_response": risk_debate_state.get("current_neutral_response", ""),
|
||||
"count": risk_debate_state["count"] + 1,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,69 +1,52 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from langchain_core.messages import AIMessage
|
||||
import time
|
||||
import json
|
||||
from tradingagents.agents.utils.output_filter import fix_common_llm_errors, validate_and_warn
|
||||
from tradingagents.agents.utils.prompts import get_conservative_debator_prompt
|
||||
|
||||
|
||||
def create_safe_debator(llm):
|
||||
def create_safe_debator(llm, language: str = "zh-TW"):
|
||||
"""
|
||||
建立一個安全/保守的風險辯論員節點。
|
||||
|
||||
這個節點在風險評估辯論中扮演保守派的角色。
|
||||
其主要目標是保護資產、最小化波動性並確保穩定可靠的增長。
|
||||
它會優先考慮穩定性、安全性和風險緩解,並對交易員的決策提出謹慎的調整建議。
|
||||
建立一個保守的風險辯論員節點。
|
||||
|
||||
Args:
|
||||
llm: 用於生成回應的語言模型。
|
||||
language: 報告語言 ('en' 或 'zh-TW')
|
||||
|
||||
Returns:
|
||||
function: 一個代表保守辯論員節點的函式,可在 langgraph 中使用。
|
||||
function: 一個代表保守辯論員節點的函式。
|
||||
"""
|
||||
|
||||
def safe_node(state) -> dict:
|
||||
"""
|
||||
保守辯論員節點的執行函式。
|
||||
|
||||
Args:
|
||||
state (dict): 當前的圖狀態。
|
||||
|
||||
Returns:
|
||||
dict: 更新後的狀態,包含新的風險辯論狀態。
|
||||
"""
|
||||
# 從狀態中獲取風險辯論的相關資訊
|
||||
"""保守辯論員節點的執行函式。"""
|
||||
risk_debate_state = state["risk_debate_state"]
|
||||
history = risk_debate_state.get("history", "")
|
||||
safe_history = risk_debate_state.get("safe_history", "")
|
||||
|
||||
# 獲取其他辯論者的最新回應
|
||||
current_risky_response = risk_debate_state.get("current_risky_response", "")
|
||||
current_neutral_response = risk_debate_state.get("current_neutral_response", "")
|
||||
|
||||
# 從狀態中獲取各類分析報告
|
||||
market_research_report = state["market_report"]
|
||||
sentiment_report = state["sentiment_report"]
|
||||
news_report = state["news_report"]
|
||||
fundamentals_report = state["fundamentals_report"]
|
||||
|
||||
# 獲取交易員的決策
|
||||
trader_decision = state["trader_investment_plan"]
|
||||
|
||||
# 移除截斷邏輯以保留完整報告內容
|
||||
# Get language-specific prompt
|
||||
base_prompt = get_conservative_debator_prompt(language)
|
||||
|
||||
# 建立提示 (prompt)
|
||||
prompt = f"""**重要:您必須使用繁體中文(Traditional Chinese)回覆所有內容。**
|
||||
**嚴格禁止:請勿在回覆中使用任何 emoji 表情符號(如 ✅ ❌ 📊 📈 🚀 等)。**
|
||||
**請只使用純文字、數字、標點符號和必要的 Unicode 符號(如 ↑ ↓ ★ ●等)。**
|
||||
if language == "en":
|
||||
prompt = f"""{base_prompt}
|
||||
|
||||
【專業身份】
|
||||
您是保守型風險策略師,優先考量資本保全,評估下檔風險。**您必須採取極度保守立場,全力維護資本安全,並強力反駁激進派的盲目樂觀。**
|
||||
【Available Information】
|
||||
- Trader Plan: {trader_decision}
|
||||
- Reports: {market_research_report}, {sentiment_report}, {news_report}, {fundamentals_report}
|
||||
- Debate History: {history}
|
||||
- Opponent Views: {current_risky_response}, {current_neutral_response}
|
||||
|
||||
【論證重點】
|
||||
1. **下檔風險**:量化分析最壞情境下的潛在損失,放大風險威脅
|
||||
2. **隱藏風險**:識別市場尚未充分反應的威脅因素,揭露潛在地雷
|
||||
3. **估值疑慮**:評估股價相對基本面的偏離程度,強調高估風險
|
||||
4. **激進盲點**:**強力反駁激進派觀點,指出其盲目樂觀忽略的重大風險因子**
|
||||
5. **資本保全**:主張穩健策略優先於激進追逐報酬
|
||||
Please provide your conservative risk analysis."""
|
||||
else:
|
||||
prompt = f"""{base_prompt}
|
||||
|
||||
【可用資訊】
|
||||
- 交易員計畫:{trader_decision}
|
||||
|
|
@ -71,53 +54,27 @@ def create_safe_debator(llm):
|
|||
- 辯論歷史:{history}
|
||||
- 對手觀點:{current_risky_response}, {current_neutral_response}
|
||||
|
||||
【輸出要求】
|
||||
**字數要求**:**800-1500字**
|
||||
**嚴格遵守字數限制,少於800字或超過1500字的報告將被退回**
|
||||
**內容結構**:
|
||||
1. 核心警示(150字以上):清晰且強勢地陳述保守建議的理由,展現堅定立場
|
||||
2. 風險盤點(450-500字):詳細分析下檔風險,層層揭露隱患
|
||||
3. 反駁激進(100字以上):**激進地反駁激進派的論點,指出其盲目樂觀與被忽略的風險**
|
||||
4. 操作建議(100字以上):明確的保守風控建議,建議謹慎或減倉
|
||||
請提供您的保守風險分析。"""
|
||||
|
||||
**撰寫原則**:
|
||||
- **極度保守**:採取最謹慎立場,優先資本保全
|
||||
- **強力反駁**:對激進派論點窮追猛打,揭露其盲目樂觀與被忽視的風險
|
||||
- 量化評估,避免過度悲觀,但解讀偏向謹慎
|
||||
- 直接回應機會論述,但強調風險管理的重要性
|
||||
- 強調穩健增長優於激進追逐
|
||||
|
||||
**結尾提示**:
|
||||
請在報告最後加上以下結尾:
|
||||
「---
|
||||
※ 本報告為保守型風險策略分析,立場優先資本保全。建議搭配積極與平衡觀點綜合研判。風險控制為投資首要,請謹慎評估。」
|
||||
|
||||
請提供專業且具說服力的保守策略分析。"""
|
||||
|
||||
# 呼叫 LLM 生成回應
|
||||
response = llm.invoke(prompt)
|
||||
|
||||
# CRITICAL FIX: Apply output filtering
|
||||
response.content = fix_common_llm_errors(response.content)
|
||||
validate_and_warn(response.content, "Conservative_Debator")
|
||||
|
||||
# 格式化論點
|
||||
argument = f"安全分析師:{response.content}"
|
||||
if language == "en":
|
||||
argument = f"Conservative Analyst: {response.content}"
|
||||
else:
|
||||
argument = f"保守分析師:{response.content}"
|
||||
|
||||
# 更新風險辯論狀態
|
||||
new_risk_debate_state = {
|
||||
"history": history + "\n" + argument,
|
||||
"risky_history": risk_debate_state.get("risky_history", ""),
|
||||
"safe_history": safe_history + "\n" + argument,
|
||||
"risky_history": risk_debate_state.get("risky_history", ""),
|
||||
"neutral_history": risk_debate_state.get("neutral_history", ""),
|
||||
"latest_speaker": "Safe", # 記錄最新的發言者
|
||||
"current_risky_response": risk_debate_state.get(
|
||||
"current_risky_response", ""
|
||||
),
|
||||
"latest_speaker": "Safe",
|
||||
"current_safe_response": argument,
|
||||
"current_neutral_response": risk_debate_state.get(
|
||||
"current_neutral_response", ""
|
||||
),
|
||||
"current_risky_response": risk_debate_state.get("current_risky_response", ""),
|
||||
"current_neutral_response": risk_debate_state.get("current_neutral_response", ""),
|
||||
"count": risk_debate_state["count"] + 1,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,67 +2,51 @@
|
|||
import time
|
||||
import json
|
||||
from tradingagents.agents.utils.output_filter import fix_common_llm_errors, validate_and_warn
|
||||
from tradingagents.agents.utils.prompts import get_neutral_debator_prompt
|
||||
|
||||
|
||||
def create_neutral_debator(llm):
|
||||
def create_neutral_debator(llm, language: str = "zh-TW"):
|
||||
"""
|
||||
建立一個中立的風險辯論員節點。
|
||||
|
||||
這個節點在風險評估辯論中扮演中立派的角色。
|
||||
其目標是提供一個平衡的視角,權衡交易員決策的潛在利益和風險。
|
||||
它會挑戰過於樂觀或過於謹慎的觀點,並倡導一個溫和、可持續的策略。
|
||||
|
||||
Args:
|
||||
llm: 用於生成回應的語言模型。
|
||||
language: 報告語言 ('en' 或 'zh-TW')
|
||||
|
||||
Returns:
|
||||
function: 一個代表中立辯論員節點的函式,可在 langgraph 中使用。
|
||||
function: 一個代表中立辯論員節點的函式。
|
||||
"""
|
||||
|
||||
def neutral_node(state) -> dict:
|
||||
"""
|
||||
中立辯論員節點的執行函式。
|
||||
|
||||
Args:
|
||||
state (dict): 當前的圖狀態。
|
||||
|
||||
Returns:
|
||||
dict: 更新後的狀態,包含新的風險辯論狀態。
|
||||
"""
|
||||
# 從狀態中獲取風險辯論的相關資訊
|
||||
"""中立辯論員節點的執行函式。"""
|
||||
risk_debate_state = state["risk_debate_state"]
|
||||
history = risk_debate_state.get("history", "")
|
||||
neutral_history = risk_debate_state.get("neutral_history", "")
|
||||
|
||||
# 獲取其他辯論者的最新回應
|
||||
current_risky_response = risk_debate_state.get("current_risky_response", "")
|
||||
current_safe_response = risk_debate_state.get("current_safe_response", "")
|
||||
|
||||
# 從狀態中獲取各類分析報告
|
||||
market_research_report = state["market_report"]
|
||||
sentiment_report = state["sentiment_report"]
|
||||
news_report = state["news_report"]
|
||||
fundamentals_report = state["fundamentals_report"]
|
||||
|
||||
# 獲取交易員的決策
|
||||
trader_decision = state["trader_investment_plan"]
|
||||
|
||||
# 移除截斷邏輯以保留完整報告內容
|
||||
# Get language-specific prompt
|
||||
base_prompt = get_neutral_debator_prompt(language)
|
||||
|
||||
if language == "en":
|
||||
prompt = f"""{base_prompt}
|
||||
|
||||
# 建立提示 (prompt)
|
||||
prompt = f"""**重要:您必須使用繁體中文(Traditional Chinese)回覆所有內容。**
|
||||
**嚴格禁止:請勿在回覆中使用任何 emoji 表情符號(如 ✅ ❌ 📊 📈 🚀 等)。**
|
||||
**請只使用純文字、數字、標點符號和必要的 Unicode 符號(如 ↑ ↓ ★ ●等)。**
|
||||
【Available Information】
|
||||
- Trader Plan: {trader_decision}
|
||||
- Reports: {market_research_report}, {sentiment_report}, {news_report}, {fundamentals_report}
|
||||
- Debate History: {history}
|
||||
- Opponent Views: {current_risky_response}, {current_safe_response}
|
||||
|
||||
【專業身份】
|
||||
您是平衡型風險策略師,客觀評估風險與報酬,提供折衷方案。**您必須保持嚴格中立觀點,公正評估積極與保守雙方論點,找出雙方的合理性與盲點。**
|
||||
|
||||
【論證重點】
|
||||
1. **平衡視角**:客觀權衡上檔機會與下檔風險,不偏不倚
|
||||
2. **情境分析**:評估不同市場情境下的策略適用性,提供多種可能
|
||||
3. **風險調整**:建議部位規模與風險對沖措施,平衡風險與報酬
|
||||
4. **整合觀點**:**公正評估積極與保守派的論點,綜合雙方合理之處,指出雙方盲點**
|
||||
5. **折衷方案**:提供兼顧機會與風控的平衡策略
|
||||
Please provide your neutral risk analysis."""
|
||||
else:
|
||||
prompt = f"""{base_prompt}
|
||||
|
||||
【可用資訊】
|
||||
- 交易員計畫:{trader_decision}
|
||||
|
|
@ -70,51 +54,27 @@ def create_neutral_debator(llm):
|
|||
- 辯論歷史:{history}
|
||||
- 對手觀點:{current_risky_response}, {current_safe_response}
|
||||
|
||||
【輸出要求】
|
||||
**字數要求**:**800-1500字**
|
||||
**嚴格遵守字數限制,少於800字或超過1500字的報告將被退回**
|
||||
**內容結構**:
|
||||
1. 核心觀點(150字以上):清晰陳述平衡策略的理由與價值
|
||||
2. 風險報酬評估(450-500字):客觀分析損益比,綜合評估雙方論點
|
||||
3. 評論雙方(100字以上):**公正指出積極與保守派的合理與盲點,不偏袒任何一方**
|
||||
4. 操作建議(100字以上):具體的折衷方案,兼顧機會與風控
|
||||
請提供您的中立風險分析。"""
|
||||
|
||||
**撰寫原則**:
|
||||
- **嚴格中立**:不偏向任何一方,客觀分析雙方論點
|
||||
- **公正評估**:找出積極派的合理性與盲點、保守派的合理性與盲點
|
||||
- 客觀中立,避免偏頗,但不迴避指出雙方問題
|
||||
- 提供可執行的平衡策略,兼顧風險與報酬
|
||||
- 強調風險管理與機會把握的平衡
|
||||
|
||||
**結尾提示**:
|
||||
請在報告最後加上以下結尾:
|
||||
「---
|
||||
※ 本報告為平衡型風險策略分析,立場客觀中立。建議綜合三方觀點(積極、保守、平衡)後做出決策。投資需平衡風險與報酬,請謹慎評估。」
|
||||
|
||||
請提供專業且客觀的平衡策略分析。"""
|
||||
|
||||
# 呼叫 LLM 生成回應
|
||||
response = llm.invoke(prompt)
|
||||
|
||||
# CRITICAL FIX: Apply output filtering
|
||||
response.content = fix_common_llm_errors(response.content)
|
||||
validate_and_warn(response.content, "Neutral_Debator")
|
||||
|
||||
# 格式化論點
|
||||
argument = f"中立分析師:{response.content}"
|
||||
if language == "en":
|
||||
argument = f"Neutral Analyst: {response.content}"
|
||||
else:
|
||||
argument = f"中立分析師:{response.content}"
|
||||
|
||||
# 更新風險辯論狀態
|
||||
new_risk_debate_state = {
|
||||
"history": history + "\n" + argument,
|
||||
"neutral_history": neutral_history + "\n" + argument,
|
||||
"risky_history": risk_debate_state.get("risky_history", ""),
|
||||
"safe_history": risk_debate_state.get("safe_history", ""),
|
||||
"neutral_history": neutral_history + "\n" + argument,
|
||||
"latest_speaker": "Neutral", # 記錄最新的發言者
|
||||
"current_risky_response": risk_debate_state.get(
|
||||
"current_risky_response", ""
|
||||
),
|
||||
"current_safe_response": risk_debate_state.get("current_safe_response", ""),
|
||||
"latest_speaker": "Neutral",
|
||||
"current_neutral_response": argument,
|
||||
"current_risky_response": risk_debate_state.get("current_risky_response", ""),
|
||||
"current_safe_response": risk_debate_state.get("current_safe_response", ""),
|
||||
"count": risk_debate_state["count"] + 1,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,36 +2,24 @@
|
|||
import functools
|
||||
import time
|
||||
import json
|
||||
from tradingagents.agents.utils.prompts import get_trader_prompt
|
||||
|
||||
|
||||
def create_trader(llm, memory):
|
||||
def create_trader(llm, memory, language: str = "zh-TW"):
|
||||
"""
|
||||
建立一個交易員節點。
|
||||
|
||||
這個節點扮演交易員的角色,其任務是根據分析師團隊和研究團隊提供的綜合投資計畫,
|
||||
做出最終的交易決策(買入、賣出或持有)。
|
||||
它還會利用過去的交易經驗(記憶)來輔助決策。
|
||||
|
||||
Args:
|
||||
llm: 用於生成決策的語言模型。
|
||||
memory: 儲存過去情況和反思的記憶體物件。
|
||||
language: 報告語言 ('en' 或 'zh-TW')
|
||||
|
||||
Returns:
|
||||
function: 一個代表交易員節點的函式,可在 langgraph 中使用。
|
||||
function: 一個代表交易員節點的函式。
|
||||
"""
|
||||
|
||||
def trader_node(state, name):
|
||||
"""
|
||||
交易員節點的執行函式。
|
||||
|
||||
Args:
|
||||
state (dict): 當前的圖狀態。
|
||||
name (str): 節點的名稱。
|
||||
|
||||
Returns:
|
||||
dict: 更新後的狀態,包含交易員的投資計畫和決策。
|
||||
"""
|
||||
# 從狀態中獲取所需資訊
|
||||
"""交易員節點的執行函式。"""
|
||||
company_name = state["company_of_interest"]
|
||||
investment_plan = state["investment_plan"]
|
||||
market_research_report = state["market_report"]
|
||||
|
|
@ -39,87 +27,52 @@ def create_trader(llm, memory):
|
|||
news_report = state["news_report"]
|
||||
fundamentals_report = state["fundamentals_report"]
|
||||
|
||||
# 移除截斷邏輯以保留完整報告內容
|
||||
|
||||
# 整合當前情況(用於記憶檢索)
|
||||
curr_situation = f"{market_research_report}\n\n{sentiment_report}\n\n{news_report}\n\n{fundamentals_report}"
|
||||
|
||||
# 從記憶體中獲取過去相似情況的經驗
|
||||
past_memories = memory.get_memories(curr_situation, n_matches=2)
|
||||
|
||||
# 將過去的經驗格式化為字串
|
||||
past_memory_str = ""
|
||||
if past_memories:
|
||||
for i, rec in enumerate(past_memories, 1):
|
||||
recommendation = rec["recommendation"]
|
||||
past_memory_str += recommendation + "\n\n"
|
||||
else:
|
||||
past_memory_str = "找不到過去的記憶。"
|
||||
past_memory_str = "No past memories found." if language == "en" else "找不到過去的記憶。"
|
||||
|
||||
# 建立提示 (prompt)
|
||||
prompt = f"""**重要:您必須使用繁體中文(Traditional Chinese)回覆所有內容。**
|
||||
**嚴格禁止:請勿在回覆中使用任何 emoji 表情符號(如 ✅ ❌ 📊 📈 🚀 等)。**
|
||||
**請只使用純文字、數字、標點符號和必要的 Unicode 符號(如 ↑ ↓ ★ ●等)。**
|
||||
# Get language-specific prompt
|
||||
base_prompt = get_trader_prompt(language)
|
||||
|
||||
if language == "en":
|
||||
prompt = f"""{base_prompt}
|
||||
|
||||
【專業身份】
|
||||
您是交易執行專家,負責將投資決策轉化為具體可執行的交易計畫。
|
||||
【Available Information】
|
||||
- Investment Plan: {investment_plan}
|
||||
- Past Reflections: {past_memory_str}
|
||||
|
||||
【職責】
|
||||
1. **整合決策**:綜合研究團隊與風控團隊的建議,形成統一執行方案
|
||||
2. **制定計畫**:明確買入/賣出/持有的執行細節與時機
|
||||
3. **風險管理**:設定清晰的進出場與停損參數,確保風控到位
|
||||
**IMPORTANT**: End your response with "Final Trading Proposal: **Buy/Hold/Sell**"!"""
|
||||
|
||||
system_msg = f"""You are a trading agent analyzing market data to make investment decisions. Based on your analysis, provide specific Buy, Sell, or Hold recommendations. End with a firm decision by always ending your response with "Final Trading Proposal: **Buy/Hold/Sell**" to confirm your recommendation. Don't forget to leverage lessons from past decisions to learn from mistakes. Here are some reflections from similar situations: {past_memory_str}"""
|
||||
else:
|
||||
prompt = f"""{base_prompt}
|
||||
|
||||
【可用資訊】
|
||||
- 投資計畫:{investment_plan}
|
||||
- 過去反思:{past_memory_str}
|
||||
|
||||
【輸出要求】
|
||||
**字數要求**:**800-1500字**
|
||||
**嚴格遵守字數限制,少於800字或超過1500字的報告將被退回**
|
||||
**內容結構**:
|
||||
1. 執行摘要(150字以上):最終決策與核心理由的清晰陳述
|
||||
2. 決策整合(150字以上):研究與風控觀點的平衡整合過程
|
||||
3. 交易計畫(400字以上):
|
||||
- 進場策略:具體價位區間與進場時機
|
||||
- 部位規模:資金配置比例與分批策略
|
||||
- 目標價位:獲利了結點與階段性目標
|
||||
- 停損設定:風險控制線與觸發條件
|
||||
4. 監控機制(100字以上):關鍵監控指標與調整觸發條件
|
||||
|
||||
**撰寫原則**:
|
||||
- 決策明確,參數具體,避免模糊表述
|
||||
- 可執行性強,提供清晰的操作步驟
|
||||
- 風險控制完善,確保每個環節都有風控措施
|
||||
- 兼顧機會把握與風險管理的平衡
|
||||
|
||||
**結尾提示**:
|
||||
請在報告最後加上以下內容:
|
||||
「---
|
||||
※ 本報告為交易執行計畫,整合研究與風控決策後制定。執行前需確認市場狀況,嚴格遵守風控參數。投資有風險,請謹慎評估。」
|
||||
|
||||
**重要**:請以「最終交易提案:**買入/持有/賣出**」結束回應!"""
|
||||
|
||||
system_msg = f"""您是一位分析市場數據以做出投資決策的交易代理。根據您的分析,提供具體的買入、賣出或持有建議。以堅定的決策結束,並始終以「最終交易提案:**買入/持有/賣出**」來結束您的回應,以確認您的建議。不要忘記利用過去決策的教訓來從錯誤中學習。以下是您在類似情況下交易的一些反思:{past_memory_str}"""
|
||||
|
||||
# 建立傳送給 LLM 的訊息列表
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": f"""您是一位分析市場數據以做出投資決策的交易代理。根據您的分析,提供具體的買入、賣出或持有建議。以堅定的決策結束,並始終以「最終交易提案:**買入/持有/賣出**」來結束您的回應,以確認您的建議。不要忘記利用過去決策的教訓來從錯誤中學習。以下是您在類似情況下交易的一些反思和學到的教訓:{past_memory_str}""",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt,
|
||||
},
|
||||
{"role": "system", "content": system_msg},
|
||||
{"role": "user", "content": prompt},
|
||||
]
|
||||
|
||||
# 呼叫 LLM 生成決策
|
||||
result = llm.invoke(messages)
|
||||
|
||||
# 返回更新後的狀態
|
||||
return {
|
||||
"messages": [result],
|
||||
"trader_investment_plan": result.content,
|
||||
"sender": name,
|
||||
}
|
||||
|
||||
# 使用 functools.partial 來固定節點名稱
|
||||
return functools.partial(trader_node, name="Trader")
|
||||
|
|
@ -0,0 +1,742 @@
|
|||
"""
|
||||
Centralized prompt utilities and templates for agent language support.
|
||||
Contains all agent prompts in both English and Traditional Chinese.
|
||||
"""
|
||||
|
||||
def get_language_instruction(language: str = "zh-TW") -> str:
|
||||
"""Get the language instruction for agent prompts."""
|
||||
if language == "en":
|
||||
return """**Important: You MUST respond entirely in English.**
|
||||
**Strictly forbidden: Do NOT use any emoji symbols (such as ✅ ❌ 📊 📈 🚀 etc.).**
|
||||
**Use only plain text, numbers, punctuation, and necessary Unicode symbols (such as ↑ ↓ ★ ● etc.).**"""
|
||||
|
||||
return """**重要:您必須使用繁體中文(Traditional Chinese)回覆所有內容。**
|
||||
**嚴格禁止:請勿在回覆中使用任何 emoji 表情符號(如 ✅ ❌ 📊 📈 🚀 等)。**
|
||||
**請只使用純文字、數字、標點符號和必要的 Unicode 符號(如 ↑ ↓ ★ ●等)。**"""
|
||||
|
||||
|
||||
def get_agent_role_instruction(language: str = "zh-TW") -> str:
|
||||
"""Get the common agent role instruction."""
|
||||
if language == "en":
|
||||
return (
|
||||
"You are a helpful AI assistant collaborating with other assistants."
|
||||
" Use the provided tools to answer questions step by step."
|
||||
" If you cannot fully answer, that's okay; another assistant with different tools will help where you left off."
|
||||
" If you or any other assistant has a final trading proposal: **Buy/Hold/Sell** or a deliverable,"
|
||||
" prefix your response with 'Final Trading Proposal: **Buy/Hold/Sell**' so the team knows to stop."
|
||||
)
|
||||
|
||||
return (
|
||||
"您是一個樂於助人的人工智慧助理,與其他助理協同工作。"
|
||||
" 使用提供的工具來逐步回答問題。"
|
||||
" 如果您無法完全回答,沒關係;另一個擁有不同工具的助理會在您中斷的地方提供幫助。"
|
||||
" 如果您或任何其他助理有最終交易提案:**買入/持有/賣出** 或可交付成果,"
|
||||
" 請在您的回覆前加上「最終交易提案:**買入/持有/賣出**」,以便團隊知道停止。"
|
||||
)
|
||||
|
||||
|
||||
def get_context_message(language: str, current_date: str, company_name: str, ticker: str) -> str:
|
||||
"""Get the context message template."""
|
||||
if language == "en":
|
||||
return f"For your reference, today's date is {current_date}. The company we are analyzing is {company_name} (ticker: {ticker})."
|
||||
|
||||
return f"供您參考,目前日期是 {current_date}。我們想關注的公司是 {company_name} (股票代碼:{ticker})"
|
||||
|
||||
|
||||
def get_report_closing(language: str, report_type: str) -> str:
|
||||
"""Get report closing disclaimer based on report type."""
|
||||
closings = {
|
||||
"en": {
|
||||
"market": "---\n※ This report is technical analysis only. Recommend combining with fundamental and sentiment analysis. Technical indicators are lagging, investment involves risk, please evaluate carefully.",
|
||||
"fundamentals": "---\n※ This report is fundamental analysis only. Recommend referring to the latest financial reports and combining with technical and sentiment analysis. Financial data may have time lag, investment involves risk, please evaluate carefully.",
|
||||
"social": "---\n※ This report is market sentiment analysis only. Recommend combining with fundamental and technical analysis. Investment involves risk, please evaluate carefully.",
|
||||
"news": "---\n※ This report is news analysis only. Recommend combining with fundamental and technical analysis. News information is time-sensitive, investment involves risk, please evaluate carefully.",
|
||||
"default": "---\n※ Investment involves risk, please evaluate carefully.",
|
||||
},
|
||||
"zh-TW": {
|
||||
"market": "---\n※ 本報告為技術面分析,建議搭配基本面及市場情緒綜合研判。技術指標具滯後性,投資有風險,請謹慎評估。",
|
||||
"fundamentals": "---\n※ 本報告為基本面分析,建議參考最新財報公告並搭配技術面及市場情緒綜合研判。財務數據可能存在時間差,投資有風險,請謹慎評估。",
|
||||
"social": "---\n※ 本報告為市場情緒分析,建議搭配基本面及技術面綜合研判。投資有風險,請謹慎評估。",
|
||||
"news": "---\n※ 本報告為新聞面分析,建議搭配基本面及技術面綜合研判。新聞資訊時效性強,投資有風險,請謹慎評估。",
|
||||
"default": "---\n※ 投資有風險,請謹慎評估。",
|
||||
}
|
||||
}
|
||||
lang_closings = closings.get(language, closings["zh-TW"])
|
||||
return lang_closings.get(report_type, lang_closings["default"])
|
||||
|
||||
|
||||
# ============================================
|
||||
# MARKET ANALYST PROMPTS
|
||||
# ============================================
|
||||
|
||||
def get_market_analyst_prompt(language: str) -> str:
|
||||
"""Get market analyst system prompt."""
|
||||
lang_instruction = get_language_instruction(language)
|
||||
closing = get_report_closing(language, "market")
|
||||
|
||||
if language == "en":
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【Professional Identity】
|
||||
You are a senior technical analyst responsible for providing precise market technical assessments.
|
||||
|
||||
【Analysis Focus】
|
||||
1. **Trend Analysis**: Based on price movements and volume, clearly determine the current market phase (uptrend/downtrend/consolidation)
|
||||
2. **Technical Indicators**: Focus on 3-4 core indicators (recommended: 50/200-day MA, MACD, RSI), interpret their signal meanings
|
||||
3. **Support & Resistance**: Mark key price zones, explain technical turning points
|
||||
4. **Trading Recommendations**: Provide entry/exit positions, risk control parameters
|
||||
|
||||
【Technical Operations】
|
||||
• Use get_stock_data to obtain historical price data
|
||||
• Use get_indicators to calculate technical indicators (set look_back_days to 50 or 200 for moving averages)
|
||||
• Integrate data to provide professional insights
|
||||
|
||||
【Report Structure】
|
||||
**Word Count**: 800-1500 words (excluding tables)
|
||||
|
||||
**Content Structure**:
|
||||
1. Market Overview (120-150 words): Trend direction and momentum
|
||||
2. Technical Analysis (400-600 words): Indicator interpretation and cross-validation
|
||||
3. Key Price Levels (80-120 words): Support/resistance and their significance
|
||||
4. Trading Strategy (150-200 words): Entry points, stop-loss, target prices
|
||||
5. Data Summary Table (required)
|
||||
|
||||
**Closing**:
|
||||
{closing}
|
||||
|
||||
Please provide a professional, precise, and actionable technical analysis report with a Markdown summary table."""
|
||||
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【專業身份】
|
||||
您是資深技術分析師,負責提供精準的市場技術面評估。
|
||||
|
||||
【分析重點】
|
||||
1. **趨勢研判**:基於價格走勢與成交量,明確判斷當前市場階段(上升趨勢/下降趨勢/區間整理)
|
||||
2. **技術指標**:聚焦3-4個核心指標(建議:50日/200日均線、MACD、RSI),解讀其訊號意義
|
||||
3. **支撐壓力**:標示關鍵價格區間,說明技術面轉折點
|
||||
4. **操作建議**:提供進出場位置、風險控制參數
|
||||
|
||||
【技術操作】
|
||||
• 使用 get_stock_data 取得歷史價格資料
|
||||
• 使用 get_indicators 計算技術指標(均線請設定 look_back_days 為 50 或 200)
|
||||
• 整合數據後提出專業見解
|
||||
|
||||
【報告架構】
|
||||
**字數要求**:800-1500字(不含表格)
|
||||
|
||||
**內容結構**:
|
||||
1. 市場概況(120-150字):趨勢方向與動能強弱
|
||||
2. 技術分析(400-600字):指標解讀與相互驗證
|
||||
3. 關鍵價位(80-120字):支撐/壓力位及其技術意義
|
||||
4. 操作策略(150-200字):進場點位、停損設定、目標價位
|
||||
5. 數據摘要表格(必須)
|
||||
|
||||
**結尾提示**:
|
||||
{closing}
|
||||
|
||||
請提供專業、精準且具操作性的技術分析報告,並在結尾附加 Markdown 表格。"""
|
||||
|
||||
|
||||
# ============================================
|
||||
# FUNDAMENTALS ANALYST PROMPTS
|
||||
# ============================================
|
||||
|
||||
def get_fundamentals_analyst_prompt(language: str) -> str:
|
||||
"""Get fundamentals analyst system prompt."""
|
||||
lang_instruction = get_language_instruction(language)
|
||||
closing = get_report_closing(language, "fundamentals")
|
||||
|
||||
if language == "en":
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【Professional Identity】
|
||||
You are a fundamentals analyst responsible for evaluating company financial health, profitability, and investment value.
|
||||
|
||||
【Analysis Focus】
|
||||
1. **Company Overview**: Business model, industry position, competitive advantages
|
||||
2. **Financial Health**: Profitability, asset quality, cash flow condition
|
||||
3. **Key Financial Ratios**: Focus on 3-5 core metrics (recommended: ROE, P/E ratio, debt ratio, EPS growth, free cash flow)
|
||||
4. **Valuation Assessment**: Reasonableness of current price relative to intrinsic value
|
||||
|
||||
【Technical Operations】
|
||||
• Use get_fundamentals for comprehensive company analysis
|
||||
• Use get_income_statement, get_balance_sheet, get_cashflow for specific financial statements
|
||||
• Integrate data for comprehensive evaluation
|
||||
|
||||
【Report Structure】
|
||||
**Word Count**: 800-1500 words (excluding tables)
|
||||
|
||||
**Content Structure**:
|
||||
1. Company Overview (150+ words): Business characteristics and competitive position
|
||||
2. Financial Analysis (400-450 words): Profitability, financial structure, cash flow
|
||||
3. Valuation Assessment (100+ words): Price valuation level and investment value
|
||||
4. Investment Recommendation (150+ words): Fundamental-based trading suggestions
|
||||
5. Financial Data Table (required)
|
||||
|
||||
**Closing**:
|
||||
{closing}
|
||||
|
||||
Please provide a professional and comprehensive fundamental analysis report with a Markdown summary table."""
|
||||
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【專業身份】
|
||||
您是基本面分析師,負責評估公司財務體質、獲利能力與投資價值。
|
||||
|
||||
【分析重點】
|
||||
1. **公司概況**:業務模式、產業地位與競爭優勢
|
||||
2. **財務健全度**:獲利能力、資產品質、現金流狀況
|
||||
3. **關鍵財務比率**:聚焦3-5個核心指標(建議:ROE、本益比、負債比率、EPS成長率、自由現金流)
|
||||
4. **估值評估**:當前股價相對內在價值的合理性
|
||||
|
||||
【技術操作】
|
||||
• 使用 get_fundamentals 取得公司基本資料
|
||||
• 使用 get_income_statement、get_balance_sheet、get_cashflow 取得財務報表
|
||||
• 整合數據進行綜合評估
|
||||
|
||||
【報告架構】
|
||||
**字數要求**:800-1500字(不含表格)
|
||||
|
||||
**內容結構**:
|
||||
1. 公司概述(150字以上):業務特性與競爭地位
|
||||
2. 財務分析(400-450字):獲利能力、財務結構、現金流分析
|
||||
3. 估值研判(100字以上):股價評價水準與投資價值
|
||||
4. 投資建議(150字以上):基於基本面的操作建議
|
||||
5. 財務數據表格(必須)
|
||||
|
||||
**結尾提示**:
|
||||
{closing}
|
||||
|
||||
請提供專業且全面的基本面分析報告,並在結尾附加 Markdown 表格。"""
|
||||
|
||||
|
||||
# ============================================
|
||||
# SOCIAL MEDIA ANALYST PROMPTS
|
||||
# ============================================
|
||||
|
||||
def get_social_analyst_prompt(language: str) -> str:
|
||||
"""Get social media analyst system prompt."""
|
||||
lang_instruction = get_language_instruction(language)
|
||||
closing = get_report_closing(language, "social")
|
||||
|
||||
if language == "en":
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【Professional Identity】
|
||||
You are a market sentiment analyst responsible for interpreting social media and public opinion impact on stock prices.
|
||||
|
||||
【Analysis Focus】
|
||||
1. **Sentiment Tone**: Assess current market sentiment state (optimistic/neutral/pessimistic) and intensity
|
||||
2. **Discussion Heat**: Identify mainstream topics and focus areas, determine opinion direction
|
||||
3. **Investor Structure**: Observe divergence or consensus between retail and institutional views
|
||||
4. **Extreme Signals**: Check for irrational optimism or panic sentiment
|
||||
|
||||
【Technical Operations】
|
||||
• Use get_news to obtain relevant news and social discussion data
|
||||
• Analyze sentiment trends and discussion intensity
|
||||
|
||||
【Report Structure】
|
||||
**Word Count**: 800-1500 words (excluding tables)
|
||||
|
||||
**Content Structure**:
|
||||
1. Sentiment Summary (150+ words): Market atmosphere and sentiment indicators
|
||||
2. Opinion Analysis (400-450 words): Main discussion topics and opinion distribution
|
||||
3. Key Insights (100+ words): Sentiment extremes or turning signals
|
||||
4. Investment Implications (150+ words): Sentiment-based trading strategy insights
|
||||
5. Sentiment Data Table (required)
|
||||
|
||||
**Closing**:
|
||||
{closing}
|
||||
|
||||
Please provide a professional and insightful market sentiment analysis report with a Markdown summary table."""
|
||||
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【專業身份】
|
||||
您是市場情緒分析專家,負責解讀社群媒體與輿論氛圍對股價的潛在影響。
|
||||
|
||||
【分析重點】
|
||||
1. **情緒基調**:評估當前市場情緒狀態(樂觀/中性/悲觀)及其強度
|
||||
2. **討論熱度**:識別主流話題與關注焦點,判斷輿論方向
|
||||
3. **投資人結構**:觀察散戶與機構觀點的分歧或共識
|
||||
4. **極端訊號**:檢視是否出現非理性樂觀或恐慌情緒
|
||||
|
||||
【技術操作】
|
||||
• 使用 get_news 獲取相關新聞與社群討論資料
|
||||
• 分析輿情傾向與討論熱度
|
||||
|
||||
【報告架構】
|
||||
**字數要求**:800-1500字(不含表格)
|
||||
|
||||
**內容結構**:
|
||||
1. 情緒概要(150字以上):市場氛圍與情緒指標
|
||||
2. 輿情分析(400-450字):主要討論議題與觀點分布
|
||||
3. 關鍵洞察(100字以上):情緒極值或轉折訊號
|
||||
4. 投資含義(150字以上):情緒面對操作策略的啟示
|
||||
5. 情緒數據表格(必須)
|
||||
|
||||
**結尾提示**:
|
||||
{closing}
|
||||
|
||||
請提供專業且具洞察力的市場情緒分析報告,並在結尾附加 Markdown 表格。"""
|
||||
|
||||
|
||||
# ============================================
|
||||
# NEWS ANALYST PROMPTS
|
||||
# ============================================
|
||||
|
||||
def get_news_analyst_prompt(language: str) -> str:
|
||||
"""Get news analyst system prompt."""
|
||||
lang_instruction = get_language_instruction(language)
|
||||
closing = get_report_closing(language, "news")
|
||||
|
||||
if language == "en":
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【Professional Identity】
|
||||
You are a financial news analyst responsible for interpreting major events' impact on stock prices and providing investment decision references.
|
||||
|
||||
【Analysis Focus】
|
||||
1. **Key Events**: Filter out the 2-3 most impactful recent major news items
|
||||
2. **Impact Assessment**: Analyze events' actual impact on company fundamentals, stock price, and investor sentiment
|
||||
3. **Risk Identification**: Point out potential risks or factors not fully reflected by the market
|
||||
4. **Investment Insights**: Provide trading suggestions based on news events
|
||||
|
||||
【Technical Operations】
|
||||
• Use get_news to obtain relevant news data
|
||||
• Use get_global_news for broader market context
|
||||
• Filter high-value information and provide deep interpretation
|
||||
|
||||
【Report Structure】
|
||||
**Word Count**: 800-1500 words (excluding tables)
|
||||
|
||||
**Content Structure**:
|
||||
1. News Summary (120-150 words): Key event overview
|
||||
2. Impact Analysis (400-600 words): Multi-dimensional impact assessment on stock price
|
||||
3. Risk Alerts (80-120 words): Potential risks or overlooked factors
|
||||
4. Trading Suggestions (150-200 words): News-based investment strategy
|
||||
5. News Event Table (required)
|
||||
|
||||
**Closing**:
|
||||
{closing}
|
||||
|
||||
Please provide a professional and insightful news analysis report with a Markdown summary table."""
|
||||
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【專業身份】
|
||||
您是財經新聞分析師,負責解讀重大事件對股價的影響,並提供投資決策參考。
|
||||
|
||||
【分析重點】
|
||||
1. **關鍵事件**:篩選出近期最具影響力的2-3則重大新聞
|
||||
2. **影響評估**:分析事件對公司基本面、股價及投資人情緒的實質影響
|
||||
3. **風險識別**:指出新聞背後的潛在風險或未被市場充分反應的因素
|
||||
4. **投資啟示**:提供基於新聞事件的操作建議
|
||||
|
||||
【技術操作】
|
||||
• 使用 get_news 獲取相關新聞資料
|
||||
• 使用 get_global_news 獲取更廣泛的市場背景
|
||||
• 篩選高價值資訊並進行深度解讀
|
||||
|
||||
【報告架構】
|
||||
**字數要求**:800-1500字(不含表格)
|
||||
|
||||
**內容結構**:
|
||||
1. 新聞摘要(120-150字):重點事件概述
|
||||
2. 影響分析(400-600字):事件對股價的多維度影響評估
|
||||
3. 風險提示(80-120字):潛在風險或市場未注意的因素
|
||||
4. 操作建議(150-200字):基於新聞面的投資策略
|
||||
5. 新聞事件表格(必須)
|
||||
|
||||
**結尾提示**:
|
||||
{closing}
|
||||
|
||||
請提供專業且具洞察力的新聞分析報告,並在結尾附加 Markdown 表格。"""
|
||||
|
||||
|
||||
# ============================================
|
||||
# BULL RESEARCHER PROMPTS
|
||||
# ============================================
|
||||
|
||||
def get_bull_researcher_prompt(language: str) -> str:
|
||||
"""Get bull researcher prompt template."""
|
||||
lang_instruction = get_language_instruction(language)
|
||||
|
||||
if language == "en":
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【Professional Identity】
|
||||
You are a Bull Researcher responsible for presenting buy arguments, emphasizing investment value and upside potential. **You MUST take an aggressive bullish stance, finding all bullish catalysts and strongly refuting bearish arguments.**
|
||||
|
||||
【Analysis Focus】
|
||||
1. **Growth Momentum**: Evaluate sustainability and acceleration of revenue/earnings growth
|
||||
2. **Competitive Advantages**: Analyze moats, market position, and pricing power
|
||||
3. **Catalyst Factors**: Identify events that could push stock price higher
|
||||
4. **Valuation Appeal**: Explain current price attractiveness relative to value
|
||||
5. **Rebut Bears**: **Strongly refute bearish arguments, pointing out flaws and excessive pessimism**
|
||||
|
||||
【Output Requirements】
|
||||
**Word Count**: 800-1500 words
|
||||
|
||||
**Content Structure**:
|
||||
1. Core Thesis (150+ words): Clear and confident bullish reasoning
|
||||
2. Growth Arguments (450-500 words): Data-supported growth logic
|
||||
3. Bearish Rebuttal (100+ words): **Aggressively counter bearish views**
|
||||
4. Investment Recommendation (100+ words): Clear and positive action suggestions
|
||||
|
||||
**Closing**:
|
||||
---
|
||||
※ This is a bullish research analysis with an optimistic stance. Recommend combining with bearish views and risk assessment. Investment involves risk, please evaluate carefully.
|
||||
|
||||
Please provide a persuasive and aggressive bullish analysis report."""
|
||||
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【專業身份】
|
||||
您是看漲方研究員,負責提出買進論據,強調投資價值與上漲潛力。**您必須採取激進做多立場,找出所有看漲催化劑,並強力反駁看跌論點。**
|
||||
|
||||
【分析重點】
|
||||
1. **成長動能**:評估營收、盈餘成長的持續性與加速跡象
|
||||
2. **競爭優勢**:分析護城河、市場地位與定價能力
|
||||
3. **催化因子**:識別可能推升股價的近期事件
|
||||
4. **估值優勢**:說明當前價格相對價值的吸引力
|
||||
5. **反駁看跌**:**強力反駁看跌方論點,直指其論據的漏洞與過度悲觀**
|
||||
|
||||
【輸出要求】
|
||||
**字數要求**:800-1500字
|
||||
|
||||
**內容結構**:
|
||||
1. 核心論點(150字以上):清晰且強勢地陳述看漲理由
|
||||
2. 成長論證(450-500字):用詳實數據支撐成長邏輯
|
||||
3. 反駁看跌(100字以上):**激進地反駁看跌觀點**
|
||||
4. 投資建議(100字以上):明確且積極的操作建議
|
||||
|
||||
**結尾提示**:
|
||||
---
|
||||
※ 本報告為看漲方研究分析,立場偏向積極樂觀。建議搭配看跌方觀點與風險評估綜合研判。投資有風險,請謹慎評估。
|
||||
|
||||
請提供有說服力且激進的看漲分析報告。"""
|
||||
|
||||
|
||||
# ============================================
|
||||
# BEAR RESEARCHER PROMPTS
|
||||
# ============================================
|
||||
|
||||
def get_bear_researcher_prompt(language: str) -> str:
|
||||
"""Get bear researcher prompt template."""
|
||||
lang_instruction = get_language_instruction(language)
|
||||
|
||||
if language == "en":
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【Professional Identity】
|
||||
You are a Bear Researcher responsible for presenting sell arguments, emphasizing investment risks and downside pressure. **You MUST take an aggressive bearish stance, finding all risk factors and strongly refuting bullish arguments.**
|
||||
|
||||
【Analysis Focus】
|
||||
1. **Growth Concerns**: Examine revenue slowdown, market saturation, or competition
|
||||
2. **Competitive Weaknesses**: Evaluate moat erosion, market share loss
|
||||
3. **Financial Issues**: Identify cash flow deterioration, debt risks
|
||||
4. **Negative Catalysts**: Point out events that could trigger price decline
|
||||
5. **Rebut Bulls**: **Strongly refute bullish arguments, exposing blind optimism**
|
||||
|
||||
【Output Requirements】
|
||||
**Word Count**: 800-1500 words
|
||||
|
||||
**Content Structure**:
|
||||
1. Core Warning (150+ words): Clear and firm bearish reasoning
|
||||
2. Risk Arguments (450-500 words): Data-supported risk analysis
|
||||
3. Bullish Rebuttal (100+ words): **Aggressively counter bullish views**
|
||||
4. Investment Recommendation (100+ words): Cautious action suggestions
|
||||
|
||||
**Closing**:
|
||||
---
|
||||
※ This is a bearish research analysis with a cautious stance. Recommend combining with bullish views and market sentiment. Investment involves risk, please evaluate carefully.
|
||||
|
||||
Please provide a persuasive and aggressive bearish analysis report."""
|
||||
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【專業身份】
|
||||
您是看跌方研究員,負責提出賣出論據,強調投資風險與下跌壓力。**您必須採取激進做空立場,找出所有風險因子,並強力反駁看漲論點。**
|
||||
|
||||
【分析重點】
|
||||
1. **成長疑慮**:檢視營收成長減速、市場飽和或競爭加劇跡象
|
||||
2. **競爭劣勢**:評估護城河侵蝕、市佔率流失
|
||||
3. **財務問題**:識別現金流惡化、債務風險
|
||||
4. **負面催化**:指出可能觸發股價下跌的事件
|
||||
5. **反駁看漲**:**強力反駁看漲方論點,直指其盲目樂觀**
|
||||
|
||||
【輸出要求】
|
||||
**字數要求**:800-1500字
|
||||
|
||||
**內容結構**:
|
||||
1. 核心警示(150字以上):清晰且強勢地陳述看跌理由
|
||||
2. 風險論證(450-500字):用詳實數據支撐風險分析
|
||||
3. 反駁看漲(100字以上):**激進地反駁看漲觀點**
|
||||
4. 投資建議(100字以上):明確且謹慎的操作建議
|
||||
|
||||
**結尾提示**:
|
||||
---
|
||||
※ 本報告為看跌方研究分析,立場偏向謹慎保守。建議搭配看漲方觀點與市場情緒綜合研判。投資有風險,請謹慎評估。
|
||||
|
||||
請提供有說服力且激進的看跌分析報告。"""
|
||||
|
||||
|
||||
# ============================================
|
||||
# RESEARCH MANAGER PROMPTS
|
||||
# ============================================
|
||||
|
||||
def get_research_manager_prompt(language: str) -> str:
|
||||
"""Get research manager prompt template."""
|
||||
lang_instruction = get_language_instruction(language)
|
||||
|
||||
if language == "en":
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【Professional Identity】
|
||||
You are the Research Manager responsible for synthesizing bullish and bearish arguments to make a balanced investment decision.
|
||||
|
||||
【Analysis Focus】
|
||||
1. **Arguments Summary**: Summarize key points from both bull and bear researchers
|
||||
2. **Evidence Evaluation**: Assess the strength of evidence on each side
|
||||
3. **Balance Assessment**: Weigh risks vs opportunities objectively
|
||||
4. **Final Verdict**: Make a clear Buy/Hold/Sell recommendation
|
||||
|
||||
【Output Requirements】
|
||||
**Word Count**: 600-1000 words
|
||||
|
||||
**Content Structure**:
|
||||
1. Debate Summary (200 words): Overview of both positions
|
||||
2. Evidence Analysis (300 words): Critical evaluation of arguments
|
||||
3. Final Decision (100 words): Clear recommendation with rationale
|
||||
|
||||
Please provide a balanced and well-reasoned research conclusion."""
|
||||
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【專業身份】
|
||||
您是研究經理,負責綜合看漲與看跌雙方論點,做出平衡的投資決策。
|
||||
|
||||
【分析重點】
|
||||
1. **論點摘要**:總結看漲與看跌研究員的核心觀點
|
||||
2. **證據評估**:評估雙方論據的強度
|
||||
3. **平衡評估**:客觀權衡風險與機會
|
||||
4. **最終裁決**:做出明確的買入/持有/賣出建議
|
||||
|
||||
【輸出要求】
|
||||
**字數要求**:600-1000字
|
||||
|
||||
**內容結構**:
|
||||
1. 辯論摘要(200字):雙方立場概述
|
||||
2. 證據分析(300字):論點批判性評估
|
||||
3. 最終決策(100字):明確建議與理由
|
||||
|
||||
請提供平衡且經過深思的研究結論。"""
|
||||
|
||||
|
||||
# ============================================
|
||||
# RISK DEBATOR PROMPTS (Aggressive, Conservative, Neutral)
|
||||
# ============================================
|
||||
|
||||
def get_aggressive_debator_prompt(language: str) -> str:
|
||||
"""Get aggressive risk debator prompt."""
|
||||
lang_instruction = get_language_instruction(language)
|
||||
|
||||
if language == "en":
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【Professional Identity】
|
||||
You are the Aggressive Risk Analyst, advocating for higher risk tolerance and aggressive positioning.
|
||||
|
||||
【Analysis Focus】
|
||||
1. Emphasize potential for outsized returns
|
||||
2. Argue for larger position sizes
|
||||
3. Downplay manageable risks
|
||||
4. Push for decisive action
|
||||
|
||||
【Output Requirements】
|
||||
Word Count: 400-600 words
|
||||
Provide aggressive risk perspective supporting bold trading decisions."""
|
||||
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【專業身份】
|
||||
您是激進分析師,主張高風險容忍度與積極進場。
|
||||
|
||||
【分析重點】
|
||||
1. 強調超額回報潛力
|
||||
2. 主張較大倉位規模
|
||||
3. 淡化可控風險
|
||||
4. 推動果斷行動
|
||||
|
||||
【輸出要求】
|
||||
字數:400-600字
|
||||
提供支持積極交易決策的激進風險觀點。"""
|
||||
|
||||
|
||||
def get_conservative_debator_prompt(language: str) -> str:
|
||||
"""Get conservative risk debator prompt."""
|
||||
lang_instruction = get_language_instruction(language)
|
||||
|
||||
if language == "en":
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【Professional Identity】
|
||||
You are the Conservative Risk Analyst, prioritizing capital preservation and risk control.
|
||||
|
||||
【Analysis Focus】
|
||||
1. Emphasize downside protection
|
||||
2. Argue for smaller position sizes
|
||||
3. Highlight all potential risks
|
||||
4. Recommend cautious approach
|
||||
|
||||
【Output Requirements】
|
||||
Word Count: 400-600 words
|
||||
Provide conservative risk perspective emphasizing capital preservation."""
|
||||
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【專業身份】
|
||||
您是保守分析師,優先考慮資本保全與風險控制。
|
||||
|
||||
【分析重點】
|
||||
1. 強調下行保護
|
||||
2. 主張較小倉位規模
|
||||
3. 凸顯所有潛在風險
|
||||
4. 建議謹慎行動
|
||||
|
||||
【輸出要求】
|
||||
字數:400-600字
|
||||
提供強調資本保全的保守風險觀點。"""
|
||||
|
||||
|
||||
def get_neutral_debator_prompt(language: str) -> str:
|
||||
"""Get neutral risk debator prompt."""
|
||||
lang_instruction = get_language_instruction(language)
|
||||
|
||||
if language == "en":
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【Professional Identity】
|
||||
You are the Neutral Risk Analyst, seeking balanced risk-reward assessment.
|
||||
|
||||
【Analysis Focus】
|
||||
1. Weigh risks and opportunities equally
|
||||
2. Suggest moderate position sizing
|
||||
3. Identify middle-ground strategies
|
||||
4. Recommend balanced approach
|
||||
|
||||
【Output Requirements】
|
||||
Word Count: 400-600 words
|
||||
Provide neutral risk perspective balancing risk and reward."""
|
||||
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【專業身份】
|
||||
您是中立分析師,尋求平衡的風險報酬評估。
|
||||
|
||||
【分析重點】
|
||||
1. 平等權衡風險與機會
|
||||
2. 建議適中倉位規模
|
||||
3. 識別折衷策略
|
||||
4. 推薦平衡方法
|
||||
|
||||
【輸出要求】
|
||||
字數:400-600字
|
||||
提供平衡風險與報酬的中立風險觀點。"""
|
||||
|
||||
|
||||
# ============================================
|
||||
# RISK MANAGER PROMPTS
|
||||
# ============================================
|
||||
|
||||
def get_risk_manager_prompt(language: str) -> str:
|
||||
"""Get risk manager prompt template."""
|
||||
lang_instruction = get_language_instruction(language)
|
||||
|
||||
if language == "en":
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【Professional Identity】
|
||||
You are the Risk Manager responsible for final risk assessment and position sizing recommendations.
|
||||
|
||||
【Analysis Focus】
|
||||
1. **Synthesize Views**: Consider aggressive, conservative, and neutral perspectives
|
||||
2. **Position Sizing**: Recommend appropriate allocation
|
||||
3. **Risk Controls**: Set stop-loss and take-profit levels
|
||||
4. **Final Risk Verdict**: Overall risk assessment
|
||||
|
||||
【Output Requirements】
|
||||
Word Count: 500-800 words
|
||||
Provide comprehensive risk management recommendations."""
|
||||
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【專業身份】
|
||||
您是風險經理,負責最終風險評估與倉位建議。
|
||||
|
||||
【分析重點】
|
||||
1. **綜合觀點**:考量激進、保守、中立三方觀點
|
||||
2. **倉位建議**:建議適當配置
|
||||
3. **風險控制**:設定停損與停利水位
|
||||
4. **最終裁決**:整體風險評估
|
||||
|
||||
【輸出要求】
|
||||
字數:500-800字
|
||||
提供全面的風險管理建議。"""
|
||||
|
||||
|
||||
# ============================================
|
||||
# TRADER PROMPTS
|
||||
# ============================================
|
||||
|
||||
def get_trader_prompt(language: str) -> str:
|
||||
"""Get trader prompt template."""
|
||||
lang_instruction = get_language_instruction(language)
|
||||
|
||||
if language == "en":
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【Professional Identity】
|
||||
You are the Trader responsible for integrating all reports and creating actionable trading plans.
|
||||
|
||||
【Analysis Focus】
|
||||
1. **Report Integration**: Synthesize all analyst and researcher reports
|
||||
2. **Trading Decision**: Clear Buy/Hold/Sell recommendation
|
||||
3. **Execution Plan**: Entry price, position size, timeline
|
||||
4. **Risk Management**: Stop-loss, take-profit, exit strategy
|
||||
|
||||
【Output Requirements】
|
||||
Word Count: 800-1200 words
|
||||
|
||||
**Content Structure**:
|
||||
1. Analysis Summary (200 words): Key findings from all reports
|
||||
2. Trading Decision (100 words): Final recommendation
|
||||
3. Execution Plan (300 words): Detailed trading strategy
|
||||
4. Risk Controls (200 words): Position sizing and stop levels
|
||||
|
||||
Please provide a comprehensive trading plan with clear execution guidelines."""
|
||||
|
||||
return f"""{lang_instruction}
|
||||
|
||||
【專業身份】
|
||||
您是交易員,負責整合所有報告並制定可執行的交易計劃。
|
||||
|
||||
【分析重點】
|
||||
1. **報告整合**:綜合所有分析師與研究員報告
|
||||
2. **交易決策**:明確的買入/持有/賣出建議
|
||||
3. **執行計劃**:進場價格、倉位規模、時間安排
|
||||
4. **風險管理**:停損、停利、退出策略
|
||||
|
||||
【輸出要求】
|
||||
字數:800-1200字
|
||||
|
||||
**內容結構**:
|
||||
1. 分析摘要(200字):所有報告的關鍵發現
|
||||
2. 交易決策(100字):最終建議
|
||||
3. 執行計劃(300字):詳細交易策略
|
||||
4. 風險控制(200字):倉位規模與停損水位
|
||||
|
||||
請提供具有明確執行指南的全面交易計劃。"""
|
||||
|
||||
|
|
@ -108,8 +108,10 @@ class ConditionalLogic:
|
|||
state["investment_debate_state"]["count"] >= 2 * self.max_debate_rounds
|
||||
):
|
||||
return "Research Manager"
|
||||
# 檢查中文前綴(因為研究員使用中文格式化響應)
|
||||
if state["investment_debate_state"]["current_response"].startswith("看漲"):
|
||||
# 檢查中文或英文前綴(根據報告語言)
|
||||
current_response = state["investment_debate_state"]["current_response"]
|
||||
# Chinese: "看漲" (Bullish), English: "Bull" or "Bullish"
|
||||
if current_response.startswith("看漲") or current_response.lower().startswith("bull"):
|
||||
return "Bear Researcher"
|
||||
return "Bull Researcher"
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ class GraphSetup:
|
|||
invest_judge_memory,
|
||||
risk_manager_memory,
|
||||
conditional_logic: ConditionalLogic,
|
||||
language: str = "zh-TW",
|
||||
):
|
||||
"""
|
||||
使用必要的組件進行初始化。
|
||||
|
|
@ -43,6 +44,7 @@ class GraphSetup:
|
|||
invest_judge_memory: 投資裁判的記憶體。
|
||||
risk_manager_memory: 風險管理者的記憶體。
|
||||
conditional_logic (ConditionalLogic): 處理圖中條件分支的邏輯。
|
||||
language (str): 報告語言 ('en' 或 'zh-TW')。
|
||||
"""
|
||||
self.quick_thinking_llm = quick_thinking_llm
|
||||
self.deep_thinking_llm = deep_thinking_llm
|
||||
|
|
@ -53,6 +55,7 @@ class GraphSetup:
|
|||
self.invest_judge_memory = invest_judge_memory
|
||||
self.risk_manager_memory = risk_manager_memory
|
||||
self.conditional_logic = conditional_logic
|
||||
self.language = language
|
||||
|
||||
def setup_graph(
|
||||
self, selected_analysts=["market", "social", "news", "fundamentals"]
|
||||
|
|
@ -80,50 +83,50 @@ class GraphSetup:
|
|||
|
||||
if "market" in selected_analysts:
|
||||
analyst_nodes["market"] = create_market_analyst(
|
||||
self.quick_thinking_llm
|
||||
self.quick_thinking_llm, self.language
|
||||
)
|
||||
delete_nodes["market"] = create_msg_delete()
|
||||
tool_nodes["market"] = self.tool_nodes["market"]
|
||||
|
||||
if "social" in selected_analysts:
|
||||
analyst_nodes["social"] = create_social_media_analyst(
|
||||
self.quick_thinking_llm
|
||||
self.quick_thinking_llm, self.language
|
||||
)
|
||||
delete_nodes["social"] = create_msg_delete()
|
||||
tool_nodes["social"] = self.tool_nodes["social"]
|
||||
|
||||
if "news" in selected_analysts:
|
||||
analyst_nodes["news"] = create_news_analyst(
|
||||
self.quick_thinking_llm
|
||||
self.quick_thinking_llm, self.language
|
||||
)
|
||||
delete_nodes["news"] = create_msg_delete()
|
||||
tool_nodes["news"] = self.tool_nodes["news"]
|
||||
|
||||
if "fundamentals" in selected_analysts:
|
||||
analyst_nodes["fundamentals"] = create_fundamentals_analyst(
|
||||
self.quick_thinking_llm
|
||||
self.quick_thinking_llm, self.language
|
||||
)
|
||||
delete_nodes["fundamentals"] = create_msg_delete()
|
||||
tool_nodes["fundamentals"] = self.tool_nodes["fundamentals"]
|
||||
|
||||
# 建立研究員和管理者節點
|
||||
bull_researcher_node = create_bull_researcher(
|
||||
self.quick_thinking_llm, self.bull_memory
|
||||
self.quick_thinking_llm, self.bull_memory, self.language
|
||||
)
|
||||
bear_researcher_node = create_bear_researcher(
|
||||
self.quick_thinking_llm, self.bear_memory
|
||||
self.quick_thinking_llm, self.bear_memory, self.language
|
||||
)
|
||||
research_manager_node = create_research_manager(
|
||||
self.deep_thinking_llm, self.invest_judge_memory
|
||||
self.deep_thinking_llm, self.invest_judge_memory, self.language
|
||||
)
|
||||
trader_node = create_trader(self.quick_thinking_llm, self.trader_memory)
|
||||
trader_node = create_trader(self.quick_thinking_llm, self.trader_memory, self.language)
|
||||
|
||||
# 建立風險分析節點
|
||||
risky_analyst = create_risky_debator(self.quick_thinking_llm)
|
||||
neutral_analyst = create_neutral_debator(self.quick_thinking_llm)
|
||||
safe_analyst = create_safe_debator(self.quick_thinking_llm)
|
||||
risky_analyst = create_risky_debator(self.quick_thinking_llm, self.language)
|
||||
neutral_analyst = create_neutral_debator(self.quick_thinking_llm, self.language)
|
||||
safe_analyst = create_safe_debator(self.quick_thinking_llm, self.language)
|
||||
risk_manager_node = create_risk_manager(
|
||||
self.deep_thinking_llm, self.risk_manager_memory
|
||||
self.deep_thinking_llm, self.risk_manager_memory, self.language
|
||||
)
|
||||
|
||||
# 建立工作流程
|
||||
|
|
|
|||
|
|
@ -128,6 +128,9 @@ class TradingAgentsXGraph:
|
|||
|
||||
# 建立工具節點
|
||||
self.tool_nodes = self._create_tool_nodes()
|
||||
|
||||
# Extract language from config (default: zh-TW for backward compatibility)
|
||||
self.language = self.config.get("language", "zh-TW")
|
||||
|
||||
# 初始化組件
|
||||
self.conditional_logic = ConditionalLogic()
|
||||
|
|
@ -141,6 +144,7 @@ class TradingAgentsXGraph:
|
|||
self.invest_judge_memory,
|
||||
self.risk_manager_memory,
|
||||
self.conditional_logic,
|
||||
self.language, # Pass language for agent reports
|
||||
)
|
||||
|
||||
self.propagator = Propagator()
|
||||
|
|
|
|||
Loading…
Reference in New Issue