diff --git a/docs/frontend_terminal_redesign_proposal.html b/docs/frontend_terminal_redesign_proposal.html new file mode 100644 index 0000000..c8b57b7 --- /dev/null +++ b/docs/frontend_terminal_redesign_proposal.html @@ -0,0 +1,544 @@ +三国量化 · 终端化重构提案 + + +
+ + +
+
// frontend redesign proposal · v0.1
+

从「研究工具」
量化终端

+

当前前端是合格的暗色后台——功能齐全,但视觉停留在 GitHub Dark 工具感,信息密度低、无命令面板、无归因深度。本提案把它重做成一台 科幻终端:荧光青 × 琥珀数据 × 多面板高密度,对标 Bloomberg / Wind 的终端 UX,QuantConnect 的分析深度。

+
+
现状风格
GitHub Dark · 单列低密度
+
目标风格
深空黑 · 荧光青 · 琥珀
+
设计系统
已变量化 · CSS 换肤友好
+
调研覆盖
10+ 开源项目 · 11 平台
+
+
+ + +
+
+ [ 01 ] +

现状诊断

+ cta_7a92d1f6 截图 · 功能骨架已齐 · 视觉与密度待升级 +
+
+
PAIN

单列堆叠 · 信息密度低

11 个指标卡横排 + 6 张图纵向 el-card 堆叠,一屏只塞得下 1-2 张图。tokens.css 注释写着"高密度",实现却是低密度——设计意图与实现的落差。

+
PAIN

无命令面板 · 无快捷键

Bloomberg <GO>、Wind 键盘精灵、iFinD 代码简拼——三家顶级终端无一例外都有命令行式交互。当前全靠侧栏点选,power-user 效率为零。

+
GAP

分析深度停在 P0 基线

缺月度收益热力图、Rolling Sharpe/Beta、回撤 Top5 标注、每日收益柱状图;指标卡缺胜率/盈亏比/换手率(11 项里只有 8 项)。归因(Brinson/Barra)完全空白。

+
GAP

无导出 · 无对比 · 无选股闭环

所有数据表无 Excel/CSV 导出;无多回测对比页;无选股器(EQS→回测→归因是专业终端的产品灵魂,4 家终端都做闭环)。

+
BASE

设计系统已变量化 ✅

--bg / --brand / --mono / --r-sm / --ease + 统一 echartsDark 主题(红涨绿跌)。科幻风改造主要落在 CSS 变量层 + 视觉叠加层,不用重写 Element Plus 组件。

+
BASE

功能框架齐全 ✅

单标的 CTA 全链路(新建→进度→结果→历史)+ 组合回测 + 参数优化 + 因子分析 + 模拟盘 + 实盘模拟 + Dashboard。美化为主,功能查漏为辅。

+
+
+ + +
+
+ [ 02 ] +

视觉语言

+ 深空黑 · 荧光青 · 琥珀 · 等宽数据 +
+
+
+
+
VOID#05080d
+
PANEL#0d141f
+
CYAN · 主强调#00e5ff
+
AMBER · 数据高亮#ffb000
+
UP · 红涨(A股)#ff4d5e
+
DOWN · 绿跌#2ee68a
+
+
+
RUNNING
+
WARN
+
CRITICAL
+
+
+
+
DISPLAY · 终端标题终端化
+
BODY · 正文叙述量化研究台正文阅读
+
DATA · 等宽 tabular-nums+18.42%
+
规则数据全右对齐 · 等宽列对齐 · 直角/2px 圆角 · 几何角标代替 emoji · 1px 网格底 · 克制动效(扫描线/呼吸灯)
+
+
+
+ + +
+
+ [ 03 ] +

改造后 · 回测结果页

+ 高保真 mockup · 多面板终端布局 · 非实现,视觉目标 +
+ +
+ +
+ cta_7a92d1f6 + DoubleMa·600519 贵州茅台·2023-01-01 ~ 2024-06-30·日线·基准 沪深300 + + ● DONE · 1.8s + ⌘K + +
+ +
+ +
+
核心指标
+
+
年化收益+18.42%
+
Sharpe1.34
+
Sortino1.71
+
最大回撤-9.80%
+
Alpha6.20%
+
Beta0.78
+
胜率54%
+
盈亏比1.82
+
换手率3.2x
+
PSR0.92
+
+
+ + +
+ +
+
+ 净值曲线 · 策略 / 基准 / 超额 + + 策略 + 沪深300 + 超额 + +
+
+
+ + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ 回撤水下图 · Top5 回撤期标注 + 回撤Top5 +
+
+ + + + + + 12 + +
+
+ + +
+
月度收益热力图 · 2023–2024
+
+
+
1
2
3
4
5
6
7
8
9
10
11
12
+
23
+
+3
-2
+6
+1
-3
+4
+7
-1
+2
-4
+3
+5
+
24
+
-3
+5
+2
-2
+4
+6
+
+
+
+
+ + +
+
持仓 · 信号
+
+
600519
+
贵州茅台 · 多头
+
持仓400 股
+
成本1,685.20
+
现价1,722.50
+
浮盈+2.21%
+
暴露68.9%
+
+
▲ BUY2024-05-16 · 1,680
+
▼ SELL2024-06-20 · 1,722
+
▲ BUY2024-06-28 · 1,705
+
风险
+
+
VaR(95%)-2.1%
+
行业暴露食饮 69%
+
风格大盘·价值
+
+
+
+ + +
+
TRADES ▸
+
+ BUY 600519 @1685 ×400 + SELL 600519 @1702 ×400 + BUY 600519 @1660 ×400 + SELL 600519 @1688 ×400 + BUY 600519 @1672 ×400 + BUY 600519 @1685 ×400 + SELL 600519 @1702 ×400 + BUY 600519 @1660 ×400 + SELL 600519 @1688 ×400 + BUY 600519 @1672 ×400 +
+
+
+

// 上图为视觉目标 mockup(假数据)。一屏承载:10 指标 + 净值/基准/超额 + 回撤Top5 + 月度热力图 + 持仓信号 + 风险 + 交易滚动条 —— 信息密度对标 Wind/Choice 综合屏。

+
+ + +
+
+ [ 04 ] +

功能优先级

+ 调研对照 · P0 立即补齐 / P1 差异化 / P2 长期 +
+
+
+
P0立即补齐1–2 周
+
    +
  • 科幻视觉层:CSS 变量换肤(深空黑/荧光青/琥珀)+ 网格底 + 信号灯 + tabular-nums,全站换肤不重构组件
  • +
  • Cmd+K 命令面板:代码/拼音/命令三模式,全局快捷入口
  • +
  • 月度收益热力图:QuantConnect 标杆,全头部都有
  • +
  • 回撤 Top5 标注 + 每日收益柱状图
  • +
  • 补 3 指标:胜率 / 盈亏比 / 换手率
  • +
  • 导出:所有表加 Excel/CSV 按钮
  • +
+
+
+
P1差异化体验1–2 月
+
    +
  • 多面板可拖拽布局:vue-grid-layout + 模板切换 + 联动
  • +
  • 股票代码智能补全:代码/中文/拼音 fuzzy(米筐独占级体验)
  • +
  • 选股→回测→归因闭环:选股器 + universe 透传
  • +
  • Crisis Events:2015股灾/2018贸易战/2020新冠/2024微盘崩期间表现
  • +
  • Rolling Sharpe/Beta(6M/12M 滚动)+ PSR 概率化夏普
  • +
+
+
+
P2长期 / 选型视定位
+
    +
  • Brinson / Barra 归因:国内聚宽/优矿/Choice 杀手锏
  • +
  • 无代码策略生成器:指标 + 风控可视化搭建
  • +
  • AI 策略助手:对话式开发 + 研报总结
  • +
  • 社区 / 策略广场:热度榜 + 实体引用
  • +
  • Bar Replay 历史回放(TradingView 独占级)
  • +
+
+
+
+ + +
+
+ [ 05 ] +

终端 UX 精华落地

+ Bloomberg / Wind / iFinD / Choice 共识 · 7 条 +
+
+
01

全局命令面板 ⌘K

Bloomberg <GO> / Wind 键盘精灵 / iFinD 简拼——三终端无一例外。三模式:600519切标的 · gzmt拼音 · bt/fac/opt路由。配合 vim 风格单键跳转。

SRC: Bloomberg · Wind · iFinD
+
02

多面板可拖拽工作区

四面板 + Tabbed Panel + 布局保存(4 家终端都做)。预设模板:单策略深挖 / 多策略 2×2 对比 / 选股+回测 / 组合归因。面板间联动——点持仓高亮净值图买卖点。

SRC: Bloomberg · Wind · iFinD · Choice
+
03

语义化色彩 · 颜色承载信息

严格 token:涨/跌/中性/预警/信息分层(项目已有红涨绿跌,补预警/信息色)。数字右对齐 + tabular-nums(Bloomberg 做得最彻底)。涨跌色三档可切换。

SRC: Bloomberg amber-on-black
+
04

选股 → 回测 → 归因闭环

Bloomberg EQS→BTST→PORT / Wind EQS→EQBT→PMS。产品流灵魂:选股结果"送入回测"→结果页"因子归因"tab→统一 portfolio schema 跨模块透传。

SRC: 4 家专业终端共识
+
05

数据密度 + 零摩擦导出

ag-Grid / TanStack Table:tabular-nums + 冻结表头 + 内联 sparkline(持仓行迷你净值)。所有表右上角"导出 Excel"(exceljs 前端生成多 sheet)。数字格式全平台统一。

SRC: Bloomberg XLTP · Wind =WSD() · iFinD
+
06

实体引用 · 符号触发

编辑器/备注/讨论区:$SH600519$→股票卡片 · #DoubleMa#→策略聚合 · @因子:momentum_20d→因子详情。从股票页进新建回测自动填 universe。

SRC: 雪球
+
+
+ + +
+
+ [ 06 ] +

落地路线图

+ 4 Sprint · 视觉先行 · 每步可验收 +
+
+
+
SPRINT 01
+

视觉层 · 设计系统

+
~ 1 周 · 低风险
+
    +
  • CSS 变量层换肤(深空黑/青/琥珀)
  • +
  • 等宽数据字体 + 全局 tabular-nums
  • +
  • 网格底 / 信号灯 / 扫描线 / 角标
  • +
  • 不改组件结构,全站换肤
  • +
+
+
+
SPRINT 02
+

结果页重构 + P0 功能

+
~ 2 周
+
    +
  • 结果页转多面板终端布局
  • +
  • 月度热力图 / 回撤 Top5 / 每日收益
  • +
  • 补胜率/盈亏比/换手率指标
  • +
  • Cmd+K 命令面板 + 表导出
  • +
+
+
+
SPRINT 03
+

P1 差异化

+
~ 4–6 周
+
    +
  • 多面板可拖拽 + 布局保存
  • +
  • 股票智能补全(代码/中文/拼音)
  • +
  • 选股→回测→归因闭环
  • +
  • Crisis Events / Rolling / PSR
  • +
+
+
+
SPRINT 04
+

P2 选型

+
视定位
+
    +
  • Brinson / Barra 因子归因
  • +
  • 无代码策略生成器
  • +
  • AI 策略助手
  • +
  • 社区 / 策略广场 / Bar Replay
  • +
