Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eaa40bd26b | ||
|
|
8e2e4f0335 |
@@ -0,0 +1,342 @@
|
||||
# 📈 智能荐股系统(Stock Advisor)
|
||||
|
||||
一个专业的股票推荐与量化分析系统:**多因子荐股评分 + RAG 增强 AI 研报 + 主流量化策略回测 + 舆情驱动自动化通知**。
|
||||
|
||||
- **技术栈**:Python Flask + SQLite + Chroma 向量库 + bge-large-zh 语义检索 + DeepSeek 大模型
|
||||
- **默认端口**:16095
|
||||
- **Git**:`hz4th_coder/stock-advisor`(v1.3.0)
|
||||
- **数据模式**:当前为模拟数据(`IS_MOCK=True`),后期可无缝切换真实数据
|
||||
|
||||
---
|
||||
|
||||
## ✨ 功能总览
|
||||
|
||||
| 模块 | 页面 | 说明 |
|
||||
|---|---|---|
|
||||
| 📊 仪表盘 | `/` | 三大指数、市场情绪、今日荐股 TOP5、行业热度、最新要闻、自选股 |
|
||||
| 🏢 股票池 | `/stocks` | 68 只股票搜索/行业/板块/评级筛选,一键 ⭐ 自选 |
|
||||
| 🎯 荐股中心 | `/recommend` | 六因子评分排名 + 推荐理由 + AI 深度分析入口 |
|
||||
| 📈 量化策略 | `/strategies` | 6 主流策略全市场回测榜 + 单股净值曲线 + 交易明细 |
|
||||
| 📰 财经新闻 | `/news` | 新闻分类/搜索/情感标签/关联个股,RAG 语料库 |
|
||||
| 🏦 机构动向 | `/institutions` | 27 家机构、评级变动榜、基金增减持榜、机构详情 |
|
||||
| 📄 个股详情 | `/stock/<code>` | ECharts K线、技术指标、六因子评分、相关资讯、AI 研报、历史分析 |
|
||||
| 📑 AI 分析详情 | `/analysis/<id>` | 研报正文 + **大模型参考的数据源**(RAG新闻/概况/指标/评级/持仓/提示词) |
|
||||
| 🔧 系统设置 | `/settings` | 邮件 SMTP、大模型接口、舆情监控参数配置 |
|
||||
| ⚙️ 数据管理 | `/admin` | 数据统计、向量库状态、一键重灌、重建回测、依赖体检 |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 技术架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 前端(模板 + 原生JS + ECharts) │
|
||||
│ 仪表盘 / 股票池 / 荐股 / 策略 / 新闻 / 机构 / 设置 / 详情 │
|
||||
└──────────────────────────┬──────────────────────────────┘
|
||||
│ HTTP (Flask)
|
||||
┌──────────────────────────▼──────────────────────────────┐
|
||||
│ app.py(路由/API 层) │
|
||||
│ ┌──────────┬──────────┬──────────┬───────────────────┐ │
|
||||
│ │ 评分荐股 │ AI研报 │ 量化回测 │ 舆情监控+设置 │ │
|
||||
│ │scoring.py│analyst.py│strategies│notifier+settings │ │
|
||||
│ └──────────┴──────────┴──────────┴───────────────────┘ │
|
||||
│ indicators.py(技术指标) vector_store.py(向量REST) │
|
||||
└───────────────┬──────────────────────────┬──────────────┘
|
||||
│ │
|
||||
┌──────────▼──────────┐ ┌──────────▼───────────────┐
|
||||
│ SQLite (本地库) │ │ Chroma(16010) + bge(16011)│
|
||||
│ stock_advisor.db │ │ 新闻索引/公司概况索引 │
|
||||
└─────────────────────┘ └──────────────────────────┘
|
||||
│
|
||||
┌──────────▼──────────┐
|
||||
│ DeepSeek (RAG研报) │ SMTP (舆情邮件通知)
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
- **向量库**:纯 REST 直连(无 chromadb 客户端依赖),Embedding 用 bge-large-zh-v1.5(1024 维)
|
||||
- **大模型**:DeepSeek `deepseek-v4-flash`(推理型,研报生成约 25-40 秒,后台线程 + 前端轮询)
|
||||
- **定时任务**:舆情监控调度器(后台守护线程,默认 30 分钟一轮)
|
||||
|
||||
---
|
||||
|
||||
## 📂 目录结构
|
||||
|
||||
```
|
||||
stock-advisor/
|
||||
├── app.py # Flask 主应用(页面 + 全部 API)
|
||||
├── config.py # 全局配置(端口/LLM/向量库/邮件/监控默认值)
|
||||
├── database.py # SQLite 连接与表结构(13 张表)
|
||||
├── settings.py # 设置管理器(settings 表 + 默认值合并)
|
||||
├── seed_data.py # 模拟数据生成器(行情/新闻/机构/持仓 + 向量/回测构建)
|
||||
├── start.sh # 启动/停止/重启/状态/重灌脚本
|
||||
├── requirements.txt # 依赖:flask, requests
|
||||
├── engine/
|
||||
│ ├── indicators.py # 技术指标:MA/RSI/MACD/KDJ/量比/波动率
|
||||
│ ├── scoring.py # 六因子评分模型 + 规则化推荐理由
|
||||
│ ├── analyst.py # DeepSeek 研报 + RAG 增强 + 历史记录
|
||||
│ ├── strategies.py # 量化策略信号生成 + 回测引擎
|
||||
│ └── notifier.py # 舆情监控:重要度评分 + SMTP 邮件 + 调度器
|
||||
├── rag/
|
||||
│ └── vector_store.py # Chroma/Embedding 纯 REST 封装
|
||||
├── templates/ # 10 个页面模板
|
||||
├── static/
|
||||
│ ├── css/style.css
|
||||
│ ├── js/ # 各页面脚本
|
||||
│ └── lib/marked.min.js # Markdown 渲染(本地无 CDN)
|
||||
├── data/ # stock_advisor.db(运行时生成)
|
||||
└── logs/ # 运行日志
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
```bash
|
||||
cd works/stock-advisor
|
||||
./start.sh start # 启动(端口 16095)
|
||||
./start.sh status # 查看状态
|
||||
./start.sh stop # 停止
|
||||
./start.sh restart # 重启
|
||||
./start.sh seed # 一键重灌数据(含向量重建 + 策略回测,约 1-3 分钟)
|
||||
|
||||
# 手动执行数据生成(可选参数)
|
||||
/home/hz1/miniconda3/envs/openclaw/bin/python3 seed_data.py # 全量(DB+向量+回测)
|
||||
/home/hz1/miniconda3/envs/openclaw/bin/python3 seed_data.py --skip-vector # 跳过向量重建
|
||||
/home/hz1/miniconda3/envs/openclaw/bin/python3 seed_data.py --no-strategies # 跳过策略回测
|
||||
```
|
||||
|
||||
**访问**:`http://<服务器IP>:16095/`
|
||||
|
||||
**依赖环境**:openclaw conda 环境(`flask`、`requests`);外部服务 Chroma 16010 + Embedding 16011(本机常驻)。
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ 数据库设计(SQLite,13 张表)
|
||||
|
||||
| 表 | 说明 | 关键字段 |
|
||||
|---|---|---|
|
||||
| `stocks` | 股票基础信息(68 只) | code/name/industry/board/market_cap/pe/pb/description |
|
||||
| `stock_daily` | 日线行情(12240 条) | code/date/OHLC/volume/amount/change_pct |
|
||||
| `market_index` | 上证/深证/创业板指数 | date/sh/sz/cy |
|
||||
| `news` | 财经新闻(305 条,RAG 语料) | title/content/category/sentiment/related_stocks |
|
||||
| `institutions` | 机构实体(27 家) | name/type(公募/券商/保险/外资/私募)/description |
|
||||
| `inst_ratings` | 机构评级(165 条) | inst_id/stock_code/rating/target_price/rating_date |
|
||||
| `fund_holdings` | 基金季度持仓(363 条) | inst_id/stock_code/quarter/hold_value/change_pct |
|
||||
| `watchlist` | 用户自选股 | code |
|
||||
| `analysis_cache` | 研报最新缓存 | code/report |
|
||||
| `analysis_history` | AI 分析历史(含数据源 JSON) | code/focus/report/sources |
|
||||
| `strategy_backtests` | 策略回测结果(408 条) | strategy/code/metrics/equity/trades |
|
||||
| `settings` | 系统设置 KV | key/value |
|
||||
| `notification_log` | 舆情通知日志 | news_id/importance/status |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 核心模块详解
|
||||
|
||||
### 1. 六因子评分模型(engine/scoring.py)
|
||||
|
||||
满分 100,评级:**强烈推荐 ≥82 / 推荐 ≥68 / 关注 ≥55 / 观望 <55**
|
||||
|
||||
| 因子 | 满分 | 依据 |
|
||||
|---|---|---|
|
||||
| 趋势 | 25 | 均线多头排列 + 站上 MA20 |
|
||||
| 动量 | 20 | 5 日涨幅区间映射(过急扣分防追高) |
|
||||
| 技术 | 15 | RSI 健康区间 / 超买超卖 |
|
||||
| 量能 | 10 | 量比 |
|
||||
| 消息 | 15 | 近 7 日相关新闻情感均值 |
|
||||
| 机构 | 15 | 近 30 日正面评级数 + 基金持仓动向 |
|
||||
|
||||
同时生成**规则化推荐理由**(趋势/动量/RSI/量能/消息/机构),列表页直接展示,无需 LLM。
|
||||
|
||||
### 2. AI 深度研报(engine/analyst.py,RAG 增强)
|
||||
|
||||
```
|
||||
触发 → RAG检索(个股新闻向量命中 + 公司概况) + 技术指标 + 机构评级/持仓 + 六因子评分
|
||||
→ 组装提示词 → DeepSeek 推理生成 Markdown 研报
|
||||
→ 写入 analysis_history(含数据源快照)→ 前端轮询展示
|
||||
```
|
||||
|
||||
研报结构:公司概况 / 技术面 / 消息面 / 机构动向 / 风险提示 / 操作建议(目标区间·支撑·压力位)。
|
||||
|
||||
**数据源详情页 `/analysis/<id>`** 可追溯每次分析参考的:
|
||||
- 📰 RAG 相关资讯(含情感 + 向量相似度)
|
||||
- 🏢 公司概况 / 📈 技术指标 / 🎯 综合评分
|
||||
- 🏦 机构评级 / 💼 基金持仓 / 🧠 完整提示词
|
||||
|
||||
### 3. 量化策略回测(engine/strategies.py)
|
||||
|
||||
**6 个主流策略**(全市场 × 6 = 408 条预计算,秒级加载):
|
||||
|
||||
| 策略 | 参数 | 类型 |
|
||||
|---|---|---|
|
||||
| 双均线金叉 | MA5/MA20 | 趋势 |
|
||||
| MACD 金叉 | 12/26/9 | 趋势 |
|
||||
| RSI 超买超卖 | RSI(14) 30/70 | 反转 |
|
||||
| 布林带回归 | 20日/2σ | 均值回归 |
|
||||
| 20日动量 | 20日涨幅/MA20 | 动量 |
|
||||
| N日新高突破 | 20日高低点 | 突破 |
|
||||
|
||||
**回测规则**:收盘产生信号 → **次日开盘成交**(全仓多头,避免未来函数);指标含总收益/年化/最大回撤/夏普/胜率/盈亏比/交易次数/超额收益;并对比**买入持有**基准净值曲线。
|
||||
|
||||
> 扩展新策略:在 `STRATEGIES` 注册 name/desc + 实现 `_signals()` 即可。
|
||||
|
||||
### 4. 舆情驱动自动化(engine/notifier.py)
|
||||
|
||||
**流程**:后台调度线程(默认 30 分钟)→ 扫描新新闻(水位去重)→ **重要度评分** → 达标 → **SMTP 邮件通知**
|
||||
|
||||
**重要度评分(0-100)**:
|
||||
```
|
||||
类别基础分(公司40/业绩35/机构30/行业25/市场15)
|
||||
+ |情感| × 情感权重 × 100
|
||||
+ 关键词命中 ×12/个(回购/中标/减持/问询/超预期…)
|
||||
+ 关联个股 ×5/只
|
||||
```
|
||||
|
||||
**防打扰机制**:`monitor_last_news_id` 水位只扫新增;首次启动只建水位不通知历史;单封邮件最多 20 条。
|
||||
|
||||
### 5. 系统设置(/settings)
|
||||
|
||||
- **📧 邮件**:SMTP 服务器/端口/加密模式(plain/starttls/ssl)/账号/密码/收件人 + 测试邮件
|
||||
- **🤖 大模型**:base_url / api_key / model(**运行时生效**)+ 测试连接
|
||||
- **📰 监控**:开关/间隔/分类/阈值/关键词 + 立即扫描 + 通知日志
|
||||
|
||||
> 设置存入 `settings` 表,优先于 `config.py` 默认值,无需改代码。
|
||||
|
||||
---
|
||||
|
||||
## 🔌 API 文档
|
||||
|
||||
### 页面
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| GET | `/` `/stocks` `/recommend` `/strategies` `/news` `/institutions` `/settings` `/admin` | 各页面 |
|
||||
| GET | `/stock/<code>` | 个股详情页 |
|
||||
| GET | `/analysis/<id>` | AI 分析详情页 |
|
||||
|
||||
### 行情/股票
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| GET | `/api/health` | 健康检查 |
|
||||
| GET | `/api/overview` | 仪表盘聚合数据(指数/情绪/荐股/要闻/自选) |
|
||||
| GET | `/api/stocks?keyword=&industry=&board=&rating=&sort=&page=` | 股票列表(含评分) |
|
||||
| GET | `/api/stock/<code>` | 个股详情 + 指标 + 评分 |
|
||||
| GET | `/api/stock/<code>/kline?days=` | K线数据 + MA 序列 |
|
||||
| GET | `/api/stock/<code>/news` | 个股相关新闻 |
|
||||
| GET | `/api/stock/<code>/institutions` | 个股机构评级 + 基金持仓 |
|
||||
|
||||
### 荐股/AI
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| GET | `/api/recommend?rating=&limit=` | 评分排名 |
|
||||
| POST | `/api/stock/<code>/analyze` | 提交 AI 研报任务 `{focus}` |
|
||||
| GET | `/api/stock/<code>/analyze/status` | 轮询研报状态 |
|
||||
| GET | `/api/stock/<code>/analyses` | 历史分析列表 |
|
||||
| GET | `/api/analyses/<id>` | 分析详情(含数据源) |
|
||||
|
||||
### 自选
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| GET | `/api/watchlist` | 自选列表 |
|
||||
| POST | `/api/watchlist/<code>` | 加入自选 |
|
||||
| DELETE | `/api/watchlist/<code>` | 移出自选 |
|
||||
|
||||
### 新闻/机构
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| GET | `/api/news?keyword=&category=&page=` | 新闻列表(分页) |
|
||||
| GET | `/api/news/<id>` | 新闻详情(含关联股票) |
|
||||
| GET | `/api/institutions?type=` | 机构列表 |
|
||||
| GET | `/api/institutions/<id>` | 机构详情 |
|
||||
| GET | `/api/ratings/upgrades` | 评级变动榜 |
|
||||
| GET | `/api/holdings/moves` | 基金增减持榜 |
|
||||
|
||||
### 量化策略
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| GET | `/api/strategies` | 策略列表 + 全市场统计 |
|
||||
| GET | `/api/backtest/market?strategy=` | 某策略全市场收益榜 |
|
||||
| GET | `/api/backtest?strategy=&code=` | 单股回测详情(净值+交易) |
|
||||
| POST | `/api/backtest/rebuild` | 重建全市场回测 |
|
||||
|
||||
### 设置/监控
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| GET | `/api/settings` | 读取全部设置 |
|
||||
| POST | `/api/settings` | 保存设置 `{mail, llm, monitor}` |
|
||||
| POST | `/api/settings/test-email` | 发送测试邮件 |
|
||||
| POST | `/api/settings/test-llm` | 测试大模型连接 |
|
||||
| POST | `/api/monitor/scan` | 立即扫描新闻 |
|
||||
| GET | `/api/monitor/log` | 通知日志 |
|
||||
|
||||
### 数据管理
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| GET | `/api/admin/stats` | 表统计 + 向量库状态 |
|
||||
| POST | `/api/admin/reseed` | 一键重灌数据 |
|
||||
| GET | `/api/admin/healthcheck` | 外部依赖连通性检查 |
|
||||
|
||||
---
|
||||
|
||||
## 🔌 外部依赖
|
||||
|
||||
| 服务 | 地址 | 用途 |
|
||||
|---|---|---|
|
||||
| Chroma 向量库 | `121.40.164.32:16010` | 新闻/概况向量索引 |
|
||||
| Embedding (bge-large-zh-v1.5) | `121.40.164.32:16011` | 1024 维语义向量 |
|
||||
| DeepSeek API | `https://api.deepseek.com` | AI 研报生成 |
|
||||
| SMTP | `mail.tphai.com:587` | 舆情邮件通知 |
|
||||
|
||||
> 向量集合:`stock_news_v1`(新闻 474 条)/ `stock_profiles_v1`(公司概况 68 条)
|
||||
|
||||
---
|
||||
|
||||
## 📦 模拟数据说明
|
||||
|
||||
当前所有行情/新闻/机构数据均为**模拟数据**(页面有"模拟数据"标识),用于功能演示与系统验证:
|
||||
|
||||
- 68 只 A 股风格股票,覆盖 20+ 行业(白酒/新能源/半导体/医药/银行/券商…)
|
||||
- 180 个交易日日线(随机游走 + 趋势分化,让荐股/回测有区分度)
|
||||
- 305 条财经新闻(业绩/行业/公司/机构观点/市场 5 类,带情感标签 + 关联个股)
|
||||
- 27 家机构(公募/券商/保险资管/外资/私募)+ 165 条评级 + 363 条季度持仓
|
||||
|
||||
### 接入真实数据
|
||||
|
||||
```python
|
||||
# 1. 改 config.py
|
||||
IS_MOCK = False
|
||||
|
||||
# 2. 替换 seed_data.py 的数据源(真实行情/新闻/机构接口),保持表结构不变
|
||||
# - stocks / stock_daily / market_index → 真实行情
|
||||
# - news → 真实新闻(保留 category / sentiment / related_stocks 字段)
|
||||
# - institutions / inst_ratings / fund_holdings → 真实机构数据
|
||||
|
||||
# 3. 重灌
|
||||
./start.sh seed
|
||||
```
|
||||
|
||||
分析/检索/回测/监控逻辑**零改动**即可对接真实数据。
|
||||
|
||||
---
|
||||
|
||||
## 📝 版本历史
|
||||
|
||||
| 版本 | 内容 |
|
||||
|---|---|
|
||||
| v1.0.0 | 基础版:股票池/行情/新闻/机构/多因子评分/AI 研报(RAG) |
|
||||
| v1.0.1 | 修复:重灌后机构 ID 自增未重置、行业板块字段错位、研报评分口径 |
|
||||
| v1.1.0 | AI 分析历史记录 + 数据源详情页(/analysis/<id>,展示 RAG 新闻/提示词等) |
|
||||
| v1.2.0 | 量化策略模块:6 主流策略全市场回测 + 单股净值曲线 + 交易明细 |
|
||||
| v1.3.0 | 舆情驱动自动化(定期扫新闻→邮件通知)+ 系统设置区(邮件/大模型/监控可配) |
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ 后续规划(可叠加的高级能力)
|
||||
|
||||
- 模拟盘/实盘信号推送(每日盘前舆情日报)
|
||||
- 策略参数调优、策略组合与仓位管理
|
||||
- 自选股策略联动、财务因子库(真实财报数据)
|
||||
- 多模型对比研报、行业轮动分析
|
||||
|
||||
---
|
||||
|
||||
> ⚠️ 本系统所有内容基于模拟数据生成,仅供系统演示与量化研究,**不构成任何投资建议**。
|
||||
@@ -55,6 +55,11 @@ def page_strategies():
|
||||
return render_template("strategies.html", service=SERVICE_NAME, is_mock=IS_MOCK)
|
||||
|
||||
|
||||
@app.route("/tracking")
|
||||
def page_tracking():
|
||||
return render_template("tracking.html", service=SERVICE_NAME, is_mock=IS_MOCK)
|
||||
|
||||
|
||||
@app.route("/admin")
|
||||
def page_admin():
|
||||
return render_template("admin.html", service=SERVICE_NAME, is_mock=IS_MOCK)
|
||||
@@ -550,15 +555,17 @@ def api_backtest_rebuild():
|
||||
# ------------------------------------------------------------------ 设置与舆情监控
|
||||
@app.route("/api/settings")
|
||||
def api_settings():
|
||||
from settings import all_settings, mail_config, monitor_config, monitor_state
|
||||
from settings import all_settings, mail_config, monitor_config, monitor_state, \
|
||||
tracking_config, tracking_state
|
||||
from config import LLM_BASE_URL, LLM_API_KEY, LLM_MODEL
|
||||
s = all_settings()
|
||||
# 返回带默认值的完整配置
|
||||
mail = mail_config()
|
||||
mono = monitor_config()
|
||||
track = tracking_config()
|
||||
return jsonify({
|
||||
"mail": mail,
|
||||
"monitor": mono,
|
||||
"tracking": {**track, **tracking_state()},
|
||||
"llm": {
|
||||
"base_url": s.get("llm_base_url", LLM_BASE_URL),
|
||||
"api_key": s.get("llm_api_key", LLM_API_KEY),
|
||||
@@ -589,6 +596,11 @@ def api_settings_save():
|
||||
"monitor_sentiment", "monitor_importance", "monitor_keywords"):
|
||||
if k in mono:
|
||||
set_setting(k, mono[k])
|
||||
# 持仓跟踪
|
||||
track = body.get("tracking") or {}
|
||||
for k in ("tracking_enabled", "tracking_interval", "tracking_notify", "tracking_impact_threshold"):
|
||||
if k in track:
|
||||
set_setting(k, track[k])
|
||||
return jsonify({"ok": True, "msg": "设置已保存"})
|
||||
|
||||
|
||||
@@ -637,6 +649,62 @@ def api_monitor_log():
|
||||
return jsonify({"items": notification_log()})
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 持仓跟踪
|
||||
@app.route("/api/tracking")
|
||||
def api_tracking_list():
|
||||
from engine.agent import list_reports
|
||||
return jsonify({"items": list_reports()})
|
||||
|
||||
|
||||
@app.route("/api/tracking/stock/<code>")
|
||||
def api_tracking_stock(code):
|
||||
from engine.agent import latest_reports
|
||||
return jsonify({"items": latest_reports(code)})
|
||||
|
||||
|
||||
@app.route("/api/tracking/<int:rid>")
|
||||
def api_tracking_detail(rid):
|
||||
from engine.agent import get_report
|
||||
import json as _json
|
||||
r = get_report(rid)
|
||||
if not r:
|
||||
return jsonify({"error": "记录不存在"}), 404
|
||||
r["meta"] = _json.loads(r["meta"] or "{}")
|
||||
r["sources"] = _json.loads(r["sources"] or "{}")
|
||||
return jsonify(r)
|
||||
|
||||
|
||||
@app.route("/api/tracking/run", methods=["POST"])
|
||||
def api_tracking_run():
|
||||
"""立即跟踪全部自选股(后台)"""
|
||||
import threading
|
||||
|
||||
def run():
|
||||
from engine.agent import track_watchlist
|
||||
try:
|
||||
track_watchlist()
|
||||
except Exception as e:
|
||||
log.error("track run fail: %s", e)
|
||||
|
||||
threading.Thread(target=run, daemon=True).start()
|
||||
return jsonify({"ok": True, "msg": "跟踪任务已启动(逐只分析,约每只30-60秒)"})
|
||||
|
||||
|
||||
@app.route("/api/tracking/run/<code>", methods=["POST"])
|
||||
def api_tracking_run_one(code):
|
||||
from engine.agent import track_stock
|
||||
import threading
|
||||
|
||||
def run():
|
||||
try:
|
||||
track_stock(code)
|
||||
except Exception as e:
|
||||
log.error("track %s fail: %s", code, e)
|
||||
|
||||
threading.Thread(target=run, daemon=True).start()
|
||||
return jsonify({"ok": True, "msg": "跟踪已启动"})
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 数据管理
|
||||
@app.route("/api/admin/stats")
|
||||
def api_admin_stats():
|
||||
@@ -694,6 +762,8 @@ def api_admin_healthcheck():
|
||||
if __name__ == "__main__":
|
||||
init_db()
|
||||
from engine.notifier import start_monitor
|
||||
from engine.agent import start_tracking
|
||||
start_monitor()
|
||||
start_tracking()
|
||||
print(f"✅ {SERVICE_NAME} 启动: http://0.0.0.0:{SERVICE_PORT}")
|
||||
app.run(host=SERVICE_HOST, port=SERVICE_PORT, threaded=True)
|
||||
@@ -65,3 +65,11 @@ MONITOR_DEFAULTS = {
|
||||
"monitor_importance": "45", # 重要度阈值(0-100),>= 则通知
|
||||
"monitor_keywords": "回购,中标,减持,问询,停牌,上调,下调,超预期,不及预期,预警,重组,增持,定增,业绩,退市",
|
||||
}
|
||||
|
||||
# ---------------- 持仓跟踪智能体(默认值,可在设置区修改) ----------------
|
||||
TRACKING_DEFAULTS = {
|
||||
"tracking_enabled": "1",
|
||||
"tracking_interval": "60", # 分钟
|
||||
"tracking_notify": "1", # 重大变化时邮件通知
|
||||
"tracking_impact_threshold": "65", # 影响度评分 >= 此值判定为重大变化
|
||||
}
|
||||
+14
-1
@@ -140,6 +140,19 @@ CREATE TABLE IF NOT EXISTS notification_log (
|
||||
sent_at TEXT DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tracking_reports (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code TEXT NOT NULL,
|
||||
stock_name TEXT DEFAULT '',
|
||||
industry TEXT DEFAULT '',
|
||||
report TEXT DEFAULT '', -- 产业链深度分析(markdown)
|
||||
meta TEXT DEFAULT '{}', -- JSON:significance/impact_score/summary/新闻统计
|
||||
sources TEXT DEFAULT '{}', -- JSON:个股/上游/下游/同业 采集的资讯 + 指标 + 评级
|
||||
status TEXT DEFAULT 'done',
|
||||
created_at TEXT DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_tracking_code ON tracking_reports(code);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS market_index (
|
||||
date TEXT PRIMARY KEY,
|
||||
sh REAL DEFAULT 0, -- 上证指数(点)
|
||||
@@ -206,7 +219,7 @@ def wipe_all():
|
||||
"""清空业务表(保留结构)+ 重置自增序列,用于重灌数据"""
|
||||
for t in ("stock_daily", "inst_ratings", "fund_holdings", "news",
|
||||
"institutions", "stocks", "watchlist", "analysis_cache", "analysis_history",
|
||||
"market_index", "strategy_backtests", "notification_log"):
|
||||
"market_index", "strategy_backtests", "notification_log", "tracking_reports"):
|
||||
with db() as conn:
|
||||
conn.execute(f'DELETE FROM "{t}"')
|
||||
with db() as conn:
|
||||
|
||||
+393
@@ -0,0 +1,393 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
持仓跟踪智能体(AI Agent + 工作流)
|
||||
|
||||
对持仓/自选股票进行定期深度跟踪,工作流:
|
||||
1. 上下文构建 读取产业链知识库(上游/下游/同业 + 关键词)
|
||||
2. 数据采集 个股动态 + 上游产业链 + 下游产业链 + 同业动态(DB + RAG 向量检索)
|
||||
3. 智能体分析 大模型扮演产业链跟踪分析师,输出深度专业分析 + 影响度判定
|
||||
4. 沉淀与通知 结果入库 tracking_reports;重大变化(impact_score 达阈值)邮件通知
|
||||
|
||||
「智能体」体现在:大模型自主综合个股与产业链多环节信息,输出结构化的
|
||||
个股动态 / 上游供给成本 / 下游需求景气 / 同业竞争 / 传导影响 / 风险与关注要点,
|
||||
并给出 0-100 影响度评分与变化性质判定。
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
|
||||
from database import query, query_one, execute
|
||||
from settings import tracking_config, tracking_state, set_tracking_state, mail_config
|
||||
from engine.chain_data import get_chain
|
||||
from engine.indicators import compute_indicators
|
||||
from rag.vector_store import query_vectors
|
||||
from config import CHROMA_NEWS_COLLECTION
|
||||
from engine.analyst import llm_chat
|
||||
|
||||
log = logging.getLogger("agent")
|
||||
|
||||
_track_lock = threading.Lock()
|
||||
_cycle_running = False
|
||||
_jobs = {} # code -> {status, error, ts}
|
||||
|
||||
|
||||
# ===================================================================== 数据采集
|
||||
def _db_news(codes, days=40, limit=8):
|
||||
"""按关联股票代码取新闻(近 days 天)"""
|
||||
if not codes:
|
||||
return []
|
||||
conds, args = [], []
|
||||
for c in codes:
|
||||
conds.append("(related_stocks=? OR related_stocks LIKE ? OR related_stocks LIKE ?)")
|
||||
args += [c, f"%,{c}", f"{c},%"]
|
||||
args.append(limit)
|
||||
return query(
|
||||
f"SELECT id,title,content,source,category,publish_date,sentiment FROM news "
|
||||
f"WHERE ({' OR '.join(conds)}) AND publish_date >= date('now','-{days} day') "
|
||||
f"ORDER BY publish_date DESC LIMIT ?", args)
|
||||
|
||||
|
||||
def _rag_news_text(question, where=None, top_k=4):
|
||||
"""向量语义检索,返回紧凑文本"""
|
||||
try:
|
||||
hits = query_vectors(question, n_results=top_k, where=where, name=CHROMA_NEWS_COLLECTION)
|
||||
out = []
|
||||
for h in hits:
|
||||
m = h.get("metadata", {})
|
||||
out.append(f" [{m.get('date','')}] {m.get('title','')} (情感{m.get('sentiment',0):+.2f}) "
|
||||
f"{h.get('document','')[:90]}")
|
||||
return out
|
||||
except Exception as e:
|
||||
log.warning("rag fail: %s", e)
|
||||
return []
|
||||
|
||||
|
||||
def _fmt_news(items):
|
||||
return "\n".join(
|
||||
f" [{n['publish_date']}] {n['title']} (情感{n['sentiment']:+.2f}) {n['content'][:90]}"
|
||||
for n in items) or " (暂无)"
|
||||
|
||||
|
||||
def collect_chain(code):
|
||||
"""采集个股 + 产业链各环节资讯,返回结构化 dict"""
|
||||
stock = query_one("SELECT * FROM stocks WHERE code=?", (code,))
|
||||
if not stock:
|
||||
return None
|
||||
chain = get_chain(stock["industry"])
|
||||
seg = {}
|
||||
|
||||
# 1. 个股直接动态
|
||||
direct = _db_news([code], days=40, limit=8)
|
||||
direct_rag = _rag_news_text(f"{stock['name']} 最新动态 业绩 公告 重大事项", where={"code": code}, top_k=4)
|
||||
seg["direct"] = {"db": _fmt_news(direct), "rag": "\n".join(direct_rag) or " (暂无)"}
|
||||
|
||||
# 2. 上游
|
||||
up_db = _db_news(chain["upstream"]["codes"], days=40, limit=6)
|
||||
up_rag = _rag_news_text(f"{stock['industry']} 上游 {chain['upstream']['keywords']}", top_k=4)
|
||||
seg["upstream"] = {"codes": chain["upstream"]["codes"],
|
||||
"keywords": chain["upstream"]["keywords"],
|
||||
"db": _fmt_news(up_db), "rag": "\n".join(up_rag) or " (暂无)"}
|
||||
|
||||
# 3. 下游
|
||||
dn_db = _db_news(chain["downstream"]["codes"], days=40, limit=6)
|
||||
dn_rag = _rag_news_text(f"{stock['industry']} 下游需求 景气 {chain['downstream']['keywords']}", top_k=4)
|
||||
seg["downstream"] = {"codes": chain["downstream"]["codes"],
|
||||
"keywords": chain["downstream"]["keywords"],
|
||||
"db": _fmt_news(dn_db), "rag": "\n".join(dn_rag) or " (暂无)"}
|
||||
|
||||
# 4. 同业
|
||||
peer_db = _db_news(chain["peers"], days=40, limit=5)
|
||||
peer_rag = _rag_news_text(f"{stock['industry']} 竞争格局 同业 {chain['peers']}", top_k=3)
|
||||
seg["peers"] = {"codes": chain["peers"],
|
||||
"db": _fmt_news(peer_db), "rag": "\n".join(peer_rag) or " (暂无)"}
|
||||
|
||||
# 5. 技术面 + 机构
|
||||
ind = compute_indicators(query("SELECT date,open,high,low,close,volume FROM stock_daily "
|
||||
"WHERE code=? ORDER BY date ASC", (code,)))
|
||||
ratings = query("SELECT inst_name, rating, target_price, rating_date FROM inst_ratings "
|
||||
"WHERE stock_code=? ORDER BY rating_date DESC LIMIT 5", (code,))
|
||||
holdings = query("SELECT inst_name, quarter, hold_value, change_pct FROM fund_holdings "
|
||||
"WHERE stock_code=? ORDER BY quarter DESC LIMIT 5", (code,))
|
||||
seg["stock"] = stock
|
||||
seg["chain"] = chain
|
||||
seg["indicators"] = ind
|
||||
seg["ratings"] = ratings
|
||||
seg["holdings"] = holdings
|
||||
return seg
|
||||
|
||||
|
||||
# ===================================================================== 分析提示词
|
||||
def _fmt_ind(ind):
|
||||
return (f"最新价 {ind.get('close')}({ind.get('change_pct',0):+.2f}%),5日{ind.get('chg_5d',0):+.2f}% / "
|
||||
f"20日{ind.get('chg_20d',0):+.2f}%,RSI={ind.get('rsi')},量比{ind.get('vol_ratio')},"
|
||||
f"MA20={ind.get('ma20')}")
|
||||
|
||||
|
||||
def build_prompt(seg):
|
||||
s = seg["stock"]
|
||||
chain = seg["chain"]
|
||||
rated = "、".join(f"{r['inst_name']}({r['rating']},目标{r['target_price']})" for r in seg["ratings"]) or "暂无"
|
||||
held = ";".join(f"{h['inst_name']} {h['quarter']}持仓{h['hold_value']:.0f}万 环比{h['change_pct']:+.1f}%"
|
||||
for h in seg["holdings"]) or "暂无"
|
||||
return f"""你是资深产业链跟踪分析师,正在对【持仓标的】{s['name']}({s['code']}) 进行深度跟踪。请综合【个股】与【产业链上下游/同业】的全部动态,输出一份专业、有洞察的产业链跟踪分析。
|
||||
|
||||
【个股基本面】
|
||||
{s.get('description','')}
|
||||
|
||||
【技术面】{_fmt_ind(seg['indicators'])}
|
||||
【机构动向】评级:{rated} | 持仓:{held}
|
||||
|
||||
【一、个股直接动态】(公告/新闻/机构观点)
|
||||
{seg['direct']['db']}
|
||||
{seg['direct']['rag']}
|
||||
|
||||
【二、上游产业链】(供给/原材料/成本端;关联股票 {seg['upstream']['codes'] or '无'},关键词:{seg['upstream']['keywords']})
|
||||
{seg['upstream']['db']}
|
||||
{seg['upstream']['rag']}
|
||||
|
||||
【三、下游产业链】(需求/客户/景气端;关联股票 {seg['downstream']['codes'] or '无'},关键词:{seg['downstream']['keywords']})
|
||||
{seg['downstream']['db']}
|
||||
{seg['downstream']['rag']}
|
||||
|
||||
【四、同业动态】(竞争格局;{seg['peers']['codes'] or '无'})
|
||||
{seg['peers']['db']}
|
||||
{seg['peers']['rag']}
|
||||
|
||||
【输出要求】
|
||||
第一步,先输出一个 json 代码块(必须最先输出,内容为本次判定,不要包含其他内容):
|
||||
```json
|
||||
{{"significance":"high|medium|low","impact_score":0到100的整数,"change_kind":"利好/利空/中性/震荡","summary":"一句话总结","chain_trend":"产业链趋势判断"}}
|
||||
```
|
||||
第二步,再输出 Markdown 分析报告,结构如下:
|
||||
## 一、个股最新动态
|
||||
## 二、上游产业链分析(供给、原材料、成本端变化及其传导)
|
||||
## 三、下游产业链分析(需求、客户、景气度变化及其传导)
|
||||
## 四、同行业竞争格局
|
||||
## 五、产业链传导与投资启示(上游→中游→下游,对{ s['name']}的影响路径)
|
||||
## 六、风险提示
|
||||
## 关注要点(3-5条)
|
||||
|
||||
分析须严格基于提供的资讯,避免编造。impact_score 反映本次跟踪发现的动态对股价的潜在影响程度:>=65 视为重大变化。"""
|
||||
|
||||
|
||||
def parse_judge(text):
|
||||
"""从容错地从 LLM 输出中提取 JSON 判定(支持 json 代码块/截断/缺失字段)"""
|
||||
t = text.strip()
|
||||
# 0) 优先取 json 代码块
|
||||
m = re.search(r"```json\s*(.*?)\s*```", t, re.S)
|
||||
if m:
|
||||
try:
|
||||
j = json.loads(m.group(1))
|
||||
if "significance" in j or "impact_score" in j:
|
||||
return j
|
||||
except Exception:
|
||||
pass
|
||||
# 1) 整体 JSON 对象解析
|
||||
for m in re.finditer(r"\{[^{}]*\}", t, re.S):
|
||||
try:
|
||||
j = json.loads(m.group(0))
|
||||
if "significance" in j or "impact_score" in j:
|
||||
return j
|
||||
except Exception:
|
||||
continue
|
||||
# 2) 逐字段容错提取(末尾被截断时也能拿到已输出字段)
|
||||
out = {}
|
||||
m = re.search(r'"significance"\s*:\s*"(high|medium|low)"', t)
|
||||
if m:
|
||||
out["significance"] = m.group(1)
|
||||
m = re.search(r'"impact_score"\s*:\s*(\d+)', t)
|
||||
if m:
|
||||
out["impact_score"] = int(m.group(1))
|
||||
m = re.search(r'"change_kind"\s*:\s*"([^"]{1,20})"', t)
|
||||
if m:
|
||||
out["change_kind"] = m.group(1)
|
||||
m = re.search(r'"summary"\s*:\s*"((?:[^"\\]|\\.){1,200})"', t)
|
||||
if m:
|
||||
out["summary"] = m.group(1)
|
||||
m = re.search(r'"chain_trend"\s*:\s*"((?:[^"\\]|\\.){1,120})"', t)
|
||||
if m:
|
||||
out["chain_trend"] = m.group(1)
|
||||
return out or None
|
||||
|
||||
|
||||
def strip_json_block(text):
|
||||
"""从报告文本中剥离最前面的 json 代码块,保留纯 Markdown"""
|
||||
m = re.search(r"```json\s*.*?```\s*", text, re.S)
|
||||
if m:
|
||||
return text[m.end():].strip()
|
||||
return text
|
||||
|
||||
|
||||
# ===================================================================== 执行
|
||||
def track_stock(code, focus=""):
|
||||
"""执行一次跟踪,返回 {ok, report_id, meta, ...}"""
|
||||
with _track_lock:
|
||||
seg = collect_chain(code)
|
||||
if not seg:
|
||||
return {"error": "股票不存在"}
|
||||
s = seg["stock"]
|
||||
prompt = build_prompt(seg)
|
||||
try:
|
||||
reply = llm_chat([
|
||||
{"role": "system", "content": "你是一名严谨专业的产业链跟踪分析师,输出结构化、有数据支撑的分析。"},
|
||||
{"role": "user", "content": prompt},
|
||||
]).strip()
|
||||
if not reply:
|
||||
raise RuntimeError("LLM 返回为空")
|
||||
judge = parse_judge(reply)
|
||||
# 摘要兜底:解析不到则取报告首个标题;并剥离 json 块保留纯净 Markdown
|
||||
clean_report = strip_json_block(reply)
|
||||
summary = (judge or {}).get("summary") or _first_heading(clean_report)
|
||||
meta = {
|
||||
"significance": (judge or {}).get("significance", "medium"),
|
||||
"impact_score": int((judge or {}).get("impact_score", 50)),
|
||||
"change_kind": (judge or {}).get("change_kind", "中性"),
|
||||
"summary": summary,
|
||||
"chain_trend": (judge or {}).get("chain_trend", ""),
|
||||
"news_counts": {
|
||||
"direct": _count(seg["direct"]),
|
||||
"upstream": _count(seg["upstream"]),
|
||||
"downstream": _count(seg["downstream"]),
|
||||
"peers": _count(seg["peers"]),
|
||||
},
|
||||
"focus": focus,
|
||||
}
|
||||
sources = {
|
||||
"direct": seg["direct"], "upstream": seg["upstream"],
|
||||
"downstream": seg["downstream"], "peers": seg["peers"],
|
||||
"indicators": _fmt_ind(seg["indicators"]),
|
||||
"ratings": seg["ratings"], "holdings": seg["holdings"],
|
||||
}
|
||||
execute(
|
||||
"INSERT INTO tracking_reports(code, stock_name, industry, report, meta, sources, status, created_at) "
|
||||
"VALUES(?,?,?,?,?,?,'done',datetime('now','localtime'))",
|
||||
(code, s["name"], s["industry"], clean_report, json.dumps(meta, ensure_ascii=False),
|
||||
json.dumps(sources, ensure_ascii=False)))
|
||||
rid = query_one("SELECT MAX(id) id FROM tracking_reports")["id"]
|
||||
_notify_if_significant(rid, s, meta)
|
||||
return {"ok": True, "report_id": rid, "meta": meta}
|
||||
except Exception as e:
|
||||
log.exception("track %s fail", code)
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def _count(seg):
|
||||
return (seg["db"].count("[") + seg["rag"].count("[")) // 1
|
||||
|
||||
|
||||
def _first_heading(text):
|
||||
"""取报告第一行非空文本作为摘要兜底"""
|
||||
for line in (text or "").splitlines():
|
||||
line = line.strip().lstrip("#* ").strip()
|
||||
if line:
|
||||
return line[:60]
|
||||
return ""
|
||||
|
||||
|
||||
def _notify_if_significant(rid, stock, meta):
|
||||
"""影响度达阈值且开启通知 → 邮件"""
|
||||
cfg = tracking_config()
|
||||
try:
|
||||
if cfg["notify"] and int(meta["impact_score"]) >= int(cfg["impact_threshold"]):
|
||||
from engine.notifier import send_email
|
||||
mc = mail_config()
|
||||
send_email(
|
||||
f"[持仓跟踪] {stock['name']} 出现{meta.get('change_kind','')}动态(影响度{meta['impact_score']})",
|
||||
f"""<html><body style="font-family:Microsoft YaHei;padding:20px;background:#f5f6f8;">
|
||||
<div style="max-width:640px;margin:auto;background:#fff;border-radius:8px;border:1px solid #e5e7eb;overflow:hidden;">
|
||||
<div style="background:#1e293b;color:#fff;padding:14px 20px;font-size:17px;font-weight:bold;">🧭 持仓跟踪 · {stock['name']}({stock['code']})</div>
|
||||
<div style="padding:16px 20px;">
|
||||
<p><b>影响度:</b>{meta['impact_score']}/100({'🔴 重大' if meta['impact_score']>=65 else '🟡 关注'})<br>
|
||||
<b>性质:</b>{meta.get('change_kind','')} | <b>显著性:</b>{meta.get('significance','')}</p>
|
||||
<p style="font-size:15px;"><b>摘要:</b>{meta.get('summary','')}</p>
|
||||
<p style="color:#555;"><b>产业链趋势:</b>{meta.get('chain_trend','')}</p>
|
||||
<p style="color:#888;font-size:12px;">个股资讯 {meta['news_counts']['direct']} 条 / 上游 {meta['news_counts']['upstream']} 条 / 下游 {meta['news_counts']['downstream']} 条 / 同业 {meta['news_counts']['peers']} 条</p>
|
||||
</div></div></body></html>""",
|
||||
cfg=mc)
|
||||
set_tracking_state(last_alert=int(meta["impact_score"]))
|
||||
except Exception as e:
|
||||
log.warning("track notify fail: %s", e)
|
||||
|
||||
|
||||
# ===================================================================== 批量与调度
|
||||
def track_watchlist(progress=None):
|
||||
"""串行跟踪自选股(持仓)。同一时刻只允许一个跟踪任务(防重复)"""
|
||||
global _cycle_running
|
||||
if _cycle_running:
|
||||
return {"tracked": 0, "msg": "已有跟踪任务进行中,请稍后再试"}
|
||||
_cycle_running = True
|
||||
try:
|
||||
stocks = query("SELECT w.code, s.name FROM watchlist w JOIN stocks s ON s.code=w.code ORDER BY w.added_at")
|
||||
if not stocks:
|
||||
return {"tracked": 0, "msg": "自选股为空,请先在股票池添加"}
|
||||
results = []
|
||||
for i, st in enumerate(stocks):
|
||||
r = track_stock(st["code"])
|
||||
results.append({"code": st["code"], "name": st["name"], **r})
|
||||
set_tracking_state(last_run=time.strftime("%Y-%m-%d %H:%M:%S"), last_stock=st["name"])
|
||||
if progress:
|
||||
progress(i + 1, len(stocks))
|
||||
return {"tracked": len(results), "results": results}
|
||||
finally:
|
||||
_cycle_running = False
|
||||
|
||||
|
||||
def latest_reports(code, limit=5):
|
||||
return query("SELECT id, code, stock_name, industry, meta, status, created_at "
|
||||
"FROM tracking_reports WHERE code=? ORDER BY id DESC LIMIT ?", (code, limit))
|
||||
|
||||
|
||||
def list_reports(limit=30):
|
||||
return query("SELECT id, code, stock_name, industry, meta, status, created_at "
|
||||
"FROM tracking_reports ORDER BY id DESC LIMIT ?", (limit,))
|
||||
|
||||
|
||||
def get_report(rid):
|
||||
return query_one("SELECT * FROM tracking_reports WHERE id=?", (rid,))
|
||||
|
||||
|
||||
class TrackingThread(threading.Thread):
|
||||
"""后台调度:定期跟踪全部持仓股票"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(daemon=True, name="tracking")
|
||||
self._stop = threading.Event()
|
||||
|
||||
def stop(self):
|
||||
self._stop.set()
|
||||
|
||||
def run(self):
|
||||
log.info("持仓跟踪调度器启动")
|
||||
first = True
|
||||
while not self._stop.is_set():
|
||||
try:
|
||||
cfg = tracking_config()
|
||||
if first:
|
||||
# 启动后等待一个完整间隔再首跑,避免重启即烧一轮 LLM、与手动操作冲突
|
||||
self._stop.wait(cfg.get("interval_min", 60) * 60)
|
||||
first = False
|
||||
continue
|
||||
if cfg["enabled"]:
|
||||
try:
|
||||
r = track_watchlist()
|
||||
log.info("tracking cycle: %s", r)
|
||||
except Exception as e:
|
||||
log.warning("tracking cycle error: %s", e)
|
||||
except Exception as e:
|
||||
log.warning("tracking loop error: %s", e)
|
||||
self._stop.wait(cfg.get("interval_min", 60) * 60)
|
||||
log.info("持仓跟踪调度器停止")
|
||||
|
||||
|
||||
_tracking = None
|
||||
|
||||
|
||||
def start_tracking():
|
||||
global _tracking
|
||||
if _tracking and _tracking.is_alive():
|
||||
return _tracking
|
||||
_tracking = TrackingThread()
|
||||
_tracking.start()
|
||||
return _tracking
|
||||
@@ -0,0 +1,219 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
产业链知识库:行业 → 上游/下游/同业 + 检索关键词
|
||||
用于「持仓跟踪智能体」:跟踪某股票时,除个股动态外,还深度覆盖其
|
||||
上游(供给/成本)、下游(需求/客户)、同业竞争链的动态。
|
||||
|
||||
- codes:引用本系统股票池内的代码(便于 RAG 按 code 过滤 + 关联查询)
|
||||
- keywords:行业/环节检索词(用于向量语义检索)
|
||||
后期接入真实数据时,可在此补充真实产业链公司代码与关键词。
|
||||
"""
|
||||
INDUSTRY_CHAIN = {
|
||||
"白酒": {
|
||||
"upstream": {"codes": [], "keywords": "高粱,小麦,粮食,基酒,包装,玻璃瓶,酿酒"},
|
||||
"downstream": {"codes": ["603288"], "keywords": "白酒消费,宴席,商务宴请,经销商,渠道,动销,开瓶"},
|
||||
"peers": ["000858", "600809", "000568"],
|
||||
"keywords": "白酒,茅台,五粮液,提价,批价,库存,动销,高端白酒"},
|
||||
"动力电池": {
|
||||
"upstream": {"codes": ["002460", "002648", "601899"], "keywords": "锂盐,碳酸锂,电解液,正极,负极,隔膜,铜箔,锂矿,六氟磷酸锂"},
|
||||
"downstream": {"codes": ["002594", "601633", "000625", "601238", "600104"], "keywords": "新能源车销量,装车量,动力电池装机,储能需求,电池排产"},
|
||||
"peers": [],
|
||||
"keywords": "动力电池,宁德时代,电池装机,储能,电池价格,产能利用率"},
|
||||
"新能源汽车": {
|
||||
"upstream": {"codes": ["300750", "002460", "601899"], "keywords": "动力电池,锂,芯片,钢材,铝,智能驾驶硬件"},
|
||||
"downstream": {"codes": [], "keywords": "新能源车销量,渗透率,出口,充电桩,以旧换新,价格战"},
|
||||
"peers": ["601633", "000625", "601238", "600104"],
|
||||
"keywords": "新能源汽车,比亚迪,整车,销量,智能驾驶,渗透率"},
|
||||
"光伏": {
|
||||
"upstream": {"codes": ["601600", "600309"], "keywords": "硅料,硅片,银浆,玻璃,铝框,石英砂,多晶硅"},
|
||||
"downstream": {"codes": ["300274", "600900"], "keywords": "装机量,组件招标,分布式,集中式,电网,海外订单"},
|
||||
"peers": ["600438", "601012"],
|
||||
"keywords": "光伏,组件,硅料价格,逆变器,装机,产能出清"},
|
||||
"光伏储能": {
|
||||
"upstream": {"codes": ["601012", "600438", "300750"], "keywords": "组件,硅料,储能电芯,逆变器材料"},
|
||||
"downstream": {"codes": ["600900"], "keywords": "储能装机,大储,海外储能,电网侧,工商业储能"},
|
||||
"peers": [],
|
||||
"keywords": "光伏储能,阳光电源,储能系统,逆变器,出货量"},
|
||||
"锂电材料": {
|
||||
"upstream": {"codes": ["601899"], "keywords": "锂矿,锂精矿,盐湖提锂,资源"},
|
||||
"downstream": {"codes": ["300750"], "keywords": "电池排产,锂盐需求,三元,磷酸铁锂,正极材料"},
|
||||
"peers": [],
|
||||
"keywords": "锂电材料,赣锋锂业,碳酸锂价格,锂盐,氢氧化锂"},
|
||||
"半导体": {
|
||||
"upstream": {"codes": ["688012", "002371"], "keywords": "晶圆,光刻,刻蚀,硅片,封装,EDA,靶材"},
|
||||
"downstream": {"codes": ["688041", "002475", "002241", "000725", "000063"], "keywords": "代工订单,产能利用率,下游需求,消费电子复苏,先进制程"},
|
||||
"peers": ["603986", "603501"],
|
||||
"keywords": "半导体,中芯国际,晶圆代工,产能,国产替代,芯片"},
|
||||
"半导体设备": {
|
||||
"upstream": {"codes": [], "keywords": "零部件,气体,靶材,精密机械,射频电源"},
|
||||
"downstream": {"codes": ["688981", "688041"], "keywords": "晶圆厂扩产,资本开支,设备招标,先进制程"},
|
||||
"peers": ["688012", "002371"],
|
||||
"keywords": "半导体设备,北方华创,中微公司,刻蚀,薄膜沉积,国产化率"},
|
||||
"芯片设计": {
|
||||
"upstream": {"codes": ["688981"], "keywords": "流片,先进制程,IP授权,代工"},
|
||||
"downstream": {"codes": ["002230"], "keywords": "服务器,信创,国产替代,算力,数据中心"},
|
||||
"peers": ["603986", "603501"],
|
||||
"keywords": "芯片设计,海光信息,CPU,DCU,算力,国产CPU"},
|
||||
"创新药": {
|
||||
"upstream": {"codes": ["603259"], "keywords": "临床,原料药,API,专利,生物药"},
|
||||
"downstream": {"codes": [], "keywords": "医保谈判,集采,销售放量,适应症,出海授权"},
|
||||
"peers": [],
|
||||
"keywords": "创新药,恒瑞医药,临床,获批,医保,license-out"},
|
||||
"CXO": {
|
||||
"upstream": {"codes": [], "keywords": "原料药,试剂,实验设备,动物实验"},
|
||||
"downstream": {"codes": ["600276"], "keywords": "订单,新签,产能,海外需求,生物医药融资"},
|
||||
"peers": [],
|
||||
"keywords": "CXO,药明康德,新签订单,产能利用率,生物医药,研发外包"},
|
||||
"医疗器械": {
|
||||
"upstream": {"codes": [], "keywords": "电子元器件,材料,芯片,传感器"},
|
||||
"downstream": {"codes": ["300015"], "keywords": "集采,医疗设备更新,政府采购,出海,装机"},
|
||||
"peers": [],
|
||||
"keywords": "医疗器械,迈瑞医疗,集采,设备更新,海外,装机量"},
|
||||
"医疗服务": {
|
||||
"upstream": {"codes": ["300760"], "keywords": "设备采购,耗材"},
|
||||
"downstream": {"codes": [], "keywords": "就诊量,消费医疗,医保,屈光,白内障"},
|
||||
"peers": [],
|
||||
"keywords": "医疗服务,爱尔眼科,就诊量,消费医疗,连锁"},
|
||||
"中药": {
|
||||
"upstream": {"codes": [], "keywords": "中药材,种植,提价,药材价格"},
|
||||
"downstream": {"codes": [], "keywords": "医保,OTC,养生需求,品牌,渠道"},
|
||||
"peers": ["000538", "600085"],
|
||||
"keywords": "中药,同仁堂,云南白药,药材价格,品牌,医保"},
|
||||
"银行": {
|
||||
"upstream": {"codes": [], "keywords": "存款,资金成本,同业,负债端"},
|
||||
"downstream": {"codes": ["000002", "600048"], "keywords": "信贷需求,净息差,不良率,零售,对公"},
|
||||
"peers": ["601398", "601166", "600000"],
|
||||
"keywords": "银行,净息差,信贷,存款,不良率,息差收窄"},
|
||||
"券商": {
|
||||
"upstream": {"codes": [], "keywords": "市场成交,两融,自营,利率"},
|
||||
"downstream": {"codes": ["300059", "300033"], "keywords": "市场成交额,IPO,经纪,投行,并购重组"},
|
||||
"peers": ["601688", "600999"],
|
||||
"keywords": "券商,中信证券,市场成交,投行,经纪,两融"},
|
||||
"互联网金融": {
|
||||
"upstream": {"codes": [], "keywords": "市场成交,基金,代销牌照"},
|
||||
"downstream": {"codes": ["300033"], "keywords": "市场成交额,基金销售,代销,用户数,财富管理"},
|
||||
"peers": ["300033"],
|
||||
"keywords": "互联网金融,东方财富,基金销售,代销,市场成交,财富管理"},
|
||||
"金融科技": {
|
||||
"upstream": {"codes": [], "keywords": "行情数据,云计算,AI大模型"},
|
||||
"downstream": {"codes": ["600036"], "keywords": "金融机构IT采购,量化交易,财富管理数字化"},
|
||||
"peers": ["300059"],
|
||||
"keywords": "金融科技,同花顺,行情,量化,金融机构IT,AI"},
|
||||
"保险": {
|
||||
"upstream": {"codes": [], "keywords": "利率,债券,投资收益,准备金"},
|
||||
"downstream": {"codes": [], "keywords": "保费,新业务价值,代理人,银保渠道,理赔,寿险"},
|
||||
"peers": ["601628", "601318"],
|
||||
"keywords": "保险,中国平安,保费,新业务价值,投资收益率,寿险"},
|
||||
"汽车": {
|
||||
"upstream": {"codes": ["300750", "002475", "601600"], "keywords": "电池,芯片,钢材,压铸,汽车电子"},
|
||||
"downstream": {"codes": [], "keywords": "汽车销量,新能源渗透率,出口,以旧换新,价格战"},
|
||||
"peers": ["000625", "601238", "600104", "002594"],
|
||||
"keywords": "汽车,长城汽车,销量,出口,新能源,智能驾驶"},
|
||||
"消费电子": {
|
||||
"upstream": {"codes": ["603501", "000725"], "keywords": "芯片,CIS,显示,PCB,声学,射频"},
|
||||
"downstream": {"codes": [], "keywords": "手机出货,AI终端,可穿戴,VR/AR,苹果链,折叠屏"},
|
||||
"peers": ["002241", "002475"],
|
||||
"keywords": "消费电子,立讯精密,歌尔,苹果链,AI终端,可穿戴"},
|
||||
"面板": {
|
||||
"upstream": {"codes": [], "keywords": "玻璃基板,偏光片,驱动IC,蒸镀"},
|
||||
"downstream": {"codes": ["002475"], "keywords": "面板价格,稼动率,TV,OLED,手机需求"},
|
||||
"peers": ["000100", "000725"],
|
||||
"keywords": "面板,京东方,TCL,面板价格,稼动率,OLED"},
|
||||
"家电": {
|
||||
"upstream": {"codes": ["600309"], "keywords": "铜,铝,压缩机,芯片,钢材"},
|
||||
"downstream": {"codes": [], "keywords": "空调排产,以旧换新,家电补贴,海外需求,地产竣工"},
|
||||
"peers": ["000651", "600690", "000333"],
|
||||
"keywords": "家电,美的,格力,海尔,空调排产,以旧换新,海外"},
|
||||
"化工": {
|
||||
"upstream": {"codes": [], "keywords": "原油,煤炭,天然气,MDI原料,纯苯"},
|
||||
"downstream": {"codes": ["000333", "600048"], "keywords": "MDI价格,聚氨酯需求,涂料,家电排产,地产需求"},
|
||||
"peers": ["002648", "600309"],
|
||||
"keywords": "化工,万华化学,卫星化学,MDI,聚氨酯,景气度"},
|
||||
"有色": {
|
||||
"upstream": {"codes": [], "keywords": "铜矿,金价,铝土矿,能源,品位"},
|
||||
"downstream": {"codes": ["300750", "002594", "601012"], "keywords": "铜价,铝价,金价,库存,新能源需求,电网"},
|
||||
"peers": ["601600", "601899"],
|
||||
"keywords": "有色,紫金矿业,铜价,金价,铝价,库存"},
|
||||
"工程机械": {
|
||||
"upstream": {"codes": [], "keywords": "钢材,液压件,发动机,轴承"},
|
||||
"downstream": {"codes": ["000002"], "keywords": "挖掘机销量,基建投资,地产开工,出口,更新周期"},
|
||||
"peers": ["000425", "600031"],
|
||||
"keywords": "工程机械,三一重工,徐工,挖掘机销量,基建,出口"},
|
||||
"军工": {
|
||||
"upstream": {"codes": [], "keywords": "钛合金,碳纤维,航发,军机零部件"},
|
||||
"downstream": {"codes": [], "keywords": "军费,军贸,订单,列装,航空发动机"},
|
||||
"peers": [],
|
||||
"keywords": "军工,中航沈飞,军贸,订单,航空,军费"},
|
||||
"房地产": {
|
||||
"upstream": {"codes": ["600036", "601166"], "keywords": "土地,建材,融资,按揭"},
|
||||
"downstream": {"codes": ["000333", "000651", "600690"], "keywords": "销售,竣工,政策,保交楼,土拍,去库存"},
|
||||
"peers": ["600048", "000002"],
|
||||
"keywords": "房地产,万科,保利,销售,政策,保交楼,土拍"},
|
||||
"食品饮料": {
|
||||
"upstream": {"codes": ["002714", "300498"], "keywords": "原奶,生猪,大豆,包装,原材料"},
|
||||
"downstream": {"codes": [], "keywords": "消费复苏,提价,渠道,餐饮,库存"},
|
||||
"peers": ["600519"],
|
||||
"keywords": "食品饮料,海天,伊利,双汇,提价,消费复苏,渠道"},
|
||||
"养殖": {
|
||||
"upstream": {"codes": [], "keywords": "饲料,玉米,豆粕,兽药,仔猪"},
|
||||
"downstream": {"codes": ["000895"], "keywords": "猪价,能繁母猪,出栏量,冻肉库存,生猪期货"},
|
||||
"peers": ["300498", "002714"],
|
||||
"keywords": "养殖,牧原,温氏,猪价,能繁母猪,出栏"},
|
||||
"安防": {
|
||||
"upstream": {"codes": ["603501", "000725"], "keywords": "芯片,CIS,传感器,镜头"},
|
||||
"downstream": {"codes": ["002230"], "keywords": "政府项目,数字化转型,海外安防,AI大模型,toB"},
|
||||
"peers": [],
|
||||
"keywords": "安防,海康威视,大华,政府项目,海外,AIoT"},
|
||||
"人工智能": {
|
||||
"upstream": {"codes": ["688041", "603986"], "keywords": "算力,GPU,大模型训练,芯片"},
|
||||
"downstream": {"codes": ["688111", "300033"], "keywords": "大模型落地,教育,医疗,toB订单,应用"},
|
||||
"peers": ["002415"],
|
||||
"keywords": "人工智能,科大讯飞,大模型,算力,toB,语音"},
|
||||
"软件": {
|
||||
"upstream": {"codes": ["002230", "688041"], "keywords": "算力,云,国产化,AI能力"},
|
||||
"downstream": {"codes": ["300033", "600036"], "keywords": "信创,订阅,付费用户,金融IT支出,国产替代"},
|
||||
"peers": ["600570", "688111"],
|
||||
"keywords": "软件,金山办公,恒生电子,信创,订阅,金融IT"},
|
||||
"通信设备": {
|
||||
"upstream": {"codes": ["688981", "688041"], "keywords": "芯片,基带,射频,光模块"},
|
||||
"downstream": {"codes": ["600941"], "keywords": "5G建设,运营商资本开支,算力网络,服务器,集采"},
|
||||
"peers": [],
|
||||
"keywords": "通信设备,中兴通讯,5G,光模块,算力,运营商集采"},
|
||||
"通信运营": {
|
||||
"upstream": {"codes": ["000063"], "keywords": "设备集采,5G建设,光缆"},
|
||||
"downstream": {"codes": [], "keywords": "移动用户,ARPU,算力,云业务,数字经济,AI"},
|
||||
"peers": [],
|
||||
"keywords": "通信运营,中国移动,5G,ARPU,算力,云"},
|
||||
"免税": {
|
||||
"upstream": {"codes": [], "keywords": "奢侈品,品牌商,机场租金,供货"},
|
||||
"downstream": {"codes": [], "keywords": "免税销售,出入境客流,离岛免税,消费复苏,口岸"},
|
||||
"peers": [],
|
||||
"keywords": "免税,中国中免,离岛免税,出入境,客流"},
|
||||
"电力": {
|
||||
"upstream": {"codes": [], "keywords": "来水,煤价,发电设备,水电"},
|
||||
"downstream": {"codes": ["601600"], "keywords": "用电量,绿电,电价,储能,高耗能"},
|
||||
"peers": [],
|
||||
"keywords": "电力,长江电力,水电,来水,电价,用电量"},
|
||||
"煤炭": {
|
||||
"upstream": {"codes": [], "keywords": "煤矿,安全生产,产能"},
|
||||
"downstream": {"codes": ["600900", "600309"], "keywords": "煤价,长协,库存,发电需求,火电"},
|
||||
"peers": [],
|
||||
"keywords": "煤炭,中国神华,煤价,长协,库存,火电"},
|
||||
"稀土": {
|
||||
"upstream": {"codes": [], "keywords": "稀土矿,离子型,配额,冶炼"},
|
||||
"downstream": {"codes": ["002594", "002475"], "keywords": "稀土价格,磁材,出口管制,新能源需求,电机"},
|
||||
"peers": [],
|
||||
"keywords": "稀土,北方稀土,氧化镨钕,磁材,价格,配额"},
|
||||
}
|
||||
|
||||
# 无明确产业链的兜底
|
||||
FALLBACK_CHAIN = {
|
||||
"upstream": {"codes": [], "keywords": "原材料,供应链,成本"},
|
||||
"downstream": {"codes": [], "keywords": "需求,客户,景气度"},
|
||||
"peers": [],
|
||||
"keywords": "行业动态,景气度,政策",
|
||||
}
|
||||
|
||||
|
||||
def get_chain(industry):
|
||||
return INDUSTRY_CHAIN.get(industry) or FALLBACK_CHAIN
|
||||
+28
-1
@@ -6,7 +6,7 @@
|
||||
"""
|
||||
from database import query, execute, query_one
|
||||
from config import LLM_BASE_URL, LLM_API_KEY, LLM_MODEL, LLM_MAX_TOKENS, \
|
||||
LLM_TEMPERATURE, LLM_TIMEOUT, MAIL_DEFAULTS, MONITOR_DEFAULTS
|
||||
LLM_TEMPERATURE, LLM_TIMEOUT, MAIL_DEFAULTS, MONITOR_DEFAULTS, TRACKING_DEFAULTS
|
||||
|
||||
|
||||
def get_setting(key, default=""):
|
||||
@@ -92,3 +92,30 @@ def set_monitor_state(last_news_id=None, last_scan=None, last_sent=None):
|
||||
set_setting("monitor_last_scan", last_scan)
|
||||
if last_sent is not None:
|
||||
set_setting("monitor_last_sent", last_sent)
|
||||
|
||||
|
||||
# ===================================================================== 持仓跟踪
|
||||
def tracking_config():
|
||||
return {
|
||||
"enabled": get_setting("tracking_enabled", TRACKING_DEFAULTS["tracking_enabled"]) == "1",
|
||||
"interval_min": max(15, int(get_setting("tracking_interval", TRACKING_DEFAULTS["tracking_interval"]))),
|
||||
"notify": get_setting("tracking_notify", TRACKING_DEFAULTS["tracking_notify"]) == "1",
|
||||
"impact_threshold": float(get_setting("tracking_impact_threshold", TRACKING_DEFAULTS["tracking_impact_threshold"])),
|
||||
}
|
||||
|
||||
|
||||
def tracking_state():
|
||||
return {
|
||||
"last_run": get_setting("tracking_last_run", ""),
|
||||
"last_stock": get_setting("tracking_last_stock", ""),
|
||||
"last_alert": int(get_setting("tracking_last_alert", "0")),
|
||||
}
|
||||
|
||||
|
||||
def set_tracking_state(last_run=None, last_stock=None, last_alert=None):
|
||||
if last_run is not None:
|
||||
set_setting("tracking_last_run", last_run)
|
||||
if last_stock is not None:
|
||||
set_setting("tracking_last_stock", last_stock)
|
||||
if last_alert is not None:
|
||||
set_setting("tracking_last_alert", last_alert)
|
||||
@@ -25,6 +25,13 @@ async function loadSettings() {
|
||||
const cats = mono.categories || [];
|
||||
$$('#monitorCats input').forEach(c => c.checked = cats.includes(c.value));
|
||||
renderState(curSettings.state);
|
||||
// 持仓跟踪
|
||||
const tr = curSettings.tracking || {};
|
||||
$('#trackingEnabled').checked = !!tr.enabled;
|
||||
$('#trackingInterval').value = tr.interval_min;
|
||||
$('#trackingImpact').value = tr.impact_threshold;
|
||||
$('#trackingNotify').checked = !!tr.notify;
|
||||
renderTrackingState(tr);
|
||||
loadLog();
|
||||
} catch (e) {
|
||||
toast('设置加载失败');
|
||||
@@ -38,6 +45,13 @@ function renderState(state) {
|
||||
<div class="mini-stat"><div class="ms-label">上次通知</div><div class="ms-value num">${state.last_sent_count || 0} 条</div></div>`;
|
||||
}
|
||||
|
||||
function renderTrackingState(tr) {
|
||||
$('#trackingState').innerHTML = `
|
||||
<div class="mini-stat"><div class="ms-label">上次运行</div><div class="ms-value">${tr.last_run || '—'}</div></div>
|
||||
<div class="mini-stat"><div class="ms-label">最近跟踪</div><div class="ms-value">${tr.last_stock || '—'}</div></div>
|
||||
<div class="mini-stat"><div class="ms-label">最近告警影响度</div><div class="ms-value num">${tr.last_alert ? tr.last_alert + '/100' : '—'}</div></div>`;
|
||||
}
|
||||
|
||||
function collectMail() {
|
||||
return {
|
||||
smtp_host: $('#smtpHost').value.trim(), smtp_port: $('#smtpPort').value,
|
||||
@@ -69,6 +83,12 @@ async function save(which) {
|
||||
llm_model: $('#llmModel').value.trim()
|
||||
};
|
||||
if (which === 'monitor') body.monitor = collectMonitor();
|
||||
if (which === 'tracking') body.tracking = {
|
||||
tracking_enabled: $('#trackingEnabled').checked,
|
||||
tracking_interval: $('#trackingInterval').value,
|
||||
tracking_impact_threshold: $('#trackingImpact').value,
|
||||
tracking_notify: $('#trackingNotify').checked
|
||||
};
|
||||
const r = await api('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||
toast(r.msg || '已保存');
|
||||
} catch (e) { toast('保存失败:' + e.message); }
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/* 持仓跟踪智能体 */
|
||||
async function load() {
|
||||
try {
|
||||
const d = await api('/api/tracking');
|
||||
const items = d.items || [];
|
||||
renderRecords(items);
|
||||
loadPositions(items);
|
||||
} catch (e) {
|
||||
$('#recTb').innerHTML = '<tr><td colspan="7" class="empty">加载失败</td></tr>';
|
||||
}
|
||||
loadState();
|
||||
}
|
||||
|
||||
function loadState() {
|
||||
api('/api/settings').then(d => {
|
||||
const t = d.tracking || {};
|
||||
$('#trackState').innerHTML = `
|
||||
<div class="kpi"><div class="kpi-label">自动跟踪</div><div class="kpi-value" style="font-size:15px">${t.enabled ? '✅ 开启' : '⏸ 关闭'}<span style="color:var(--muted);font-size:12px">(每 ${t.interval_min} 分钟)</span></div></div>
|
||||
<div class="kpi"><div class="kpi-label">上次运行</div><div class="kpi-value" style="font-size:15px">${t.last_run || '—'}</div></div>
|
||||
<div class="kpi"><div class="kpi-label">最近跟踪标的</div><div class="kpi-value" style="font-size:15px">${t.last_stock || '—'}</div></div>
|
||||
<div class="kpi"><div class="kpi-label">最近告警影响度</div><div class="kpi-value" style="font-size:15px">${t.last_alert ? t.last_alert + '/100' : '—'}</div></div>
|
||||
<div class="kpi"><div class="kpi-label">重大判定阈值</div><div class="kpi-value" style="font-size:15px">≥ ${t.impact_threshold}</div></div>`;
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
async function loadPositions(all) {
|
||||
try {
|
||||
const w = await api('/api/watchlist');
|
||||
const pos = w.items || [];
|
||||
if (!pos.length) {
|
||||
$('#posBox').innerHTML = '<div class="empty">自选股为空。请先在 <a href="/stocks">股票池</a> 或个股详情页 ⭐ 添加持仓/关注标的</div>';
|
||||
return;
|
||||
}
|
||||
// 每只自选股取最近一条跟踪
|
||||
const rows = [];
|
||||
for (const p of pos) {
|
||||
const latest = all.find(r => r.code === p.code);
|
||||
rows.push({ stock: p, latest });
|
||||
}
|
||||
$('#posBox').innerHTML = `<table>
|
||||
<tr><th>股票</th><th>现价</th><th>最近跟踪</th><th>影响度</th><th>性质</th><th>摘要</th><th>操作</th></tr>
|
||||
${rows.map(x => {
|
||||
const p = x.stock, r = x.latest;
|
||||
const meta = r ? (JSON.parse(r.meta || '{}')) : null;
|
||||
const sigCls = meta ? (meta.impact_score >= 65 ? 'tag-强烈推荐' : meta.impact_score >= 45 ? 'tag-推荐' : 'tag-关注') : 'tag-观望';
|
||||
return `<tr>
|
||||
<td><b>${p.name}</b><div style="color:var(--muted);font-size:12px">${p.code} · ${p.industry}</div></td>
|
||||
<td class="num">${p.close}</td>
|
||||
<td style="color:var(--muted)">${r ? r.created_at : '—'}</td>
|
||||
<td>${r && meta ? `<span class="tag ${sigCls}">${meta.impact_score}</span>` : '<span style="color:var(--muted)">未跟踪</span>'}</td>
|
||||
<td>${meta ? `<span class="tag tag-${meta.change_kind === '利好' ? '强烈推荐' : meta.change_kind === '利空' ? '负' : '关注'}">${meta.change_kind}</span>` : '—'}</td>
|
||||
<td style="max-width:300px;white-space:normal;color:var(--text2)">${meta ? escapeHtml(meta.summary || '') : '—'}</td>
|
||||
<td><div class="flex">
|
||||
<button class="btn btn-primary" onclick="runOne('${p.code}')">跟踪</button>
|
||||
${r ? `<button class="btn" onclick="showDetail(${r.id})">详情</button>` : ''}
|
||||
</div></td>
|
||||
</tr>`;
|
||||
}).join('')}
|
||||
</table>`;
|
||||
} catch (e) {
|
||||
$('#posBox').innerHTML = '<div class="empty">加载失败</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderRecords(items) {
|
||||
if (!items.length) return;
|
||||
$('#recTb').innerHTML = items.map(r => {
|
||||
let meta = {};
|
||||
try { meta = JSON.parse(r.meta || '{}'); } catch (e) {}
|
||||
const sigCls = meta.impact_score >= 65 ? 'tag-强烈推荐' : meta.impact_score >= 45 ? 'tag-推荐' : 'tag-关注';
|
||||
return `<tr class="row-link" onclick="showDetail(${r.id})">
|
||||
<td style="color:var(--muted)">${r.created_at}</td>
|
||||
<td><b>${r.stock_name}</b><div style="color:var(--muted);font-size:12px">${r.code}</div></td>
|
||||
<td style="color:var(--text2)">${escapeHtml(r.industry)}</td>
|
||||
<td><span class="tag ${sigCls}">${meta.impact_score || '—'}</span></td>
|
||||
<td><span class="tag tag-${meta.change_kind === '利好' ? '强烈推荐' : meta.change_kind === '利空' ? '负' : '关注'}">${meta.change_kind || '—'}</span></td>
|
||||
<td style="max-width:320px;white-space:normal;color:var(--text2)">${escapeHtml(meta.summary || '')}</td>
|
||||
<td><button class="btn" onclick="event.stopPropagation();showDetail(${r.id})">查看报告 ↗</button></td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function runAll() {
|
||||
if (!confirm('将对全部自选股执行一次产业链深度跟踪(每只约30-60秒,逐只进行),确定?')) return;
|
||||
try {
|
||||
const r = await api('/api/tracking/run', { method: 'POST' });
|
||||
toast(r.msg || '已启动');
|
||||
pollRefresh();
|
||||
} catch (e) { toast('启动失败:' + e.message); }
|
||||
}
|
||||
|
||||
async function runOne(code) {
|
||||
try {
|
||||
await api('/api/tracking/run/' + code, { method: 'POST' });
|
||||
toast('跟踪已启动,完成后自动刷新');
|
||||
setTimeout(() => { load(); toast('跟踪完成'); }, 80000);
|
||||
} catch (e) { toast('启动失败:' + e.message); }
|
||||
}
|
||||
|
||||
/* 详情弹窗:报告 + 数据源 */
|
||||
async function showDetail(id) {
|
||||
try {
|
||||
const d = await api('/api/tracking/' + id);
|
||||
const meta = d.meta || {}, src = d.sources || {};
|
||||
const sigCls = meta.impact_score >= 65 ? 'tag-强烈推荐' : meta.impact_score >= 45 ? 'tag-推荐' : 'tag-关注';
|
||||
const newsBlock = (label, icon, seg) => {
|
||||
if (!seg) return '';
|
||||
const txt = ((seg.db || '') + (seg.rag || '')).trim();
|
||||
return `<details><summary>${icon} ${label} <span class="src-count">${txt.includes('[') ? txt.split('[').length - 1 : 0}</span></summary>
|
||||
<div class="src-body"><pre class="src-pre">${escapeHtml(txt || '暂无')}</pre></div></details>`;
|
||||
};
|
||||
openModal(`
|
||||
<h3>🧭 产业链跟踪:${d.stock_name}(${d.code})</h3>
|
||||
<div class="news-meta" style="margin-bottom:12px">
|
||||
<span class="cat-tag">${escapeHtml(d.industry)}</span>
|
||||
<span class="tag ${sigCls}">影响度 ${meta.impact_score || '—'}/100</span>
|
||||
<span class="tag tag-${meta.change_kind === '利好' ? '强烈推荐' : meta.change_kind === '利空' ? '负' : '关注'}">${meta.change_kind || '—'}</span>
|
||||
<span style="color:var(--muted)">${d.created_at}</span>
|
||||
</div>
|
||||
<div class="markdown-body">${mdRender(d.report)}</div>
|
||||
<div style="margin-top:16px;border-top:1px solid var(--border);padding-top:10px">
|
||||
<b style="color:var(--gold)">数据源(智能体参考内容)</b>
|
||||
${newsBlock('个股直接动态', '📄', src.direct)}
|
||||
${newsBlock('上游产业链(供给/成本)', '⬆️', src.upstream)}
|
||||
${newsBlock('下游产业链(需求/景气)', '⬇️', src.downstream)}
|
||||
${newsBlock('同业竞争', '🏢', src.peers)}
|
||||
<details><summary>📈 技术面 / 机构动向</summary><div class="src-body"><pre class="src-pre">${escapeHtml(src.indicators || '')}</pre></div></details>
|
||||
</div>
|
||||
<div style="margin-top:14px;text-align:right">
|
||||
<button class="btn" onclick="document.querySelector('.modal-mask').classList.remove('show')">关闭</button>
|
||||
<a class="btn btn-primary" href="/stock/${d.code}" target="_blank">查看个股 →</a>
|
||||
</div>
|
||||
`);
|
||||
} catch (e) { toast('加载失败'); }
|
||||
}
|
||||
|
||||
let pollTimer = null;
|
||||
function pollRefresh() {
|
||||
clearTimeout(pollTimer);
|
||||
pollTimer = setTimeout(() => load(), 30000);
|
||||
}
|
||||
|
||||
load();
|
||||
@@ -30,6 +30,7 @@
|
||||
<a href="/" class="nav-item" data-nav="/">📊 仪表盘</a>
|
||||
<a href="/stocks" class="nav-item" data-nav="/stocks">🏢 股票池</a>
|
||||
<a href="/recommend" class="nav-item" data-nav="/recommend">🎯 荐股中心</a>
|
||||
<a href="/tracking" class="nav-item" data-nav="/tracking">🧭 持仓跟踪</a>
|
||||
<a href="/strategies" class="nav-item" data-nav="/strategies">📈 量化策略</a>
|
||||
<a href="/news" class="nav-item" data-nav="/news">📰 财经新闻</a>
|
||||
<a href="/institutions" class="nav-item" data-nav="/institutions">🏦 机构动向</a>
|
||||
|
||||
@@ -76,6 +76,26 @@
|
||||
<div class="mt16 mini-stats" id="monitorState"></div>
|
||||
</div>
|
||||
|
||||
<!-- 持仓跟踪 -->
|
||||
<div class="card mt16">
|
||||
<div class="between">
|
||||
<div class="card-title" style="margin-bottom:0"><span class="bar" style="background:#8b5cf6"></span>🧭 持仓跟踪智能体</div>
|
||||
<label class="switch"><input type="checkbox" id="trackingEnabled"><span></span></label>
|
||||
</div>
|
||||
<div class="mt8 settings-form">
|
||||
<div class="sf-row"><label>跟踪间隔(分钟)</label><input class="input" id="trackingInterval" type="number" min="15" style="width:120px"></div>
|
||||
<div class="sf-row"><label>重大判定阈值</label><input class="input" id="trackingImpact" type="number" min="0" max="100" style="width:120px"></div>
|
||||
<div class="sf-row"><label>重大变化邮件通知</label>
|
||||
<label class="switch"><input type="checkbox" id="trackingNotify"><span></span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt16 wrap">
|
||||
<button class="btn btn-primary" onclick="save('tracking')">💾 保存跟踪设置</button>
|
||||
<button class="btn" onclick="location.href='/tracking'">🧭 打开持仓跟踪页</button>
|
||||
</div>
|
||||
<div class="mt16 mini-stats" id="trackingState"></div>
|
||||
</div>
|
||||
|
||||
<!-- 通知日志 -->
|
||||
<div class="card mt16">
|
||||
<div class="card-title"><span class="bar" style="background:var(--cyan)"></span>通知日志</div>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}持仓跟踪{% endblock %}
|
||||
{% block page_title %}持仓跟踪智能体{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="flex between wrap">
|
||||
<div style="color:var(--text2);font-size:13px;line-height:1.9">
|
||||
🤖 <b style="color:var(--text)">大模型智能体 + 工作流</b>:定期跟踪<b>自选股(持仓)</b>,除个股公告新闻外,
|
||||
深度分析其<b>产业链上下游与同业</b>动态(上游供给/成本、下游需求/景气、竞争格局),
|
||||
输出专业跟踪报告;发现<b>重大变化自动邮件通知</b>。
|
||||
<div style="color:var(--muted);font-size:12px">跟踪对象 = 自选股(⭐ 股票池/详情页添加)· 自动跟踪间隔可在「系统设置」配置</div>
|
||||
</div>
|
||||
<div class="flex wrap">
|
||||
<button class="btn btn-primary" onclick="runAll()">⚡ 立即跟踪全部持仓</button>
|
||||
<button class="btn" onclick="load()">🔄 刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt16 kpi-grid" id="trackState"></div>
|
||||
</div>
|
||||
|
||||
<div class="card mt16">
|
||||
<div class="card-title"><span class="bar"></span>持仓(自选股)最近跟踪</div>
|
||||
<div id="posBox"><div class="loading">加载中…</div></div>
|
||||
</div>
|
||||
|
||||
<div class="card mt16">
|
||||
<div class="card-title"><span class="bar" style="background:var(--gold)"></span>全部跟踪记录</div>
|
||||
<div style="overflow:auto;max-height:520px">
|
||||
<table>
|
||||
<thead><tr><th>时间</th><th>股票</th><th>行业</th><th>影响度</th><th>性质</th><th>摘要</th><th>操作</th></tr></thead>
|
||||
<tbody id="recTb"><tr><td colspan="7" class="empty">暂无跟踪记录,点击上方「立即跟踪全部持仓」开始</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/tracking.js') }}"></script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user