Compare commits
9
Commits
51d057e2c9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e0f2c5e74 | ||
|
|
47c523852f | ||
|
|
c05413d4fa | ||
|
|
9a6c476ff1 | ||
|
|
af1391d6ae | ||
|
|
709ea0fd78 | ||
|
|
5207a2bf18 | ||
|
|
6bba181307 | ||
|
|
f8a47cb473 |
No files matched your search
@@ -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)
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,7 +124,12 @@ 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)
|
||||||
|
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 []
|
return []
|
||||||
|
|
||||||
posts = []
|
posts = []
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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,39 +296,27 @@ 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
|
||||||
try:
|
logger.info(f"Yahoo Finance: fetching {canonical} via SSH proxy...")
|
||||||
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...")
|
|
||||||
try:
|
try:
|
||||||
downloaded = _yf_download_via_proxy(canonical, start_str, end_str)
|
downloaded = _yf_download_via_proxy(canonical, start_str, end_str)
|
||||||
except Exception as proxy_err:
|
except Exception as proxy_err:
|
||||||
raise NoMarketDataError(
|
raise NoMarketDataError(
|
||||||
symbol, canonical, f"Yahoo Finance proxy also failed: {proxy_err}"
|
symbol, canonical, f"Yahoo Finance proxy failed: {proxy_err}"
|
||||||
) from proxy_err
|
) from proxy_err
|
||||||
if downloaded.empty:
|
if downloaded.empty:
|
||||||
raise NoMarketDataError(
|
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:
|
if downloaded.empty or "Close" not in downloaded.columns:
|
||||||
raise NoMarketDataError(
|
raise NoMarketDataError(
|
||||||
|
|||||||
@@ -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,7 +55,12 @@ 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)
|
||||||
|
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__}>"
|
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 []
|
||||||
|
|||||||
@@ -36,17 +36,13 @@ 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:
|
|
||||||
# Fallback: use SSH proxy via foreign server
|
|
||||||
logger.warning(f"Yahoo Finance local failed for {symbol}, trying SSH proxy...")
|
|
||||||
proxy_df = _yf_download_via_proxy(canonical, start_date, end_inclusive)
|
proxy_df = _yf_download_via_proxy(canonical, start_date, end_inclusive)
|
||||||
if proxy_df.empty:
|
if proxy_df.empty:
|
||||||
raise NoMarketDataError(
|
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")
|
proxy_df = proxy_df.set_index("Date")
|
||||||
data = proxy_df
|
data = proxy_df
|
||||||
|
|
||||||
|
|||||||
+640
@@ -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> </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)
|
||||||
Reference in new issue
Block a user