+
+
+
+ +
+ SG + 三国量化研究台 · 终端化重构提案 + + 调研:10+ 开源项目 · 11 专业平台 + sanguo_vnpy_v2 · 前后端 session +
+ +
diff --git a/docs/preview_code_editor.png b/docs/preview_code_editor.png new file mode 100644 index 0000000..9ec9677 Binary files /dev/null and b/docs/preview_code_editor.png differ diff --git a/docs/preview_dashboard.png b/docs/preview_dashboard.png new file mode 100644 index 0000000..283f520 Binary files /dev/null and b/docs/preview_dashboard.png differ diff --git a/docs/preview_result.png b/docs/preview_result.png new file mode 100644 index 0000000..acb0717 Binary files /dev/null and b/docs/preview_result.png differ diff --git a/docs/preview_result_top.png b/docs/preview_result_top.png new file mode 100644 index 0000000..afa6cf6 Binary files /dev/null and b/docs/preview_result_top.png differ diff --git a/docs/preview_strategy_v2.png b/docs/preview_strategy_v2.png new file mode 100644 index 0000000..64ea2d4 Binary files /dev/null and b/docs/preview_strategy_v2.png differ diff --git a/docs/spec_quant_terminal_redesign.md b/docs/spec_quant_terminal_redesign.md new file mode 100644 index 0000000..83c1750 --- /dev/null +++ b/docs/spec_quant_terminal_redesign.md @@ -0,0 +1,308 @@ +# 三国量化研究台 · 终端化重构 Spec + +> 范围:前端视觉/功能改造 + 后端引擎适配 + 模拟实盘功能改造 +> 制定:前后端 session · 2026-08-12 · 状态:**待评审** + +--- + +## 1. 背景与目标 + +**现状**:功能骨架齐全(回测/组合/优化/因子/模拟/实盘/Dashboard),视觉停留在 GitHub Dark 工具风——信息密度低、无命令面板、分析深度停在 P0 基线、组合策略只回测得了跑不了模拟实盘。 + +**目标**:重做为科幻量化终端(深空黑+荧光青+琥珀),对标 Bloomberg/Wind 终端 UX + QuantConnect 分析深度,组合策略贯通 backtest→paper→live。 + +**三大改造**: +1. 前端终端化(视觉 + 功能补齐 + 多面板) +2. 组合策略引擎贯通模拟/实盘(适配层接入) +3. 模拟实盘功能完善(组合/多账户/多策略同账户) + +--- + +## 2. 核心原则(不可违背) + +| # | 原则 | 说明 | +|---|------|------| +| **P1** | **vnpy 是适配不是改造** | `vnpy_v4.4.0/` 为上游源码,**零改动**;CTA 侧通过子类化适配层(`ashare_engine.py` 模式)扩展。验收:`git diff vnpy_v4.4.0/` 恒为空 | +| **P2** | **组合策略为主线** | 选股+择时+调仓+风控一体(bullet_trade 聚宽风格)是产品重心;未来加 AI 辅助、多策略同账户 | +| **P3** | **CTA 不淡化** | 所有功能 CTA/组合都做,CTA 优先级靠后。关注同功能下两者实现分歧,一套代码覆盖优先 | +| **P4** | **miniQMT 只管交易** | 下单通道与策略引擎解耦,CTA/组合共用 QMT;策略差异在引擎层不在 QMT 层 | +| **P5** | **自研可改、上游只适配** | `bullet_trade`/`sanguo_trader`/`sanguo_live`/`sanguo_qmt_bridge` 是自研可改;vnpy 是上游只适配 | + +--- + +## 3. 现状基线(改造起点) + +### 3.1 引擎层 +| 引擎 | 归属 | backtest | paper 模拟 | live 实盘 | +|------|------|:---:|:---:|:---:| +| vnpy CTA(ashare_engine 适配) | 上游·只适配 | ✅ | ✅ per-symbol | ✅ 单标的 | +| bullet_trade 组合 | 自研·可改 | ✅ | ❌ | runner_live 有 / **web 接入层缺** | +| sanguo_trader 自研撮合 | 自研·可改 | — | paper 用 | — | +| sanguo_qmt_bridge | 自研·可改 | — | — | QMT 下单通道(共用) | + +### 3.2 前端层 +- 回测:New/Progress/Result/History/Optimize/PortfolioBacktest +- 模拟:paper(New/List/Result/Live) · live(New/List/Monitor) +- 设计系统已变量化(CSS tokens + `echartsDark`)→ 换肤友好 + +### 3.3 模拟实盘 3 种运行方式 +| 方式 | 引擎 | 数据源 | 撮合 | 策略范式 | +|------|------|--------|------|---------| +| ① paper-replay 回放 | 自研撮合 | 历史 parquet | 快速重放·立即出结果 | CTA per-symbol(+组合待加)| +| ② paper-live 实走 | 自研撮合 | 日终 parquet | 每日定时 step·checkpoint 续跑 | CTA(+组合待加)| +| ③ live-miniQMT 实盘模拟 | QMT 直连 | QMT 实时行情 | 券商模拟账户真实下单 | CTA 单标的(+组合 runner_live 待接入)| + +--- + +## 4. 前端改造 + +### 4.1 视觉语言(科幻终端) +- **色板**:深空黑 `#05080d` + 荧光青 `#00e5ff`(主强调)+ 琥珀 `#ffb000`(数据高亮)+ 红涨 `#ff4d5e`/绿跌 `#2ee68a` +- **字体**:等宽数据字体(全局 `tabular-nums`)+ 无衬线正文 +- **元素**:1px 网格底 / 信号灯(绿运行·琥警告·红 critical)/ 扫描线 / 几何角标(无 emoji)/ 直角·2px 圆角 +- **落地**:CSS 变量层换肤,不改 Element Plus 组件结构,全站一次性换肤 +- **参考**:提案页 `docs/frontend_terminal_redesign_proposal.html`(已验证视觉) + +### 4.2 功能补齐(回测结果页,CTA+组合共用一套) +| 功能 | 优先级 | 说明 | +|------|:---:|------| +| 月度收益热力图 / 每日收益柱状 / 回撤 Top5 / 收益分布 | P0 | QuantConnect 标杆 | +| 指标卡补齐:胜率/盈亏比/换手率/PSR | P0 | 现 11 项缺这 4 个 | +| 净值图超额线 + brush 缩放 | P0 | 现仅策略+基准两线 | +| 克隆重跑 / 导出报告(Excel/PDF) | P0 | 效率闭环 | +| Cmd+K 命令面板 / 所有表导出 | P0 | 终端 UX 共识 | +| 多回测对比页 / Rolling Sharpe·Beta | P1 | 差异化 | +| 参数优化热力图·3D 曲面 / 选股→回测→归因闭环 | P1 | vnpy 只有表格,超越 | +| Crisis Events 压力测试 | P1 | QuantConnect 独占 | +| Brinson/Barra 归因 / 多面板可拖拽 / 股票智能补全 | P1 | 专业化 | +| AI 策略助手 / 无代码策略生成器 / 社区广场 / Bar Replay | P2 | 长期 | + +### 4.3 页面改造 +- **结果页**:单列堆叠 → 多面板终端布局(指标塔 + 主图 + 回撤 + 月度热力 + 持仓信号 + 交易 ticker),一屏高密度 +- **模拟盘/实盘监控**:科幻风监控台(状态灯 + 实时净值 + 持仓 + 信号 + 告警 + 下次运行时刻) +- **多账户/多策略**:多账户管理列表 + 一账户多策略分策略展示 +- **组合策略创建入口**:新建回测/模拟/实盘时能选组合策略 + 股票池(不只是 CTA 类名 + 单 symbol) + +--- + +## 5. 后端改造(引擎适配层) + +> 原则 P1/P5:vnpy 只适配不改源码;bullet_trade/sanguo_trader/sanguo_live/sanguo_qmt_bridge 自研可改 + +### 5.1 任务 A — 组合 paper 模拟 +- **目标**:组合策略能跑模拟盘(replay 回放 + live 实走) +- **方案**:bullet_trade 接入 `sanguo_trader/matcher`(自研撮合),复用 paper 的 checkpoint / 跨日 pending 机制 +- **vnpy**:不涉及(纯自研引擎链路) + +### 5.2 任务 B — 组合 live 接入 web +- **现状**:`sanguo_portfolio/runner_live.py` 引擎就绪(bullet_trade `LiveEngine` + `QmtBroker` + AllWeather),但未接 web +- **方案**: + - `sanguo_live/supervisor` 识别"组合账户"类型 → 起 bullet_trade `LiveEngine`(非 vnpy engine) + - 新建 `sanguo_api/routes_portfolio_live`(创建/启停/监控组合实盘) + - 前端组合实盘入口 + 监控台 +- **下单**:复用 `QmtBroker`/miniQMT,**不碰 QMT 适配层** +- **vnpy**:不涉及 + +### 5.3 任务 C — 多策略同账户 + 多账户 +- **多账户**:架构已支持(`live_accounts` 表每行一账户 + per-account `mini_path`)→ 多行记录 + 多 miniQMT 客户端实例;**待 VPS 实测**单机并行实例数 +- **多策略同账户**:supervisor + engine 支持一账户多策略路由 + 资金/持仓隔离 +- **前端**:多账户管理 + 一账户多策略视图 + +### 5.4 CTA 侧(适配层,不改造 vnpy) +- CTA paper/live 继续走 `ashare_engine` / vnpy 适配层 +- 功能补齐(同组合)通过适配层扩展,**vnpy_v4.4.0 源码零改动** + +### 5.5 CTA vs 组合实现分歧(评估) +| 功能 | 分歧 | 处理 | +|------|:---:|------| +| backtest | 小 | 各有引擎,已就绪 | +| paper 撮合 | 中 | 组合接 matcher | +| live web 接入 | 中 | 组合接 supervisor+routes | +| 下单通道 | 无 | 共用 miniQMT | +| 结果分析/监控/导出 | 无 | **一套组件 CTA+组合共用**(P5 一套优先)| + +--- + +## 6. 模拟实盘功能改造目标 + +| 目标 | 现状 | 改造后 | +|------|------|--------| +| 组合策略跑模拟 | ❌ | ✅ paper-replay + paper-live | +| 组合策略跑实盘 | runner_live 有、未接 web | ✅ web 创建/启停/监控 | +| 多账户 | 架构支持、未暴露 | ✅ 多账户管理 UI | +| 多策略同账户 | ❌ | ✅ 一账户多策略 + 隔离 | +| 监控视觉 | 朴素 | 科幻风监控台 | +| 结果分析 | 基础 | 复用回测深度(月度/归因/Top5)| + +--- + +## 7. 路线图 + +| Sprint | 内容 | 风险 | 优先级 | +|--------|------|------|--------| +| **S1** 视觉层 | CSS 变量换肤全站 + 结果页科幻样板 | 低 | 即时 | +| **S2** 结果页重构 + P0 功能 | 多面板布局 + 月度/Top5/每日收益 + 指标补齐 + 克隆/导出/Cmd+K | 中 | 高 | +| **S2.5** 策略管理 | 菜单重构(策略+账户两组) + 策略配置CRUD(类+参数+标的 命名预设) + 策略在线代码编辑(Monaco+热重载) | 中 | 高·已启动 | +| **S3** 组合贯通模拟实盘 | 任务 A(组合 paper)+ B(组合 live 接 web)+ C(多账户/多策略前端)| 中高 | 高(组合主线)| +| **S4** P1 差异化 | 多回测对比 + Rolling + 优化可视化 + 选股闭环 + Crisis + 归因 + 多面板拖拽 | 中 | 中 | +| **S5** P2 选型 | AI 助手 + 无代码策略 + 社区 + Bar Replay | 高 | 长期 | +| CTA 并行 | 各 Sprint CTA 侧同步(适配层),优先级靠后 | — | P3 靠后 | + +--- + +## 8. 职责与依赖 + +| Session | 职责 | +|---------|------| +| **前后端**(本 session)| 前端全部 + 后端引擎适配(A/B/C)+ CTA 适配层 + Spec/路线图 | +| 数据 | 组合策略所需数据/因子(归因数据、风格因子)| +| 策略 | 策略逻辑(all_weather 等 bullet_trade 策略本身)| +| 基建 | CI/CD/部署(commit 标 [vps]/[nas],NAS 自动/VPS dispatch)| + +> 引擎改造归属已记 memory:`portfolio-engine-paper-live-ownership.md`(防职责混淆) + +--- + +## 9. 验收标准 + +- **视觉**:全站科幻终端风;结果页一屏多面板(对标提案 mockup) +- **功能**:P0 全齐;CTA 与组合共用一套结果分析组件 +- **组合贯通**:组合策略能 backtest → paper(replay+live) → live(miniQMT) 全链路 +- **模拟实盘**:3 种方式都支持组合 + 多账户 + 多策略同账户 +- **原则 P1**:`git diff vnpy_v4.4.0/` 恒空(vnpy 零改动) +- **CTA**:功能不退化,同组合功能,优先级靠后 + +--- + +## 10. 待评审决策点 + +1. spec 整体方向认可吗? ——>认可 +3. S1 先做(视觉换肤 + 结果页样板)作为第一步? -> 没问题,所有页面都给我一个样板,假的数据带页面跳转即可 +4. 多账户多 miniQMT 实例数 → 安排 VPS 实测时机?-> 作为一个长期todu就行,现在不需要 +5. 归因(Brinson/Barra)放 S4,还是要提前(依赖数据 session 的风格因子数据)? -> 可以放s4 + +--- + +## 11. 菜单结构(2026-08-12 定稿) + +> 用户决策:在现有 5 组基础上加「策略」「账户」两组,最小破坏现有分组。 + +``` +工作台 +策略 ← 新增 + ├ 策略库 策略配置 CRUD(列表/新建/编辑/删除) + ├ 新建策略 + └ 代码编辑 策略在线代码编辑(Monaco) +回测 + ├ 新建回测 / ├ 组合回测 / ├ 参数优化 / └ 历史任务 +投研 + └ 因子分析 +模拟 + ├ 模拟盘列表 / └ 新建模拟盘 +实盘模拟 + ├ 实盘列表 / └ 新建实盘 +账户 ← 新增(多账户/多 miniQMT 统一视图) +``` + +## 12. 策略管理(2026-08-12 增补) + +两层管理,对象与工作量不同: + +### 12.1 策略配置 CRUD(参数层) +- **对象**:策略类 + 参数 + 标的/池 + 基准 的命名预设(如"茅台双均线(10,30)") +- **痛点**:当前每次回测/模拟现选现填、跑完即丢;CRUD 让配置可命名/复用/一键发起回测·模拟·实盘 +- **后端**:`strategies` 表(id/name/strategy_class/params JSON/symbol_or_pool/benchmark/interval/remark/created_at) + 5 路由(list/create/update/delete/duplicate),复用 strategy_registry 校验类名。**纯自研,不碰 vnpy** +- **前端**:策略库列表(卡片) + 新建/编辑表单(选类自动载入参数) + 各 New 页"从已存策略载入" +- **工作量**:~1-2 天 | **状态**:✅ 前端完成(mock) / ⏳ 后端待做(task16) + +### 12.2 策略在线代码编辑(代码层) +- **对象**:策略 Python 代码本身(.py 文件) +- **边界(P1 原则)**:只编辑**自研策略**(`sanguo_trader/strategy`、`sanguo_portfolio/strategies`、`sanguo_live/strategies`);**不编辑 vnpy 上游包**(`vnpy_ctastrategy/strategies` 属适配不改造红线) +- **方案**:Monaco Editor(VSCode 同款)+ 文件树 + 语法校验(py_compile) + 保存 + 模板新建 +- **架构利好**:回测走 ProcessPool spawn(每次新进程)→ 改完代码下次回测自动加载最新,**天然热重载**,无需处理 Python reload 缓存 +- **安全**:单用户内部研究台,`py_compile` 校验 + 进程超时 + ProcessPool 隔离崩溃即可,不需沙箱 +- **层次**:①只读查看(~0.5天) / ②**在线编辑**⭐(~2.5-3天) / ③无代码生成(~1-2周·P2) +- **工作量(②)**:Monaco 集成 ~1.5天 + 后端文件 CRUD ~0.5天 + registry 扩展扫描自研目录 + 模板 ~1天 +- **状态**:📋 本轮先做前端原型(Monaco mock)供体验,后端待体验后定 + +### 12.3 决策记录 +- 策略 CRUD 对象 = **策略配置实例**(非代码类)✅ +- 菜单 = 加「策略」「账户」两组(保留现有分组)✅ +- 在线编辑 = 先前端原型体验,再定是否上后端 +- 多账户多 miniQMT 实例数实测 = 长期 todo(不阻塞) + +### 12.4 业界调研结论(2026-08-12 · 7 平台 agent 调研) + +调研聚宽/米筐/掘金/QuantConnect/Quantopian/Alpaca/VnPy,核心结论: + +1. **策略 = 代码**(6/7 一致,Alpaca 无策略层)。本轮"策略=代码"重做方向正确。 +2. **"先建策略 → 再发起回测/模拟/实盘"是唯一主流模式**(6/6)。回测/模拟/实盘是策略的运行模式,不是独立顶层对象。没有一家用"回测页直接填配置"。 +3. **🔑 两层模型(VnPy 范式,推荐采纳)**:策略类(代码)+ 策略实例(`setting` 参数 dict)。 + - 顶层 = 策略类列表(从 strategies/ 自动发现) + - 二级 = 该类下的实例(每组参数 + 标的/池 = 一个实例,VnPy `strategy_name`) + - 每实例一键:回测 / 模拟 / 实盘 + - **这正是"编辑策略=编辑参数"纠结的正解**:参数属于"实例",不属于"策略代码",也不该独立叫"策略" +4. **新建策略分流**:业界按**资产类别**(股票/期货/加密)分流,不按组合/CTA。本项目按"组合/CTA 范式"分流(A股组合 vs CTA 差异大),是合理的本地化,保留。 +5. **参数归属**:策略专用参数(快慢线/持仓数)绑策略类(`parameters=[]` 反射生成 UI 输入框);回测运行配置(日期/资金/基准/频率/滑点/手续费)独立,每次可改。 + +**组合策略 UI 配置项清单**(业界共识): +- 实例参数层:选股池/Universe、调仓周期(日/周/月)、持仓数 Top-N、仓位算法(等权/风险平价)、风控(止损%/止盈%/单标的最大仓位%)、策略专用参数(反射 `parameters`) +- 运行配置层:初始资金、起止日期、基准、频率、滑点、手续费 +- 不配置化(留代码):选股逻辑、因子计算、下单触发条件 + +**修正**:当前重做(S2.5)只做了"策略类(代码)"第一层;应补**"策略实例"第二层**(策略类下创建多组参数实例,每实例一键回测/模拟/实盘)。待用户确认后补。 + +### 12.5 最终落地设计(2026-08-12 定稿 · 已实现前端 mock) + +策略管理「三层 + 4 运行全生命周期」: + +``` +策略代码(.py · 编辑代码) + └ 策略实例(参数变体 + 标的/池) + ├ [回测] 历史全量 · 秒出 · 验证逻辑 + ├ [模拟·回放] 历史 parquet · 快速重放 · 验证撮合/滑点 + ├ [模拟·实走] 日终 parquet · 每日推进+断点续跑 · 模拟真实节奏 + └ [实盘] miniQMT · 真下单(券商模拟/真账户) +``` + +**命名定稿**(消除撞名): +- 回测(不变) +- 模拟盘 = 原"模拟"(自研撮合,**不走 QMT**),内含 回放/实走 两 mode +- 实盘 = 原"实盘模拟"(**miniQMT 真下单**;"实盘模拟"撞名已删) + +**菜单最终**:工作台 / 策略(策略库·新建策略·代码编辑) / 回测 / 投研 / 模拟盘 / 实盘 / 账户 + +**策略库 UI**(已实现):按策略代码分组,每组下列实例;每实例一行 = 参数摘要 + 标的/池 + 最近收益 + 4 运行状态灯(已测/运行/已停/—) + 一键发起按钮。代码无实例时提示"编辑代码后可创建"。 + +业界「策略=代码+实例」两层 + 「先建策略→再发起运行」模式,全部落地。后端待补:策略代码文件 CRUD + 实例 CRUD + 各运行入口接实例参数。 + +## 13. 撮合机制与回放设计(2026-08-12 定稿) + +### 13.1 撮合频率原则(理论硬约束) +**撮合频率 = 策略决策频率**。撮合 < 决策频率 → 丢信号 + 价格失真。业界(聚宽 run_minute / vnpy on_bar / backtrader cerebro)均逐 bar 撮合,**没有"统一日级"的设计**。 + +用户选 3 样,撮合频率由它们推导,**不独立选**(避免矛盾配置如"15m策略+日终撮合"): +- **interval**(数据频率 d/15m)— 决定决策频率 +- **match_session**(撮合时点:开盘价/收盘价/集合竞价) +- **运行模式**(回测/回放/实走/实盘) + +### 13.2 撮合频率 = interval × 运行模式 +| 运行模式 | 撮合频率 | 适合策略 | +|---------|---------|---------| +| 回测 / 回放 | = interval(逐历史 bar)| 全频率 | +| paper 实走 | **固定日级**(日终一次)| **只日线** | +| 实盘(miniQMT)| **实时**(行情驱动)| 全频率(含分钟盘中)| + +**两路分工**:日线策略 → paper 实走(日终低成本模拟);分钟策略 → miniQMT 实盘(盘中实时)。paper 实走为日线设计,分钟策略不该迁就它。**UI 校验**:分钟策略(interval=15m) + paper 实走 = 不匹配,提示引导到 实盘/回放。 + +### 13.3 回放设计 +回放 = 历史数据按选定 interval **逐根 K 线快速重放 + 自研 A 股撮合**。 +- **精度 = interval**:日线 = 每天 1 撮合点;15m = 每天 ~16 点(每根 15m bar 撮合)。**选 interval 即选撮合频率**。 +- **逐 bar 流程**:撮合上根 pending → 喂 `on_bar` 算信号 → 新单按 match_session 当根/下根撮合 → 记账 + 涨跌停封板/最小手数/滑点/费率/T+1 校验。 +- **速度**:本地历史毫秒级/根,一段历史秒~分钟跑完,立即出结果。 +- **vs 回测**:回放用自研 A 股精细撮合(封板/集合竞价/滑点/T+1),比 vnpy 回测更贴近实盘真实发生。 +- **数据精度**:当前支持 **d / 15m**(baostock 5.5 年 15m);**1m / 5m 待数据源补齐后扩展**。 + +### 13.4 标的 / 池控件(按策略 type) +- **CTA(单标的)**:股票代码输入框 + 智能补全 +- **组合**:**池选择框**(沪深300 / 中证500 / 中证1000 / 中证2000 / 全市场 / 自定义成分),不手输 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c63f855..1b6a6a9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "axios": "^1.18.1", "echarts": "^6.1.0", "element-plus": "^2.14.2", + "monaco-editor": "^0.56.0", "pinia": "^3.0.4", "vue": "^3.5.39", "vue-router": "^5.1.0" @@ -362,7 +363,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -374,7 +374,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -385,7 +384,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -502,7 +500,6 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -563,7 +560,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -580,7 +576,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -597,7 +592,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -614,7 +608,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -631,7 +624,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -648,7 +640,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -665,7 +656,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -682,7 +672,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -699,7 +688,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -716,7 +704,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -733,7 +720,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -750,7 +736,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -767,7 +752,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -786,7 +770,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -803,7 +786,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -831,7 +813,6 @@ "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -888,12 +869,19 @@ "version": "24.13.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.18.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/web-bluetooth": { "version": "0.0.21", "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", @@ -1710,6 +1698,15 @@ "node": ">=8" } }, + "node_modules/dompurify": { + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz", + "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1959,7 +1956,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -2311,7 +2307,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2332,7 +2327,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2353,7 +2347,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2374,7 +2367,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2395,7 +2387,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2416,7 +2407,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2437,7 +2427,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2458,7 +2447,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2479,7 +2467,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2500,7 +2487,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2521,7 +2507,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2609,6 +2594,18 @@ "url": "https://github.com/sponsors/sxzz" } }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2713,6 +2710,16 @@ "pathe": "^2.0.1" } }, + "node_modules/monaco-editor": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.56.0.tgz", + "integrity": "sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==", + "license": "MIT", + "dependencies": { + "dompurify": "3.4.8", + "marked": "14.0.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3371,7 +3378,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD", "optional": true }, @@ -3409,7 +3415,7 @@ "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unplugin": { diff --git a/frontend/package.json b/frontend/package.json index eb44a79..6e8c8ae 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "axios": "^1.18.1", "echarts": "^6.1.0", "element-plus": "^2.14.2", + "monaco-editor": "^0.56.0", "pinia": "^3.0.4", "vue": "^3.5.39", "vue-router": "^5.1.0" diff --git a/frontend/src/api/backtest.ts b/frontend/src/api/backtest.ts index 462a221..725b92d 100644 --- a/frontend/src/api/backtest.ts +++ b/frontend/src/api/backtest.ts @@ -8,6 +8,9 @@ export interface CtaSubmit { end: string benchmark?: string interval?: string + slippage?: number + commission_rate?: number + stamp_duty_rate?: number } export interface TaskStatus { @@ -101,6 +104,7 @@ export interface TaskListItem { symbol: string start: string end: string + instance?: string } export async function getTasks(type?: string): Promise { diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 9cc1a73..c76529a 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1,6 +1,7 @@ -import axios, { AxiosError } from 'axios' +import axios, { AxiosError, type AxiosResponse } from 'axios' import { useAuthStore } from '@/stores/auth' import { router } from '@/router' +import { matchMock, MOCK_ON } from '@/mock' export const apiClient = axios.create({ baseURL: '/api/v1', @@ -12,6 +13,25 @@ apiClient.interceptors.request.use((config) => { if (auth.token) { config.headers.Authorization = `Bearer ${auth.token}` } + + // 样板 mock:命中假数据表的请求直接短路(自定义 adapter),不发网络。 + // 接真实后端联调时,把 src/mock/index.ts 的 MOCK_ON 置 false 即可恢复全部真实请求。 + if (MOCK_ON) { + const url = config.url ?? '' + const method = config.method ?? 'get' + const mock = matchMock(method, url) + if (mock !== undefined) { + const cfg = config + cfg.adapter = (): Promise => + Promise.resolve({ + data: mock, + status: 200, + statusText: 'MOCK_OK', + headers: {}, + config: cfg, + } as AxiosResponse) + } + } return config }) diff --git a/frontend/src/api/live.ts b/frontend/src/api/live.ts index 7a303bb..feb1f11 100644 --- a/frontend/src/api/live.ts +++ b/frontend/src/api/live.ts @@ -24,6 +24,7 @@ export interface LiveAccount { latest_date?: string | null total_return?: number | null position_count?: number + instance?: string } export interface LiveCreateRequest { diff --git a/frontend/src/api/paper.ts b/frontend/src/api/paper.ts index 577e74c..b3ecc15 100644 --- a/frontend/src/api/paper.ts +++ b/frontend/src/api/paper.ts @@ -36,6 +36,7 @@ export interface PaperAccount { latest_equity?: number | null latest_date?: string | null total_return?: number | null + instance?: string } export interface BalancePoint { diff --git a/frontend/src/api/strategy.ts b/frontend/src/api/strategy.ts index a8c3774..725ed7a 100644 --- a/frontend/src/api/strategy.ts +++ b/frontend/src/api/strategy.ts @@ -19,3 +19,37 @@ export async function getParams(name: string): Promise { const { data } = await apiClient.get(`/strategy/${name}/params`) return data } + +// ----- 策略配置 CRUD(策略类+参数+标的 的可命名持久化配置)----- +export interface StrategyConfig { + id: number + name: string + strategy_class: string + params: Record + symbol_or_pool: string + benchmark: string + interval: string + remark: string + created_at: string + updated_at?: string +} + +export type StrategyConfigInput = Omit + +export async function getStrategyConfigs(): Promise { + const { data } = await apiClient.get<{ strategies: StrategyConfig[] }>('/strategy/configs') + return data.strategies +} + +export async function createStrategyConfig(req: StrategyConfigInput): Promise { + const { data } = await apiClient.post<{ id: number }>('/strategy/configs', req) + return data.id +} + +export async function updateStrategyConfig(id: number, req: StrategyConfigInput): Promise { + await apiClient.put(`/strategy/configs/${id}`, req) +} + +export async function deleteStrategyConfig(id: number): Promise { + await apiClient.delete(`/strategy/configs/${id}`) +} diff --git a/frontend/src/components/MonacoEditor.vue b/frontend/src/components/MonacoEditor.vue new file mode 100644 index 0000000..28f6fa8 --- /dev/null +++ b/frontend/src/components/MonacoEditor.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/frontend/src/components/backtest/AlphaChart.vue b/frontend/src/components/backtest/AlphaChart.vue index 160eb7d..198db15 100644 --- a/frontend/src/components/backtest/AlphaChart.vue +++ b/frontend/src/components/backtest/AlphaChart.vue @@ -4,7 +4,7 @@ import type { EChartsCoreOption } from 'echarts' import { useChart } from '@/composables/useChart' import { darkTitle, darkTooltip, darkGrid, darkAxis } from '@/utils/echartsDark' -const ALPHA = '#61a0a8' // Alpha 绿 +const ALPHA = '#00e5ff' // Alpha · 荧光青 const props = defineProps<{ dates: string[]; alpha: number[] }>() const el = ref() diff --git a/frontend/src/components/backtest/BenchmarkCurve.vue b/frontend/src/components/backtest/BenchmarkCurve.vue index f9c46c5..692e743 100644 --- a/frontend/src/components/backtest/BenchmarkCurve.vue +++ b/frontend/src/components/backtest/BenchmarkCurve.vue @@ -4,8 +4,8 @@ import type { EChartsCoreOption } from 'echarts' import { useChart } from '@/composables/useChart' import { darkTitle, darkTooltip, darkGrid, darkAxis } from '@/utils/echartsDark' -const STRATEGY = '#c23531' // 策略红 -const BENCHMARK = '#2f4554' // 基准蓝 +const STRATEGY = '#00e5ff' // 策略 · 荧光青 +const BENCHMARK = '#ffb000' // 基准 · 琥珀 const props = defineProps<{ dates: string[]; strategy: number[]; benchmark: number[] }>() const el = ref() diff --git a/frontend/src/components/backtest/BetaChart.vue b/frontend/src/components/backtest/BetaChart.vue index e3a41eb..c42214b 100644 --- a/frontend/src/components/backtest/BetaChart.vue +++ b/frontend/src/components/backtest/BetaChart.vue @@ -4,7 +4,7 @@ import type { EChartsCoreOption } from 'echarts' import { useChart } from '@/composables/useChart' import { darkTitle, darkTooltip, darkGrid, darkAxis } from '@/utils/echartsDark' -const BETA = '#61a0a8' // Beta 绿 +const BETA = '#ffb000' // Beta · 琥珀 const props = defineProps<{ dates: string[]; beta: number[] }>() const el = ref() diff --git a/frontend/src/components/backtest/DrawdownChart.vue b/frontend/src/components/backtest/DrawdownChart.vue index adf8df4..c740c20 100644 --- a/frontend/src/components/backtest/DrawdownChart.vue +++ b/frontend/src/components/backtest/DrawdownChart.vue @@ -4,7 +4,7 @@ import type { EChartsCoreOption } from 'echarts' import { useChart } from '@/composables/useChart' import { darkTitle, darkTooltip, darkGrid, darkAxis } from '@/utils/echartsDark' -const DRAWDOWN = '#d48265' // 回撤橙 +const DRAWDOWN = '#ff4d5e' // 回撤 · 风险红 const props = defineProps<{ dates: string[]; drawdown: number[] }>() const el = ref() diff --git a/frontend/src/components/backtest/EquityCurve.vue b/frontend/src/components/backtest/EquityCurve.vue index 93acd08..538629e 100644 --- a/frontend/src/components/backtest/EquityCurve.vue +++ b/frontend/src/components/backtest/EquityCurve.vue @@ -29,8 +29,8 @@ function render(): void { name: '净值', smooth: true, showSymbol: false, - lineStyle: { color: '#c23531', width: 1.6 }, - areaStyle: { color: '#c23531', opacity: 0.12 }, + lineStyle: { color: '#00e5ff', width: 1.6 }, + areaStyle: { color: '#00e5ff', opacity: 0.12 }, data: props.equity, }, ], diff --git a/frontend/src/components/backtest/VolatilityChart.vue b/frontend/src/components/backtest/VolatilityChart.vue index 1b66420..b8d8d38 100644 --- a/frontend/src/components/backtest/VolatilityChart.vue +++ b/frontend/src/components/backtest/VolatilityChart.vue @@ -4,8 +4,8 @@ import type { EChartsCoreOption } from 'echarts' import { useChart } from '@/composables/useChart' import { darkTitle, darkTooltip, darkGrid, darkAxis } from '@/utils/echartsDark' -const STRATEGY = '#c23531' // 策略红 -const BENCHMARK = '#2f4554' // 基准蓝 +const STRATEGY = '#00e5ff' // 策略 · 荧光青 +const BENCHMARK = '#ffb000' // 基准 · 琥珀 const props = defineProps<{ dates: string[]; strategy: number[]; benchmark: number[] }>() const el = ref() diff --git a/frontend/src/mock/data.ts b/frontend/src/mock/data.ts new file mode 100644 index 0000000..f13aa3e --- /dev/null +++ b/frontend/src/mock/data.ts @@ -0,0 +1,425 @@ +/* 业务假数据 —— 覆盖各接口的核心展示数据(样板用,连真实后端时不触发) */ +import { genEquity, genBenchmark, genRisk, genDailyPnl, genPaperEquity, genDates } from './series' + +const N = 117 + +/* ========== 策略 ========== */ +export const strategiesMock = { + strategies: [ + { name: '双均线', class_name: 'AShareDoubleMaStrategy' }, + { name: '布林通道', class_name: 'AShareBollStrategy' }, + { name: 'ATR 突破', class_name: 'AShareAtrStrategy' }, + { name: '全天候(组合)', class_name: 'AllWeatherStrategy' }, + ], +} + +export const strategyParamsMock: Record }> = { + AShareDoubleMaStrategy: { parameters: ['fast_window', 'slow_window', 'size'], defaults: { fast_window: 10, slow_window: 30, size: 100 } }, + AShareBollStrategy: { parameters: ['boll_window', 'boll_dev', 'size'], defaults: { boll_window: 20, boll_dev: 2.0, size: 100 } }, + AShareAtrStrategy: { parameters: ['atr_window', 'atr_multiplier', 'size'], defaults: { atr_window: 14, atr_multiplier: 3.0, size: 100 } }, + AllWeatherStrategy: { parameters: ['rebalance_day', 'top_n'], defaults: { rebalance_day: 1, top_n: 5 } }, +} + +/* ========== 回测历史任务列表 ========== */ +export const taskListMock = { + tasks: [ + { id: 101, task_id: 'cta_7a92d1f6', type: 'cta', status: 'done', strategy: 'AShareDoubleMaStrategy', symbol: '600519.SH', start: '2024-01-02', end: '2024-06-28', instance: '茅台双均线(10,30)·15m' }, + { id: 100, task_id: 'cta_c287f4b5', type: 'cta', status: 'done', strategy: 'AShareBollStrategy', symbol: '000858.SZ', start: '2023-06-01', end: '2024-06-01', instance: '五粮液双均线(5,20)·日线' }, + { id: 99, task_id: 'opt_3f8a2c', type: 'optimize', status: 'done', strategy: 'AShareDoubleMaStrategy', symbol: '600519.SH', start: '2023-01-03', end: '2024-06-28', instance: '茅台双均线(10,30)·15m' }, + { id: 98, task_id: 'pf_a1b2c3', type: 'portfolio', status: 'done', strategy: 'AllWeatherStrategy', symbol: '中证1000成份', start: '2022-01-04', end: '2024-06-28', instance: '全天候·中证1000·周调仓' }, + { id: 97, task_id: 'cta_e5d4c3', type: 'cta', status: 'failed', strategy: 'AShareAtrStrategy', symbol: '300750.SZ', start: '2024-03-01', end: '2024-06-28', instance: '宁德布林(20,2)·15m' }, + { id: 96, task_id: 'fc_88aabb', type: 'factor', status: 'done', strategy: '动量因子MOM20', symbol: '全市场', start: '2020-01-02', end: '2024-06-28', instance: '' }, + { id: 95, task_id: 'cta_112233', type: 'cta', status: 'running', strategy: 'AShareDoubleMaStrategy', symbol: '510300.SH', start: '2024-01-02', end: '2024-08-12', instance: '茅台双均线(10,30)·15m' }, + ], +} + +/* ========== CTA 回测结果 ========== */ +export const ctaMetricsMock = { + relative_metrics: { + total_return: 0.3842, + annual_return: 0.3125, + alpha: 0.0821, + beta: 0.7128, + sharpe_ratio: 1.4203, + sortino_ratio: 1.8841, + information_ratio: 0.9635, + annual_volatility: 0.1822, + max_drawdown: -0.1278, + benchmark_return: 0.0721, + benchmark_volatility: 0.1513, + }, + statistics: { + total_return: '0.3842', + annual_return: '0.3125', + sharpe_ratio: '1.4203', + sortino_ratio: '1.8841', + max_drawdown: '-0.1278', + total_trade_count: '48', + win_rate: '0.5417', + avg_return: '0.0080', + max_dd_duration: '42', + daily_net_pnl: '0', + total_net_pnl: '384200', + total_commission: '4128', + total_slippage: '1820', + total_turnover: '0.84', + }, + task_id: 'cta_7a92d1f6', + symbol: '600519.SH', + start: '2024-01-02', + end: '2024-06-28', + strategy: 'AShareDoubleMaStrategy', + params: { fast_window: 10, slow_window: 30, size: 100 }, + status: 'done', +} + +export const equityCurveMock = { equity_curve: genEquity(N) } + +export const benchmarkCurveMock = (() => { + const e = genEquity(N) + const b = genBenchmark(N) + return { dates: e.map((p) => p.date), strategy: e.map((p) => p.balance), benchmark: b.map((p) => p.balance) } +})() + +export const riskSeriesMock = genRisk(N) +export const dailyPnlMock = { daily_pnl: genDailyPnl(N) } + +export const tradesMock = { + trades: [ + { datetime: '2024-01-05 09:30:00', direction: 'LONG', offset: 'OPEN', price: 1685.2, volume: 100, vt_symbol: '600519.SH' }, + { datetime: '2024-01-18 14:50:00', direction: 'LONG', offset: 'OPEN', price: 1702.5, volume: 100, vt_symbol: '600519.SH' }, + { datetime: '2024-02-08 09:35:00', direction: 'SHORT', offset: 'CLOSE', price: 1748.0, volume: 200, vt_symbol: '600519.SH' }, + { datetime: '2024-03-04 09:30:00', direction: 'LONG', offset: 'OPEN', price: 1710.6, volume: 100, vt_symbol: '600519.SH' }, + { datetime: '2024-04-01 10:15:00', direction: 'SHORT', offset: 'CLOSE', price: 1756.3, volume: 100, vt_symbol: '600519.SH' }, + { datetime: '2024-04-22 09:30:00', direction: 'LONG', offset: 'OPEN', price: 1632.8, volume: 100, vt_symbol: '600519.SH' }, + { datetime: '2024-05-20 13:40:00', direction: 'SHORT', offset: 'CLOSE', price: 1718.9, volume: 100, vt_symbol: '600519.SH' }, + { datetime: '2024-06-12 09:30:00', direction: 'LONG', offset: 'OPEN', price: 1508.2, volume: 100, vt_symbol: '600519.SH' }, + ], +} + +export const klineMock = { + kline: genDates('2024-01-02', 60).map((date, i) => ({ + datetime: date, + open: 1680 + Math.sin(i / 5) * 30 + i * 0.4, + high: 1690 + Math.sin(i / 5) * 30 + i * 0.4, + low: 1670 + Math.sin(i / 5) * 30 + i * 0.4, + close: 1685 + Math.sin(i / 5) * 30 + i * 0.4, + volume: 1200000 + Math.sin(i / 3) * 400000, + })), +} + +export const optResultsMock = { + results: [ + { params: { fast_window: 5, slow_window: 20 }, statistics: { sharpe_ratio: 1.12, total_return: 0.28, max_drawdown: -0.15 } }, + { params: { fast_window: 5, slow_window: 30 }, statistics: { sharpe_ratio: 1.35, total_return: 0.33, max_drawdown: -0.14 } }, + { params: { fast_window: 10, slow_window: 20 }, statistics: { sharpe_ratio: 1.48, total_return: 0.37, max_drawdown: -0.12 } }, + { params: { fast_window: 10, slow_window: 30 }, statistics: { sharpe_ratio: 1.42, total_return: 0.38, max_drawdown: -0.13 } }, + { params: { fast_window: 10, slow_window: 40 }, statistics: { sharpe_ratio: 1.51, total_return: 0.41, max_drawdown: -0.11 } }, + { params: { fast_window: 15, slow_window: 30 }, statistics: { sharpe_ratio: 1.36, total_return: 0.35, max_drawdown: -0.14 } }, + { params: { fast_window: 20, slow_window: 60 }, statistics: { sharpe_ratio: 1.23, total_return: 0.30, max_drawdown: -0.16 } }, + ], +} + +export const logMock = { log: '[2024-06-28 09:30:00] load_data bars=117\n[09:30:01] run_backtesting start\n[09:30:04] run_backtesting done\n[09:30:04] calculate_result shape=(117,25)\n[09:30:04] compute_metrics 27ms\n[09:30:04] write metrics.json done\n' } + +/* ========== 模拟盘 paper ========== */ +export const paperListMock = { + accounts: [ + { id: 1, name: '茅台双均线·回放', mode: 'replay', interval: 'd', status: 'running', symbols: '600519.SH', initial_capital: 1000000, start_date: '2024-01-02', end_date: '2024-06-28', last_run_date: '2024-06-28', next_run_at: null, checkpoint_date: '2024-06-27', error_msg: null, latest_equity: 1384200, latest_date: '2024-06-28', total_return: 0.3842 }, + { id: 2, name: '15分钟动量·实走', mode: 'live', interval: '15m', status: 'running', symbols: '000858.SZ,002594.SZ', initial_capital: 500000, start_date: '2024-05-01', end_date: null, last_run_date: '2024-08-12', next_run_at: '2024-08-13 15:05', checkpoint_date: '2024-08-12', error_msg: null, latest_equity: 521800, latest_date: '2024-08-12', total_return: 0.0436 }, + { id: 3, name: '全天候组合·实走', mode: 'live', interval: 'd', status: 'pending', symbols: '中证1000池', initial_capital: 1000000, start_date: '2024-03-01', end_date: null, last_run_date: '2024-08-11', next_run_at: '2024-08-13 15:05', checkpoint_date: '2024-08-11', error_msg: null, latest_equity: 1046200, latest_date: '2024-08-11', total_return: 0.0462 }, + { id: 4, name: '布林通道测试', mode: 'replay', interval: 'd', status: 'failed', symbols: '300750.SZ', initial_capital: 300000, start_date: '2024-06-01', end_date: '2024-08-01', last_run_date: '2024-08-01', next_run_at: null, checkpoint_date: null, error_msg: 'load_data bars=0:区间无日线', latest_equity: null, latest_date: null, total_return: null }, + ], +} + +export const paperDetailMock = paperListMock.accounts[0] +export const paperEquityMock = genPaperEquity(120) +export const paperTradesMock = tradesMock.trades.map((t, i) => ({ + strategy_id: 'doublema_' + (i % 2), + symbol: '600519.SH', + direction: t.direction, + price: t.price, + volume: t.volume, + commission: Math.round(t.price * t.volume * 0.0003), + rejected: 0, + reject_reason: '', + bar_date: t.datetime.slice(0, 10), +})) +export const paperStrategiesMock = [ + { strategy_id: 'doublema_0', total_orders: 32, filled: 28, rejected: 4, commission: 1280 }, + { strategy_id: 'boll_1', total_orders: 16, filled: 16, rejected: 0, commission: 720 }, +] +export const paperPositionsMock = [ + { symbol: '600519.SH', volume: 200, frozen: 0, avg_price: 1689.3 }, + { symbol: '000858.SZ', volume: 500, frozen: 0, avg_price: 168.2 }, +] +export const paperPendingMock = [ + { strategy_id: 'doublema_0', symbol: '002594.SZ', side: 'BUY', price: 0, volume: 300, is_market: true, match_session: 'next_open', listing_days: 1200 }, +] + +/* ========== 实盘 live ========== */ +export const liveListMock = { + accounts: [ + { id: 11, name: '茅台 miniQMT', account: '8888001100', vt_symbol: '600519.SH', strategy_class: 'AShareDoubleMaStrategy', strategy_name: '双均线', setting: '{"fast_window":10,"slow_window":30,"size":100}', status: 'running', interval: 'd', initial_capital: 1000000, connect_wait_sec: 15, init_wait_sec: 8, mini_path: 'D:\\miniQMTuserdata', error_msg: null, created_at: '2024-07-01 10:00:00', updated_at: '2024-08-12 15:05:00', latest_equity: 1384200, latest_date: '2024-08-12', total_return: 0.3842, position_count: 2 }, + { id: 12, name: '宁德 miniQMT', account: '8888001101', vt_symbol: '300750.SZ', strategy_class: 'AShareBollStrategy', strategy_name: '布林通道', setting: '{"boll_window":20,"boll_dev":2.0,"size":100}', status: 'stopped', interval: 'd', initial_capital: 500000, connect_wait_sec: 15, init_wait_sec: 8, mini_path: 'D:\\miniQMTuserdata', error_msg: null, created_at: '2024-07-10 14:00:00', updated_at: '2024-08-10 15:05:00', latest_equity: 485200, latest_date: '2024-08-10', total_return: -0.0296, position_count: 1 }, + { id: 13, name: '五粮液 miniQMT', account: '8888001102', vt_symbol: '000858.SZ', strategy_class: 'AShareAtrStrategy', strategy_name: 'ATR突破', setting: '{"atr_window":14,"atr_multiplier":3.0,"size":100}', status: 'error', interval: 'd', initial_capital: 300000, connect_wait_sec: 15, init_wait_sec: 8, mini_path: 'D:\\miniQMTuserdata', error_msg: 'xtquant connect timeout (mini_path 未登录)', created_at: '2024-08-01 09:00:00', updated_at: '2024-08-12 09:32:00', latest_equity: null, latest_date: null, total_return: null, position_count: 0 }, + ], +} +export const liveDetailMock = liveListMock.accounts[0] +export const livePositionsMock = [ + { symbol: '600519.SH', volume: 200, frozen: 0, avg_price: 1689.3, updated_at: '2024-08-12 15:00:00' }, + { symbol: '000858.SZ', volume: 500, frozen: 0, avg_price: 168.2, updated_at: '2024-08-12 15:00:00' }, +] +export const liveTradesMock = tradesMock.trades.slice(0, 5).map((t, i) => ({ + account_id: 11, strategy_name: '双均线', symbol: '600519.SH', direction: t.direction, offset: t.offset, price: t.price, volume: t.volume, traded_at: t.datetime, vt_tradeid: 'xt_' + i, +})) +export const liveAccountMock = { account_id: 11, date: '2024-08-12', cash: 612400, market_value: 771800, total: 1384200 } +export const liveStatusMock = { account_id: 11, status: 'running', name: '茅台 miniQMT', account: '8888001100', vt_symbol: '600519.SH', strategy_name: '双均线', updated_at: '2024-08-12 15:05:00', error_msg: null } + +/* ========== 组合回测 portfolio ========== */ +export const portfolioResultMock = { + strategy: 'AllWeatherStrategy', + period: { start: '2022-01-04', end: '2024-06-28', trading_days: 596 }, + stocks_selected: [ + { code: '002594.SZ', name: '比亚迪', amount: 200, avg_cost: 245.3, price: 268.5, value: 53700 }, + { code: '300750.SZ', name: '宁德时代', amount: 50, avg_cost: 198.4, price: 212.6, value: 10630 }, + { code: '600519.SH', name: '贵州茅台', amount: 10, avg_cost: 1689.3, price: 1718.9, value: 17189 }, + { code: '000858.SZ', name: '五粮液', amount: 300, avg_cost: 168.2, price: 158.4, value: 47520 }, + { code: '601012.SH', name: '隆基绿能', amount: 800, avg_cost: 32.1, price: 28.6, value: 22880 }, + ], + trades: tradesMock.trades.slice(0, 6).map((t, i) => ({ datetime: t.datetime, code: '600519.SH', side: i % 2 === 0 ? 'BUY' : 'SELL', amount: 100, filled_amount: 100, price: t.price, filled_price: t.price, commission: 18.5, status: 'FILLED' })), + equity_curve: genEquity(180, { from: '2022-01-04' }).map((p) => ({ date: p.date, equity: p.balance })), + metrics: { + total_return: 0.1523, annual_return: 0.0582, max_drawdown: -0.168, sharpe: 0.78, + win_rate_daily: 0.521, win_rate_trade: 0.483, trading_days: 596, + }, +} + +/* ========== 策略配置(CRUD) ========== */ +export const strategyConfigsMock = { + strategies: [ + { id: 1, name: '茅台双均线(10,30)', strategy_class: 'AShareDoubleMaStrategy', params: { fast_window: 10, slow_window: 30, size: 100 }, symbol_or_pool: '600519.SH', benchmark: 'hs300', interval: 'd', remark: '茅台日线双均线', created_at: '2024-07-01', updated_at: '2024-08-10' }, + { id: 2, name: '宁德布林(20,2)', strategy_class: 'AShareBollStrategy', params: { boll_window: 20, boll_dev: 2, size: 100 }, symbol_or_pool: '300750.SZ', benchmark: 'zz500', interval: 'd', remark: '宁德时代布林通道', created_at: '2024-07-15', updated_at: '2024-08-09' }, + { id: 3, name: '全天候组合', strategy_class: 'AllWeatherStrategy', params: { rebalance_day: 1, top_n: 5 }, symbol_or_pool: '中证1000池', benchmark: 'zz500', interval: 'd', remark: '组合主线·月度调仓', created_at: '2024-06-01', updated_at: '2024-08-12' }, + { id: 4, name: '五粮液ATR突破', strategy_class: 'AShareAtrStrategy', params: { atr_window: 14, atr_multiplier: 3, size: 100 }, symbol_or_pool: '000858.SZ', benchmark: 'hs300', interval: 'd', remark: '', created_at: '2024-08-01', updated_at: '2024-08-11' }, + ], +} + +/* ========== 策略代码文件(在线编辑 mock) ========== */ +const DOUBLE_MA_CODE = `"""双均线策略(A股适配 · 基于 vnpy StrategyTemplate)。 + +fast_window 周期均线上穿 slow_window 周期均线 → 买入; +下穿 → 卖出。定寸 size 股下单(A股 100 股一手)。 +""" +from typing import List +from vnpy_ctastrategy import StrategyTemplate +from vnpy.trader.object import BarData + + +class DoubleMaStrategy(StrategyTemplate): + """双均线 CTA 策略。""" + + author = "sanguo" + fast_window = 10 + slow_window = 30 + size = 100 + + parameters = ["fast_window", "slow_window", "size"] + variables = ["fast_ma", "slow_ma"] + + def __init__(self, cta_engine, strategy_name, vt_symbol, setting): + super().__init__(cta_engine, strategy_name, vt_symbol, setting) + self.fast_ma = 0.0 + self.slow_ma = 0.0 + + def on_init(self) -> None: + self.write_log("策略初始化") + self.load_bar(self.slow_window) + + def on_bar(self, bar: BarData) -> None: + am = self.am + am.update_bar(bar) + if not am.inited: + return + self.fast_ma = am.sma(self.fast_window, array=True)[-1] + self.slow_ma = am.sma(self.slow_window, array=True)[-1] + cross_over = self.fast_ma > self.slow_ma + cross_below = self.fast_ma < self.slow_ma + if cross_over and self.pos == 0: + self.buy(bar.close_price + 0.01, self.size) + elif cross_below and self.pos > 0: + self.sell(bar.close_price - 0.01, self.size) + self.put_event() +` + +const ALL_WEATHER_CODE = `"""聚宽"全天候轮动"策略 · BulletTrade 框架移植。 + +选股 + 大小盘轮动 + 海外 ETF 兜底 + 涨停盯盘,月度调仓。 +数据/下单全部走注入的 provider 和 broker_facade(聚宽风格 API)。 +""" +import logging +import numpy as np +from dataclasses import dataclass, field +from typing import Any, Callable, List, Optional + +logger = logging.getLogger(__name__) + + +@dataclass +class AllWeatherConfig: + stock_num: int = 3 # 持仓数 + trend_window: int = 10 # 趋势涨幅窗口 + trend_threshold: float = 10.0 # "无敌好行情"阈值 + stop_loss_pct: float = 0.92 # 止损线 + benchmark: str = "000300.XSHG" + + +class AllWeatherStrategy: + """全天候轮动(组合主线策略)。""" + + def __init__(self, provider: Any, broker: Any = None, config: Optional[AllWeatherConfig] = None): + self.provider = provider + self.broker = broker + self.config = config or AllWeatherConfig() + self.hold_list: List[str] = [] + + def initialize(self, context: Any) -> None: + b = self.broker + b.set_benchmark(self.config.benchmark) + b.run_daily(self.prepare_stock_list, "9:05") + b.run_monthly(self.monthly_adjustment, 1, "9:30") + b.run_daily(self.stop_loss, "14:00") + + def monthly_adjustment(self, context: Any) -> None: + """月度调仓:大小盘轮动 + 选股 + 调仓下单(详见完整源码)。""" + cfg = self.config + b_stocks = self._stock_pool("000300.XSHG", context.previous_date) + target = self._pick_big_universe(b_stocks, context) + for code in target: + self.broker.order_target_value(code, 0) # 占位:实际按 target 等权 + logger.info("monthly_adjustment target=%s", target) + + def stop_loss(self, context: Any) -> None: + """持仓跌幅超 stop_loss_pct 止损。""" + pass # 详见完整实现 +` + +const BOLL_CODE = `"""布林带通道策略(A股)。价格突破上轨买入,跌破下轨卖出。""" +from vnpy_ctastrategy import StrategyTemplate + + +class BollChannelStrategy(StrategyTemplate): + author = "sanguo" + boll_window = 20 + boll_dev = 2.0 + size = 100 + parameters = ["boll_window", "boll_dev", "size"] + variables = ["boll_up", "boll_down"] + + def on_init(self): + self.load_bar(self.boll_window) + + def on_bar(self, bar): + am = self.am + am.update_bar(bar) + if not am.inited: + return + self.boll_up, self.boll_down = am.boll(self.boll_window, self.boll_dev) + if am.close > self.boll_up and self.pos == 0: + self.buy(bar.close_price, self.size) + elif am.close < self.boll_down and self.pos > 0: + self.sell(bar.close_price, self.size) +` + +const ATR_CODE = `"""ATR 通道突破策略(A股)。基于 ATR 的动态通道突破。""" +from vnpy_ctastrategy import StrategyTemplate + + +class AtrRsiStrategy(StrategyTemplate): + author = "sanguo" + atr_window = 14 + atr_multiplier = 3.0 + size = 100 + parameters = ["atr_window", "atr_multiplier", "size"] + variables = ["atr_value", "entry_price"] + + def on_init(self): + self.load_bar(self.atr_window + 5) + + def on_bar(self, bar): + am = self.am + am.update_bar(bar) + if not am.inited: + return + self.atr_value = am.atr(self.atr_window) + entry = am.close[-2] + self.atr_value * self.atr_multiplier + if am.close > entry and self.pos == 0: + self.buy(entry, self.size) + self.entry_price = entry +` + +export interface StrategyFile { + name: string + dir: string + class_name: string + type: 'portfolio' | 'cta' + lines: number + modified: string + code: string +} +export const strategyFilesMock: { files: StrategyFile[] } = { + files: [ + { name: 'all_weather.py', dir: 'sanguo_portfolio/strategies/', class_name: 'AllWeatherStrategy', type: 'portfolio', lines: 575, modified: '2024-08-12 14:30', code: ALL_WEATHER_CODE }, + { name: 'momentum_timing.py', dir: 'sanguo_portfolio/strategies/', class_name: 'MomentumTimingStrategy', type: 'portfolio', lines: 412, modified: '2024-08-10 10:20', code: ALL_WEATHER_CODE }, + { name: 'double_ma.py', dir: 'sanguo_live/strategies/', class_name: 'AShareDoubleMaStrategy', type: 'cta', lines: 68, modified: '2024-07-20 09:15', code: DOUBLE_MA_CODE }, + { name: 'boll_channel.py', dir: 'sanguo_live/strategies/', class_name: 'AShareBollStrategy', type: 'cta', lines: 32, modified: '2024-07-22 10:00', code: BOLL_CODE }, + { name: 'atr_breakout.py', dir: 'sanguo_live/strategies/', class_name: 'AShareAtrStrategy', type: 'cta', lines: 30, modified: '2024-08-01 11:20', code: ATR_CODE }, + ], +} + +/* ========== 策略实例(实例层:代码下的参数变体,每实例一键 4 运行) ========== */ +export interface StrategyInstance { + id: number + code_file: string + name: string + type: 'portfolio' | 'cta' + params: Record + symbol_or_pool: string + interval: string + match_session: string + status: { backtest: string; replay: string; paper_live: string; live: string } + last_return: number | null + updated_at: string +} +export const strategyInstancesMock: { instances: StrategyInstance[] } = { + instances: [ + { id: 1, code_file: 'all_weather.py', name: '全天候·HS300·月调仓', type: 'portfolio', params: { stock_num: 3, trend_window: 10 }, symbol_or_pool: 'HS300', interval: 'd', match_session: 'next_open', status: { backtest: 'done', replay: 'done', paper_live: 'running', live: '-' }, last_return: 0.1523, updated_at: '2024-08-12' }, + { id: 2, code_file: 'all_weather.py', name: '全天候·中证1000·周调仓', type: 'portfolio', params: { stock_num: 5, trend_window: 5 }, symbol_or_pool: '中证1000', interval: 'd', match_session: 'next_open', status: { backtest: 'done', replay: '-', paper_live: '-', live: '-' }, last_return: 0.082, updated_at: '2024-08-10' }, + { id: 3, code_file: 'double_ma.py', name: '茅台双均线(10,30)·15m', type: 'cta', params: { fast_window: 10, slow_window: 30, size: 100 }, symbol_or_pool: '600519.SH', interval: '15m', match_session: 'next_open', status: { backtest: 'done', replay: 'done', paper_live: 'running', live: 'running' }, last_return: 0.3842, updated_at: '2024-08-12' }, + { id: 4, code_file: 'double_ma.py', name: '五粮液双均线(5,20)·日线', type: 'cta', params: { fast_window: 5, slow_window: 20, size: 100 }, symbol_or_pool: '000858.SZ', interval: 'd', match_session: 'current_close', status: { backtest: 'done', replay: '-', paper_live: '-', live: '-' }, last_return: -0.052, updated_at: '2024-08-08' }, + { id: 5, code_file: 'boll_channel.py', name: '宁德布林(20,2)·15m', type: 'cta', params: { boll_window: 20, boll_dev: 2, size: 100 }, symbol_or_pool: '300750.SZ', interval: '15m', match_session: 'next_open', status: { backtest: 'done', replay: '-', paper_live: '-', live: 'stopped' }, last_return: -0.0296, updated_at: '2024-08-10' }, + ], +} + +/* ========== 因子 factor ========== */ +export const factorListMock = { + factors: [ + { name: '动量因子 MOM20', category: '动量' }, + { name: '换手率 TURN5', category: '量价' }, + { name: '波动率 VOL20', category: '波动' }, + { name: '市值 SIZE', category: '风格' }, + { name: 'ROE_TTM', category: '质量' }, + { name: '毛利率 GROSS', category: '质量' }, + ], +} +export const icSummaryMock = { + ic_summary: { + MOM20: { IC_mean: 0.042, ICIR: 0.51, t_stat: 3.21, positive_ratio: 0.58 }, + TURN5: { IC_mean: -0.028, ICIR: -0.33, t_stat: -2.05, positive_ratio: 0.41 }, + VOL20: { IC_mean: -0.035, ICIR: -0.42, t_stat: -2.6, positive_ratio: 0.38 }, + }, +} + +/* ========== 任务状态(progress 轮询) ========== */ +export const taskStatusDoneMock = { task_id: 'cta_7a92d1f6', status: 'done', stage: 'compute_metrics' } +export const taskStatusRunningMock = { task_id: 'cta_running01', status: 'running', stage: 'run_backtesting' } diff --git a/frontend/src/mock/index.ts b/frontend/src/mock/index.ts new file mode 100644 index 0000000..35b3428 --- /dev/null +++ b/frontend/src/mock/index.ts @@ -0,0 +1,97 @@ +/* mock 路由匹配器 —— 按 method + URL 正则返回假数据 + * 仅在后端完全不可达(无 HTTP response)时由 client 兜底调用; + * 连真实后端时(含 4xx/5xx)不触发,真实错误照常暴露。 + * 样板阶段用于让每个页面都有内容可看。接真实后端时无需改这里。 */ +import * as D from './data' + +export const MOCK_ON = true + +type Builder = (url: string) => unknown +interface Route { + m: string + re: RegExp + build: Builder +} + +const routes: Route[] = [ + // strategy + { m: 'get', re: /^\/strategy\/list$/, build: () => D.strategiesMock }, + // strategy instances(实例层 · 代码下参数变体) + { m: 'get', re: /^\/strategy\/instances$/, build: () => D.strategyInstancesMock }, + { m: 'get', re: /^\/strategy\/instances\/[^/]+$/, build: (url) => { const id = Number(url.split('/')[3]); const inst = D.strategyInstancesMock.instances.find((i) => i.id === id); return { instance: inst ?? D.strategyInstancesMock.instances[0] } } }, + { m: 'post', re: /^\/strategy\/instances$/, build: () => ({ id: 99 }) }, + { m: 'delete', re: /^\/strategy\/instances\/[^/]+$/, build: () => ({ ok: true }) }, + // strategy code files(在线编辑) + { m: 'get', re: /^\/strategy\/files$/, build: () => D.strategyFilesMock }, + { m: 'post', re: /^\/strategy\/file\/[^/]+$/, build: () => ({ ok: true }) }, + // strategy configs(CRUD) + { m: 'get', re: /^\/strategy\/configs$/, build: () => D.strategyConfigsMock }, + { m: 'post', re: /^\/strategy\/configs$/, build: () => ({ id: 99 }) }, + { m: 'put', re: /^\/strategy\/configs\/[^/]+$/, build: () => ({ ok: true }) }, + { m: 'delete', re: /^\/strategy\/configs\/[^/]+$/, build: () => ({ ok: true }) }, + // factor + { m: 'get', re: /^\/factor\/list$/, build: () => D.factorListMock }, + { m: 'get', re: /^\/task\/[^/]+\/ic-summary$/, build: () => D.icSummaryMock }, + { m: 'post', re: /^\/factor\/analyze$/, build: () => ({ task_id: 'fc_mock_submit' }) }, + { + m: 'get', + re: /^\/strategy\/[^/]+\/params$/, + build: (url) => D.strategyParamsMock[decodeURIComponent(url.split('/')[2])] ?? { parameters: [], defaults: {} }, + }, + // task + { m: 'get', re: /^\/task$/, build: () => D.taskListMock }, + { m: 'get', re: /^\/task\/[^/]+$/, build: () => D.taskStatusDoneMock }, + { m: 'get', re: /^\/task\/[^/]+\/result$/, build: () => D.ctaMetricsMock }, + { m: 'get', re: /^\/task\/[^/]+\/equity-curve$/, build: () => D.equityCurveMock }, + { m: 'get', re: /^\/task\/[^/]+\/benchmark-curve$/, build: () => D.benchmarkCurveMock }, + { m: 'get', re: /^\/task\/[^/]+\/risk-series$/, build: () => D.riskSeriesMock }, + { m: 'get', re: /^\/task\/[^/]+\/daily-pnl$/, build: () => D.dailyPnlMock }, + { m: 'get', re: /^\/task\/[^/]+\/trades$/, build: () => D.tradesMock }, + { m: 'get', re: /^\/task\/[^/]+\/log$/, build: () => D.logMock }, + { m: 'get', re: /^\/task\/[^/]+\/optimization-results$/, build: () => D.optResultsMock }, + // kline + { m: 'get', re: /^\/kline$/, build: () => D.klineMock }, + // paper + { m: 'get', re: /^\/paper$/, build: () => D.paperListMock }, + { m: 'get', re: /^\/paper\/[^/]+$/, build: () => D.paperDetailMock }, + { m: 'get', re: /^\/paper\/[^/]+\/equity$/, build: () => D.paperEquityMock }, + { m: 'get', re: /^\/paper\/[^/]+\/trades$/, build: () => D.paperTradesMock }, + { m: 'get', re: /^\/paper\/[^/]+\/strategies$/, build: () => D.paperStrategiesMock }, + { m: 'get', re: /^\/paper\/[^/]+\/positions$/, build: () => D.paperPositionsMock }, + { m: 'get', re: /^\/paper\/[^/]+\/pending$/, build: () => D.paperPendingMock }, + // live + { m: 'get', re: /^\/live$/, build: () => D.liveListMock }, + { m: 'get', re: /^\/live\/[^/]+$/, build: () => D.liveDetailMock }, + { m: 'get', re: /^\/live\/[^/]+\/positions$/, build: () => D.livePositionsMock }, + { m: 'get', re: /^\/live\/[^/]+\/trades$/, build: () => D.liveTradesMock }, + { m: 'get', re: /^\/live\/[^/]+\/account$/, build: () => D.liveAccountMock }, + { m: 'get', re: /^\/live\/[^/]+\/status$/, build: () => D.liveStatusMock }, + // portfolio + { m: 'get', re: /^\/portfolio\/task\/[^/]+\/result$/, build: () => D.portfolioResultMock }, + // POST 写操作 → 成功响应(样板) + { m: 'post', re: /^\/auth\/login$/, build: () => ({ token: 'mock-token-sanguo-terminal' }) }, + { m: 'post', re: /^\/backtest\/cta$/, build: () => ({ task_id: 'cta_mock_submit' }) }, + { m: 'post', re: /^\/backtest\/optimize$/, build: () => ({ task_id: 'opt_mock_submit' }) }, + { m: 'post', re: /^\/portfolio\/backtest$/, build: () => ({ task_id: 'pf_mock_submit' }) }, + { m: 'post', re: /^\/paper\/create$/, build: () => ({ account_id: 99 }) }, + { m: 'post', re: /^\/live\/create$/, build: () => ({ account_id: 99, status: 'stopped' }) }, + { + m: 'post', + re: /^\/live\/[^/]+\/start$/, + build: (url) => ({ account_id: Number(url.split('/')[2]), status: 'running' }), + }, + { + m: 'post', + re: /^\/live\/[^/]+\/stop$/, + build: (url) => ({ account_id: Number(url.split('/')[2]), status: 'stopped' }), + }, +] + +export function matchMock(method: string, url: string): unknown | undefined { + if (!MOCK_ON) return undefined + const m = method.toLowerCase() + for (const r of routes) { + if (r.m === m && r.re.test(url)) return r.build(url) + } + return undefined +} diff --git a/frontend/src/mock/series.ts b/frontend/src/mock/series.ts new file mode 100644 index 0000000..b4fdf24 --- /dev/null +++ b/frontend/src/mock/series.ts @@ -0,0 +1,86 @@ +/* 时序假数据生成器 —— 确定性(基于 index 的函数),刷新不变,图表稳定 */ + +/** 从 start 起生成 n 个交易日(跳周末) */ +export function genDates(start: string, n: number): string[] { + const d = new Date(start + 'T00:00:00') + const out: string[] = [] + let guard = 0 + while (out.length < n && guard < n * 2 + 20) { + const day = d.getDay() + if (day !== 0 && day !== 6) out.push(d.toISOString().slice(0, 10)) + d.setDate(d.getDate() + 1) + guard++ + } + return out +} + +/** 策略净值:趋势 + 双频波动 + 周期回撤 */ +export function genEquity(n: number, opts?: { start?: number; drift?: number; from?: string }): + { date: string; balance: number }[] { + const start = opts?.start ?? 1 + const drift = opts?.drift ?? 0.0009 + const dates = genDates(opts?.from ?? '2024-01-02', n) + let v = start + return dates.map((date, i) => { + const wave = Math.sin(i / 8) * 0.006 + Math.sin(i / 21) * 0.004 + const dd = i % 55 < 14 ? -0.004 * Math.sin(((i % 55) / 14) * Math.PI) : 0 + v = v * (1 + drift + wave + dd) + return { date, balance: Number(v.toFixed(4)) } + }) +} + +/** 基准:弱趋势(跑输策略) */ +export function genBenchmark(n: number, from = '2024-01-02'): { date: string; balance: number }[] { + const dates = genDates(from, n) + let v = 1 + return dates.map((date, i) => { + v = v * (1 + 0.0003 + Math.sin(i / 13) * 0.004) + return { date, balance: Number(v.toFixed(4)) } + }) +} + +/** 每日盈亏(基点,偏正) */ +export function genDailyPnl(n: number): { date: string; pnl: number }[] { + const eq = genEquity(n) + return eq.map((p, i) => { + const prev = i === 0 ? 1 : eq[i - 1].balance + return { date: p.date, pnl: Number(((p.balance - prev) * 10000).toFixed(0)) } + }) +} + +/** alpha/beta/drawdown/vol 时间序列 */ +export function genRisk(n: number, from = '2024-01-02') { + const dates = genDates(from, n) + const eq = genEquity(n, { from }).map((p) => p.balance) + let runMax = 0 + const drawdown = eq.map((v) => { + runMax = Math.max(runMax, v) + return Number((((v - runMax) / runMax) * 100).toFixed(2)) + }) + return { + dates, + alpha: dates.map((_, i) => Number((0.08 + Math.sin(i / 17) * 0.05).toFixed(3))), + beta: dates.map((_, i) => Number((0.72 + Math.sin(i / 25) * 0.12).toFixed(3))), + drawdown, + strategy_vol: dates.map((_, i) => Number((0.18 + Math.sin(i / 11) * 0.03).toFixed(3))), + benchmark_vol: dates.map((_, i) => Number((0.15 + Math.sin(i / 13) * 0.02).toFixed(3))), + } +} + +/** 模拟盘/实盘净值(含 cash/market_value/总权益,带 checkpoint 标记) */ +export function genPaperEquity(n: number): { + date: string; cash: number; market_value: number; total_equity: number; is_checkpoint: number +}[] { + const eq = genEquity(n, { start: 1000000, drift: 0.0006 }) + return eq.map((p, i) => { + const total = p.balance * 1000000 + const mv = total * (0.55 + Math.sin(i / 9) * 0.12) + return { + date: p.date, + cash: Number((total - mv).toFixed(0)), + market_value: Number(mv.toFixed(0)), + total_equity: Number(total.toFixed(0)), + is_checkpoint: i % 15 === 0 ? 1 : 0, + } + }) +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 8329626..5a85960 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -9,6 +9,12 @@ const routes: RouteRecordRaw[] = [ children: [ { path: '', redirect: '/dashboard' }, { path: 'dashboard', name: 'dashboard', component: () => import('@/views/Dashboard.vue') }, + { path: 'strategy', name: 'strategy-list', component: () => import('@/views/strategy/List.vue') }, + { path: 'strategy/new', name: 'strategy-new', component: () => import('@/views/strategy/Edit.vue') }, + { path: 'strategy/code', name: 'strategy-code', component: () => import('@/views/strategy/Code.vue') }, + { path: 'strategy/instance/new', name: 'instance-new', component: () => import('@/views/strategy/InstanceEdit.vue') }, + { path: 'strategy/instance/:id', name: 'instance-edit', component: () => import('@/views/strategy/InstanceEdit.vue') }, + { path: 'account', name: 'account', component: () => import('@/views/account/List.vue') }, { path: 'backtest/new', name: 'bt-new', component: () => import('@/views/backtest/New.vue') }, { path: 'backtest/portfolio', name: 'bt-portfolio', component: () => import('@/views/backtest/PortfolioBacktest.vue') }, { path: 'backtest/progress/:id', name: 'bt-progress', component: () => import('@/views/backtest/Progress.vue') }, diff --git a/frontend/src/styles/chips.css b/frontend/src/styles/chips.css index 36b1a0c..2a259a8 100644 --- a/frontend/src/styles/chips.css +++ b/frontend/src/styles/chips.css @@ -1,63 +1,77 @@ -/* 全局 chip / 状态色 —— 所有页面复用(红涨绿跌 A 股配色,深色主题) */ +/* 全局 chip / 状态色 —— 科幻终端(荧光青+琥珀+红涨绿跌,深色主题) */ .chip { - display: inline-block; + display: inline-flex; + align-items: center; padding: 1px 8px; - border-radius: 4px; - font-size: 12px; - font-weight: 500; + border-radius: var(--r-sm); + font-size: 11.5px; + font-weight: 600; line-height: 18px; + letter-spacing: 0.3px; white-space: nowrap; + font-family: var(--mono); + border: 1px solid transparent; } /* 任务类型 */ .chip-cta { color: var(--brand); - background: rgba(24, 144, 255, 0.14); + background: rgba(0, 229, 255, 0.1); + border-color: rgba(0, 229, 255, 0.3); } .chip-optimize { - color: #13c2c2; - background: rgba(19, 194, 194, 0.14); + color: var(--brand); + background: rgba(0, 229, 255, 0.06); + border-color: rgba(0, 229, 255, 0.2); } .chip-factor { - color: #b37feb; - background: rgba(179, 127, 235, 0.14); + color: #b48cff; + background: rgba(180, 140, 255, 0.1); + border-color: rgba(180, 140, 255, 0.3); } .chip-paper { - color: #d29922; - background: rgba(210, 153, 34, 0.14); + color: var(--amber); + background: rgba(255, 176, 0, 0.1); + border-color: rgba(255, 176, 0, 0.3); +} +.chip-portfolio { + color: var(--amber); + background: rgba(255, 176, 0, 0.08); + border-color: rgba(255, 176, 0, 0.25); } -/* 状态 */ +/* 状态(终端信号灯配色) */ .st-done { - color: var(--down); - background: rgba(63, 185, 80, 0.14); + color: var(--lamp-ok); + background: rgba(46, 230, 138, 0.1); + border-color: rgba(46, 230, 138, 0.3); } .st-failed { - color: var(--danger); - background: rgba(248, 81, 73, 0.14); + color: var(--lamp-crit); + background: rgba(255, 77, 94, 0.1); + border-color: rgba(255, 77, 94, 0.3); } .st-running { color: var(--brand); - background: rgba(24, 144, 255, 0.14); + background: rgba(0, 229, 255, 0.1); + border-color: rgba(0, 229, 255, 0.3); } .st-pending { - color: var(--warn); - background: rgba(210, 153, 34, 0.14); + color: var(--lamp-warn); + background: rgba(255, 176, 0, 0.1); + border-color: rgba(255, 176, 0, 0.3); } /* 涨跌(红涨绿跌) */ -.up { - color: var(--up); -} -.down { - color: var(--down); -} +.up { color: var(--up); } +.down { color: var(--down); } /* 通用文字层级 */ .mono { font-family: var(--mono); font-size: 12px; color: var(--text); + font-variant-numeric: tabular-nums; } .muted { color: var(--text-2); @@ -70,13 +84,52 @@ margin-left: 8px; padding: 0 8px; min-width: 24px; - height: 22px; - line-height: 22px; + height: 20px; + line-height: 20px; text-align: center; - font-size: 12px; + font-size: 11px; font-weight: 600; + font-family: var(--mono); color: var(--text-2); background: var(--bg-hover); - border-radius: 11px; + border: 1px solid var(--border); + border-radius: var(--r-sm); vertical-align: middle; } + +/* 终端按钮 · 描边发光风(全站复用) */ +.term-btn { + padding: 6px 14px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--r-sm); + color: var(--text-2); + font-family: var(--mono); + font-size: 11.5px; + letter-spacing: 0.3px; + cursor: pointer; + transition: border-color 0.15s var(--ease), color 0.15s var(--ease), box-shadow 0.15s var(--ease); +} +.term-btn:hover { + border-color: rgba(0, 229, 255, 0.5); + color: var(--brand); + box-shadow: 0 0 12px rgba(0, 229, 255, 0.18); +} +.term-btn.primary { + background: var(--cyan-soft); + border-color: rgba(0, 229, 255, 0.5); + color: var(--brand); + font-weight: 600; +} +.term-btn.primary:hover { + background: rgba(0, 229, 255, 0.22); + border-color: var(--brand); +} +.term-btn.sm { padding: 3px 9px; font-size: 10.5px; } +.term-btn.danger { color: var(--lamp-crit); } +.term-btn.danger:hover { + border-color: rgba(255, 77, 94, 0.5); + color: var(--lamp-crit); + box-shadow: 0 0 12px rgba(255, 77, 94, 0.18); +} +.term-btn:disabled { opacity: 0.5; cursor: not-allowed; } diff --git a/frontend/src/styles/element-dark.css b/frontend/src/styles/element-dark.css index 433b8e7..d8d37c1 100644 --- a/frontend/src/styles/element-dark.css +++ b/frontend/src/styles/element-dark.css @@ -1,13 +1,13 @@ -/* Element Plus 深色覆盖 —— html.dark 已启用 EP 官方深色,此处精修到量化主题 */ +/* Element Plus 科幻终端覆盖 —— html.dark 下精修到荧光青 / 深空黑主题 */ html.dark { - /* 主色 → 品牌 */ - --el-color-primary: #1890ff; - --el-color-primary-light-3: #2f7fce; - --el-color-primary-light-5: #1f5e9c; - --el-color-primary-light-7: #164670; - --el-color-primary-light-8: #113a5e; - --el-color-primary-light-9: #0e2f4d; - --el-color-primary-dark-2: #3a9eff; + /* 主色 → 荧光青(深色主题色阶越 light 越深,作 tint 底) */ + --el-color-primary: #00e5ff; + --el-color-primary-light-3: #2a8095; + --el-color-primary-light-5: #1c5a6a; + --el-color-primary-light-7: #143f4a; + --el-color-primary-light-8: #0f3038; + --el-color-primary-light-9: #0a2228; + --el-color-primary-dark-2: #33eaff; /* 背景层级 */ --el-bg-color: var(--bg-card); @@ -19,7 +19,7 @@ html.dark { --el-text-color-regular: var(--text-2); --el-text-color-secondary: var(--text-3); --el-text-color-placeholder: var(--text-3); - --el-text-color-disabled: #4b5158; + --el-text-color-disabled: #3a4654; /* 边框 */ --el-border-color: var(--border); @@ -39,39 +39,51 @@ html.dark { --el-fill-color-blank: transparent; /* 遮罩/禁用 */ - --el-mask-color: rgba(1, 4, 9, 0.72); + --el-mask-color: rgba(2, 4, 9, 0.82); --el-disabled-bg-color: var(--bg-card); --el-disabled-text-color: var(--text-3); --el-disabled-border-color: var(--border-2); - /* 语义色(danger 用红、success 用绿,契合涨跌但保持语义) */ - --el-color-success: #3fb950; - --el-color-warning: #d29922; - --el-color-danger: #f85149; - --el-color-error: #f85149; - --el-color-info: #8b949e; + /* 语义色 */ + --el-color-success: #2ee68a; + --el-color-success-light-9: rgba(46, 230, 138, 0.12); + --el-color-warning: #ffb000; + --el-color-warning-light-9: rgba(255, 176, 0, 0.12); + --el-color-danger: #ff4d5e; + --el-color-danger-light-9: rgba(255, 77, 94, 0.12); + --el-color-error: #ff4d5e; + --el-color-info: #7a8a9a; } -/* 组件细节精修 */ +/* 组件细节精修 —— 科幻终端 */ html.dark { - /* 卡片更紧凑、去多余阴影 */ + /* 卡片:去阴影,加顶部荧光高光线 */ .el-card { background-color: var(--bg-card); border: 1px solid var(--border); border-radius: var(--r-md); box-shadow: none; + position: relative; + } + .el-card::before { + content: ''; + position: absolute; + inset: 0 0 auto 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(0, 229, 255, 0.4), transparent); + opacity: 0.6; } .el-card__header { - padding: 12px 16px; + padding: 10px 16px; border-bottom: 1px solid var(--border); font-weight: 600; + font-size: 13px; color: var(--text); + letter-spacing: 0.4px; } - .el-card__body { - padding: 16px; - } + .el-card__body { padding: 16px; } - /* 表格密度 */ + /* 表格密度 + 等宽数字 */ .el-table { --el-table-bg-color: transparent; --el-table-tr-bg-color: transparent; @@ -81,17 +93,17 @@ html.dark { --el-table-border: 1px solid var(--border-2); --el-table-text-color: var(--text); --el-table-header-text-color: var(--text-2); - font-size: 13px; + font-size: 12.5px; + font-variant-numeric: tabular-nums; } .el-table th.el-table__cell { background-color: var(--bg-hover) !important; font-weight: 600; + letter-spacing: 0.3px; } - .el-table .cell { - line-height: 1.5; - } + .el-table .cell { line-height: 1.5; } - /* 菜单 */ + /* 菜单:激活项荧光青 + 左侧发光指示条 */ .el-menu { --el-menu-bg-color: transparent; --el-menu-text-color: var(--text-2); @@ -99,38 +111,56 @@ html.dark { --el-menu-active-color: var(--brand); border-right: none; } - .el-menu-item.is-active, - .el-sub-menu.is-active > .el-sub-menu__title { + .el-menu-item.is-active { color: var(--brand); + background: var(--cyan-soft) !important; + box-shadow: inset 2px 0 0 var(--brand); } + .el-sub-menu.is-active > .el-sub-menu__title { color: var(--brand); } - /* 输入/选择器贴主题 */ + /* 输入/选择器:聚焦荧光发光 */ .el-input__wrapper, .el-textarea__inner, .el-select__wrapper { background-color: var(--bg) !important; box-shadow: 0 0 0 1px var(--border) inset !important; + border-radius: var(--r-sm) !important; } .el-input__wrapper:hover, .el-select__wrapper:hover { - box-shadow: 0 0 0 1px var(--brand) inset !important; + box-shadow: 0 0 0 1px var(--cyan-dim) inset !important; } .el-input.is-focus .el-input__wrapper, .el-select__wrapper.is-focused { - box-shadow: 0 0 0 1px var(--brand) inset !important; + box-shadow: 0 0 0 1px var(--brand) inset, 0 0 12px rgba(0, 229, 255, 0.18) !important; } - /* 描述列表/标签页 边框 */ - .el-descriptions__border { - background-color: transparent; + /* 主按钮:荧光青描边发光风 */ + .el-button--primary { + background: var(--cyan-soft); + border-color: rgba(0, 229, 255, 0.5); + color: var(--brand); + font-weight: 600; + letter-spacing: 0.3px; + } + .el-button--primary:hover { + background: rgba(0, 229, 255, 0.22); + border-color: var(--brand); + box-shadow: 0 0 14px rgba(0, 229, 255, 0.25); } - /* 滚动条贴深色 */ - .el-scrollbar__bar { - opacity: 0.5; - } + /* 标签页激活下划线荧光青 */ + .el-tabs__active-bar { background-color: var(--brand); } + .el-tabs__item.is-active { color: var(--brand); } + .el-tabs__item:hover { color: var(--cyan-dim); } - /* 数字用等宽(涨跌色由业务层控制) */ + /* 描述列表边框 */ + .el-descriptions__border { background-color: transparent; } + + /* 滚动条 */ + .el-scrollbar__bar { opacity: 0.5; } + + /* 数字等宽 */ .num { font-family: var(--mono); font-variant-numeric: tabular-nums; diff --git a/frontend/src/styles/reset.css b/frontend/src/styles/reset.css index c4fc750..5eeaf3f 100644 --- a/frontend/src/styles/reset.css +++ b/frontend/src/styles/reset.css @@ -1,4 +1,4 @@ -/* 全局 reset —— 后台基础:深色 / 紧凑 / 高数据密度 */ +/* 全局 reset —— 科幻终端基础:深空黑 / 紧凑 / 高数据密度 */ *, *::before, *::after { @@ -13,11 +13,12 @@ body, body { margin: 0; - background: var(--bg); + background-color: var(--bg); color: var(--text); font-family: var(--sans); font-size: 13px; line-height: 1.5; + font-variant-numeric: tabular-nums; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; @@ -35,15 +36,139 @@ a { color: var(--brand); text-decoration: none; } +a:hover { color: var(--cyan-dim); } -/* 深色滚动条 */ +/* 深色滚动条 · 青调 */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } -::-webkit-scrollbar-thumb:hover { background: #424a53; } +::-webkit-scrollbar-thumb:hover { background: rgba(0, 229, 255, 0.4); } + +/* 选区荧光青 */ +::selection { + background: rgba(0, 229, 255, 0.25); + color: var(--text); +} + +/* ====== 科幻装饰工具类(页面复用) ====== */ + +/* 终端网格底 —— 给主内容区/面板叠一层 1px 青网格 */ +.term-grid { + background-image: + linear-gradient(var(--grid-line) 1px, transparent 1px), + linear-gradient(90deg, var(--grid-line) 1px, transparent 1px); + background-size: var(--grid-size) var(--grid-size); +} + +/* 信号灯 · 圆点 + 脉冲(配合 .lamp-ok/.lamp-warn/.lamp-crit/.lamp-idle) */ +.lamp { + display: inline-block; + width: 7px; + height: 7px; + border-radius: 50%; + position: relative; + vertical-align: middle; +} +.lamp::after { + content: ''; + position: absolute; + inset: -3px; + border-radius: 50%; + background: inherit; + opacity: 0.35; + filter: blur(2px); +} +.lamp-ok { background: var(--lamp-ok); } +.lamp-warn { background: var(--lamp-warn); } +.lamp-crit { background: var(--lamp-crit); } +.lamp-idle { background: var(--lamp-idle); } +.lamp-pulse { animation: lamp-pulse 1.8s var(--ease) infinite; } +@keyframes lamp-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.35; } +} +@media (prefers-reduced-motion: reduce) { + .lamp-pulse { animation: none; } +} + +/* 几何角标 —— 面板四角的 L 形装饰 */ +.term-corners { + position: relative; +} +.term-corners::before, +.term-corners::after { + content: ''; + position: absolute; + width: 10px; + height: 10px; + border-color: rgba(0, 229, 255, 0.5); + border-style: solid; + pointer-events: none; +} +.term-corners::before { + top: 0; left: 0; + border-width: 1px 0 0 1px; +} +.term-corners::after { + bottom: 0; right: 0; + border-width: 0 1px 1px 0; +} + +/* 扫描线 —— 叠在监控/实时面板顶部,缓慢下扫 */ +.term-scan { + position: relative; + overflow: hidden; +} +.term-scan::after { + content: ''; + position: absolute; + left: 0; right: 0; top: 0; + height: 40%; + background: linear-gradient(180deg, rgba(0, 229, 255, 0.06), transparent); + animation: term-scan-move 4s linear infinite; + pointer-events: none; +} +@keyframes term-scan-move { + 0% { transform: translateY(-100%); } + 100% { transform: translateY(250%); } +} +@media (prefers-reduced-motion: reduce) { + .term-scan::after { animation: none; display: none; } +} + +/* 终端面板基类 —— 带 1px 青色顶高光 + 角标 */ +.term-panel { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--r-md); + position: relative; +} +.term-panel-head { + display: flex; + align-items: center; + justify-content: space-between; + padding: 9px 14px; + border-bottom: 1px solid var(--border); + font-size: 12px; + font-weight: 600; + letter-spacing: 0.5px; + color: var(--text-2); + text-transform: uppercase; + font-family: var(--mono); +} +.term-panel-head::before { + content: ''; + display: inline-block; + width: 3px; + height: 12px; + background: var(--brand); + margin-right: 8px; + box-shadow: 0 0 8px rgba(0, 229, 255, 0.6); +} +.term-panel-body { padding: 14px; } /* ====== 布局工具类 ====== */ .page { @@ -65,18 +190,22 @@ a { font-size: 16px; font-weight: 600; color: var(--text); + letter-spacing: 0.3px; } .page-subtitle { font-size: 12px; color: var(--text-3); + font-family: var(--mono); } .section-title { - font-size: 13px; + font-size: 12px; font-weight: 600; color: var(--text-2); - letter-spacing: 0.3px; + letter-spacing: 0.5px; + text-transform: uppercase; + font-family: var(--mono); } /* 涨跌色工具 */ diff --git a/frontend/src/styles/tokens.css b/frontend/src/styles/tokens.css index bee8d7a..d97151f 100644 --- a/frontend/src/styles/tokens.css +++ b/frontend/src/styles/tokens.css @@ -1,28 +1,53 @@ -/* 量化后台设计 token —— 深色 / 高密度 / 红涨绿跌(A股惯例) */ +/* 三国量化研究台 · 科幻终端设计 token + 深空黑 + 荧光青 + 琥珀 · 高密度数据终端 + 换肤原则:只改值不改名,全站页面零改动即刻生效 */ :root { - /* 背景层级 */ - --bg: #0d1117; /* 主背景 */ - --bg-card: #161b22; /* 卡片/面板 */ - --bg-hover: #1c2128; /* 悬停/表头 */ - --bg-overlay: #1c2128; /* 弹层 */ + /* —— 背景层级(深空黑,蓝调冷底)—— */ + --bg: #05080d; /* 主背景 · 深空黑 */ + --bg-card: #0a0f17; /* 卡片/面板 · 略亮冷蓝 */ + --bg-hover: #0e1622; /* 悬停/表头 */ + --bg-overlay: #0d1320; /* 弹层 */ + --panel: #070b12; /* 嵌套面板 · 比主背景略深,多层叠 */ - /* 文字 */ - --text: #e6edf3; /* 主 */ - --text-2: #8b949e; /* 次 */ - --text-3: #6e7681; /* 弱/占位 */ + /* —— 文字(冷调)—— */ + --text: #d4e4f0; /* 主 · 冷白 */ + --text-2: #7a8a9a; /* 次 */ + --text-3: #4a5868; /* 弱/占位 */ - /* 语义色:红涨绿跌 */ - --up: #f85149; /* 涨/红 */ - --down: #3fb950; /* 跌/绿 */ - --brand: #1890ff; /* 品牌/链接/激活 */ - --warn: #d29922; - --danger: #f85149; + /* —— 主强调:荧光青 —— */ + --brand: #00e5ff; /* 品牌/链接/激活 · 荧光青 */ + --cyan: #00e5ff; /* 语义别名 */ + --cyan-dim: #00b8cc; /* 暗青(hover/次要激活) */ + --cyan-soft: rgba(0, 229, 255, 0.14); /* 青色填充底 */ - /* 边框 */ - --border: #30363d; - --border-2: #21262d; /* 更弱的分隔线 */ + /* —— 数据高亮:琥珀 —— */ + --amber: #ffb000; /* 数据高亮/警告 */ + --warn: #ffb000; /* 语义别名 */ + --amber-soft: rgba(255, 176, 0, 0.14); - /* 间距 */ + /* —— 涨跌(A股:红涨绿跌)—— */ + --up: #ff4d5e; /* 涨 · 红 */ + --down: #2ee68a; /* 跌 · 绿 */ + --danger: #ff4d5e; + + /* —— 信号灯(终端状态)—— */ + --lamp-ok: #2ee68a; /* 运行/正常 · 绿 */ + --lamp-warn: #ffb000; /* 告警 · 琥珀 */ + --lamp-crit: #ff4d5e; /* 临界 · 红 */ + --lamp-idle: #4a5868; /* 空闲 · 灰 */ + + /* —— 边框(冷调,低明度)—— */ + --border: #16202e; /* 主边框 */ + --border-2: #0f1822; /* 更弱分隔线 */ + + /* —— 科幻装饰 —— */ + --grid-line: rgba(0, 229, 255, 0.035); /* 1px 网格底 */ + --grid-size: 32px; + --scan: rgba(0, 229, 255, 0.05); /* 扫描线 */ + --glow-cyan: 0 0 0 1px rgba(0, 229, 255, 0.22), 0 0 18px rgba(0, 229, 255, 0.08); + --glow-amber: 0 0 0 1px rgba(255, 176, 0, 0.22), 0 0 18px rgba(255, 176, 0, 0.08); + + /* —— 间距 —— */ --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; @@ -30,22 +55,22 @@ --sp-5: 24px; --sp-6: 32px; - /* 圆角 */ - --r-sm: 4px; - --r-md: 6px; - --r-lg: 8px; + /* —— 圆角(终端:近直角)—— */ + --r-sm: 2px; + --r-md: 3px; + --r-lg: 4px; - /* 阴影 */ - --shadow: 0 8px 24px rgba(0, 0, 0, 0.45); - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4); + /* —— 阴影 —— */ + --shadow: 0 8px 32px rgba(0, 0, 0, 0.6); + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.5); - /* 字体 */ + /* —— 字体 —— */ --mono: ui-monospace, 'JetBrains Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Roboto, sans-serif; - /* 布局 */ - --sidebar-w: 208px; - --header-h: 52px; + /* —— 布局 —— */ + --sidebar-w: 200px; + --header-h: 48px; --ease: cubic-bezier(0.4, 0, 0.2, 1); } diff --git a/frontend/src/utils/echartsDark.ts b/frontend/src/utils/echartsDark.ts index e62cc57..d1a803c 100644 --- a/frontend/src/utils/echartsDark.ts +++ b/frontend/src/utils/echartsDark.ts @@ -1,32 +1,36 @@ -/* ECharts 深色量化主题 —— 全站统一:深色轴线/文字/网格,红涨绿跌,净值品牌色 */ +/* ECharts 科幻终端主题 —— 深空黑底 / 荧光青主轴 / 琥珀高亮 / 红涨绿跌 */ -export const UP = '#f85149' // 红涨(A股惯例) -export const DOWN = '#3fb950' // 绿跌 -export const BRAND = '#1890ff' -export const TEXT = '#e6edf3' -export const TEXT_DIM = '#8b949e' -export const AXIS = '#30363d' -export const SPLIT = '#21262d' -export const TOOLTIP_BG = '#1c2128' +export const UP = '#ff4d5e' // 红涨(A股惯例) +export const DOWN = '#2ee68a' // 绿跌 +export const BRAND = '#00e5ff' // 荧光青(净值/主序列) +export const AMBER = '#ffb000' // 琥珀(高亮/超额) +export const TEXT = '#d4e4f0' +export const TEXT_DIM = '#7a8a9a' +export const AXIS = '#16202e' +export const SPLIT = '#0f1822' +export const TOOLTIP_BG = '#0a0f17' /** 通用深色坐标轴片段(合并进 xAxis/yAxis) */ export function darkAxis(): Record { return { axisLine: { lineStyle: { color: AXIS } }, axisTick: { lineStyle: { color: AXIS } }, - axisLabel: { color: TEXT_DIM, fontSize: 11 }, + axisLabel: { color: TEXT_DIM, fontSize: 11, fontFamily: 'var(--mono)' }, splitLine: { lineStyle: { color: SPLIT } }, } } -/** 通用深色 tooltip */ +/** 通用深色 tooltip —— 荧光青边框 + 深空底 */ export function darkTooltip(): Record { return { trigger: 'axis', backgroundColor: TOOLTIP_BG, - borderColor: AXIS, + borderColor: 'rgba(0, 229, 255, 0.4)', borderWidth: 1, textStyle: { color: TEXT, fontSize: 12 }, + axisPointer: { + lineStyle: { color: 'rgba(0, 229, 255, 0.5)', type: 'dashed' }, + }, } } diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue index 34b9493..f26d5ad 100644 --- a/frontend/src/views/Dashboard.vue +++ b/frontend/src/views/Dashboard.vue @@ -47,7 +47,7 @@ function open(row: TaskListItem): void { router.push(path) } -const typeLabelMap: Record = { cta: '回测', optimize: '优化', factor: '因子' } +const typeLabelMap: Record = { cta: '回测', optimize: '优化', factor: '因子', portfolio: '组合' } function typeLabel(t: string): string { return typeLabelMap[t] || t } @@ -197,7 +197,9 @@ function statusLabel(s: string): string { width: 36px; height: 36px; border-radius: var(--r-sm); - background: rgba(24, 144, 255, 0.14); + background: var(--cyan-soft); + border: 1px solid rgba(0, 229, 255, 0.3); + box-shadow: 0 0 12px rgba(0, 229, 255, 0.15); color: var(--brand); font-weight: 700; font-size: 13px; diff --git a/frontend/src/views/Layout.vue b/frontend/src/views/Layout.vue index ab41589..e0c35f3 100644 --- a/frontend/src/views/Layout.vue +++ b/frontend/src/views/Layout.vue @@ -1,5 +1,5 @@ + + + + diff --git a/frontend/src/views/backtest/History.vue b/frontend/src/views/backtest/History.vue index c5a4c09..af6223d 100644 --- a/frontend/src/views/backtest/History.vue +++ b/frontend/src/views/backtest/History.vue @@ -128,6 +128,9 @@ function statusLabel(s: string): string { + + + + + + diff --git a/frontend/src/views/live/New.vue b/frontend/src/views/live/New.vue index 147475c..528b103 100644 --- a/frontend/src/views/live/New.vue +++ b/frontend/src/views/live/New.vue @@ -1,11 +1,14 @@ + + + + diff --git a/frontend/src/views/strategy/Edit.vue b/frontend/src/views/strategy/Edit.vue new file mode 100644 index 0000000..74e2598 --- /dev/null +++ b/frontend/src/views/strategy/Edit.vue @@ -0,0 +1,238 @@ + + + + + diff --git a/frontend/src/views/strategy/InstanceEdit.vue b/frontend/src/views/strategy/InstanceEdit.vue new file mode 100644 index 0000000..0cb84ba --- /dev/null +++ b/frontend/src/views/strategy/InstanceEdit.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/frontend/src/views/strategy/List.vue b/frontend/src/views/strategy/List.vue new file mode 100644 index 0000000..5b2e860 --- /dev/null +++ b/frontend/src/views/strategy/List.vue @@ -0,0 +1,263 @@ + + + + + diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index dc512b2..eeba68c 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ '/ws': { target: 'ws://192.168.2.154:8000', ws: true, changeOrigin: true }, }, }, + optimizeDeps: { exclude: ['monaco-editor'] }, build: { outDir: 'dist', emptyOutDir: true }, test: { environment: 'jsdom', globals: true }, })