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 863 additions and 75 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", "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) 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 import requests
from .ssh_proxy_pool import ssh_proxy_fetch_json
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
GAMMA_BASE = "https://gamma-api.polymarket.com" GAMMA_BASE = "https://gamma-api.polymarket.com"
# Network timeout (seconds), consistent with the other vendors. # 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 number of markets to return, ranked by traded volume.
DEFAULT_LIMIT = 6 DEFAULT_LIMIT = 6
def _request(path: str, params: dict) -> dict: def _request(path: str, params: dict) -> dict:
response = requests.get( url = f"{GAMMA_BASE}/{path}"
f"{GAMMA_BASE}/{path}", params=params, timeout=REQUEST_TIMEOUT # Skip local request, go directly through SSH proxy
) query = "&".join(f"{k}={v}" for k, v in params.items())
response.raise_for_status() full_url = f"{url}?{query}"
return response.json() return ssh_proxy_fetch_json(full_url, timeout=REQUEST_TIMEOUT)
def _parse_json_list(value) -> list: def _parse_json_list(value) -> list:
+8 -2
View File
@@ -31,6 +31,7 @@ from urllib.parse import urlencode
from urllib.request import Request, urlopen from urllib.request import Request, urlopen
from .symbol_utils import crypto_base from .symbol_utils import crypto_base
from .stockstats_utils import _fetch_via_ssh
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -123,8 +124,13 @@ def _fetch_subreddit_rss(
except (OSError, http.client.HTTPException, ET.ParseError) as exc: except (OSError, http.client.HTTPException, ET.ParseError) as exc:
# OSError covers URLError/TimeoutError/connection resets; HTTPException # OSError covers URLError/TimeoutError/connection resets; HTTPException
# covers chunked-transfer errors (IncompleteRead/BadStatusLine, #1024). # 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)
return [] 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 = [] posts = []
for entry in root.findall("atom:entry", _ATOM_NS)[:limit]: for entry in root.findall("atom:entry", _ATOM_NS)[:limit]:
+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)
+20 -51
View File
@@ -43,19 +43,9 @@ def _get_ssh_config():
def _fetch_via_ssh(url: str, timeout: int = 30) -> str: def _fetch_via_ssh(url: str, timeout: int = 30) -> str:
"""Fetch a URL through the foreign server via SSH tunnel.""" """Fetch a URL through the foreign server via SSH tunnel (uses connection pool)."""
host, port, password = _get_ssh_config() from .ssh_proxy_pool import ssh_proxy_fetch
cmd = [ return ssh_proxy_fetch(url, timeout)
"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
def _yf_download_via_proxy(symbol: str, start: str, end: str) -> pd.DataFrame: 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 retry them internally. This wrapper adds retry logic specifically
for rate limits. Other exceptions propagate immediately. for rate limits. Other exceptions propagate immediately.
""" """
for attempt in range(max_retries + 1): # Skip local Yahoo Finance entirely - always use SSH proxy
try: raise YFRateLimitError("Local Yahoo Finance disabled, use SSH proxy")
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
def _ensure_date_column(data: pd.DataFrame) -> pd.DataFrame: def _ensure_date_column(data: pd.DataFrame) -> pd.DataFrame:
@@ -315,40 +296,28 @@ def load_ohlcv(symbol: str, curr_date: str) -> pd.DataFrame:
if os.path.exists(data_file): if os.path.exists(data_file):
cached = pd.read_csv(data_file, on_bad_lines="skip", encoding="utf-8") cached = pd.read_csv(data_file, on_bad_lines="skip", encoding="utf-8")
if not cached.empty and "Close" in cached.columns: 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 data = cached
if data is None: if data is None:
if use_alpha_vantage: if use_alpha_vantage:
downloaded = _load_ohlcv_alpha_vantage(symbol, curr_date) downloaded = _load_ohlcv_alpha_vantage(symbol, curr_date)
else: else:
downloaded = None # Skip local Yahoo Finance, go directly through SSH proxy
logger.info(f"Yahoo Finance: fetching {canonical} via SSH proxy...")
try: try:
raw = yf_retry(lambda: yf.download( downloaded = _yf_download_via_proxy(canonical, start_str, end_str)
canonical, except Exception as proxy_err:
start=start_str, raise NoMarketDataError(
end=end_str, symbol, canonical, f"Yahoo Finance proxy failed: {proxy_err}"
multi_level_index=False, ) from proxy_err
progress=False, if downloaded.empty:
auto_adjust=True, raise NoMarketDataError(
)) symbol, canonical, f"Yahoo Finance proxy returned no data"
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...")
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}"
) from proxy_err
if downloaded.empty:
raise NoMarketDataError(
symbol, canonical, f"Yahoo Finance proxy also returned no data"
)
if downloaded.empty or "Close" not in downloaded.columns: if downloaded.empty or "Close" not in downloaded.columns:
raise NoMarketDataError( raise NoMarketDataError(
symbol, canonical, "Yahoo Finance returned no rows" symbol, canonical, "Yahoo Finance returned no rows"
+8 -2
View File
@@ -20,6 +20,7 @@ import logging
from urllib.request import Request, urlopen from urllib.request import Request, urlopen
from .symbol_utils import crypto_base from .symbol_utils import crypto_base
from .stockstats_utils import _fetch_via_ssh
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -54,8 +55,13 @@ def fetch_stocktwits_messages(ticker: str, limit: int = 30, timeout: float = 10.
except (OSError, http.client.HTTPException, json.JSONDecodeError) as exc: except (OSError, http.client.HTTPException, json.JSONDecodeError) as exc:
# OSError covers URLError/TimeoutError/connection resets; HTTPException # OSError covers URLError/TimeoutError/connection resets; HTTPException
# covers chunked-transfer errors (IncompleteRead/BadStatusLine, #1024). # 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)
return f"<stocktwits unavailable: {type(exc).__name__}>" 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 [] messages = data.get("messages", []) if isinstance(data, dict) else []
if not messages: if not messages:
+9 -13
View File
@@ -36,19 +36,15 @@ def get_YFin_data_online(
# end_date row (and the current day when end_date is today). Request one day # 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). # past end_date so the requested range is actually inclusive (#986/#987).
end_inclusive = (end_dt + relativedelta(days=1)).strftime("%Y-%m-%d") end_inclusive = (end_dt + relativedelta(days=1)).strftime("%Y-%m-%d")
try: # Skip local Yahoo Finance (rate-limited), go directly through SSH proxy
data = yf_retry(lambda: ticker.history(start=start_date, end=end_inclusive)) logger.info(f"Yahoo Finance: fetching {canonical} via SSH proxy...")
except Exception: proxy_df = _yf_download_via_proxy(canonical, start_date, end_inclusive)
# Fallback: use SSH proxy via foreign server if proxy_df.empty:
logger.warning(f"Yahoo Finance local failed for {symbol}, trying SSH proxy...") raise NoMarketDataError(
proxy_df = _yf_download_via_proxy(canonical, start_date, end_inclusive) symbol, canonical, f"no rows between {start_date} and {end_date} (proxy failed)"
if proxy_df.empty: )
raise NoMarketDataError( proxy_df = proxy_df.set_index("Date")
symbol, canonical, f"no rows between {start_date} and {end_date} (proxy also failed)" data = proxy_df
)
# Convert to match expected format
proxy_df = proxy_df.set_index("Date")
data = proxy_df
# Empty result means the symbol is unknown/delisted. Raise a typed error # Empty result means the symbol is unknown/delisted. Raise a typed error
# instead of returning prose: the routing layer turns it into a single # instead of returning prose: the routing layer turns it into a single
+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)