Skip to content

Commit 47143ab

Browse files
committed
feat: force hot stock analysis via tradingagents deepseek
1 parent 377b56b commit 47143ab

7 files changed

Lines changed: 170 additions & 11 deletions
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# 热门智能固定调用 TradingAgents 与 DeepSeek
2+
3+
## 背景 / 目标
4+
5+
当前热门股票智能分析模块虽然已经具备 `TradingAgents` 适配层,但调用行为仍允许通过配置切换 provider / model,默认值也依赖运行时配置。需要把“热门个股讨论分析”明确固定为调用本地 `TradingAgents` 项目,并使用 `DeepSeek API` 作为 LLM 后端。
6+
7+
本次目标:
8+
9+
- 明确热门智能模块的讨论分析来源就是 `/Users/jie.feng/work/github/TradingAgents`
10+
- 在适配层中显式使用 `DeepSeek` 官方 API 配置,不再沿用默认 OpenAI 配置。
11+
- 在服务元信息与 README 中体现当前热门智能的分析后端。
12+
13+
## 任务分解(checklist)
14+
15+
- [ ] 梳理 `TradingAgentsAdapter``HotStockAIService` 的现有调用链。
16+
- [ ] 在适配层中补充 `DeepSeek` provider / backend URL / key 校验逻辑。
17+
- [ ] 调整热门智能服务元信息,使前端能看到当前讨论后端为 `TradingAgents + DeepSeek`
18+
- [ ] 补充测试,覆盖适配器配置与元信息输出。
19+
- [ ] 更新 `README.md` 对热门智能模块与 TradingAgents 接入方式的描述。
20+
- [ ] 执行测试与静态检查。
21+
- [ ] 执行 `./restart.sh`,完成 `git add / commit / push`
22+
23+
## 验收标准
24+
25+
- 热门智能模块对热门股的讨论分析明确通过本地 `TradingAgents` 项目触发。
26+
- 适配层默认且显式使用 `DeepSeek` API。
27+
- 接口或页面元信息能看出当前分析后端是 `TradingAgents + DeepSeek`
28+
- 测试通过,服务可正常重启。
29+
30+
## 风险与回滚
31+
32+
-`DEEPSEEK_API_KEY` 缺失,真实讨论分析会失败,需要在错误信息中清晰暴露。
33+
-`TradingAgents` 本地依赖变更,适配层导入路径可能失效。
34+
- 回滚方式:恢复本次提交前的适配器和热门智能服务逻辑并重启。
35+
36+
## 关键决策记录
37+
38+
- 热门智能基础量化打分继续保留,用于排序和三池;`TradingAgents` 负责单股深度讨论与加减分。
39+
- 将 provider 固定到 `DeepSeek`,减少运行时分叉和排障复杂度。

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ Alpha 是一个面向 A 股市场的**自进化量化选股系统**。它不只
136136
137137
- **实时输入**:直接复用 `/api/market/hot-stocks` 的实时热门股接口,默认取前 20
138138
- **逐股分析**:结合热度排名、当日涨幅、趋势位置、20 日量额比和 `Kronos` 三日预测
139+
- **深度讨论**:对命中的热门股调用本地 [TradingAgents](</Users/jie.feng/work/github/TradingAgents/README.md>) 项目,通过 `DeepSeek API` 产出多代理讨论结论,再映射为加减分
139140
- **解释性评分**:每只股票都输出分数组成、风险扣分、标签和一段摘要分析
140141
- **三池拆分**:默认阈值 `8 / 11.5 / 14.5` 分,对应候选 / 重点关注 / 买入候选
141142
- **自动刷新**:默认每 5 分钟自动扫描一次;为保证站点响应,后台自动任务走轻量模式(缩小样本并跳过 `TradingAgents` / `Kronos`),页面手动触发仍执行完整分析

app/services/hot_stock_ai_service.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@
1313
from app.services.kronos_predict_service import KronosPredictService
1414
from app.services.sqlite_store import SQLiteStateStore
1515
from app.services.time_utils import now_cn
16-
from app.services.tradingagents_adapter import TradingAgentsAdapter
16+
from app.services.tradingagents_adapter import (
17+
DEFAULT_DEEP_MODEL,
18+
DEFAULT_QUICK_MODEL,
19+
DEEPSEEK_PROVIDER,
20+
TradingAgentsAdapter,
21+
)
1722

