diff --git a/.gitignore b/.gitignore
index 97d58fa..9e289f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,10 +45,14 @@ backend/data/**
!backend/data/.gitkeep
data/**
!data/.gitkeep
-# 扩展数据: 全部纳入版本控制(含 config.json 表结构 + part.parquet 数据文件)
-# 注意 gitignore 否定规则特性: 需先放行目录才能放行内部文件
+# 扩展数据: 仅放行 ext_gn_ths(概念)与 ext_hy_ths(行业)两个数据源
+# 含 config.json 表结构 + part.parquet 数据文件;其余 ext_data 子目录均不入库
+# (gitignore 否定规则特性: 要放行嵌套文件,需逐级放行目录 + /** 放行内部文件)
!data/ext_data/
-!data/ext_data/**
+!data/ext_data/ext_gn_ths/
+!data/ext_data/ext_gn_ths/**
+!data/ext_data/ext_hy_ths/
+!data/ext_data/ext_hy_ths/**
# ===== Secrets =====
.env
diff --git a/README.md b/README.md
index 867e173..f3b53ed 100644
--- a/README.md
+++ b/README.md
@@ -11,32 +11,24 @@
[](./Dockerfile)
[](https://github.com/shy3130/tickflow-stock-panel/stargazers)
-基于 [TickFlow](https://tickflow.org/auth/register?ref=V3KDKGXPEA) 数据 · 🚀 **开箱即用**(单容器 / None 模式)
-能力驱动,适配 None → Expert 全档位订阅 · 🔌 **自由接入第三方扩展数据**(例如 Tushare、自有量化项目数据)
-
-**[核心功能](#-核心功能)** · **[快速开始](#-快速开始)** · **[配置](#️-配置)** · **[路线图](#-路线图)**
+**[快速开始](#-快速开始)** · **[核心功能](#-核心功能)** · **[配置](#️-配置)** · **[路线图](#-路线图)**
-> **⚠️说明**:目前项目基于[TickFlow](https://tickflow.org/auth/register?ref=V3KDKGXPEA)数据源。无需付费即可使用策略+回测等功能.自有个性化数据源可接入扩展数据使用。
+- 🆓 **开箱即用** — 留空 Key 即进 None 模式,历史日 K 免费体验,**无需付费**
+- 🏠 **自托管零运维** — Docker 单容器部署,数据完全掌握在自己手里
+- 🔍 **三位一体** — 选股(20 内置策略)+ 实时监控 + 向量化回测,Polars 毫秒级扫描全 A 股
+- 🤖 **AI 加持** — 一句话生成策略代码,任意 OpenAI 兼容接口均可接入(留空即关闭)
+- 🔌 **自由扩展** — 接入 Tushare(开发中) / 自有量化项目数据,与内置数据同台分析
+- 🇨🇳 **A 股专用** — 连板梯队、涨停动量、内置ths 概念 / 行业
- 如果觉得有用,请Star支持一下,蟹蟹🌹
+> 基于 [TickFlow](https://tickflow.org/auth/register?ref=V3KDKGXPEA) 数据源。**明确不做**:不对标同花顺 / 通达信,不内置「AI 荐股 / 涨停预测」。
+觉得有用可以点个 Star,蟹蟹 🌹
---
-## 🎯 项目定位
-
-让任何**个人散户 / 量化爱好者**,**零运维**地拥有一套**属于自己策略定制**的 A 股分析、选股、监控工作台。
-基于 [TickFlow](https://tickflow.org/auth/register?ref=V3KDKGXPEA) Key **零成本**获取数据,并使用策略定制+监控+回测功能。
-**内置ths概念、ths行业数据**。可接入第三方自有个性化扩展数据(人气、资金流向等)。
-
-**项目所需配置**:
-
-| 配置项 | 说明 | 是否必填 |
-| :--- | :--- | :--- |
-| **TickFlow API Key** | 数据源凭证,留空启用 None 模式;免费注册 Key 后进入 Free 模式,可使用历史日K与自选股实时监控 | 可选 |
-| **AI 大模型 API Key** | 用于 AI 生成策略、个股分析、财务分析等,任意 OpenAI 兼容接口,留空关闭 | 可选 |
+## 📸 界面预览
@@ -44,140 +36,32 @@
| 策略 Screener |
-  |
-  |
+  |
+  |
| 回测 Backtest |
监控中心 Monitor |
-  |
-  |
-
+  |
+  |
| 连板梯队 Limit Ladder |
- 概念分析 Concept |
+ 概念分析 Concept |
-  |
-  |
+  |
+  |
-> ### ⚠️ 🚧 项目持续优化,功能陆续开放,敬请期待。
+
-> **明确不做**:不对标同花顺/通达信的全功能股票软件;不内置任何「AI 荐股 / 涨停预测」。
+### 📸 [查看更多界面截图 »](./screenshots/README.md)
----
-
-## ✨ 核心功能
-
-### 🔍 选股引擎(Screener)
-
-**20 个内置策略** —— 每个策略是一个独立 Python 文件(`backend/app/strategy/builtin/`),基于 Polars 表达式实现:
-
-| 类型 | 代表策略 |
-| :--- | :--- |
-| 趋势 | 趋势突破 · 均线多头 · 缩量回踩 |
-| 形态 | MA 金叉 · MACD 金叉放量 · 布林突破 |
-| 量价 | 量价齐升 · 高换手强势 · 强势高开 |
-| 涨停 | 连板股 · 断板反包 · 逼近涨停 · 涨停动量 |
-| 反转 | 超跌反弹 · 超卖反转 · 新低反转 |
-| 波动 | 低波动龙头 · 回踩 MA20 反弹 |
-
-- **自定义信号系统** —— 在 UI 上用 `字段 + 操作符 + 阈值` 组合(entry / exit / both),编译成 Polars 表达式热加载,**无需写代码**即可定义自己的买卖信号。
-- **策略商店** —— 内置策略 + 用户自定义策略统一管理,支持参数覆盖(`params` 暴露阈值)。
-
-#### ➕ 添加自己的策略
-
-除 20 个内置策略外,你可以用两种方式扩展:
-
-| 方式 | 说明 | 前提 |
-| :--- | :--- | :--- |
-| **🤖 AI 生成** | 用自然语言描述策略思路,LLM 读取 [strategy-guide.md](./docs/strategy-guide.md) 自动生成完整 Polars 策略文件(经 `ast` 安全校验,限定 `import polars as pl`)。生成后落入 `data/strategies/ai/`,即刻可用 | 需先在 [配置](#️-配置) 中填入 AI Key |
-| **📝 代码自定义 / 策略迁移** | 参照 [策略开发指南](./docs/strategy-guide.md) 的文件结构模板,把你**已有的自有策略**改写为 Polars 文件放入 `data/strategies/custom/`(文件名/ID 建议 `custom_时间戳`),引擎自动发现加载——**轻松迁移你现成的量化项目策略**,无需从头重写 | 无 |
-| **🎛️ 自定义信号配置** | 不写代码,在 UI 上用 `字段 + 操作符 + 阈值` 组合(entry / exit / both),编译成 Polars 表达式热加载,即可定义自己的买卖信号 | 无 |
-
-> 引擎按 `source` 标记来源:`builtin`(内置)/ `custom`(手写或迁移)/ `ai`(生成),三者统一进入策略商店管理。
-
-### 📊 指标流水线(Indicators)
-
-原生 Polars 向量化计算,全 A 股一次扫表落盘为 enriched Parquet:
-
-| 分类 | 指标 |
-| :--- | :--- |
-| 均线系 | MA(5/10/20/30/60)· EMA(5/10/12/20/26/30/60) |
-| 趋势系 | MACD(DIF/DEA/HIST)· 动量(5/10/20/30/60d)· 布林带(上/下轨) |
-| 震荡系 | RSI(可配周期)· KDJ(K/D/J) |
-| 波动系 | ATR(14)· 年化波动率(20d)· 振幅 |
-| 量能系 | 量比(5d/10d)· 量均线 |
-| 涨跌停 | 涨停信号 · 连板数 · 涨跌幅 · 涨跌额 |
-| 原子信号 | MA 金叉/死叉 · MA20 突破/跌破 · MACD 金叉/死叉 · N 日新高/新低 · 布林突破 |
-| 复权 | 基于除权因子自动计算前复权(`ex_factor` / `cum_factor`),回测与指标一致 |
-
-### 🧪 回测引擎(Backtest)
-
-基于 vectorbt:
-
-- **三种回测模式**:个股 · 策略组合 · 自由信号组合
-- **真实约束**:T+1 · 手续费 · 滑点(基点) · 止损 · 最大持仓天数
-- **组合管理**:最大持仓数 · 最大敞口 · 等权 / 自定义仓位
-- **SSE 流式进度**:长任务实时推送进度,支持刷新 / 切页后**重连恢复**(相同参数任务只启动一次)
-- **统计输出**:净值曲线 · 夏普 · 最大回撤 · 胜率 · 每笔交易明细
-
-### 📡 监控中心(Monitor)
-
-**统一监控规则引擎** —— 一个页面管理所有类型的监控,实时推送 + 持久化触发记录:
-
-- **四类监控**:策略监控 · 个股信号监控(选信号即加) · 个股价格/涨跌监控 · 全市场异动监控
-- **灵活条件**:多条件 AND/OR 组合 + 冷却期去重(防刷屏) + 严重级别(info/warn/critical)
-- **多入口配置**:监控中心页面新建规则 · 个股详情页「加监控」· 策略卡片一键开启
-- **实时 SSE 推送**:命中规则后右下角弹窗通知(可配声效) + 持久化到 `alerts.jsonl`
-- **触发记录**:时间倒序展示,支持按来源过滤 · 单条删除 · 清空 · 点击查看个股日K
-- **菜单未读徽标**:离开监控中心后有新触发,菜单显示未读数;进入页面后清零
-
-### 🤖 AI 策略生成(可选)
-
-- **自然语言 → 策略代码**:用一句话描述策略思路,LLM 读取 `docs/strategy-guide.md` 生成完整 Polars 策略文件
-- **沙箱约束**:生成代码经 `ast` 校验、限定 `import polars as pl`,避免逐行循环,优先向量化表达
-- **可插拔**:留空 AI 配置即跳过整个模块,不影响核心功能
-
-### 📈 个股分析(Beta)
-
-**以「行情 + 关键价位」为视觉主体的单标的决策页**,与「财务分析」(财务质量评级)定位互补:
-
-- **专用日 K 图表**:不复用行情浏览图表,主图 + 成交量 + 滑块三段布局,默认展示近 6 个月,9 类关键价位可逐组开关
-- **9 类关键价位**(均纯函数实时计算,毫秒级):
-
-| 类型 | 算法 | 说明 |
-| :--- | :--- | :--- |
-| 压力支撑 | 布林带上下轨 | 近期波动边界 |
-| 成交密集区 | 成交量分布 POC + 高成交带 | 筹码密集价位 |
-| 枢轴点 | 经典 Pivot P/R1~R3/S1~S3 | 可配档位(1~3 档) |
-| 前高前低 | 60/250 日极值 + swing 高低点 | 历史转折参照 |
-| Keltner 通道 | MA20/60/120 ± n×ATR(短/中/长) | 波动自适应趋势边界 |
-| ATR 止损 | close ± 1.5/2×ATR | 动态止盈止损位 |
-| 缺口位 | 近 120 日未回补跳空缺口 | 天然支撑/阻力 |
-| 斐波那契 | 近期波段 0.236~0.786 回撤 | 经典回撤位 |
-| 整数关口 | 当前价附近心理整数位 | 自适应步长 |
-
-- **AI 四维分析**:技术面 / 基本面 / 财务面 / 消息面流式生成,NDJSON 推送,「实战派交易员」视角输出买卖区间与操作建议
-- **蓝色胶囊**:与财务分析(紫色)并存的全局气泡,支持最小化后台生成、复制全文、历史报告(最多 50 条)
-- **记忆最近查看**:进入页面自动回显上次查看的个股
-
-### 🧰 数据与扩展
-
-- **多源数据**:TickFlow 日 K / 分钟 K / 指数 / 财务(利润 / 资产负债 / 现金流)/ 实时行情
-- **实时行情分档**:None 仅历史日K(当日数据通常盘后 1-2 小时可用);Free 可监控自选页前 5 个标的(最低 6 秒刷新);Starter+ 使用全市场实时行情
-- **🔌 第三方数据接入(重点)** —— TickFlow 之外的数据也能用:
- - 支持 **Tushare** 等第三方数据源,通过 **HTTP 定时拉取**自动入库
- - 支持 **CSV / Excel 上传** · **JSON 写入**,自动 schema 发现与符号归一
- - **页面可视化配置**扩展数据表,无需改代码
- - 可接入**你自己的量化项目数据**,统一并入 DuckDB 查询面,与内置数据同台分析
-- **盘后定时管道**:APScheduler 15:30 CST 自动拉日 K + 重算 enriched 表 + 跑监控
-- **令牌桶限流**:适配各档位 rpm / batch 上限,批量合并 + 增量拉取,同一份数据多面板复用
+
---
@@ -185,119 +69,146 @@
### 前置依赖
-| 工具 | 版本 | 安装 |
-| :--- | :--- | :--- |
-| Python | ≥ 3.11 | [python.org](https://www.python.org/) |
-| Node | ≥ 20 | [nodejs.org](https://nodejs.org/) |
+| 工具 | 版本 | 安装 |
+| :--------------------------------- | :----- | :------------------------------------------------- |
+| Python | ≥ 3.11 | [python.org](https://www.python.org/) |
+| Node | ≥ 20 | [nodejs.org](https://nodejs.org/) |
| [`uv`](https://docs.astral.sh/uv/) | latest | `curl -LsSf https://astral.sh/uv/install.sh \| sh` |
-| `pnpm` | 9 | `npm i -g pnpm` 或 `corepack enable && corepack prepare pnpm@9 --activate` |
+| `pnpm` | 9 | `npm i -g pnpm` |
-
-
-### 方式 A:Dev 模式(二次开发,最推荐)
+### 方式 A:Dev 模式(二次开发推荐)
```bash
-cp .env.example .env # 填 TICKFLOW_API_KEY,留空则启用 None 模式
+cp .env.example .env # 按需填 TICKFLOW_API_KEY(留空 = None 模式)
+./dev.sh # Windows: .\dev.ps1
```
-**一键启动**(推荐,自动检查\下载依赖 / 释放端口 / 同时起前后端,Ctrl-C 一并关闭):
+自动检查 / 下载依赖、释放端口、同时起前后端,Ctrl-C 一并关闭。默认:
-| 平台 | 命令 |
-| :--- | :--- |
-| **macOS / Linux** | `./dev.sh` |
-| **Windows (PowerShell)** | `.\dev.ps1` |
+- 后端 → · 前端 →
+- 自定义端口:`BACKEND_PORT=8000 FRONTEND_PORT=5173 ./dev.sh`
-
-
-### 方式 B:Docker(最省心,可部署)
+### 方式 B:Docker(部署最省心)
```bash
-cp .env.example .env # 按需填写 Key(留空即 None 模式,可直接体验历史日K)
+cp .env.example .env
docker compose up --build
# 打开 http://localhost:3018
```
-如果你的 VPS / 物理机 CPU 较老,`docker compose up --build` 时出现类似
-`Missing required CPU features: avx2, fma, bmi1...` 或容器 `exit code 132`,
-先在 `.env` 里打开 Polars 兼容运行时再重建:
-
-```ini
-BACKEND_EXTRAS=legacy-cpu
-# 需要回测可同时启用(空格分隔):BACKEND_EXTRAS=legacy-cpu backtest
-```
-
-```bash
-docker compose up --build
-```
-
-`legacy-cpu` 会让后端安装 `polars[rtcompat]`,适用于不支持 AVX2/FMA 的老 CPU;
-默认留空则保持 Polars 高性能运行时。该 wheel 约 50MB,启用后镜像体积会相应增大。
-
-
-首次运行会自动安装前后端依赖(约 1-2 分钟),之后直接启动:
-
-- 后端 →
-- 前端 →
-
-自定义端口:`BACKEND_PORT=8000 FRONTEND_PORT=5173 ./dev.sh`(Windows:`.\dev.ps1 -BackendPort 8000 -FrontendPort 5173`)
-
-手动分别启动(备选)
+环境适配与高级选项(老 CPU · 手动启动 · 回测依赖)
+
+**老 CPU 兼容(avx2/fma 缺失报错或 exit 132)**:在 `.env` 打开 `BACKEND_EXTRAS=legacy-cpu` 后重建,会给 Polars 切到 `rtcompat` 运行时;需回测则 `BACKEND_EXTRAS=legacy-cpu backtest`。
+
+**手动分别启动:**
```bash
-# 终端 1:后端
-cd backend
-uv sync
+# 后端
+cd backend && uv sync --extra backtest # 含回测依赖
uv run uvicorn app.main:app --reload --port 3018
-# 终端 2:前端
-cd frontend
-pnpm install
-pnpm dev # http://localhost:3011
+# 前端
+cd frontend && pnpm install && pnpm dev # http://localhost:3011
```
+**回测依赖**:vectorbt → numba 体积较大,作为可选 extras(`uv sync --extra backtest`)。macOS / Intel 无预构建 wheel 时需 `brew install cmake` 现场编译。
+
-> **启用回测**:`cd backend && uv sync --extra backtest`
-> vectorbt → numba 体积较大,故作为可选 extras。macOS / Intel 无预构建 wheel 时需 `brew install cmake` 现场编译。
+### 🧭 跑起来后的第一次使用
-> **老 CPU 兼容**:`cd backend && uv sync --extra legacy-cpu`
-> 当机器缺少 `avx2/fma` 等指令集时,给 Polars 切到 `rtcompat` 运行时。
+1. **设置 → 凭据与能力** → 点 **重新检测**,确认档位标签
+2. **设置** → **立即跑盘后管道**:拉日 K + 计算 enriched 表(None / Free 走 free-api,当日数据盘后 1-2 小时可用)
+3. **自选**页加标的 → **选股**页点策略卡片扫描 / 配自定义信号
+4. **回测**页选策略 + 区间 → 看净值 / 夏普 / 交易明细(SSE 实时进度)
+5. **监控中心**配规则(策略 / 个股信号 / 价格 / 异动),盘中实时弹窗 + 持久化记录
---
-## 🧭 第一次使用
+## ✨ 核心功能
-1. 打开面板 → **设置 → 凭据与能力** → 点 **重新检测**,确认 Tier Label
-2. 点 **立即跑盘后管道** —— 拉日 K + 计算 enriched 表
- - **None / Free 用户**:历史日K走 free-api 通道;当日数据通常盘后 1-2 小时可用
- - **Starter+**:同步全 A 或可获取的 instruments 列表
-3. **自选**页:添加跟踪标的;Free 档实时行情会自动监控自选页前 5 个标的,可用「移到顶部」调整优先级;点代码进 **K 线**页看蜡烛图 + 买卖点
-4. **选股**页:点任一内置策略卡片即时扫描;或用自定义信号组合条件
-5. **回测**页:选策略 / 信号 + 时间区间 → 跑回测 → 看净值 / 夏普 / 交易明细(SSE 实时进度)
-6. **监控中心**页:配置监控规则(策略/个股信号/价格/市场异动),盘中 SSE 实时弹窗通知 + 持久化触发记录;或在个股详情页点「加监控」快速添加
+### 🔍 选股引擎(Screener)
+
+**20 个内置策略**,每个策略一个独立 Python 文件,基于 Polars 表达式向量化实现(`backend/app/strategy/builtin/`):
+
+| 类型 | 代表策略 |
+| :---------- | :------------------------------------------------------- |
+| 趋势 / 形态 | 趋势突破 · 均线多头 · MA 金叉 · MACD 金叉放量 · 布林突破 |
+| 量价 / 涨停 | 量价齐升 · 高换手强势 · 连板股 · 断板反包 · 涨停动量 |
+| 反转 / 波动 | 超跌反弹 · 超卖反转 · 新低反转 · 低波动龙头 · 回踩 MA20 |
+
+**扩展策略的三种方式:**
+
+| 方式 | 说明 |
+| :---------------- | :---------------------------------------------------------------------------------------------------- |
+| **🎛️ 自定义信号** | 不写代码,UI 上 `字段 + 操作符 + 阈值` 组合编译成 Polars 表达式热加载 |
+| **🤖 AI 生成** | 一句话描述思路,LLM 读 `strategy-guide.md` 生成完整策略文件(经 `ast` 校验)→ 落入 `data/strategies/ai/` |
+| **📝 代码迁移** | 参照开发指南把已有策略改写为 Polars 文件放入 `data/strategies/custom/`,引擎自动发现 |
+
+### 📊 指标流水线(Indicators)
+
+原生 Polars 向量化,全 A 股一次扫表落盘 enriched Parquet:
+
+- **均线 / 趋势**:MA(5-60)· EMA · MACD · 动量 · 布林带
+- **震荡 / 波动**:RSI · KDJ · ATR · 年化波动率 · 振幅
+- **量能 / 涨跌停**:量比 · 量均线 · 涨停信号 · 连板数
+- **原子信号**:MA / MACD 金叉死叉 · N 日新高新低 · 布林突破
+- **复权**:基于除权因子自动前复权,回测与指标口径一致
+
+### 🧪 回测引擎(Backtest)
+
+基于 vectorbt:**三种模式**(个股 / 策略组合 / 自由信号组合),真实约束(T+1 · 手续费 · 滑点 · 止损 · 最大持仓天数),组合管理(最大持仓 · 敞口 · 等权 / 自定义仓位)。SSE 流式进度支持切页重连,输出净值曲线 · 夏普 · 最大回撤 · 胜率 · 交易明细。
+
+### 📡 监控中心(Monitor)
+
+统一规则引擎,一个页面管理**四类监控**(策略 · 个股信号 · 价格涨跌 · 全市场异动):
+
+- 多条件 AND/OR + 冷却期去重 + 严重级别(info/warn/critical)
+- 多入口配置:监控中心新建 / 个股详情页「加监控」/ 策略卡片一键开启
+- 命中后右下角弹窗(可配声效)+ 持久化到 `alerts.jsonl`,菜单未读徽标
+
+### 📈 个股分析(Beta)
+
+以「行情 + 关键价位」为主体的单标的决策页:
+
+- **专用日 K 图表**:主图 + 成交量 + 滑块,默认近 6 个月
+- **9 类关键价位**(纯函数实时计算,毫秒级):压力支撑 · 成交密集区 · 枢轴点 · 前高前低 · Keltner 通道 · ATR 止损 · 缺口位 · 斐波那契 · 整数关口
+- **AI 四维分析**:技术 / 基本面 / 财务 / 消息面流式生成,实战派交易员视角
+
+### 🧰 数据与扩展
+
+- **TickFlow 多源数据**:日 K / 分钟 K / 指数 / 财务 / 实时行情
+- **🔌 第三方接入(重点)**:Tushare 等 HTTP 定时拉取 · CSV / Excel 上传 · JSON 写入,自动 schema 发现 + 符号归一,页面可视化配置,**可与自有量化项目数据并入 DuckDB 同台分析**
+- **盘后定时管道**:APScheduler 15:30 CST 自动拉日 K + 重算 enriched + 跑监控
+- **令牌桶限流**:适配各档位 rpm / batch,批量合并 + 增量拉取
---
## ⚙️ 配置
-所有配置通过项目根目录的 `.env` 文件读取(复制 `.env.example` 开始)。配置也可在面板 **设置** 页面内修改。
+所有配置从根目录 `.env` 读取(复制 `.env.example` 开始),也可在面板 **设置** 页修改。
### 数据源:TickFlow
-TickFlow 提供订阅制 A 股数据。**留空 `TICKFLOW_API_KEY` 即启用 None 模式,可通过 free-api 使用历史日K;当日数据通常需盘后 1-2 小时可用**。免费注册并填写 Key 后进入 Free 模式,可开启自选股实时监控。
-
```ini
-TICKFLOW_API_KEY= # 留空 = None 模式;填入 Key = 按订阅档位解锁
+TICKFLOW_API_KEY= # 留空 = None 模式(历史日K免费);填 Key = 按订阅档位解锁
```
-> 完整能力矩阵见 [tickflow.org/pricing](https://tickflow.org/pricing/)。系统启动时会自动探测你的真实能力集,UI 显示「Free / Starter / Pro / Expert」等友好标签。高等档位包含较低档位的全部权益。
->
-> 当前面板使用的实时能力:Free = 自选页前 5 个标的实时监控(最低 6 秒刷新);Starter+ = 全市场实时行情;Pro = 分钟K + 盘口;Expert = WebSocket + 财务数据。
+留空即 None 模式,通过 free-api 使用历史日 K(当日数据盘后 1-2 小时可用);免费注册 Key 后进 Free 模式,开启自选股实时监控。**实时行情按档位**:
-### AI(可选):策略生成
+| 档位 | 实时能力 |
+| :------- | :--------------------------------------- |
+| Free | 自选页前 5 个标的实时监控(最低 6 秒刷新) |
+| Starter+ | 全市场实时行情 |
+| Pro | 分钟 K + 盘口 |
+| Expert | WebSocket + 财务数据 |
-AI 模块用于「自然语言生成策略代码」。**所有配置留空即跳过 AI 功能,不影响核心使用**。支持任何 **OpenAI 兼容接口**:
+> 完整能力矩阵见 [tickflow.org/pricing](https://tickflow.org/pricing/),高等档位含较低档全部权益。
+
+### AI(可选)
+
+用于自然语言生成策略。**所有配置留空即跳过**,不影响核心功能。支持任意 OpenAI 兼容接口:
```ini
AI_PROVIDER=openai_compat # openai_compat | ollama
@@ -307,8 +218,6 @@ AI_MODEL=deepseek-chat
AI_DAILY_TOKEN_BUDGET=500000 # 每日 token 预算上限
```
-> 切换 `AI_PROVIDER=ollama` 时无需 `AI_API_KEY`,适合本地部署大模型。
-
### 服务与数据
```ini
@@ -322,50 +231,36 @@ DATA_DIR=./data # Parquet / DuckDB 数据存储目录
## 🏗️ 技术栈
-| 层 | 选型 |
-| :--- | :--- |
-| **后端** | FastAPI · Pydantic v2 · APScheduler · sse-starlette |
-| **数据** | Polars(计算)· DuckDB(查询)· Parquet(存储)· PyArrow |
-| **回测** | vectorbt(全项目唯一 pandas 边界) |
-| **数据源** | [TickFlow](https://tickflow.org/auth/register?ref=V3KDKGXPEA) 官方 SDK(`tickflow[all]`) |
-| **AI**(可选) | OpenAI 兼容接口(DeepSeek / 通义 / Ollama 等) |
-| **前端** | React 18 · Vite · TypeScript · Tailwind CSS · Framer Motion · Tanstack Query · Lightweight Charts · ECharts · dnd-kit |
-| **部署** | Docker 两阶段构建,前端 dist 拷进后端镜像,**单容器** |
+| 层 | 选型 |
+| :----------- | :------------------------------------------------------------------------------------------------ |
+| **后端** | FastAPI · Pydantic v2 · APScheduler · sse-starlette |
+| **数据** | Polars(计算)· DuckDB(查询)· Parquet(存储) |
+| **回测** | vectorbt(全项目唯一 pandas 边界) |
+| **数据源** | [TickFlow](https://tickflow.org/auth/register?ref=V3KDKGXPEA) 官方 SDK 、其他数据源后续迭代实装 |
+| **AI**(可选) | OpenAI 兼容接口(DeepSeek / 通义 / Ollama 等) |
+| **前端** | React 18 · Vite · TypeScript · Tailwind · Tanstack Query · Lightweight Charts · ECharts · dnd-kit |
+| **部署** | Docker 两阶段构建,前端 dist 拷进后端镜像,**单容器** |
---
## 🗺️ 路线图
-| Phase | 内容 | 状态 |
-| :--- | :--- | :--- |
-| **0** | 仓库骨架 / FastAPI 壳 / Vite + React SPA / Docker 一键起 | ✅ |
-| **1** | 能力探测 + Kline 同步 + K 线分析页 | ✅ |
-| **2** | Polars enriched 流水线 + Screener + 信号扫描 | ✅ |
-| **3** | vectorbt 回测 + T+1 + 手续费 + 止损 + max-hold | ✅ |
-| **4** | 监控引擎 + 告警规则 + Webhook + APScheduler 盘后定时 | ✅ |
-| **5** | 统一监控中心 + 四类监控规则 + 实时推送 + 持久化触发记录 + 声效通知 | ✅ |
-| **6** | 个股分析(专用日 K + 9 类关键价位 + AI 四维分析 + 报告持久化) | ✅ |
-| **v2** | Webhook 推送(QMT/掘金下单) · 板块异动 · 早晚报 · 更多扩展 | 🚧 |
+| Phase | 内容 | 状态 |
+| :----- | :----------------------------------------------------------------- | :--- |
+| 0-1 | 仓库骨架 · FastAPI 壳 · 能力探测 · K 线同步与分析页 | ✅ |
+| 2-3 | Polars enriched 流水线 · Screener · vectorbt 回测(T+1/手续费/止损) | ✅ |
+| 4-5 | 监控引擎 · 四类监控规则 · 实时 SSE 推送 · 持久化记录 | ✅ |
+| 6 | 个股分析(专用日 K + 9 类关键价位 + AI 四维分析) | ✅ |
+| **v2** | Webhook 推送(QMT/掘金下单)· 板块异动 · 早晚报 · 更多扩展 | 🚧 |
---
-## 📚 文档
+## 📚 文档与贡献
-- [docs/strategy-guide.md](./docs/strategy-guide.md) —— 策略开发指南(AI 生成器与手写策略的规范)
+- [docs/strategy-guide.md](./docs/strategy-guide.md) —— 策略开发指南(AI 生成与手写规范)
- [docs/](./docs) —— 策略构建步骤、示例
----
-
-## 🤝 贡献
-
-欢迎 Issue 和 PR。本地开发:
-
-```bash
-cd backend && uv sync --extra backtest # 含回测依赖
-cd ../frontend && pnpm install && pnpm dev
-```
-
-新增内置策略:在 `backend/app/strategy/builtin/` 参照现有策略文件,实现 `StrategyDef` 即可被引擎自动发现。
+欢迎 Issue 和 PR。新增内置策略:在 `backend/app/strategy/builtin/` 参照现有文件实现 `StrategyDef`,引擎自动发现。
---
@@ -373,13 +268,9 @@ cd ../frontend && pnpm install && pnpm dev
本项目仅供**学习与量化研究**,**不构成任何投资建议**。回测结果不代表未来收益。A 股有风险,入市需谨慎。数据准确性以数据源 TickFlow 官方为准。
----
-
## 📄 License
-[MIT](./LICENSE) © tickflow-stock-panel contributors
-
-本项目依赖 [TickFlow](https://tickflow.org/auth/register?ref=V3KDKGXPEA) 提供数据服务,使用前请遵守其服务条款。
+[MIT](./LICENSE) © tickflow-stock-panel contributors · 本项目依赖 [TickFlow](https://tickflow.org/auth/register?ref=V3KDKGXPEA) 提供数据服务,使用前请遵守其服务条款。
## 社区
diff --git a/backend/app/__init__.py b/backend/app/__init__.py
index 31c4976..27d020f 100644
--- a/backend/app/__init__.py
+++ b/backend/app/__init__.py
@@ -2,7 +2,7 @@
import sys
-__version__ = "0.1.60"
+__version__ = "0.1.61"
# Windows 默认 stdout/stderr 编码为 GBK(cp936),TickFlow SDK 内部输出含 emoji 的
# 指数/标的名称(如 \U0001f193)时会抛 UnicodeEncodeError,导致请求失败。
diff --git a/backend/app/services/market_recap.py b/backend/app/services/market_recap.py
index 6c5ef3d..eb4dc2a 100644
--- a/backend/app/services/market_recap.py
+++ b/backend/app/services/market_recap.py
@@ -24,6 +24,15 @@ from app.services.market_overview_builder import build_market_overview
logger = logging.getLogger(__name__)
+# 指数简称映射:摘要里用简称(上/深/创/科),全称太长列表放不下。与前端 INDEX_SHORT 对齐。
+_INDEX_SHORT = {
+ "上证指数": "上",
+ "深证成指": "深",
+ "创业板指": "创",
+ "科创综指": "科",
+ "科创50": "科",
+}
+
# ================================================================
# 系统提示词(市场策略师人格 + 固定七节模板)
# ================================================================
@@ -54,7 +63,7 @@ _SYSTEM_PROMPT = """你是一位拥有 15 年 A 股一线实战经验的资深
成交额结构(增量/存量)、市场宽度(上涨占比、站上均线占比)、量能指标(量比)解读;风险偏好是修复还是转弱。
### 6. 📰 消息催化
-结合提供的近期新闻,提炼真正影响明日交易节奏的催化或扰动。明确区分"已兑现"与"待发酵"。**若提供了"无新闻数据"的说明,则本节基于量价异动进行[推断],并如实标注,不要编造具体消息。**
+结合提供的近期新闻,提炼真正影响明日交易节奏的催化或扰动,明确区分"已兑现"与"待发酵"。**若无新闻数据,则直接从量价异动推断可能的催化逻辑并给出结论,不要标注"[推断]"之类的过程标签,更不要编造具体消息。**
### 7. 🎯 明日交易计划
- 进攻 / 均衡 / 防守:基于今日盘面给出次日基调
@@ -68,6 +77,7 @@ _SYSTEM_PROMPT = """你是一位拥有 15 年 A 股一线实战经验的资深
## 分析准则(务必遵守)
+0. **只输出结论,不输出思考过程**:禁止复述你的分析步骤或方法论。不要写"我先按...做结构化复盘""接下来看...""基于上述数据我认为"这类元话语——直接给结论。读者要的是复盘结果,不是你怎么推导出来的。
1. **数据说话**:每个判断引用具体数值,严禁空泛套话("情绪回暖"必须改成"涨停 68 家较前日 +22,封板率 75%")
2. **诚实中立**:看多就写多,看空就写空,不要骑墙;数据不支持时直言无法判断
3. **结构优先**:先看指数同步性与量能结构,再看板块与情绪,最后才是消息
@@ -202,7 +212,7 @@ def _build_user_prompt(overview: dict, news: list[dict], focus: str) -> str:
"",
"## 近期市场新闻",
"(暂无新闻数据:本功能新闻检索能力将在后续版本接入。"
- "请按系统提示词第 6 节的说明,基于量价异动进行[推断],并如实标注,不要编造具体消息。)",
+ "消息催化一节请直接从量价异动给出可能的催化逻辑结论,不要编造具体消息,也不要复述本说明。)",
])
if focus.strip():
@@ -216,7 +226,10 @@ def _build_user_prompt(overview: dict, news: list[dict], focus: str) -> str:
# ================================================================
def _recap_summary(overview: dict) -> str:
- """一句话摘要(供 meta 事件与历史列表展示)。"""
+ """一句话摘要(供 meta 事件与历史列表展示)。
+
+ 指数用简称(上/深/创/科),与前端摘要条一致,避免列表里全称放不下。
+ """
indices = overview.get("indices") or []
emo = overview.get("emotion") or {}
lim = overview.get("limit") or {}
@@ -224,8 +237,8 @@ def _recap_summary(overview: dict) -> str:
total_amount = (amt.get("total") or 0) / 1e8
idx_str = "、".join(
- f"{(i.get('name') or '')}{(i.get('change_pct') or 0):+.2f}%"
- for i in indices[:3]
+ f"{_INDEX_SHORT.get(i.get('name') or '', i.get('name') or '')}{(i.get('change_pct') or 0):+.2f}%"
+ for i in indices[:4]
) or "指数缺失"
return (
f"{idx_str} | 情绪{emo.get('score',50)}({emo.get('label','—')}) | "
diff --git a/backend/app/services/market_recap_reports.py b/backend/app/services/market_recap_reports.py
index f3ad6c4..06be304 100644
--- a/backend/app/services/market_recap_reports.py
+++ b/backend/app/services/market_recap_reports.py
@@ -28,7 +28,7 @@ from pathlib import Path
logger = logging.getLogger(__name__)
-MAX_REPORTS = 30
+MAX_REPORTS = 20
def _path() -> Path:
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 2d7b567..4bc1742 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "tickflow-stock-panel-backend"
-version = "0.1.60"
+version = "0.1.61"
description = "A 股选股 + 监控 + 回测面板 — TickFlow 适配"
readme = "../README.md"
requires-python = ">=3.11"
diff --git a/data/.gitkeep b/data/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/data/ext_data/ext_gn_ths/config.json b/data/ext_data/ext_gn_ths/config.json
index e83ed20..c935093 100644
--- a/data/ext_data/ext_gn_ths/config.json
+++ b/data/ext_data/ext_gn_ths/config.json
@@ -29,7 +29,7 @@
"label": "所属概念"
}
],
- "description": "ths概念、日更",
+ "description": "",
"symbol_map": {
"type": "mapped",
"col": "股票代码"
@@ -39,6 +39,6 @@
"from": "symbol",
"method": "strip_exchange"
},
- "created_at": "2026-06-19T19:56:33.098883",
- "updated_at": "2026-06-19T19:56:33.098883"
+ "created_at": "2026-06-27T12:18:23.629390",
+ "updated_at": "2026-06-27T12:18:23.629390"
}
\ No newline at end of file
diff --git a/data/ext_data/ext_gn_ths/part.parquet b/data/ext_data/ext_gn_ths/part.parquet
index 53d1c9d..1525760 100644
Binary files a/data/ext_data/ext_gn_ths/part.parquet and b/data/ext_data/ext_gn_ths/part.parquet differ
diff --git a/data/ext_data/ext_hy_ths/config.json b/data/ext_data/ext_hy_ths/config.json
index 411faf3..97dabc8 100644
--- a/data/ext_data/ext_hy_ths/config.json
+++ b/data/ext_data/ext_hy_ths/config.json
@@ -26,10 +26,20 @@
{
"name": "所属同花顺行业",
"dtype": "string",
- "label": "所属行业"
+ "label": "所属同花顺行业"
+ },
+ {
+ "name": "thscode",
+ "dtype": "string",
+ "label": "thscode"
+ },
+ {
+ "name": "time",
+ "dtype": "string",
+ "label": "time"
}
],
- "description": "ths行业、月更",
+ "description": "",
"symbol_map": {
"type": "mapped",
"col": "股票代码"
@@ -39,6 +49,6 @@
"from": "symbol",
"method": "strip_exchange"
},
- "created_at": "2026-06-19T19:57:17.004819",
- "updated_at": "2026-06-19T19:57:17.004819"
+ "created_at": "2026-06-27T12:18:42.205794",
+ "updated_at": "2026-06-27T12:18:42.205794"
}
\ No newline at end of file
diff --git a/data/ext_data/ext_hy_ths/part.parquet b/data/ext_data/ext_hy_ths/part.parquet
index b41abe0..018630b 100644
Binary files a/data/ext_data/ext_hy_ths/part.parquet and b/data/ext_data/ext_hy_ths/part.parquet differ
diff --git a/data/ext_data/ext_kz_ths/config.json b/data/ext_data/ext_kz_ths/config.json
deleted file mode 100644
index 540b5d2..0000000
--- a/data/ext_data/ext_kz_ths/config.json
+++ /dev/null
@@ -1,79 +0,0 @@
-{
- "id": "ext_kz_ths",
- "label": "扩展快照ths",
- "mode": "snapshot",
- "fields": [
- {
- "name": "symbol",
- "dtype": "string",
- "label": "标的代码"
- },
- {
- "name": "code",
- "dtype": "string",
- "label": "代码"
- },
- {
- "name": "股票代码",
- "dtype": "string",
- "label": "股票代码"
- },
- {
- "name": "股票简称",
- "dtype": "string",
- "label": "股票简称"
- },
- {
- "name": "所属概念",
- "dtype": "string",
- "label": "所属概念"
- },
- {
- "name": "人气排名",
- "dtype": "int",
- "label": "人气排名"
- },
- {
- "name": "资金流向",
- "dtype": "float",
- "label": "资金流向"
- },
- {
- "name": "PE",
- "dtype": "float",
- "label": "PE"
- },
- {
- "name": "PB",
- "dtype": "float",
- "label": "PB"
- },
- {
- "name": "ROE",
- "dtype": "float",
- "label": "ROE"
- },
- {
- "name": "营收",
- "dtype": "float",
- "label": "营收"
- },
- {
- "name": "净利润",
- "dtype": "float",
- "label": "净利润"
- }
- ],
- "description": "",
- "symbol_map": {
- "type": "mapped",
- "col": "股票代码"
- },
- "code_map": {
- "type": "computed",
- "from": "symbol",
- "method": "strip_exchange"
- },
- "created_at": "2026-06-05T13:48:50.034495",
- "updated_at": "2026-06-05T16:02:52.060612"
-}
\ No newline at end of file
diff --git a/data/ext_data/ext_kz_ths/part.parquet b/data/ext_data/ext_kz_ths/part.parquet
deleted file mode 100644
index e6e5782..0000000
Binary files a/data/ext_data/ext_kz_ths/part.parquet and /dev/null differ
diff --git a/frontend/package.json b/frontend/package.json
index d7d3f5f..ff873ea 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "tickflow-stock-panel-frontend",
"private": true,
- "version": "0.1.60",
+ "version": "0.1.61",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/components/financials/MarkdownRenderer.tsx b/frontend/src/components/financials/MarkdownRenderer.tsx
index 8fdfc93..53fa293 100644
--- a/frontend/src/components/financials/MarkdownRenderer.tsx
+++ b/frontend/src/components/financials/MarkdownRenderer.tsx
@@ -143,7 +143,7 @@ export function MarkdownRenderer({ content }: { content: string }) {
{header.map((cell, ci) => (
- |
+ |
{renderInline(cell, `th-${key}-${ci}`)}
|
))}
@@ -153,7 +153,7 @@ export function MarkdownRenderer({ content }: { content: string }) {
{body.map((row, ri) => (
{row.map((cell, ci) => (
- |
+ |
{renderInline(cell, `td-${key}-${ri}-${ci}`)}
|
))}
@@ -178,7 +178,7 @@ export function MarkdownRenderer({ content }: { content: string }) {
blocks.push(
{items.map((item, ii) => (
- -
+
-
{renderInline(item, `li-${key}-${ii}`)}
@@ -202,7 +202,7 @@ export function MarkdownRenderer({ content }: { content: string }) {
{ii + 1}
- {renderInline(item, `ol-${key}-${ii}`)}
+ {renderInline(item, `ol-${key}-${ii}`)}
))}
,
@@ -212,7 +212,7 @@ export function MarkdownRenderer({ content }: { content: string }) {
// 普通段落
blocks.push(
-
+
{renderInline(trimmed, `p-${key}`)}
,
)
diff --git a/frontend/src/lib/reviewStore.ts b/frontend/src/lib/reviewStore.ts
new file mode 100644
index 0000000..78473ef
--- /dev/null
+++ b/frontend/src/lib/reviewStore.ts
@@ -0,0 +1,162 @@
+/**
+ * 复盘生成状态的全局单例 store —— 脱离 Review 组件生命周期。
+ *
+ * 解决的问题:生成中切换到其他页面,Review 组件卸载会丢失 phase/content。
+ * 本 store 把流式生成的状态提到模块级,组件卸载后流仍在后台继续跑,
+ * 回到页面订阅即可恢复显示。
+ *
+ * 设计:
+ * - 模块级 state(phase/content/meta/focus),唯一的生成实例
+ * - AbortController 存模块级 ref,组件卸载不中断流
+ * - 订阅者列表(notify 机制),Review mount 时订阅、unmount 时退订
+ */
+import { api } from '@/lib/api'
+
+export type ReviewPhase = 'idle' | 'loading' | 'streaming' | 'done' | 'error'
+
+export interface ReviewMeta {
+ as_of?: string
+ emotion_score?: number
+ emotion_label?: string
+ summary?: string
+}
+
+export interface ReviewState {
+ phase: ReviewPhase
+ content: string
+ error: string
+ meta: ReviewMeta | null
+ focus: string
+}
+
+const INITIAL: ReviewState = { phase: 'idle', content: '', error: '', meta: null, focus: '' }
+
+// ===== 模块级单例状态(组件卸载不销毁)=====
+let state: ReviewState = { ...INITIAL }
+let abortCtrl: AbortController | null = null
+
+// ===== 订阅机制 =====
+type Listener = () => void
+const listeners = new Set()
+
+function notify() {
+ for (const l of listeners) l()
+}
+
+export function getReviewState(): ReviewState {
+ return state
+}
+
+export function subscribeReview(listener: Listener): () => void {
+ listeners.add(listener)
+ return () => { listeners.delete(listener) }
+}
+
+// 暴露给组件直接读取最新 meta(用于自动归档,避免闭包取旧值)
+export function getReviewMeta(): ReviewMeta | null {
+ return state.meta
+}
+
+/** 是否正在生成(loading 或 streaming) */
+export function isReviewGenerating(): boolean {
+ return state.phase === 'loading' || state.phase === 'streaming'
+}
+
+/**
+ * 启动复盘生成。返回后流在后台独立运行,组件卸载不影响。
+ * @param asOf 复盘日期
+ * @param focus 用户追加的复盘关注点
+ * @param onDone 完成回调(供调用方做自动归档)
+ */
+export async function startReviewGeneration(
+ asOf: string | undefined,
+ focus: string,
+ onDone?: (fullContent: string, meta: ReviewMeta | null) => void,
+): Promise {
+ // 已在生成中,不重复启动
+ if (isReviewGenerating()) return
+
+ state = { phase: 'loading', content: '', error: '', meta: null, focus }
+ notify()
+
+ abortCtrl = new AbortController()
+ let buf = ''
+ let failed = false
+ let doneMeta: ReviewMeta | null = null
+
+ try {
+ for await (const evt of api.reviewStream(asOf, focus)) {
+ if (abortCtrl.signal.aborted) break
+ if (evt.type === 'meta') {
+ doneMeta = evt
+ state = { ...state, meta: evt }
+ notify()
+ } else if (evt.type === 'delta' && evt.content) {
+ buf += evt.content
+ state = { ...state, content: buf, phase: 'streaming' }
+ notify()
+ } else if (evt.type === 'error') {
+ failed = true
+ state = { ...state, error: evt.message ?? '复盘失败', phase: 'error' }
+ notify()
+ return
+ } else if (evt.type === 'done') {
+ state = { ...state, phase: 'done' }
+ notify()
+ }
+ }
+ // 流正常结束但无 done 事件,按 done 处理
+ if (buf && !failed) {
+ state = { ...state, phase: 'done' }
+ notify()
+ // 自动归档
+ if (buf && !failed) {
+ onDone?.(buf, doneMeta)
+ }
+ }
+ } catch (e: any) {
+ if (!abortCtrl.signal.aborted) {
+ state = { ...state, error: e?.message ?? '复盘失败', phase: 'error' }
+ notify()
+ }
+ } finally {
+ abortCtrl = null
+ }
+}
+
+/** 中断当前生成(供"查看历史"等场景主动中断流)。 */
+export function abortReviewGeneration(): void {
+ abortCtrl?.abort()
+ abortCtrl = null
+}
+
+/** 设置当前查看的历史报告(把 store 状态切到 done + 该报告内容)。 */
+export function setViewingReport(report: {
+ content: string
+ as_of?: string
+ emotion_score?: number | null
+ emotion_label?: string
+ summary?: string
+}): void {
+ abortCtrl?.abort()
+ abortCtrl = null
+ state = {
+ phase: 'done',
+ content: report.content,
+ error: '',
+ meta: {
+ as_of: report.as_of,
+ emotion_score: report.emotion_score ?? undefined,
+ emotion_label: report.emotion_label,
+ summary: report.summary,
+ },
+ focus: state.focus,
+ }
+ notify()
+}
+
+/** 重置到 idle(清空当前显示)。 */
+export function resetReview(): void {
+ state = { ...INITIAL }
+ notify()
+}
diff --git a/frontend/src/lib/useReviewStore.ts b/frontend/src/lib/useReviewStore.ts
new file mode 100644
index 0000000..3fc874f
--- /dev/null
+++ b/frontend/src/lib/useReviewStore.ts
@@ -0,0 +1,14 @@
+/**
+ * 订阅 reviewStore 的 React hook。
+ *
+ * mount 时订阅,store 变化触发 re-render;unmount 时退订(但 store 流继续跑)。
+ * 用 useSyncExternalStore 保证与 React 18 并发模式兼容。
+ */
+import { useSyncExternalStore } from 'react'
+import {
+ getReviewState, subscribeReview, type ReviewState,
+} from '@/lib/reviewStore'
+
+export function useReviewState(): ReviewState {
+ return useSyncExternalStore(subscribeReview, getReviewState, getReviewState)
+}
diff --git a/frontend/src/pages/Financials.tsx b/frontend/src/pages/Financials.tsx
index a8552e0..c8302bd 100644
--- a/frontend/src/pages/Financials.tsx
+++ b/frontend/src/pages/Financials.tsx
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
-import { RefreshCw, Lock, Loader2, X, Search, FileText, Database, Clock, CheckCircle2, Hourglass } from 'lucide-react'
+import { RefreshCw, Lock, Loader2, X, Search, FileText, Database, Clock, CheckCircle2, Hourglass, Lightbulb, ExternalLink } from 'lucide-react'
import { PageHeader } from '@/components/PageHeader'
import { EmptyState } from '@/components/EmptyState'
import { useCapabilities } from '@/lib/useSharedQueries'
@@ -70,6 +70,25 @@ export function Financials() {
财务数据接口仅 Expert 套餐可用。升级后此页自动显示财务数据面板。
+ {/* 当前财务数据源(TickFlow)需付费,后续将接入免费数据源;期间欢迎在 issues 推荐免费源 */}
+
+
+
+ 关于数据源
+
+
+ 当前财务数据源需付费,后续会接入免费数据源。如你常用某个免费财务数据源,欢迎在 Issues 中多多推荐哈 ~
+
+
+ 前往 Issues 推荐
+
+
+
>
diff --git a/frontend/src/pages/Review.tsx b/frontend/src/pages/Review.tsx
index 36c7674..14d5a3a 100644
--- a/frontend/src/pages/Review.tsx
+++ b/frontend/src/pages/Review.tsx
@@ -1,10 +1,10 @@
/**
- * AI 大盘复盘页 —— 盘后复盘看板 + 流式 LLM 复盘报告 + 历史归档。
+ * AI 大盘复盘页 —— 以流式 LLM 复盘报告为主体的盘后复盘工作台。
*
- * 数据分工:
- * - 顶部看板(指数/涨跌/连板/封板/情绪雷达)来自 GET /api/overview/market
- * - 复盘报告(markdown)由 POST /api/market-recap/analyze 流式生成
- * 视觉语言对齐 Dashboard:A 股红涨绿跌、rounded-card 卡片、SectionTitle 层级。
+ * 设计定位:极简专注型。不复刻 Dashboard 的看板(KPI/雷达/板块排名),
+ * 仅保留一行「市场摘要条」作为报告上下文参照;AI 报告 + 历史归档是页面主体。
+ * - 摘要数据:GET /api/overview/market
+ * - 报告流式:POST /api/market-recap/analyze
*/
import { useCallback, useEffect, useRef, useState } from 'react'
import { Link } from 'react-router-dom'
@@ -12,16 +12,21 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { motion } from 'framer-motion'
import {
BookOpenCheck, RefreshCw, Sparkles, Trash2, History, ChevronRight, AlertTriangle,
- BarChart3, Activity, Layers, ArrowUpRight, ArrowDownRight, Database, Wand2,
+ Database, Wand2, Copy, Download,
} from 'lucide-react'
import { api, type OverviewMarket, type AiReviewReport } from '@/lib/api'
import { QK } from '@/lib/queryKeys'
import { cn } from '@/lib/cn'
-import { fmtPrice } from '@/lib/format'
+import { fmtBigNum } from '@/lib/format'
import { PageHeader } from '@/components/PageHeader'
import { MarkdownRenderer } from '@/components/financials/MarkdownRenderer'
import { toast } from '@/components/Toast'
+import { useReviewState } from '@/lib/useReviewStore'
+import {
+ startReviewGeneration, resetReview, isReviewGenerating,
+ type ReviewPhase,
+} from '@/lib/reviewStore'
// ================================================================
// 涨跌幅格式化(注意单位差异)
@@ -47,19 +52,27 @@ function scoreColor(v: number | null | undefined): string {
return '#12B76A'
}
-type Phase = 'idle' | 'loading' | 'streaming' | 'done' | 'error'
+// 归档时刻格式化:ISO → "MM-DD HH:mm"(用于历史列表显示复盘时间)
+function fmtArchivedAt(iso: string): string {
+ const d = new Date(iso)
+ if (Number.isNaN(d.getTime())) return iso
+ const mm = String(d.getMonth() + 1).padStart(2, '0')
+ const dd = String(d.getDate()).padStart(2, '0')
+ const hh = String(d.getHours()).padStart(2, '0')
+ const mi = String(d.getMinutes()).padStart(2, '0')
+ return `${mm}-${dd} ${hh}:${mi}`
+}
+
+// Phase 类型复用 store 的定义(单一来源)
export function Review() {
const qc = useQueryClient()
// 复盘日期:当前固定取最新交易日(后续如需日期选择可改回 useState)
const asOf: string | undefined = undefined
const [focus, setFocus] = useState('')
- const [phase, setPhase] = useState('idle')
- const [content, setContent] = useState('')
- const [error, setError] = useState('')
- const [meta, setMeta] = useState<{ as_of?: string; emotion_score?: number; emotion_label?: string; summary?: string } | null>(null)
+ // 生成状态走全局 store:切走页面流不中断,回来可恢复
+ const { phase, content, error, meta } = useReviewState()
const [viewing, setViewing] = useState(null) // 查看历史报告
- const abortRef = useRef(null)
const reportEndRef = useRef(null)
// 看板数据(与总览页同源)
@@ -92,80 +105,70 @@ export function Review() {
}
}, [content, phase])
- // 主流程:生成复盘
- const generate = useCallback(async () => {
- if (phase === 'loading' || phase === 'streaming') return
- setViewing(null)
- setPhase('loading')
- setContent('')
- setError('')
- setMeta(null)
-
- const ctrl = new AbortController()
- abortRef.current = ctrl
- let buf = ''
- let failed = false
- try {
- for await (const evt of api.reviewStream(asOf, focus)) {
- if (ctrl.signal.aborted) break
- if (evt.type === 'meta') {
- setMeta(evt)
- } else if (evt.type === 'delta' && evt.content) {
- buf += evt.content
- setContent(buf)
- setPhase('streaming')
- } else if (evt.type === 'error') {
- failed = true
- setError(evt.message ?? '复盘失败')
- setPhase('error')
- return
- } else if (evt.type === 'done') {
- setPhase('done')
- }
- }
- // 流正常结束但无 done 事件,按 done 处理
- if (buf && !failed) setPhase('done')
- } catch (e: any) {
- if (!ctrl.signal.aborted) {
- setError(e?.message ?? '复盘失败')
- setPhase('error')
- }
- } finally {
- abortRef.current = null
- }
- }, [asOf, focus, phase])
-
- // 保存当前报告
- const saveCurrent = useCallback(async () => {
- if (!content) return
- const reportAsOf = meta?.as_of ?? marketQuery.data?.as_of ?? asOf ?? new Date().toISOString().slice(0, 10)
+ // 自动归档(生成完成后台静默保存)—— 通过回调注入 store,避免 store 直接依赖 qc/marketQuery
+ const onGenerationDone = useCallback(async (fullContent: string, doneMeta: { as_of?: string; summary?: string; emotion_score?: number; emotion_label?: string } | null) => {
+ const reportAsOf = doneMeta?.as_of ?? marketQuery.data?.as_of ?? asOf ?? new Date().toISOString().slice(0, 10)
try {
await api.reviewReportSave({
as_of: reportAsOf,
focus,
- content,
- summary: meta?.summary,
- emotion_score: meta?.emotion_score ?? null,
- emotion_label: meta?.emotion_label ?? '',
+ content: fullContent,
+ summary: doneMeta?.summary,
+ emotion_score: doneMeta?.emotion_score ?? null,
+ emotion_label: doneMeta?.emotion_label ?? '',
})
qc.invalidateQueries({ queryKey: QK.reviewReports })
- toast('复盘已归档', 'success')
- } catch { /* request() 已 toast */ }
- }, [content, meta, asOf, focus, marketQuery.data, qc])
+ } catch { /* 静默 */ }
+ }, [focus, asOf, marketQuery.data, qc])
- // 查看历史报告
+ // 主流程:生成复盘(委托给全局 store,流在后台独立运行)
+ const generate = useCallback(() => {
+ if (isReviewGenerating()) return
+ setViewing(null)
+ resetReview()
+ startReviewGeneration(asOf, focus, (full, doneMeta) => {
+ onGenerationDone(full, doneMeta).catch(() => { /* 静默 */ })
+ })
+ }, [asOf, focus, onGenerationDone])
+
+ // 复制全文到剪贴板(viewing 优先,与主区域显示一致)
+ const copyContent = useCallback(async () => {
+ const text = viewing?.content ?? content
+ if (!text) return
+ try {
+ await navigator.clipboard.writeText(text)
+ toast('已复制到剪贴板', 'success')
+ } catch {
+ toast('复制失败,请手动选择文本', 'error')
+ }
+ }, [content, viewing])
+
+ // 下载为 .md 文件(viewing 优先)
+ const downloadContent = useCallback(() => {
+ const text = viewing?.content ?? content
+ if (!text) return
+ const reportDate = viewing?.as_of ?? meta?.as_of ?? asOf ?? new Date().toISOString().slice(0, 10)
+ const blob = new Blob([text], { type: 'text/markdown;charset=utf-8' })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = `复盘_${reportDate}.md`
+ a.click()
+ URL.revokeObjectURL(url)
+ }, [content, viewing, meta, asOf])
+
+ // 查看历史报告(不中断后台生成:仅临时把 viewing 覆盖到主区域,
+ // 生成中的流仍在 store 里继续跑,点"生成中"项即可切回)
const viewReport = useCallback((r: AiReviewReport) => {
- abortRef.current?.abort()
setViewing(r)
- setContent(r.content)
- setMeta({ as_of: r.as_of, emotion_score: r.emotion_score ?? undefined, emotion_label: r.emotion_label, summary: r.summary })
- setPhase('done')
- setError('')
}, [])
const isGenerating = phase === 'loading' || phase === 'streaming'
const displayDate = viewing?.as_of ?? meta?.as_of ?? marketQuery.data?.as_of ?? asOf ?? '最新'
const data = marketQuery.data
+ // 主区域显示的内容:viewing(查看历史)优先于 store 的生成 content,
+ // 这样点历史报告不会覆盖后台生成中的流。
+ const displayContent = viewing?.content ?? content
return (
<>
@@ -179,7 +182,7 @@ export function Review() {
onClick={() => { marketQuery.refetch() }}
disabled={marketQuery.isFetching}
className="inline-flex items-center gap-1 rounded-btn border border-border bg-elevated px-2 py-1 text-[11px] text-secondary transition-colors hover:text-foreground disabled:opacity-50"
- title="刷新看板数据"
+ title="刷新市场数据"
>
刷新
@@ -204,7 +207,7 @@ export function Review() {
/>
-
+
{marketQuery.isLoading && !data ? (
@@ -233,27 +236,8 @@ export function Review() {
) : (
<>
- {/* ===== 指数行情条(对齐 Dashboard IndexTicker) ===== */}
-
- {data.indices.map(item => )}
-
-
- {/* ===== KPI 网格 ===== */}
-
- {data.breadth.up}/{data.breadth.flat}/{data.breadth.down}>} sub={`上涨率 ${data.breadth.up_pct.toFixed(1)}%`} />
- {data.limit.limit_up}/{data.limit.limit_down}>} sub={`封板率 ${(data.limit.seal_rate ?? 0).toFixed(0)}% · 炸板 ${data.limit.broken ?? 0}`} />
-
-
-
-
-
-
- {/* ===== 情绪雷达 + 板块排名 双栏 ===== */}
-
-
-
-
-
+ {/* ===== 市场摘要条(轻量上下文,非重复看板)===== */}
+
{/* ===== 关注点输入 ===== */}
@@ -270,15 +254,16 @@ export function Review() {
)}
- {/* ===== 报告 + 历史 双栏 ===== */}
+ {/* ===== 报告 + 历史 双栏(报告为主体)===== */}
@@ -286,7 +271,9 @@ export function Review() {
reports={historyQuery.data?.reports ?? []}
loading={historyQuery.isLoading}
viewingId={viewing?.id ?? null}
+ generating={isGenerating}
onView={viewReport}
+ onBackToGenerating={() => setViewing(null)}
onDelete={(id) => deleteMut.mutate(id)}
/>
@@ -299,184 +286,94 @@ export function Review() {
}
// ================================================================
-// 指数行情卡(对齐 Dashboard IndexTicker)
+// 市场摘要条 —— 复盘页的轻量上下文(非重复看板)
+// 仅一行:三大指数涨跌 · 情绪分 · 涨停结构 · 成交额
+// 详细数据请去 Dashboard 看,这里只给 AI 报告提供背景参照
// ================================================================
-function IndexTicker({ item }: { item: OverviewMarket['indices'][number] }) {
- const pct = item.change_pct
- const isUp = (pct ?? 0) >= 0
- return (
-
-
{item.name || item.symbol}
-
{fmtPctAlready(pct, 2, true)}
-
{item.symbol}
-
- {isUp ?
:
}
- {fmtPrice(item.last_price)}
-
-
- )
+// 指数简称映射:全称太长(上证指数/深证成指/创业板指/科创综指)摘要条放不下,统一缩成单字
+const INDEX_SHORT: Record
= {
+ '上证指数': '上', '深证成指': '深', '创业板指': '创', '科创综指': '科', '科创50': '科',
+}
+function indexShort(name?: string | null, symbol?: string): string {
+ if (!name) return symbol ?? '—'
+ return INDEX_SHORT[name] ?? (name.replace(/指数|成指|A股|综指|50/g, '').slice(0, 2) || name.slice(0, 1))
}
-// ================================================================
-// KPI 单元(对齐 Dashboard KpiCell)
-// ================================================================
-function KpiCell({ label, value, sub, tone }: {
- label: React.ReactNode
- value: React.ReactNode
- sub?: string
- tone?: 'bull' | 'bear' | 'accent'
-}) {
- const isPlain = typeof value === 'string' || typeof value === 'number'
- const color = tone === 'bull' ? 'text-bull' : tone === 'bear' ? 'text-bear' : tone === 'accent' ? 'text-accent' : 'text-foreground'
- return (
-
-
{label}
-
{value}
- {sub &&
{sub}
}
-
- )
+// 批量替换文本中的指数全称为简称(用于历史列表 summary 显示,
+// 兼容存量旧报告 —— 它们存盘时 summary 还是全称)。
+const _INDEX_FULL_RE = /上证指数|深证成指|创业板指|科创综指|科创50/g
+function shortenIndexNames(text: string): string {
+ return text.replace(_INDEX_FULL_RE, (m) => INDEX_SHORT[m] ?? m)
}
-// ================================================================
-// 章节标题(对齐 Dashboard SectionTitle)
-// ================================================================
-function SectionTitle({ icon: Icon, title, hint }: { icon: typeof Activity; title: string; hint?: React.ReactNode }) {
- return (
-
-
-
-
{title}
-
- {hint &&
{hint}}
-
- )
+// 从 summary 的指数段(如「上-2.26%、深-3.44%、创-4.07%、科-2.02%」)
+// 解析出 [{name, pctStr, pctNum}],供列表项按涨跌染色渲染。
+const _INDEX_PCT_RE = /([上深创科])([+-]?\d+\.\d+%)/g
+function parseIndexPcts(indexSegment: string): { name: string; pctStr: string; pctNum: number }[] {
+ const out: { name: string; pctStr: string; pctNum: number }[] = []
+ for (const m of indexSegment.matchAll(_INDEX_PCT_RE)) {
+ out.push({ name: m[1], pctStr: m[2], pctNum: parseFloat(m[2]) })
+ }
+ return out
}
-// ================================================================
-// 情绪雷达章节(SVG 雷达图,对齐 Dashboard EmotionRadar)
-// ================================================================
-function EmotionSection({ data }: { data: OverviewMarket }) {
- const score = data.emotion.score
- const color = scoreColor(score)
- const radar = data.radar ?? []
- const size = 220
- const cx = size / 2
- const cy = size / 2
- const maxR = 68
-
- const points = radar.map((r, i) => {
- const angle = -Math.PI / 2 + i * 2 * Math.PI / radar.length
- const radius = maxR * Math.max(0, Math.min(100, r.value)) / 100
- return {
- ...r,
- x: cx + Math.cos(angle) * radius,
- y: cy + Math.sin(angle) * radius,
- lx: cx + Math.cos(angle) * (maxR + 24),
- ly: cy + Math.sin(angle) * (maxR + 24),
- gx: cx + Math.cos(angle) * maxR,
- gy: cy + Math.sin(angle) * maxR,
- }
- })
- const polygon = points.map(p => `${p.x},${p.y}`).join(' ')
- const gridPolygons = [1, 0.66, 0.33].map((level, idx) => ({
- level, idx,
- points: radar.map((_, i) => {
- const angle = -Math.PI / 2 + i * 2 * Math.PI / radar.length
- return `${cx + Math.cos(angle) * maxR * level},${cy + Math.sin(angle) * maxR * level}`
- }).join(' '),
- }))
+function MarketSummaryBar({ data }: { data: OverviewMarket }) {
+ const score = data.emotion?.score ?? null
+ const emoColor = scoreColor(score)
+ const indices = (data.indices ?? []).slice(0, 4)
return (
-
-
- {radar.length === 0 ? (
- 暂无雷达数据
- ) : (
-
-
+
+ {/* 情绪分(带色徽章)—— 复盘的核心定调 */}
+
+
+ {score ?? '—'}
+
+
+
{data.emotion?.label ?? '情绪'}
+
情绪温度
- )}
-
- )
-}
-
-// ================================================================
-// 板块排名章节(领涨/领跌)
-// ================================================================
-function SectorSection({ title, rank, tone }: {
- title: string
- rank: OverviewMarket['concept_rank'] | OverviewMarket['industry_rank']
- tone: 'concept' | 'industry'
-}) {
- const leading = rank?.leading ?? []
- const lagging = rank?.lagging ?? []
- const hasData = leading.length > 0 || lagging.length > 0
- return (
-
-
- {!hasData ? (
- 暂无数据
- ) : (
-
-
-
-
- )}
-
- )
-}
-
-function RankColumn({ rows, tone }: { rows: OverviewMarket['concept_rank']['leading']; tone: 'bull' | 'bear' }) {
- return (
-
-
- {tone === 'bull' ? '领涨' : '领跌'}
- {rows.slice(0, 5).map((r, idx) => (
-
-
{idx + 1}
-
-
{r.name}
-
{r.count}只 · {r.leader?.name ?? '—'}
+
+
+
+ {/* 四大指数(简称:上深创科)*/}
+
+ {indices.map(idx => (
+
+ {indexShort(idx.name, idx.symbol)}
+
+ {fmtPctAlready(idx.change_pct, 2, true)}
+
-
- {fmtPctAlready((r.avg_pct ?? 0) * 100, 2, true)}
-
-
- ))}
- {rows.length === 0 &&
—
}
+ ))}
+
+
+
+
+ {/* 涨跌结构 */}
+
+ 涨跌
+ {data.breadth?.up ?? 0}
+ /
+ {data.breadth?.down ?? 0}
+
+
+ {/* 涨停结构 */}
+
+ 涨停
+ {data.limit?.limit_up ?? 0}
+ 封板 {(data.limit?.seal_rate ?? 0).toFixed(0)}%
+
+
+ {/* 成交额 */}
+
+ 成交
+ {fmtBigNum(data.amount?.total)}
+
)
}
@@ -485,14 +382,15 @@ function RankColumn({ rows, tone }: { rows: OverviewMarket['concept_rank']['lead
// 报告面板(流式 + 错误 + 历史/完成态)
// ================================================================
function ReportPanel({
- phase, content, error, isGenerating, viewing, onSave, onRegenerate, reportEndRef,
+ phase, content, error, isGenerating, viewing, onCopy, onDownload, onRegenerate, reportEndRef,
}: {
- phase: Phase
+ phase: ReviewPhase
content: string
error: string
isGenerating: boolean
viewing: AiReviewReport | null
- onSave: () => void
+ onCopy: () => void
+ onDownload: () => void
onRegenerate: () => void
reportEndRef: React.RefObject
}) {
@@ -516,29 +414,49 @@ function ReportPanel({
if (phase === 'idle' && !content) {
return (
-
+
-
-
AI 大盘复盘
-
- 点击右上角「生成复盘」,基于今日指数结构、涨跌家数、连板梯队、板块轮动与情绪雷达,
- 生成可直接指导次日仓位与节奏的盘后复盘报告。
+
AI 大盘复盘
+
+ 一键生成今日盘后复盘报告 —— 从一句话定调到明日交易计划,
+ 结构化输出可直接指导次日仓位与节奏。
-
+ {/* 报告七节预览 —— 空状态也有内容感,暗示报告结构 */}
+
+ {[
+ { icon: '🎯', label: '一句话定调' },
+ { icon: '📊', label: '盘面总览' },
+ { icon: '🔥', label: '板块主线' },
+ { icon: '💰', label: '资金情绪' },
+ { icon: '📰', label: '消息催化' },
+ { icon: '🎯', label: '明日计划' },
+ { icon: '⚠️', label: '风险提示' },
+ ].map((s) => (
+
+ {s.icon}
+ {s.label}
+
+ ))}
+
+
- 七节结构化报告 · 一键归档 · 历史回看
+ 点击右上角「生成复盘」开始
)
}
- const showCursor = isGenerating
- const showSave = phase === 'done' && !!content && !viewing
+ // 仅当显示生成内容(非查看历史)且正在生成时,才显示流式光标
+ const showCursor = isGenerating && !viewing
+ // 查看历史时(即使后台在生成)也能复制/下载该历史报告
+ const showActions = !!content && (!isGenerating || !!viewing)
const showViewingTag = !!viewing
const isLoading = phase === 'loading' && !content
@@ -555,13 +473,18 @@ function ReportPanel({
{showViewingTag ? `历史复盘 · ${viewing!.as_of}` : isGenerating ? 'AI 正在复盘…' : '复盘报告'}
- {showSave && (
-
+ {showActions && (
+
+
+
+
)}
-
+
{isLoading ? (
@@ -570,8 +493,8 @@ function ReportPanel({
-
AI 正在分析今日盘面…
-
读取指数结构 · 涨跌家数 · 连板梯队 · 板块轮动 · 情绪雷达
+
AI 正在复盘今日盘面…
+
分析指数结构 · 连板梯队 · 板块轮动 · 资金情绪
) : (
@@ -591,14 +514,17 @@ function ReportPanel({
// 历史面板
// ================================================================
function HistoryPanel({
- reports, loading, viewingId, onView, onDelete,
+ reports, loading, viewingId, generating, onView, onBackToGenerating, onDelete,
}: {
reports: AiReviewReport[]
loading: boolean
viewingId: string | null
+ generating: boolean
onView: (r: AiReviewReport) => void
+ onBackToGenerating: () => void
onDelete: (id: string) => void
}) {
+ const empty = !generating && reports.length === 0
return (
@@ -609,14 +535,32 @@ function HistoryPanel({
{loading ? (
- ) : reports.length === 0 ? (
+ ) : empty ? (
暂无历史复盘
-
生成后点「归档」即可保存
+
生成完成后自动归档
) : (
+ {/* 生成中占位项:列表顶部,点击回到正在生成的流式内容 */}
+ {generating && (
+
+ )}
{reports.map((r) => {
const color = scoreColor(r.emotion_score)
return (
@@ -637,13 +581,28 @@ function HistoryPanel({
{r.emotion_label ?? '—'}
- {r.as_of}
+ {r.as_of}
-
- {r.summary ?? r.content.slice(0, 40)}
+
+ {r.summary
+ ? (() => {
+ const pcts = parseIndexPcts(shortenIndexNames(r.summary).split('|')[0])
+ if (pcts.length === 0) {
+ return {r.content.slice(0, 40)}
+ }
+ return pcts.map((p) => (
+
+ {p.name}
+ {p.pctStr}
+
+ ))
+ })()
+ : {r.content.slice(0, 40)}}
+ {r.created_at && (
+
{fmtArchivedAt(r.created_at)}
+ )}
-