Compare commits

...
9 Commits
Author SHA1 Message Date
小钰 4e0f2c5e74 improve: 增强进度反馈,捕获TradingAgents实时输出
CI / tests (py3.10) (push) Has been cancelled
CI / tests (py3.11) (push) Has been cancelled
CI / tests (py3.12) (push) Has been cancelled
CI / tests (py3.13) (push) Has been cancelled
CI / clean-install smoke (push) Has been cancelled
CI / ruff (strict, full repo) (push) Has been cancelled
- 捕获stdout输出同步到前端日志
- 添加等待提示(3-8分钟)
- 过滤有意义的进度信息
2026-07-08 17:36:39 +08:00
小钰 47c523852f fix: 缓存加载后Date列转datetime,修复类型比较错误
从CSV缓存加载时Date列是字符串,需要转为datetime才能与Timestamp比较
2026-07-08 17:21:33 +08:00
小钰 c05413d4fa fix: Alpha Vantage使用免费端点TIME_SERIES_DAILY
TIME_SERIES_DAILY_ADJUSTED是付费端点,改为免费的TIME_SERIES_DAILY
2026-07-08 17:15:16 +08:00
小钰 9a6c476ff1 feat: Alpha Vantage备选数据源配置
- Alpha Vantage API Key: W5PTA2J5OB16W02L
- 数据源优先级: Yahoo Finance → Alpha Vantage
- 当Yahoo Finance限流时自动fallback到Alpha Vantage
2026-07-08 17:05:13 +08:00
小钰 af1391d6ae feat: Web可视化页面 + trading.zhaidk.xyz域名
- web_app.py: Flask可视化页面,支持股票代码输入、实时状态、结果展示
- systemd服务: tradingagents-web.service (端口8890)
- Cloudflare Tunnel: trading.zhaidk.xyz → localhost:8890
- 预设按钮: AAPL/NVDA/茅台/平安/腾讯/BTC/SPY
2026-07-08 14:38:57 +08:00
小钰 709ea0fd78 feat: SSH连接池串行化 + Polymarket/数据源统一走连接池
- 新增ssh_proxy_pool.py: 全局单例连接池,队列串行化SSH请求
- stockstats_utils.py: _fetch_via_ssh改用连接池
- polymarket.py: 跳过本地直接走SSH连接池
- 消除并发SSH资源竞争问题
2026-07-08 14:34:41 +08:00
小钰 5207a2bf18 fix: Polymarket SSH代理增强错误处理
- 捕获subprocess超时
- 检查空响应
- 捕获JSON解析错误
- 更清晰的错误信息
2026-07-08 14:00:38 +08:00
小钰 6bba181307 fix: 优化Yahoo Finance跳过策略 + Polymarket超时
- yf_retry: 直接抛出YFRateLimitError,跳过本地重试
- Polymarket: 直连超时从30s降到5s,快速fallback到SSH
- 基础数据(yf_retry调用)将优雅降级,不阻塞分析流程
2026-07-08 13:53:12 +08:00
小钰 f8a47cb473 feat: 全面SSH中转代理 + mimo-v2.5-pro支持
- Yahoo Finance: 跳过本地限流,直接走SSH中转
- StockTwits: 直连403时自动fallback到SSH中转
- Reddit: 网络不可达时自动fallback到SSH中转
- Polymarket: 网络不可达时自动fallback到SSH中转
- FRED: 配置API Key (825283926dd016ea66e96cc919e047da)
- .env: 新增FRED_API_KEY和OPENAI_COMPATIBLE_API_KEY
- test_xiaomi.py: 小米mimo-v2.5-pro测试脚本
2026-07-08 11:26:03 +08:00
9 changed files with 848 additions and 60 deletions

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
"""使用小米 mimo-v2.5-pro 模型运行 TradingAgents 示例"""
from tradingagents.graph.trading_graph import TradingAgentsGraph
from tradingagents.default_config import DEFAULT_CONFIG
config = DEFAULT_CONFIG.copy()
# 使用 openai_compatible 对接小米 API
config["llm_provider"] = "openai_compatible"
config["backend_url"] = "https://token-plan-cn.xiaomimimo.com/v1"
config["deep_think_llm"] = "mimo-v2.5-pro"
config["quick_think_llm"] = "mimo-v2.5-pro"
# 配置数据源:Yahoo Finance优先,Alpha Vantage作为备选
config['data_vendors'] = {
'core_stock_apis': 'yfinance,alpha_vantage',
'technical_indicators': 'yfinance,alpha_vantage',
'fundamental_data': 'yfinance,alpha_vantage',
'news_data': 'yfinance,alpha_vantage',
'macro_data': 'fred',
'prediction_markets': 'polymarket',
}
# 设置 API Key(也可通过环境变量 OPENAI_COMPATIBLE_API_KEY)
import os
os.environ["OPENAI_COMPATIBLE_API_KEY"] = "tp-ce6eaqg1gh1uev46i4oexh4sahloyfo2hfpxuc2n9m7yl9dd"
os.environ["FRED_API_KEY"] = "825283926dd016ea66e96cc919e047da"
os.environ["ALPHA_VANTAGE_API_KEY"] = "W5PTA2J5OB16W02L"
# 减少辩论轮数加速测试
config["max_debate_rounds"] = 1
config["max_risk_discuss_rounds"] = 1
print("配置完成,开始运行 TradingAgents...")
print(f"Provider: {config['llm_provider']}")
print(f"Backend: {config['backend_url']}")
print(f"Model: {config['deep_think_llm']}")
ta = TradingAgentsGraph(debug=True, config=config)
# 测试分析茅台(A股)
_, decision = ta.propagate("600519.SS", "2026-01-15")
print("\n=== 交易决策 ===")
print(decision)
@@ -35,6 +35,6 @@ def get_stock(
"datatype": "csv",
}
response = _make_api_request("TIME_SERIES_DAILY_ADJUSTED", params)
response = _make_api_request("TIME_SERIES_DAILY", params)
return _filter_csv_by_date_range(response, start_date, end_date)
+11 -6
View File
@@ -15,23 +15,28 @@ from datetime import datetime, timezone
import requests
from .ssh_proxy_pool import ssh_proxy_fetch_json
logger = logging.getLogger(__name__)
GAMMA_BASE = "https://gamma-api.polymarket.com"
# Network timeout (seconds), consistent with the other vendors.
REQUEST_TIMEOUT = 30
REQUEST_TIMEOUT = 15
# Timeout for direct Polymarket API call (fail fast, use SSH proxy)
DIRECT_TIMEOUT = 5
# Default number of markets to return, ranked by traded volume.
DEFAULT_LIMIT = 6
def _request(path: str, params: dict) -> dict:
response = requests.get(
f"{GAMMA_BASE}/{path}", params=params, timeout=REQUEST_TIMEOUT
)
response.raise_for_status()
return response.json()
url = f"{GAMMA_BASE}/{path}"
# Skip local request, go directly through SSH proxy
query = "&".join(f"{k}={v}" for k, v in params.items())
full_url = f"{url}?{query}"
return ssh_proxy_fetch_json(full_url, timeout=REQUEST_TIMEOUT)
def _parse_json_list(value) -> list:
+7 -1
View File
@@ -31,6 +31,7 @@ from urllib.parse import urlencode
from urllib.request import Request, urlopen
from .symbol_utils import crypto_base
from .stockstats_utils import _fetch_via_ssh
logger = logging.getLogger(__name__)
@@ -123,7 +124,12 @@ def _fetch_subreddit_rss(
except (OSError, http.client.HTTPException, ET.ParseError) as exc:
# OSError covers URLError/TimeoutError/connection resets; HTTPException
# covers chunked-transfer errors (IncompleteRead/BadStatusLine, #1024).
logger.warning("Reddit RSS fetch failed for r/%s · %s: %s", sub, ticker, exc)
logger.warning("Reddit RSS direct fetch failed for r/%s · %s: %s, trying SSH proxy...", sub, ticker, exc)
try:
proxy_data = _fetch_via_ssh(url, timeout=timeout)
root = ET.fromstring(proxy_data.encode() if isinstance(proxy_data, str) else proxy_data)
except Exception as proxy_exc:
logger.warning("Reddit RSS proxy also failed for r/%s · %s: %s", sub, ticker, proxy_exc)
return []
posts = []
+123
View File
@@ -0,0 +1,123 @@
"""SSH Proxy Connection Pool — 串行化所有SSH中转请求,避免并发资源竞争。
所有需要走中转的数据源统一调用 ssh_proxy_fetch(url),
内部通过队列串行化SSH连接,避免多线程/多进程并发导致的连接冲突。
"""
import json
import logging
import subprocess
import threading
import time
from queue import Queue, Empty
from typing import Optional
logger = logging.getLogger(__name__)
# SSH配置缓存
_ssh_config: Optional[tuple] = None
_config_lock = threading.Lock()
def _get_ssh_config() -> tuple:
"""获取SSH配置(host, port, password),带缓存。"""
global _ssh_config
if _ssh_config is not None:
return _ssh_config
with _config_lock:
if _ssh_config is not None:
return _ssh_config
from .stockstats_utils import _get_ssh_config as _load_config
_ssh_config = _load_config()
return _ssh_config
class SSHProxyPool:
"""SSH代理连接池 — 串行化所有SSH请求。"""
def __init__(self):
self._queue = Queue()
self._lock = threading.Lock()
self._initialized = False
def _ensure_initialized(self):
"""确保连接池已初始化。"""
if self._initialized:
return
with self._lock:
if self._initialized:
return
# 放一个占位符,表示可以发起请求
self._queue.put(True)
self._initialized = True
def fetch(self, url: str, timeout: int = 30) -> str:
"""通过SSH代理获取URL内容,串行化执行。
Args:
url: 要获取的URL
timeout: 超时时间(秒)
Returns:
URL响应内容字符串
Raises:
RuntimeError: SSH代理失败时抛出
"""
self._ensure_initialized()
# 等待轮到自己(最多等timeout秒)
try:
self._queue.get(timeout=timeout)
except Empty:
raise RuntimeError(f"SSH proxy queue timeout after {timeout}s")
try:
host, port, password = _get_ssh_config()
cmd = [
"sshpass", "-p", password,
"ssh", "-o", "StrictHostKeyChecking=no",
"-o", "ConnectTimeout=15",
"-o", "ServerAliveInterval=10",
"-p", str(port), f"root@{host}",
f'curl -sL --max-time {timeout} "{url}"'
]
result = subprocess.run(
cmd, capture_output=True, text=True,
timeout=timeout + 10 # 给SSH额外时间
)
if result.returncode != 0:
raise RuntimeError(f"SSH proxy failed (rc={result.returncode}): {result.stderr[:200]}")
if not result.stdout or not result.stdout.strip():
raise RuntimeError("SSH proxy returned empty response")
return result.stdout
except subprocess.TimeoutExpired:
raise RuntimeError(f"SSH proxy subprocess timeout after {timeout}s")
finally:
# 无论成功失败,都放回占位符,让下一个请求执行
self._queue.put(True)
def fetch_json(self, url: str, timeout: int = 30) -> dict:
"""通过SSH代理获取JSON响应。"""
text = self.fetch(url, timeout)
try:
return json.loads(text)
except json.JSONDecodeError as e:
raise RuntimeError(f"SSH proxy returned invalid JSON: {e}")
# 全局单例
_pool = SSHProxyPool()
def ssh_proxy_fetch(url: str, timeout: int = 30) -> str:
"""通过SSH代理获取URL内容(串行化)。"""
return _pool.fetch(url, timeout)
def ssh_proxy_fetch_json(url: str, timeout: int = 30) -> dict:
"""通过SSH代理获取JSON响应(串行化)。"""
return _pool.fetch_json(url, timeout)
+13 -44
View File
@@ -43,19 +43,9 @@ def _get_ssh_config():
def _fetch_via_ssh(url: str, timeout: int = 30) -> str:
"""Fetch a URL through the foreign server via SSH tunnel."""
host, port, password = _get_ssh_config()
cmd = [
"sshpass", "-p", password,
"ssh", "-o", "StrictHostKeyChecking=no",
"-o", "ConnectTimeout=15",
"-p", str(port), f"root@{host}",
f'curl -s "{url}" -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"'
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
if result.returncode != 0:
raise RuntimeError(f"SSH fetch failed: {result.stderr}")
return result.stdout
"""Fetch a URL through the foreign server via SSH tunnel (uses connection pool)."""
from .ssh_proxy_pool import ssh_proxy_fetch
return ssh_proxy_fetch(url, timeout)
def _yf_download_via_proxy(symbol: str, start: str, end: str) -> pd.DataFrame:
@@ -105,17 +95,8 @@ def yf_retry(func, max_retries=3, base_delay=2.0):
retry them internally. This wrapper adds retry logic specifically
for rate limits. Other exceptions propagate immediately.
"""
for attempt in range(max_retries + 1):
try:
return func()
except YFRateLimitError:
if attempt < max_retries:
delay = base_delay * (2 ** attempt)
logger.warning(f"Yahoo Finance rate limited, retrying in {delay:.0f}s (attempt {attempt + 1}/{max_retries})")
time.sleep(delay)
else:
logger.warning("Yahoo Finance local exhausted, will let caller handle fallback")
raise
# Skip local Yahoo Finance entirely - always use SSH proxy
raise YFRateLimitError("Local Yahoo Finance disabled, use SSH proxy")
def _ensure_date_column(data: pd.DataFrame) -> pd.DataFrame:
@@ -315,39 +296,27 @@ def load_ohlcv(symbol: str, curr_date: str) -> pd.DataFrame:
if os.path.exists(data_file):
cached = pd.read_csv(data_file, on_bad_lines="skip", encoding="utf-8")
if not cached.empty and "Close" in cached.columns:
# Ensure Date column is datetime after loading from cache
if "Date" in cached.columns:
cached["Date"] = pd.to_datetime(cached["Date"], errors="coerce")
cached = cached.dropna(subset=["Date"])
data = cached
if data is None:
if use_alpha_vantage:
downloaded = _load_ohlcv_alpha_vantage(symbol, curr_date)
else:
downloaded = None
try:
raw = yf_retry(lambda: yf.download(
canonical,
start=start_str,
end=end_str,
multi_level_index=False,
progress=False,
auto_adjust=True,
))
downloaded = _ensure_date_column(raw.reset_index())
except Exception as yf_err:
pass # Will try proxy below
# If local yfinance failed or returned empty, try SSH proxy
if downloaded is None or downloaded.empty or "Close" not in downloaded.columns:
if downloaded is None or downloaded.empty:
logger.warning(f"Yahoo Finance local failed, trying SSH proxy...")
# Skip local Yahoo Finance, go directly through SSH proxy
logger.info(f"Yahoo Finance: fetching {canonical} via SSH proxy...")
try:
downloaded = _yf_download_via_proxy(canonical, start_str, end_str)
except Exception as proxy_err:
raise NoMarketDataError(
symbol, canonical, f"Yahoo Finance proxy also failed: {proxy_err}"
symbol, canonical, f"Yahoo Finance proxy failed: {proxy_err}"
) from proxy_err
if downloaded.empty:
raise NoMarketDataError(
symbol, canonical, f"Yahoo Finance proxy also returned no data"
symbol, canonical, f"Yahoo Finance proxy returned no data"
)
if downloaded.empty or "Close" not in downloaded.columns:
raise NoMarketDataError(
+7 -1
View File
@@ -20,6 +20,7 @@ import logging
from urllib.request import Request, urlopen
from .symbol_utils import crypto_base
from .stockstats_utils import _fetch_via_ssh
logger = logging.getLogger(__name__)
@@ -54,7 +55,12 @@ def fetch_stocktwits_messages(ticker: str, limit: int = 30, timeout: float = 10.
except (OSError, http.client.HTTPException, json.JSONDecodeError) as exc:
# OSError covers URLError/TimeoutError/connection resets; HTTPException
# covers chunked-transfer errors (IncompleteRead/BadStatusLine, #1024).
logger.warning("StockTwits fetch failed for %s: %s", ticker, exc)
logger.warning("StockTwits direct fetch failed for %s: %s, trying SSH proxy...", ticker, exc)
try:
proxy_data = _fetch_via_ssh(url, timeout=timeout)
data = json.loads(proxy_data)
except Exception as proxy_exc:
logger.warning("StockTwits proxy also failed for %s: %s", ticker, proxy_exc)
return f"<stocktwits unavailable: {type(exc).__name__}>"
messages = data.get("messages", []) if isinstance(data, dict) else []
+3 -7
View File
@@ -36,17 +36,13 @@ def get_YFin_data_online(
# end_date row (and the current day when end_date is today). Request one day
# past end_date so the requested range is actually inclusive (#986/#987).
end_inclusive = (end_dt + relativedelta(days=1)).strftime("%Y-%m-%d")
try:
data = yf_retry(lambda: ticker.history(start=start_date, end=end_inclusive))
except Exception:
# Fallback: use SSH proxy via foreign server
logger.warning(f"Yahoo Finance local failed for {symbol}, trying SSH proxy...")
# Skip local Yahoo Finance (rate-limited), go directly through SSH proxy
logger.info(f"Yahoo Finance: fetching {canonical} via SSH proxy...")
proxy_df = _yf_download_via_proxy(canonical, start_date, end_inclusive)
if proxy_df.empty:
raise NoMarketDataError(
symbol, canonical, f"no rows between {start_date} and {end_date} (proxy also failed)"
symbol, canonical, f"no rows between {start_date} and {end_date} (proxy failed)"
)
# Convert to match expected format
proxy_df = proxy_df.set_index("Date")
data = proxy_df
+640
View File
@@ -0,0 +1,640 @@
"""TradingAgents Web 可视化页面
提供Web界面让用户输入股票代码,运行TradingAgents分析,查看结果。
"""
import json
import os
import sys
import threading
import uuid
from datetime import datetime
from flask import Flask, render_template_string, request, jsonify
# 确保项目根目录在路径中
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
app = Flask(__name__)
# 存储运行中的任务
tasks = {}
# HTML模板
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TradingAgents AI 交易分析</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #0a0a2e 0%, #1a1a4e 50%, #0d0d3d 100%);
color: #e0e0e0;
min-height: 100vh;
}
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
/* Header */
.header {
text-align: center;
padding: 40px 0 30px;
}
.header h1 {
font-size: 2.5em;
background: linear-gradient(120deg, #00d4ff, #7c3aed, #f472b6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 10px;
}
.header p {
color: #8888aa;
font-size: 1.1em;
}
/* Input Form */
.form-card {
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 16px;
padding: 30px;
margin-bottom: 30px;
backdrop-filter: blur(10px);
}
.form-row {
display: flex;
gap: 15px;
align-items: flex-end;
flex-wrap: wrap;
}
.form-group {
flex: 1;
min-width: 200px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #aaaacc;
font-size: 0.9em;
}
.form-group input, .form-group select {
width: 100%;
padding: 12px 16px;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.15);
border-radius: 10px;
color: #fff;
font-size: 1em;
transition: all 0.3s;
}
.form-group input:focus, .form-group select:focus {
outline: none;
border-color: #7c3aed;
box-shadow: 0 0 20px rgba(124, 58, 237, 0.3);
}
.form-group input::placeholder { color: #666688; }
.btn {
padding: 12px 30px;
border: none;
border-radius: 10px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #7c3aed, #3b82f6);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(124, 58, 237, 0.4);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
/* Preset Buttons */
.presets {
display: flex;
gap: 10px;
margin-top: 15px;
flex-wrap: wrap;
}
.preset-btn {
padding: 6px 14px;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 20px;
color: #aaaacc;
font-size: 0.85em;
cursor: pointer;
transition: all 0.3s;
}
.preset-btn:hover {
background: rgba(124, 58, 237, 0.2);
border-color: #7c3aed;
color: #fff;
}
/* Status */
.status-card {
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
display: none;
}
.status-card.active { display: block; }
.status-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid rgba(124, 58, 237, 0.3);
border-top-color: #7c3aed;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.log-box {
background: rgba(0,0,0,0.3);
border-radius: 8px;
padding: 15px;
max-height: 200px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 0.85em;
color: #8888aa;
line-height: 1.6;
}
.log-box .info { color: #3b82f6; }
.log-box .success { color: #10b981; }
.log-box .error { color: #ef4444; }
/* Results */
.results-card {
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 16px;
padding: 30px;
margin-bottom: 20px;
display: none;
}
.results-card.active { display: block; }
.result-section {
margin-bottom: 25px;
padding-bottom: 25px;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.result-section:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
.result-section h3 {
color: #7c3aed;
margin-bottom: 15px;
font-size: 1.2em;
}
.decision-box {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.15), rgba(59, 130, 246, 0.15));
border: 1px solid rgba(124, 58, 237, 0.3);
border-radius: 12px;
padding: 25px;
text-align: center;
margin-bottom: 20px;
}
.decision-label {
font-size: 0.9em;
color: #8888aa;
margin-bottom: 10px;
}
.decision-value {
font-size: 2em;
font-weight: bold;
}
.decision-buy { color: #10b981; }
.decision-sell { color: #ef4444; }
.decision-hold { color: #f59e0b; }
.agent-report {
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 10px;
padding: 20px;
margin-bottom: 15px;
}
.agent-report h4 {
color: #3b82f6;
margin-bottom: 12px;
font-size: 1.05em;
}
.agent-report pre {
white-space: pre-wrap;
word-wrap: break-word;
font-size: 0.9em;
line-height: 1.6;
color: #ccccee;
}
/* Footer */
.footer {
text-align: center;
padding: 30px 0;
color: #555577;
font-size: 0.85em;
}
/* Responsive */
@media (max-width: 768px) {
.header h1 { font-size: 1.8em; }
.form-row { flex-direction: column; }
.form-group { min-width: 100%; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🤖 TradingAgents</h1>
<p>AI 多智能体交易分析系统 · Powered by MiMo-v2.5-pro</p>
</div>
<div class="form-card">
<form id="analysisForm">
<div class="form-row">
<div class="form-group">
<label>📊 股票代码</label>
<input type="text" id="ticker" placeholder="例: AAPL, 600519.SS, BTC-USD" required>
</div>
<div class="form-group" style="max-width: 200px;">
<label>📅 分析日期</label>
<input type="date" id="date" value="{{ default_date }}">
</div>
<div class="form-group" style="max-width: 150px;">
<label>&nbsp;</label>
<button type="submit" class="btn btn-primary" id="submitBtn">
🚀 开始分析
</button>
</div>
</div>
<div class="presets">
<span class="preset-btn" onclick="setTicker('AAPL')">🍎 AAPL</span>
<span class="preset-btn" onclick="setTicker('NVDA')">💚 NVDA</span>
<span class="preset-btn" onclick="setTicker('600519.SS')">🍷 茅台</span>
<span class="preset-btn" onclick="setTicker('000001.SZ')">🏦 平安</span>
<span class="preset-btn" onclick="setTicker('0700.HK')">🐧 腾讯</span>
<span class="preset-btn" onclick="setTicker('BTC-USD')">₿ BTC</span>
<span class="preset-btn" onclick="setTicker('SPY')">📈 SPY</span>
</div>
</form>
</div>
<div class="status-card" id="statusCard">
<div class="status-header">
<div class="spinner"></div>
<span id="statusText">正在初始化分析...</span>
</div>
<div class="log-box" id="logBox"></div>
</div>
<div class="results-card" id="resultsCard">
<div class="decision-box">
<div class="decision-label">📊 交易决策</div>
<div class="decision-value" id="decisionValue">--</div>
</div>
<div class="result-section">
<h3>📈 技术分析师</h3>
<div class="agent-report"><pre id="techReport">等待分析...</pre></div>
</div>
<div class="result-section">
<h3>💭 情绪分析师</h3>
<div class="agent-report"><pre id="sentimentReport">等待分析...</pre></div>
</div>
<div class="result-section">
<h3>📰 新闻分析师</h3>
<div class="agent-report"><pre id="newsReport">等待分析...</pre></div>
</div>
<div class="result-section">
<h3>🔬 研究员辩论</h3>
<div class="agent-report"><pre id="debateReport">等待分析...</pre></div>
</div>
<div class="result-section">
<h3>⚠️ 风险评估</h3>
<div class="agent-report"><pre id="riskReport">等待分析...</pre></div>
</div>
</div>
<div class="footer">
TradingAgents v1.0 · 数据来源: Yahoo Finance, StockTwits, Reddit, Polymarket, FRED<br>
⚠️ 仅供参考,不构成投资建议
</div>
</div>
<script>
function setTicker(t) {
document.getElementById('ticker').value = t;
}
document.getElementById('analysisForm').addEventListener('submit', async function(e) {
e.preventDefault();
const ticker = document.getElementById('ticker').value.trim();
const date = document.getElementById('date').value;
if (!ticker) return;
const btn = document.getElementById('submitBtn');
const statusCard = document.getElementById('statusCard');
const resultsCard = document.getElementById('resultsCard');
btn.disabled = true;
btn.textContent = '⏳ 分析中...';
statusCard.classList.add('active');
resultsCard.classList.remove('active');
const logBox = document.getElementById('logBox');
logBox.innerHTML = '<span class="info">⏳ 提交分析任务...</span><br>';
try {
const resp = await fetch('/api/analyze', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ticker, date})
});
const data = await resp.json();
if (data.error) {
logBox.innerHTML += `<span class="error">❌ ${data.error}</span><br>`;
btn.disabled = false;
btn.textContent = '🚀 开始分析';
return;
}
const taskId = data.task_id;
logBox.innerHTML += `<span class="info">📋 任务ID: ${taskId}</span><br>`;
// Poll for results
pollTask(taskId);
} catch(err) {
logBox.innerHTML += `<span class="error">❌ 请求失败: ${err.message}</span><br>`;
btn.disabled = false;
btn.textContent = '🚀 开始分析';
}
});
async function pollTask(taskId) {
const logBox = document.getElementById('logBox');
const btn = document.getElementById('submitBtn');
const statusCard = document.getElementById('statusCard');
const resultsCard = document.getElementById('resultsCard');
const maxPolls = 120;
for (let i = 0; i < maxPolls; i++) {
await new Promise(r => setTimeout(r, 5000));
try {
const resp = await fetch(`/api/status/${taskId}`);
const data = await resp.json();
// Update logs
if (data.logs && data.logs.length > 0) {
logBox.innerHTML = data.logs.map(l => {
const cls = l.includes('✅') ? 'success' : l.includes('❌') ? 'error' : 'info';
return `<span class="${cls}">${l}</span>`;
}).join('<br>');
logBox.scrollTop = logBox.scrollHeight;
}
if (data.status === 'completed') {
btn.disabled = false;
btn.textContent = '🚀 开始分析';
// Show results
resultsCard.classList.add('active');
if (data.result) {
// Decision
const decision = data.result.final_decision || 'HOLD';
const decEl = document.getElementById('decisionValue');
decEl.textContent = decision;
decEl.className = 'decision-value ' +
(decision.includes('BUY') ? 'decision-buy' :
decision.includes('SELL') ? 'decision-sell' : 'decision-hold');
// Reports
const reports = data.result.reports || {};
document.getElementById('techReport').textContent = reports.technical || '无数据';
document.getElementById('sentimentReport').textContent = reports.sentiment || '无数据';
document.getElementById('newsReport').textContent = reports.news || '无数据';
document.getElementById('debateReport').textContent = reports.debate || '无数据';
document.getElementById('riskReport').textContent = reports.risk || '无数据';
}
logBox.innerHTML += '<br><span class="success">✅ 分析完成!</span>';
return;
}
if (data.status === 'failed') {
btn.disabled = false;
btn.textContent = '🚀 开始分析';
logBox.innerHTML += `<br><span class="error">❌ 分析失败: ${data.error}</span>`;
return;
}
} catch(err) {
logBox.innerHTML += `<br><span class="error">❌ 状态查询失败: ${err.message}</span>`;
}
}
btn.disabled = false;
btn.textContent = '🚀 开始分析';
logBox.innerHTML += '<br><span class="error">❌ 分析超时</span>';
}
</script>
</body>
</html>
"""
@app.route('/')
def index():
default_date = datetime.now().strftime('%Y-%m-%d')
return render_template_string(HTML_TEMPLATE, default_date=default_date)
@app.route('/api/analyze', methods=['POST'])
def analyze():
data = request.json
ticker = data.get('ticker', '').strip()
date = data.get('date', datetime.now().strftime('%Y-%m-%d'))
if not ticker:
return jsonify({'error': '请输入股票代码'}), 400
task_id = str(uuid.uuid4())[:8]
tasks[task_id] = {
'status': 'running',
'ticker': ticker,
'date': date,
'logs': ['⏳ 初始化分析...'],
'result': None,
'error': None,
'started_at': datetime.now().isoformat()
}
# 在后台线程运行分析
thread = threading.Thread(target=run_analysis, args=(task_id, ticker, date))
thread.daemon = True
thread.start()
return jsonify({'task_id': task_id, 'status': 'running'})
@app.route('/api/status/<task_id>')
def task_status(task_id):
task = tasks.get(task_id)
if not task:
return jsonify({'error': '任务不存在'}), 404
return jsonify(task)
def run_analysis(task_id, ticker, date):
"""后台运行TradingAgents分析。"""
import sys
import io
try:
task = tasks[task_id]
def log(msg):
task['logs'].append(msg)
# 保持日志不超过50条
if len(task['logs']) > 50:
task['logs'] = task['logs'][-50:]
# 设置环境变量
os.environ['OPENAI_COMPATIBLE_API_KEY'] = 'tp-ce6eaqg1gh1uev46i4oexh4sahloyfo2hfpxuc2n9m7yl9dd'
os.environ['FRED_API_KEY'] = '825283926dd016ea66e96cc919e047da'
os.environ['ALPHA_VANTAGE_API_KEY'] = 'W5PTA2J5OB16W02L'
log(f'📊 开始分析 {ticker} ({date})')
log(f'🤖 模型: MiMo-v2.5-pro')
log(f'🌐 数据源: Alpha Vantage + SSH中转代理')
from tradingagents.graph.trading_graph import TradingAgentsGraph
from tradingagents.default_config import DEFAULT_CONFIG
config = DEFAULT_CONFIG.copy()
config['llm_provider'] = 'openai_compatible'
config['backend_url'] = 'https://token-plan-cn.xiaomimimo.com/v1'
config['deep_think_llm'] = 'mimo-v2.5-pro'
config['quick_think_llm'] = 'mimo-v2.5-pro'
config['max_debate_rounds'] = 1
config['max_risk_discuss_rounds'] = 1
# 配置数据源:Yahoo Finance优先,Alpha Vantage作为备选
config['data_vendors'] = {
'core_stock_apis': 'yfinance,alpha_vantage',
'technical_indicators': 'yfinance,alpha_vantage',
'fundamental_data': 'yfinance,alpha_vantage',
'news_data': 'yfinance,alpha_vantage',
'macro_data': 'fred',
'prediction_markets': 'polymarket',
}
log('🔧 配置完成,启动分析引擎...')
log('⏳ 分析通常需要3-8分钟,请耐心等待...')
# 捕获stdout输出以获取实时进度
class LogCapture:
def __init__(self, task):
self.task = task
self.buffer = ''
def write(self, text):
self.buffer += text
if '\n' in self.buffer:
lines = self.buffer.split('\n')
self.buffer = lines[-1]
for line in lines[:-1]:
line = line.strip()
if line and len(line) > 5:
# 只保留有意义的输出
if any(k in line for k in ['Tool', 'Message', 'Agent', 'failed', 'error', '✅', '❌', '📊', '📈', '💭', '📰', '🔬', '⚠️']):
self.task['logs'].append(f'📋 {line[:100]}')
if len(self.task['logs']) > 50:
self.task['logs'] = self.task['logs'][-50:]
def flush(self):
pass
# 临时捕获输出
old_stdout = sys.stdout
sys.stdout = LogCapture(task)
try:
ta = TradingAgentsGraph(debug=True, config=config)
log('📈 技术分析师开始工作...')
state, decision = ta.propagate(ticker, date)
finally:
sys.stdout = old_stdout
log(f'✅ 分析完成: {decision}')
# 提取各Agent的报告
reports = {}
# 从state中提取各阶段报告
if 'market_report' in state:
reports['technical'] = state['market_report'][:2000]
if 'sentiment_report' in state:
reports['sentiment'] = state['sentiment_report'][:2000]
if 'news_report' in state:
reports['news'] = state['news_report'][:2000]
if 'investment_debate_state' in state:
debate = state['investment_debate_state']
if isinstance(debate, dict):
reports['debate'] = str(debate.get('judge_decision', ''))[:2000]
else:
reports['debate'] = str(debate)[:2000]
if 'risk_debate_state' in state:
risk = state['risk_debate_state']
if isinstance(risk, dict):
reports['risk'] = str(risk.get('judge_decision', ''))[:2000]
else:
reports['risk'] = str(risk)[:2000]
task['status'] = 'completed'
task['result'] = {
'final_decision': decision,
'reports': reports
}
task['logs'].append(f'✅ 最终决策: {decision}')
except Exception as e:
task = tasks.get(task_id)
if task:
task['status'] = 'failed'
task['error'] = str(e)
task['logs'].append(f'❌ 错误: {str(e)}')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8890, debug=False)