1823
log = logging.getLogger(__name__)
1924

@@ -37,9 +42,9 @@
3742
"tradingagents_enabled": True,
3843
"tradingagents_top_n": 20,
3944
"tradingagents_timeout_seconds": 240,
40-
"tradingagents_provider": "deepseek",
41-
"tradingagents_quick_model": "deepseek-chat",
42-
"tradingagents_deep_model": "deepseek-chat",
45+
"tradingagents_provider": DEEPSEEK_PROVIDER,
46+
"tradingagents_quick_model": DEFAULT_QUICK_MODEL,
47+
"tradingagents_deep_model": DEFAULT_DEEP_MODEL,
4348
}
4449

4550
AUTO_LIGHT_TOP_N = 12
@@ -127,6 +132,9 @@ def update_config(self, patch: dict[str, Any]) -> dict[str, Any]:
127132
cfg["tradingagents_enabled"] = bool(cfg["tradingagents_enabled"])
128133
cfg["tradingagents_top_n"] = max(0, min(int(cfg["tradingagents_top_n"]), 20))
129134
cfg["tradingagents_timeout_seconds"] = max(30, min(int(cfg["tradingagents_timeout_seconds"]), 900))
135+
cfg["tradingagents_provider"] = DEEPSEEK_PROVIDER
136+
cfg["tradingagents_quick_model"] = str(cfg.get("tradingagents_quick_model") or DEFAULT_QUICK_MODEL).strip() or DEFAULT_QUICK_MODEL
137+
cfg["tradingagents_deep_model"] = str(cfg.get("tradingagents_deep_model") or DEFAULT_DEEP_MODEL).strip() or DEFAULT_DEEP_MODEL
130138
self._snapshot["config"] = cfg
131139
self._save_state()
132140
return cfg
@@ -214,6 +222,11 @@ async def _execute(self, trigger: str) -> dict[str, Any]:
214222
pools = self._build_pools(entries, cfg)
215223
trade_date = entries[0]["trade_date"] if entries else now_cn().date().isoformat()
216224
avg_score = round(sum(float(item["score"]) for item in entries) / max(len(entries), 1), 2) if entries else 0.0
225+
ta_runtime = (
226+
self.tradingagents.describe_runtime()
227+
if self.tradingagents is not None and hasattr(self.tradingagents, "describe_runtime")
228+
else {}
229+
)
217230
self.progress.update(phase="done", current=total, total=total, detail=f"完成分析 {len(entries)} 只")
218231
return {
219232
"trade_date": trade_date,
@@ -238,6 +251,10 @@ async def _execute(self, trigger: str) -> dict[str, Any]:
238251
"tradingagents_discussed": discussion_meta.get("discussed_count", 0),
239252
"tradingagents_cache_hits": discussion_meta.get("cache_hits", 0),
240253
"tradingagents_failures": discussion_meta.get("failed", 0),
254+
"tradingagents_backend": "TradingAgents + DeepSeek" if self.tradingagents is not None else "disabled",
255+
"tradingagents_repo_path": ta_runtime.get("repo_path", ""),
256+
"tradingagents_provider": ta_runtime.get("provider", DEEPSEEK_PROVIDER),
257+
"tradingagents_backend_url": ta_runtime.get("backend_url", ""),
241258
"execution_mode": "light_auto" if trigger == "auto" else "full_manual",
242259
"runtime_top_n": int(runtime_cfg["top_n"]),
243260
"thresholds": {

app/services/tradingagents_adapter.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
from __future__ import annotations
22

3+
import os
34
import re
45
import sys
56
from pathlib import Path
67
from typing import Any
78

9+
DEEPSEEK_PROVIDER = "deepseek"
10+
DEEPSEEK_BACKEND_URL = "https://api.deepseek.com"
11+
DEFAULT_QUICK_MODEL = "deepseek-chat"
12+
DEFAULT_DEEP_MODEL = "deepseek-chat"
13+
814

915
def _normalize_whitespace(text: str) -> str:
1016
return re.sub(r"\s+", " ", str(text or "")).strip()
@@ -20,6 +26,15 @@ def __init__(
2026
self.runtime_root = Path(runtime_root)
2127
self.runtime_root.mkdir(parents=True, exist_ok=True)
2228

29+
def describe_runtime(self) -> dict[str, str]:
30+
return {
31+
"repo_path": str(self.repo_path),
32+
"provider": DEEPSEEK_PROVIDER,
33+
"backend_url": DEEPSEEK_BACKEND_URL,
34+
"quick_model": DEFAULT_QUICK_MODEL,
35+
"deep_model": DEFAULT_DEEP_MODEL,
36+
}
37+
2338
@staticmethod
2439
def to_vendor_symbol(symbol: str) -> str:
2540
raw = str(symbol or "").strip()
@@ -58,19 +73,23 @@ def analyze(
5873
symbol: str,
5974
trade_date: str,
6075
*,
61-
provider: str = "deepseek",
62-
quick_model: str = "deepseek-chat",
63-
deep_model: str = "deepseek-chat",
76+
provider: str = DEEPSEEK_PROVIDER,
77+
quick_model: str = DEFAULT_QUICK_MODEL,
78+
deep_model: str = DEFAULT_DEEP_MODEL,
6479
selected_analysts: list[str] | None = None,
6580
output_language: str = "Chinese",
6681
) -> dict[str, Any]:
82+
if not os.getenv("DEEPSEEK_API_KEY"):
83+
raise RuntimeError("DEEPSEEK_API_KEY 未设置,无法调用 TradingAgents DeepSeek 分析")
84+
6785
TradingAgentsGraph, DEFAULT_CONFIG = self._load_classes()
6886
vendor_symbol = self.to_vendor_symbol(symbol)
6987

7088
config = DEFAULT_CONFIG.copy()
71-
config["llm_provider"] = provider
72-
config["quick_think_llm"] = quick_model
73-
config["deep_think_llm"] = deep_model
89+
config["llm_provider"] = DEEPSEEK_PROVIDER
90+
config["backend_url"] = DEEPSEEK_BACKEND_URL
91+
config["quick_think_llm"] = quick_model or DEFAULT_QUICK_MODEL
92+
config["deep_think_llm"] = deep_model or DEFAULT_DEEP_MODEL
7493
config["output_language"] = output_language
7594
config["max_debate_rounds"] = 1
7695
config["max_risk_discuss_rounds"] = 1
@@ -97,6 +116,8 @@ def analyze(
97116
"symbol": symbol,
98117
"vendor_symbol": vendor_symbol,
99118
"trade_date": trade_date,
119+
"provider": DEEPSEEK_PROVIDER,
120+
"backend_url": DEEPSEEK_BACKEND_URL,
100121
"decision": str(decision or "").strip().upper(),
101122
"score_bonus": self._decision_bonus(decision),
102123
"summary": summary,

app/static/app.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3391,7 +3391,8 @@ function renderHotStockAI() {
33913391
const taText = m.runtime_tradingagents_enabled
33923392
? ` · 讨论 ${m.tradingagents_discussed || 0} · 缓存 ${m.tradingagents_cache_hits || 0}`
33933393
: (m.tradingagents_enabled ? ' · 自动任务已跳过讨论' : '');
3394-
metaEl.textContent = `交易日 ${snap.trade_date || '--'} · 分析 ${m.entries_count || 0}/${m.stocks_scanned || cfg.top_n || 20}${modeText} · Kronos ${m.kronos_device || '--'}${taText} · 均分 ${fmtNum(m.avg_score || 0, 1)}`;
3394+
const backendText = m.tradingagents_backend ? ` · ${m.tradingagents_backend}` : '';
3395+
metaEl.textContent = `交易日 ${snap.trade_date || '--'} · 分析 ${m.entries_count || 0}/${m.stocks_scanned || cfg.top_n || 20}${modeText} · Kronos ${m.kronos_device || '--'}${taText}${backendText} · 均分 ${fmtNum(m.avg_score || 0, 1)}`;
33953396
}
33963397
if (summary) {
33973398
summary.innerHTML = [

tests/test_hot_stock_ai_service.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ class FakeTradingAgentsAdapter:
9797
def __init__(self):
9898
self.calls: list[tuple[str, str]] = []
9999

100+
def describe_runtime(self) -> dict[str, str]:
101+
return {
102+
"repo_path": "/Users/jie.feng/work/github/TradingAgents",
103+
"provider": "deepseek",
104+
"backend_url": "https://api.deepseek.com",
105+
}
106+
100107
def analyze(self, symbol: str, trade_date: str, **kwargs) -> dict:
101108
self.calls.append((symbol, trade_date))
102109
mapping = {
@@ -202,6 +209,7 @@ def test_hot_stock_ai_config_is_clamped(tmp_path: Path):
202209
assert cfg["max_buy_pool_size"] == 10
203210
assert cfg["tradingagents_top_n"] == 20
204211
assert cfg["tradingagents_timeout_seconds"] == 30
212+
assert cfg["tradingagents_provider"] == "deepseek"
205213

206214

207215
def test_hot_stock_ai_auto_run_uses_light_mode(tmp_path: Path):
@@ -226,3 +234,20 @@ def test_hot_stock_ai_auto_run_uses_light_mode(tmp_path: Path):
226234
assert snap["meta"]["tradingagents_discussed"] == 0
227235
assert kronos.calls == []
228236
assert adapter.calls == []
237+
238+
239+
def test_hot_stock_ai_meta_exposes_tradingagents_backend(tmp_path: Path):
240+
adapter = FakeTradingAgentsAdapter()
241+
service = HotStockAIService(
242+
provider=FakeProvider(),
243+
kline_store=FakeKlineStore(),
244+
kronos_service=FakeKronos(),
245+
state_store=SQLiteStateStore(str(tmp_path / "state.db")),
246+
tradingagents_adapter=adapter,
247+
)
248+
249+
asyncio.run(service.run(trigger="manual"))
250+
snap = service.get_snapshot()
251+
252+
assert snap["meta"]["tradingagents_backend"] == "TradingAgents + DeepSeek"
253+
assert snap["meta"]["tradingagents_provider"] == "deepseek"
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from __future__ import annotations
2+
3+
from app.services.tradingagents_adapter import (
4+
DEEPSEEK_BACKEND_URL,
5+
DEEPSEEK_PROVIDER,
6+
TradingAgentsAdapter,
7+
)
8+
9+
10+
class _FakeGraph:
11+
last_kwargs = None
12+
last_propagate = None
13+
14+
def __init__(self, *, debug, config, selected_analysts):
15+
_FakeGraph.last_kwargs = {
16+
"debug": debug,
17+
"config": config,
18+
"selected_analysts": selected_analysts,
19+
}
20+
21+
def propagate(self, symbol: str, trade_date: str):
22+
_FakeGraph.last_propagate = {"symbol": symbol, "trade_date": trade_date}
23+
return (
24+
{
25+
"final_trade_decision": "BUY with strong conviction",
26+
"investment_plan": "Accumulate on strength.",
27+
"trader_investment_plan": "Enter in two tranches.",
28+
"market_report": "Market report",
29+
"news_report": "News report",
30+
"fundamentals_report": "Fundamentals report",
31+
},
32+
"BUY",
33+
)
34+
35+
36+
def test_tradingagents_adapter_forces_deepseek(monkeypatch, tmp_path):
37+
monkeypatch.setenv("DEEPSEEK_API_KEY", "test-key")
38+
39+
adapter = TradingAgentsAdapter(repo_path=tmp_path / "TradingAgents", runtime_root=tmp_path / "runtime")
40+
adapter.repo_path.mkdir(parents=True, exist_ok=True)
41+
42+
def _fake_load_classes():
43+
return _FakeGraph, {"llm_provider": "openai", "backend_url": "https://api.openai.com/v1"}
44+
45+
monkeypatch.setattr(adapter, "_load_classes", _fake_load_classes)
46+
47+
result = adapter.analyze("600001", "2026-04-22")
48+
49+
assert _FakeGraph.last_kwargs is not None
50+
assert _FakeGraph.last_kwargs["config"]["llm_provider"] == DEEPSEEK_PROVIDER
51+
assert _FakeGraph.last_kwargs["config"]["backend_url"] == DEEPSEEK_BACKEND_URL
52+
assert _FakeGraph.last_propagate == {"symbol": "600001.SS", "trade_date": "2026-04-22"}
53+
assert result["provider"] == DEEPSEEK_PROVIDER
54+
assert result["backend_url"] == DEEPSEEK_BACKEND_URL
55+
assert result["decision"] == "BUY"

0 commit comments

Comments
 (0)