模拟库和正式库分离
This commit is contained in:
parent
7e06e43334
commit
02e73f4d01
70
DEVLOG.md
70
DEVLOG.md
|
|
@ -982,6 +982,76 @@ ALL SUITES PASS 且代码指纹与工作树一致。纪律照旧:**收盘后
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-08-28 · 页面口径两处修正:信号默认只看今天、方向色统一红涨绿跌
|
||||||
|
|
||||||
|
**做了什么**
|
||||||
|
1. **信号默认只看今天。** 风控卖出流 (bionic:signals:llm_sell_actions) 不按日期分键,
|
||||||
|
清晨没有新信号时 xrevrange 取到的「最新 N 条」全是昨天以前的, 而页面只显示时分 ——
|
||||||
|
昨天 14:49 的建议卖出挂在今天 13:20 的页面上冒充新消息。后端 upstream_signals 给
|
||||||
|
每行补 ymd 与 today 标记 (时间戳解析不了的按今天算, 风控从宽); 页面信号流速览默认
|
||||||
|
只显示今天, 昨天及以前收进底部「更早的信号」折叠区 (带日期、置灰、默认收起);
|
||||||
|
顶部消息栏不再把昨天的风控卖出算进今天的消息; 「今天的信号」列与持仓角标只数今天。
|
||||||
|
2. **方向色统一 A股口径**: 买入/看涨/流入类一律红, 卖出/看跌/流出类一律绿。原来模板
|
||||||
|
词表里没有「买入/卖出」, 买入落到 else 被涂成绿色, 方向恰好用反; 决策系统买卖广播
|
||||||
|
原来借状态色 (建议卖出=红 deny / 建议买入=绿 done) 同样是反的。新增方向配色函数
|
||||||
|
dirCls (认不出的词给中性灰, 不硬猜) 与方向药丸样式 sig-up / sig-down。
|
||||||
|
3. 顺带: 「把握 72(满分 1)」这类混乱显示统一折成百分比 —— 上游有的发 0.72、有的发
|
||||||
|
72, 页面见谁都显示 72%。
|
||||||
|
|
||||||
|
**动了哪些文件**
|
||||||
|
app/services/upstream_signals.py(风控卖出行补 ymd/today)、app/web/static/index.html。
|
||||||
|
|
||||||
|
**部署方式**
|
||||||
|
tlai4090: git pull 后 make deploy。纯展示层 + 只读接口字段增补, 无 DDL、无新 env、
|
||||||
|
无参数变更, 不影响任何下单与入账路径。
|
||||||
|
|
||||||
|
**真机判收**
|
||||||
|
开发机: 全部 20 套 585 例单测 ALL PASS; 页面内嵌脚本 node --check 与 esprima 解析
|
||||||
|
双通过; 模板标签配平。部署后开页面看三处: ① 信号流标题变成「今天 N 条」, 旧条目
|
||||||
|
只出现在「更早的信号」折叠区且带日期; ② 顶部消息栏不再出现晚于当前时刻的时间;
|
||||||
|
③ 信号里买入类是红字、卖出类是绿字, 建议卖出药丸是绿底。
|
||||||
|
|
||||||
|
**还欠着什么**
|
||||||
|
无新增欠账。pms-ws 启动日志与 ws_smoke 打印的端点仍取 settings 层 (非参数表优先的
|
||||||
|
最终生效值), 上一条节点已记, 待下轮一起改。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-08-28 · 模拟/正式双系统:定方案(一份代码两份 .env)+ compose 参数化
|
||||||
|
|
||||||
|
**做了什么**
|
||||||
|
QMT 从模拟转正式, 定下双系统方案并落了隔离改造。方案全文见 REAL_TRADING_DEPLOY_PLAN.md,
|
||||||
|
要点: 现有这套原地不动继续当模拟, 正式盘在另一台服务器单独部署; 不开分支不复制仓库,
|
||||||
|
一份代码两份 .env, 实例身份由 .env 与各自库里的参数表决定。必须隔离的五样:
|
||||||
|
PMS 业务库 (pms_* 必须新库)、Celery 总线 redis db (db8→db9, 同 db 两套 worker 互相抢
|
||||||
|
任务)、信号消费组名 (同组一条消息只投一家, PMS_SIGNAL_GROUP 已在 settings 声明,
|
||||||
|
.env 直接生效)、QMT 端点与签名密钥对 (绝不复用)、会话密钥。行情/大盘/mtf/决策系统
|
||||||
|
信号流是只读共享, 不用动。
|
||||||
|
|
||||||
|
代码侧只动了部署壳: docker-compose.yml 的容器名前缀 / 镜像标签 / 页面宿主机端口改成
|
||||||
|
从 .env 取值 (PMS_STACK_PREFIX / PMS_IMAGE / PMS_WEB_PORT_HOST), Makefile 的三处
|
||||||
|
硬编码 38100 改走 PORT 变量。**默认值与从前逐字一致, 现有部署零感知**; 将来两套要
|
||||||
|
合并到一台机器时也不用再改代码。
|
||||||
|
|
||||||
|
**动了哪些文件**
|
||||||
|
docker-compose.yml、Makefile、新增 REAL_TRADING_DEPLOY_PLAN.md。业务代码零改动。
|
||||||
|
|
||||||
|
**部署方式**
|
||||||
|
模拟 (tlai4090): 随下次 git pull + make deploy 自然带上, 无需专门发版。
|
||||||
|
正式 (新服务器): 照 REAL_TRADING_DEPLOY_PLAN.md 第四节九步走。
|
||||||
|
|
||||||
|
**真机判收**
|
||||||
|
开发机: compose YAML 解析通过; make -n health 默认仍打 38100、PORT=38200 时打 38200;
|
||||||
|
全量单测与本轮改动无交集 (业务代码零改动)。模拟侧部署后 `docker compose ps` 容器名
|
||||||
|
应与从前完全一致 (前缀默认空)。
|
||||||
|
|
||||||
|
**还欠着什么**
|
||||||
|
1. 部署前要拍板/对接的三件事在方案文档第二节: 正式 QMT 写不写 153 下游表 (决定
|
||||||
|
153 代理加逻辑库还是直连 MySQL)、正式 ws 端点与密钥交换、真实账户规模与纪律参数。
|
||||||
|
2. 正式盘上线初期纪律: shadow + propose_only + 新建仓 off, 影子期至少 3 个交易日。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
下一条节点从这里往下写,格式照抄上面:
|
下一条节点从这里往下写,格式照抄上面:
|
||||||
## YYYY-MM-DD · 一句话标题
|
## YYYY-MM-DD · 一句话标题
|
||||||
|
|
|
||||||
11
Makefile
11
Makefile
|
|
@ -19,6 +19,9 @@ SHELL := /bin/bash
|
||||||
PROFILES ?= --profile sched --profile ws
|
PROFILES ?= --profile sched --profile ws
|
||||||
DC := docker compose $(PROFILES)
|
DC := docker compose $(PROFILES)
|
||||||
# 一次性命令一律 --no-deps: 不然跑个单测都会把 beat/worker 拉起来
|
# 一次性命令一律 --no-deps: 不然跑个单测都会把 beat/worker 拉起来
|
||||||
|
# 页面宿主机端口: 与 .env 的 PMS_WEB_PORT_HOST 保持一致 (默认 38100)。
|
||||||
|
# 双实例时在正式目录里 `make health PORT=38200` 或导出环境变量。
|
||||||
|
PORT ?= 38100
|
||||||
RUN := docker compose run --rm --no-deps pms-web
|
RUN := docker compose run --rm --no-deps pms-web
|
||||||
|
|
||||||
.PHONY: help deploy deploy-local build up down ps logs test stale initdb check health \
|
.PHONY: help deploy deploy-local build up down ps logs test stale initdb check health \
|
||||||
|
|
@ -87,18 +90,18 @@ check: ## 实机连通性与表结构自检 (需真实 .env)
|
||||||
$(RUN) python scripts/check_db.py
|
$(RUN) python scripts/check_db.py
|
||||||
|
|
||||||
health: ## 页面健康检查 (配置装载 + 库连通自证)
|
health: ## 页面健康检查 (配置装载 + 库连通自证)
|
||||||
@curl -s http://127.0.0.1:38100/health | python3 -m json.tool
|
@curl -s http://127.0.0.1:$(PORT)/health | python3 -m json.tool
|
||||||
|
|
||||||
# ---------------------------------------------------------------- 上游 / 行业 / 通道
|
# ---------------------------------------------------------------- 上游 / 行业 / 通道
|
||||||
probe: ## 上游计划实机探活 (只读; 加 SNAP=1 顺带落一份名册快照)
|
probe: ## 上游计划实机探活 (只读; 加 SNAP=1 顺带落一份名册快照)
|
||||||
$(RUN) python scripts/probe_plan_api.py $(if $(SNAP),--snapshot,)
|
$(RUN) python scripts/probe_plan_api.py $(if $(SNAP),--snapshot,)
|
||||||
|
|
||||||
changes: ## 榜单变化: 谁新进谁掉榜、持仓票有没有被上游摘了
|
changes: ## 榜单变化: 谁新进谁掉榜、持仓票有没有被上游摘了
|
||||||
@curl -s 'http://127.0.0.1:38100/api/upstream/plan-changes?limit=30' \
|
@curl -s 'http://127.0.0.1:$(PORT)/api/upstream/plan-changes?limit=30' \
|
||||||
| python3 -m json.tool
|
| python3 -m json.tool
|
||||||
|
|
||||||
industry: ## 行业源实探 (ready=false 就是硬拦截失效, 别当它绿着)
|
industry: ## 行业源实探 (ready=false 就是硬拦截失效, 别当它绿着)
|
||||||
@curl -s http://127.0.0.1:38100/api/industry | python3 -m json.tool
|
@curl -s http://127.0.0.1:$(PORT)/api/industry | python3 -m json.tool
|
||||||
|
|
||||||
ws-status: ## ws 通道状态 (连接态 / seq 水位 / 出口队列)
|
ws-status: ## ws 通道状态 (连接态 / seq 水位 / 出口队列)
|
||||||
$(RUN) python scripts/ws_smoke.py status
|
$(RUN) python scripts/ws_smoke.py status
|
||||||
|
|
@ -126,7 +129,7 @@ rebuild-accept: ## 只看判收: 安全垫分布 / 批次账 / 行业集中度
|
||||||
# **默认关**: 页面把 PMS_EXEC_IMPL 改 A 才生效, 不可用自动退 B
|
# **默认关**: 页面把 PMS_EXEC_IMPL 改 A 才生效, 不可用自动退 B
|
||||||
# 实现 B = PMS 自己算 (分日配额 / 分笔 / VWAP / 回踩 / 不追高 / 14:45 兜底) —— 默认档
|
# 实现 B = PMS 自己算 (分日配额 / 分笔 / VWAP / 回踩 / 不追高 / 14:45 兜底) —— 默认档
|
||||||
# 所以 PMS_EXEC_IMPL=B (默认) 时跑 A 段, 决策系统这一段完全不参与。
|
# 所以 PMS_EXEC_IMPL=B (默认) 时跑 A 段, 决策系统这一段完全不参与。
|
||||||
A ?= http://127.0.0.1:38100
|
A ?= http://127.0.0.1:$(PORT)
|
||||||
# 中文必须原样出来: `json.tool` 默认把中文转义成 \uXXXX, 而这些接口的 reason 恰恰是最该
|
# 中文必须原样出来: `json.tool` 默认把中文转义成 \uXXXX, 而这些接口的 reason 恰恰是最该
|
||||||
# 看的东西。**不能用 `json.tool --no-ensure-ascii`** —— 这条管道跑在**宿主机**上不是容器里,
|
# 看的东西。**不能用 `json.tool --no-ensure-ascii`** —— 这条管道跑在**宿主机**上不是容器里,
|
||||||
# 宿主机的 python3 可能低于 3.9 (2026-08-03 实机: unrecognized arguments)。下面这行任何
|
# 宿主机的 python3 可能低于 3.9 (2026-08-03 实机: unrecognized arguments)。下面这行任何
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
# 正式盘部署方案(模拟 / 正式双系统)
|
||||||
|
|
||||||
|
2026-08-28 定稿,同日按拍板结果更新:正式盘部署在 **192.168.16.188**,正式 QMT 在
|
||||||
|
**192.168.16.98**(ws 端点 ws://192.168.16.98:9443/pms);正式侧**会写** 153 的下游表,
|
||||||
|
所以 153 代理上加新逻辑库(见第二节)。现有这套 PMS 保留为**模拟仓**原地不动。
|
||||||
|
**一份代码,两份 .env** —— 不开分支、不复制仓库:两份代码必然分叉,修一个账务 bug 要
|
||||||
|
修两遍测两遍;本仓库的设计本来就是「settings 只是初值,实例身份由 .env 与各自库里的
|
||||||
|
参数表决定」。
|
||||||
|
|
||||||
|
## 〇、先后次序(为什么先搭台架再测协议)
|
||||||
|
|
||||||
|
正式 QMT 侧从没成交过一笔真实买卖,所以第一优先级是**把 ws 协议在正式侧跑通并用一笔
|
||||||
|
小额真实买卖判收**,然后才谈全量部署与影子期。但协议测试本身需要的台架——出口表
|
||||||
|
pms_qmt_order、入站表 pms_qmt_inbox、ws 常驻进程、签名链路——就是这套系统的最小部署:
|
||||||
|
ws_smoke 不是独立客户端,它往出口表塞单、由 pms-ws 进程签名发出。所以次序是:
|
||||||
|
|
||||||
|
阶段A 最小台架(只起 页面+ws 通道,**不起调度器**)→ 协议实测(握手/快照/一买一卖)
|
||||||
|
阶段B 协议判收后,全量部署(加调度)→ 影子期 ≥3 个交易日 → 切 ws、放自动档
|
||||||
|
|
||||||
|
阶段A 里 beat/worker 根本不存在,系统不可能自主做任何动作;下发模式默认 shadow,
|
||||||
|
ws_smoke 发的联调单绕开这个开关、且带 SMOKE 前缀——成交回报会被联调单闸挡在账本外。
|
||||||
|
**绝不要**拿模拟机那套临时把端点切到 98 去试:模拟的账本会吃进正式账户的回报,两边
|
||||||
|
密钥也不同,历史上 SMOKE 假成交污染账本的教训就是这么来的。
|
||||||
|
|
||||||
|
## 一、两套系统各自用什么、共用什么
|
||||||
|
|
||||||
|
| 资源 | 模拟(现状) | 正式(新) | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 代码 | git 仓库 | 同一仓库 | 两边各自 `git pull`,永远同版本 |
|
||||||
|
| PMS 业务库(pms_* 全部表) | 153 代理 my_quant_db | **153 代理新逻辑库 my_quant_db_real** | 表名固定 pms_*,只能分库 |
|
||||||
|
| 下游表 trading_* / strategy_daily_results | 153 代理 | 同一代理经新逻辑库映射(共享读) | 正式侧确认会写 |
|
||||||
|
| Celery 总线 | 150 redis **db8** | 150 redis **db9** | 共用一个 db 两套 worker 互相抢任务 |
|
||||||
|
| 决策系统信号流 | 208 db2/db3(只读共享) | 同左 | 流共享,**消费组必须不同名** |
|
||||||
|
| 行情/大盘/mtf | 208 db13 / 199 / 214 | 同左 | 纯只读,直接共用 |
|
||||||
|
| QMT ws 通道 | ws://192.168.18.182:9443/pms + 模拟密钥 | **ws://192.168.16.98:9443/pms + 新密钥对** | 密钥绝不复用 |
|
||||||
|
| 管理页面 | tlai4090:38100 | 192.168.16.188:38100 | 不同机器,端口可同名 |
|
||||||
|
|
||||||
|
消费组为什么必须不同名:Redis Stream 的一个消费组内,一条消息只投递给一个消费者。
|
||||||
|
两套系统若同组,风控卖出信号会被随机一家抢走,另一家永远看不见。组名在 .env 里设
|
||||||
|
(`PMS_SIGNAL_GROUP`),settings 已声明该键,参数表为空时 .env 值直接生效。
|
||||||
|
|
||||||
|
## 二、部署前的准备件
|
||||||
|
|
||||||
|
1. **153 代理加逻辑库 my_quant_db_real**:pms_* 全部表映射到一个**新建的物理库**;
|
||||||
|
trading_position / trading_order / trading_buy_plan / strategy_daily_results 的映射
|
||||||
|
与 my_quant_db **完全相同**(共享读)。一个要核对的点:正式 QMT 写的下游表若与模拟
|
||||||
|
写的是**同一批物理表**,正式 PMS 的回放与对账会连模拟的旧行一起看见——两边下游数据
|
||||||
|
分不分表,要与 QMT/决策系统侧对一遍口径。
|
||||||
|
2. **正式密钥对**:容器里 `python scripts/gen_keys.py` 生成;seed 填正式 .env 的
|
||||||
|
PMS_QMT_SIGN_SEED_HEX,公钥带外交给 QMT 侧,拿回对方公钥填 PMS_QMT_PEER_PUBKEY_B64。
|
||||||
|
3. **真实账户规模与纪律参数**:PMS_TOTAL_SCALE 按真实资金;上线初期 shadow +
|
||||||
|
propose_only + 新建仓 off。
|
||||||
|
|
||||||
|
## 三、正式服务器 .env 与模拟的差异清单
|
||||||
|
|
||||||
|
照抄模拟的 .env,改动以下键(其余全部照抄;行情/信号这些共享源**不要改**):
|
||||||
|
|
||||||
|
# 必须不同
|
||||||
|
PROXY_DB_URL=mysql+pymysql://<user>:<pass>@192.168.16.153:3307/my_quant_db_real
|
||||||
|
PMS_REDIS_URL=redis://:<pass>@192.168.16.150:6379/9
|
||||||
|
PMS_SIGNAL_GROUP=pms_signal_consumer_real
|
||||||
|
PMS_SIGNAL_CONSUMER=pms_real_1
|
||||||
|
PMS_QMT_WS_URL=ws://192.168.16.98:9443/pms
|
||||||
|
PMS_QMT_SIGN_SEED_HEX=<gen_keys 新生成>
|
||||||
|
PMS_QMT_PEER_PUBKEY_B64=<QMT 正式侧公钥>
|
||||||
|
PMS_SESSION_SECRET=<新生成, 别抄模拟的>
|
||||||
|
|
||||||
|
# 阶段A 先关, 密钥交换完再开
|
||||||
|
PMS_QMT_WS_ENABLED=false
|
||||||
|
|
||||||
|
# 可选 (单独服务器用默认即可; 将来若与模拟合并到一台机器才需要)
|
||||||
|
# PMS_STACK_PREFIX=real-
|
||||||
|
# PMS_IMAGE=pms-real:latest
|
||||||
|
# PMS_WEB_PORT_HOST=38200
|
||||||
|
# COMPOSE_PROJECT_NAME=pms-real
|
||||||
|
|
||||||
|
## 四、阶段A:最小台架 + ws 协议实测(在 188 上)
|
||||||
|
|
||||||
|
1. 装 docker 与 docker compose;`git clone` 仓库;按第三节写好 .env(密钥先留空)。
|
||||||
|
2. 网络预检:188 要能通 153:3307、150:6379、208:6379、199:3306、214:36379、98:9443。
|
||||||
|
3. 最小台架(只起 页面+ws,不起调度):`PROFILES="--profile ws" ./scripts/deploy.sh`
|
||||||
|
—— 建镜像、起 pms-web/pms-ws、幂等建表。
|
||||||
|
4. `make check`(库/参数/通道逐项)与 `make test`(ALL SUITES PASS + 指纹一致)。
|
||||||
|
5. 生成密钥、与 QMT 侧交换公钥;.env 里填好两把钥匙并把 PMS_QMT_WS_ENABLED=true;
|
||||||
|
重跑 `PROFILES="--profile ws" ./scripts/deploy.sh --no-pull` 让新 env 生效。
|
||||||
|
6. 通道判收(不动钱):`make ws-status` 看 连接 ONLINE、心跳在走、能读到持仓快照;
|
||||||
|
`ws_smoke.py inbox` 里有 snapshot/心跳类上行。
|
||||||
|
7. S2 式发单(挂不上才是预期):盘中发一张限价远离市价的买单(--ttl 5 到点对端自动撤),
|
||||||
|
watch 里状态走 QUEUED→SENT→受理,inbox 里有 ack 与回报。
|
||||||
|
8. **一笔真实买卖**(判收核心):建议选一只当日可回转(T+0)的跨境或货币 ETF,一手
|
||||||
|
一两百块,当天买进当天卖出;用普通股票也行,但 A 股 T+1,卖出要等次日。判收三条:
|
||||||
|
两张单全成、inbox 有 trade 回报、**账本零变化**(SMOKE 联调单闸把成交挡在账外)。
|
||||||
|
9. 清场:测试买的货**必须卖光**再进阶段B——否则影子期对账会把它当真实持仓收编进账本。
|
||||||
|
|
||||||
|
## 五、阶段B:全量部署与影子期(协议判收后再做)
|
||||||
|
|
||||||
|
`make deploy`(加上调度)→ 页面参数中心设 PMS_TOTAL_SCALE / PMS_AUTONOMY=propose_only /
|
||||||
|
PMS_OPEN_AUTONOMY=off → 真实账户已有持仓则等盘前对账以 ws 快照建账 → 影子期 ≥3 个
|
||||||
|
交易日逐日判收(日报对得上、回放游标推进、无孤儿成交)→ 页面把 PMS_DISPATCH_MODE 改
|
||||||
|
ws(管理员)→ 按表现逐层放开自动档。逐条命令等阶段A 判收后再展开。
|
||||||
|
|
||||||
|
## 六、日常纪律
|
||||||
|
|
||||||
|
两台各自 `git pull` + `make deploy`,**收盘后部署**;发版前后两边都 `make test`。
|
||||||
|
DEVLOG 单仓共用,节点里写清动的是模拟侧、正式侧还是两侧。模拟侧本次**零改动**。
|
||||||
|
|
@ -95,6 +95,14 @@ def _hm(ms):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _ymd_of(ms):
|
||||||
|
"""epoch 毫秒 → YYYY-MM-DD (本地时区)。拿不到就空串 (调用方按「今天」从宽处理)。"""
|
||||||
|
try:
|
||||||
|
return time.strftime("%Y-%m-%d", time.localtime(int(ms) // 1000))
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _num(v):
|
def _num(v):
|
||||||
try:
|
try:
|
||||||
return round(float(v), 4)
|
return round(float(v), 4)
|
||||||
|
|
@ -211,8 +219,14 @@ def _read_sell_actions(c):
|
||||||
之前直接取顶层 ts_code/confidence/reason 全落空, 页面每行只剩硬编码的 SELL(2026-08-18 修)。
|
之前直接取顶层 ts_code/confidence/reason 全落空, 页面每行只剩硬编码的 SELL(2026-08-18 修)。
|
||||||
与消化端 signal_rules.parse_risk_sell 同口径拆 data; 字段名对齐 bionic 写入端
|
与消化端 signal_rules.parse_risk_sell 同口径拆 data; 字段名对齐 bionic 写入端
|
||||||
(tasks_risk/tasks_intraday): 理由是 llm_reason, 时间是 timestamp(epoch 毫秒),
|
(tasks_risk/tasks_intraday): 理由是 llm_reason, 时间是 timestamp(epoch 毫秒),
|
||||||
dominant_signal 区分风控止损与止盈(take_profit)。顶层字段留作兜底, 兼容未来可能的扁平化。"""
|
dominant_signal 区分风控止损与止盈(take_profit)。顶层字段留作兜底, 兼容未来可能的扁平化。
|
||||||
|
|
||||||
|
**每行带日期与 today 标记** (2026-08-28 页面口径修): 这条流不按日期分键, 清晨没有
|
||||||
|
新信号时 xrevrange 取到的「最新 N 条」全是昨天以前的, 而页面只显示 HH:MM —— 昨天
|
||||||
|
14:49 的建议卖出会挂在今天 13:20 的页面上冒充新消息。页面据 today 默认只显示今天,
|
||||||
|
更早的折叠并标日期。时间戳解析不了的按今天算 (风控从宽, 宁可多显示也不误藏)。"""
|
||||||
key = "bionic:signals:llm_sell_actions"
|
key = "bionic:signals:llm_sell_actions"
|
||||||
|
today = time.strftime("%Y-%m-%d")
|
||||||
out = []
|
out = []
|
||||||
for _id, f in c.xrevrange(key, count=_limit()):
|
for _id, f in c.xrevrange(key, count=_limit()):
|
||||||
raw = f.get("data")
|
raw = f.get("data")
|
||||||
|
|
@ -222,12 +236,15 @@ def _read_sell_actions(c):
|
||||||
d = json.loads(raw) if isinstance(raw, str) else dict(raw)
|
d = json.loads(raw) if isinstance(raw, str) else dict(raw)
|
||||||
except Exception:
|
except Exception:
|
||||||
d = {}
|
d = {}
|
||||||
|
ts = d.get("timestamp") or d.get("trigger_time") or f.get("ts")
|
||||||
|
ymd = _ymd_of(ts)
|
||||||
out.append({"ts_code": d.get("ts_code") or f.get("ts_code"),
|
out.append({"ts_code": d.get("ts_code") or f.get("ts_code"),
|
||||||
"action": d.get("action") or f.get("action"),
|
"action": d.get("action") or f.get("action"),
|
||||||
"confidence": _num(d.get("confidence")),
|
"confidence": _num(d.get("confidence")),
|
||||||
"dominant_signal": d.get("dominant_signal"),
|
"dominant_signal": d.get("dominant_signal"),
|
||||||
"reason": d.get("llm_reason") or d.get("reason") or f.get("reason"),
|
"reason": d.get("llm_reason") or d.get("reason") or f.get("reason"),
|
||||||
"time": _hm(d.get("timestamp") or d.get("trigger_time") or f.get("ts"))})
|
"time": _hm(ts), "ymd": ymd,
|
||||||
|
"today": (ymd == today) if ymd else True})
|
||||||
return {"key": key, "items": out}
|
return {"key": key, "items": out}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ body{margin:0;background:var(--plane);color:var(--ink);
|
||||||
.disp.done{background:var(--accent-weak);color:var(--accent);}
|
.disp.done{background:var(--accent-weak);color:var(--accent);}
|
||||||
.disp.queue{background:var(--warn-weak);color:var(--warn);}
|
.disp.queue{background:var(--warn-weak);color:var(--warn);}
|
||||||
.disp.deny{background:var(--stop-weak);color:var(--stop);}
|
.disp.deny{background:var(--stop-weak);color:var(--stop);}
|
||||||
|
.disp.sig-up{background:var(--up-weak);color:var(--up);} .disp.sig-down{background:var(--down-weak);color:var(--down);}
|
||||||
.disp.would{background:var(--surface);color:var(--accent);border:1px solid var(--accent);}
|
.disp.would{background:var(--surface);color:var(--accent);border:1px solid var(--accent);}
|
||||||
.disp.wait{background:rgba(138,136,130,.14);color:var(--ink-2);}
|
.disp.wait{background:rgba(138,136,130,.14);color:var(--ink-2);}
|
||||||
.disp.strong{background:var(--accent-weak);color:var(--accent);}
|
.disp.strong{background:var(--accent-weak);color:var(--accent);}
|
||||||
|
|
@ -483,7 +484,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<span class="txt">
|
<span class="txt">
|
||||||
<b v-if="msgLatest.ts_code">{{ nm(msgLatest.ts_code) }}</b>
|
<b v-if="msgLatest.ts_code">{{ nm(msgLatest.ts_code) }}</b>
|
||||||
<span v-if="msgLatest.ts_code" class="muted mono" style="font-size:11px">{{ msgLatest.ts_code }}</span>
|
<span v-if="msgLatest.ts_code" class="muted mono" style="font-size:11px">{{ msgLatest.ts_code }}</span>
|
||||||
<span v-if="msgLatest.dir" :class="dirUp(msgLatest.dir)?'clr-up':'clr-down'">{{ msgLatest.dir }}</span>
|
<span v-if="msgLatest.dir" :class="dirCls(msgLatest.dir)">{{ msgLatest.dir }}</span>
|
||||||
<span class="muted">{{ msgLatest.cat }}</span>
|
<span class="muted">{{ msgLatest.cat }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="msgLatest.level" class="disp" :class="lvClass(msgLatest.level)" style="padding:0 6px">{{ lvLabel(msgLatest.level) }}</span>
|
<span v-if="msgLatest.level" class="disp" :class="lvClass(msgLatest.level)" style="padding:0 6px">{{ lvLabel(msgLatest.level) }}</span>
|
||||||
|
|
@ -508,7 +509,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<b v-if="m.ts_code">{{ nm(m.ts_code) }}</b>
|
<b v-if="m.ts_code">{{ nm(m.ts_code) }}</b>
|
||||||
<span v-if="m.ts_code" class="muted mono" style="font-size:11px">{{ m.ts_code }}</span>
|
<span v-if="m.ts_code" class="muted mono" style="font-size:11px">{{ m.ts_code }}</span>
|
||||||
<span v-if="m.dir" :class="dirUp(m.dir)?'clr-up':'clr-down'">{{ m.dir }}</span>
|
<span v-if="m.dir" :class="dirCls(m.dir)">{{ m.dir }}</span>
|
||||||
<span :style="m.strong ? 'font-weight:600' : ''" :class="m.strong ? '' : 'muted'">{{ m.cat }}</span>
|
<span :style="m.strong ? 'font-weight:600' : ''" :class="m.strong ? '' : 'muted'">{{ m.cat }}</span>
|
||||||
<span v-if="m.level" class="disp" :class="lvClass(m.level)" style="padding:0 6px">{{ lvLabel(m.level) }}</span>
|
<span v-if="m.level" class="disp" :class="lvClass(m.level)" style="padding:0 6px">{{ lvLabel(m.level) }}</span>
|
||||||
<div class="why" v-if="m.why">{{ m.why }}</div>
|
<div class="why" v-if="m.why">{{ m.why }}</div>
|
||||||
|
|
@ -531,7 +532,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<aside class="rail" id="railL" :class="{open:railOpen==='l'}">
|
<aside class="rail" id="railL" :class="{open:railOpen==='l'}">
|
||||||
<div class="rail-h">
|
<div class="rail-h">
|
||||||
<span class="ttl">信号流</span>
|
<span class="ttl">信号流</span>
|
||||||
<span class="cnt">{{ sigBrief.length }} 条 · 持仓 {{ sigBriefHeldCount }}</span>
|
<span class="cnt">今天 {{ sigToday.length }} 条 · 持仓 {{ sigBriefHeldCount }}</span>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<button class="iconbtn btn-pull" title="放大成浮层" @click="openRailSide('l',true)">⤢</button>
|
<button class="iconbtn btn-pull" title="放大成浮层" @click="openRailSide('l',true)">⤢</button>
|
||||||
<button class="iconbtn btn-fold" title="收成窄条" @click="foldRail('l')">◀</button>
|
<button class="iconbtn btn-fold" title="收成窄条" @click="foldRail('l')">◀</button>
|
||||||
|
|
@ -552,14 +553,14 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
|
|
||||||
<!-- 速览: 持仓票置顶, 其余只收重要和警告 -->
|
<!-- 速览: 持仓票置顶, 其余只收重要和警告 -->
|
||||||
<template v-if="sigTab==='brief'">
|
<template v-if="sigTab==='brief'">
|
||||||
<div class="muted" v-if="!sigBrief.length">持仓的票今天没有信号,全场也没有重要或警告级的信号。</div>
|
<div class="muted" v-if="!sigToday.length">持仓的票今天没有信号,全场也没有重要或警告级的信号。</div>
|
||||||
<template v-else>
|
<template v-if="sigToday.length">
|
||||||
<div v-for="(g,i) in sigBriefHeld.slice(0,60)" :key="'h'+i" class="sigrow2" :class="{old:g.stale}">
|
<div v-for="(g,i) in sigBriefHeld.slice(0,60)" :key="'h'+i" class="sigrow2" :class="{old:g.stale}">
|
||||||
<span class="t mono">{{ hm(g.time) || '—' }}</span>
|
<span class="t mono">{{ hm(g.time) || '—' }}</span>
|
||||||
<span class="pin">持仓</span>
|
<span class="pin">持仓</span>
|
||||||
<span class="nm">{{ nm(g.ts_code) }}</span>
|
<span class="nm">{{ nm(g.ts_code) }}</span>
|
||||||
<span class="cat">{{ g.cat }}</span>
|
<span class="cat">{{ g.cat }}</span>
|
||||||
<span v-if="g.direction" :class="dirUp(g.direction)?'clr-up':'clr-down'">{{ g.direction }}</span>
|
<span v-if="g.direction" :class="dirCls(g.direction)">{{ g.direction }}</span>
|
||||||
<span class="sp"></span>
|
<span class="sp"></span>
|
||||||
<span class="disp" :class="lvClass(g.level)" style="padding:0 6px">{{ lvLabel(g.level) }}</span>
|
<span class="disp" :class="lvClass(g.level)" style="padding:0 6px">{{ lvLabel(g.level) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -570,13 +571,27 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<span class="t mono">{{ hm(g.time) || '—' }}</span>
|
<span class="t mono">{{ hm(g.time) || '—' }}</span>
|
||||||
<span class="nm">{{ nm(g.ts_code) }}</span>
|
<span class="nm">{{ nm(g.ts_code) }}</span>
|
||||||
<span class="cat">{{ g.cat }}</span>
|
<span class="cat">{{ g.cat }}</span>
|
||||||
<span v-if="g.direction" :class="dirUp(g.direction)?'clr-up':'clr-down'">{{ g.direction }}</span>
|
<span v-if="g.direction" :class="dirCls(g.direction)">{{ g.direction }}</span>
|
||||||
<span class="sp"></span>
|
<span class="sp"></span>
|
||||||
<span class="disp" :class="lvClass(g.level)" style="padding:0 6px">{{ lvLabel(g.level) }}</span>
|
<span class="disp" :class="lvClass(g.level)" style="padding:0 6px">{{ lvLabel(g.level) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="muted" style="margin-top:9px;font-size:11.5px">
|
<div class="muted" style="margin-top:9px;font-size:11.5px">
|
||||||
灰掉的是已经过时的:超过设定的时效还没有新的同类信号盖过它。不隐藏,只是提醒你别再当新消息看。</div>
|
灰掉的是已经过时的:超过设定的时效还没有新的同类信号盖过它。不隐藏,只是提醒你别再当新消息看。</div>
|
||||||
</template>
|
</template>
|
||||||
|
<div class="muted" v-if="sigOld.length" @click="showOldSig=!showOldSig"
|
||||||
|
style="margin:10px 0 5px;font-weight:600;font-size:11.5px;border-top:1px dashed var(--hair);padding-top:8px;cursor:pointer">
|
||||||
|
{{ showOldSig ? '▾' : '▸' }} 更早的信号 · {{ sigOld.length }} 条(昨天及以前,默认收起)</div>
|
||||||
|
<template v-if="showOldSig">
|
||||||
|
<div v-for="(g,i) in sigOld.slice(0,60)" :key="'d'+i" class="sigrow2 old">
|
||||||
|
<span class="t mono" style="width:auto">{{ (g.ymd||'').slice(5) }} {{ hm(g.time) || '—' }}</span>
|
||||||
|
<span class="pin" v-if="g.held">持仓</span>
|
||||||
|
<span class="nm">{{ nm(g.ts_code) }}</span>
|
||||||
|
<span class="cat">{{ g.cat }}</span>
|
||||||
|
<span v-if="g.direction" :class="dirCls(g.direction)">{{ g.direction }}</span>
|
||||||
|
<span class="sp"></span>
|
||||||
|
<span class="disp" :class="lvClass(g.level)" style="padding:0 6px">{{ lvLabel(g.level) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 按来源: 原来的「完整信号中心」搬到这里, 就地展开不跳转 -->
|
<!-- 按来源: 原来的「完整信号中心」搬到这里, 就地展开不跳转 -->
|
||||||
|
|
@ -593,13 +608,13 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<div class="sigrow2" v-for="(r,i) in SIG.metrics.slice(0,20)" :key="i"
|
<div class="sigrow2" v-for="(r,i) in SIG.metrics.slice(0,20)" :key="i"
|
||||||
:title="'原始偏离值 = '+(r.z_dd==null?'—':r.z_dd)">
|
:title="'原始偏离值 = '+(r.z_dd==null?'—':r.z_dd)">
|
||||||
<span class="nm">{{ nm(r.ts_code) }}</span>
|
<span class="nm">{{ nm(r.ts_code) }}</span>
|
||||||
<span v-if="flowText(r.direction)!=='—'" :class="dirUp(r.direction)?'clr-up':'clr-down'"
|
<span v-if="flowText(r.direction)!=='—'" :class="dirCls(r.direction)"
|
||||||
style="font-weight:600">{{ flowText(r.direction) }}</span>
|
style="font-weight:600">{{ flowText(r.direction) }}</span>
|
||||||
<span class="sp"></span>
|
<span class="sp"></span>
|
||||||
<span class="mono" style="font-weight:600" :class="(r.window_net||0)>=0?'clr-up':'clr-down'">{{ moneyCn(r.window_net) }}</span>
|
<span class="mono" style="font-weight:600" :class="(r.window_net||0)>=0?'clr-up':'clr-down'">{{ moneyCn(r.window_net) }}</span>
|
||||||
<span class="intens"><span class="ibar"><i v-for="(on,bi) in zBars(r.z_dd)" :key="bi" :class="on?'on':''"></i></span></span>
|
<span class="intens"><span class="ibar"><i v-for="(on,bi) in zBars(r.z_dd)" :key="bi" :class="on?'on':''"></i></span></span>
|
||||||
<span class="mono" style="font-weight:700;font-size:11px"
|
<span class="mono" style="font-weight:700;font-size:11px"
|
||||||
:class="dirUp(r.direction)?'clr-up':'clr-down'">{{ zBand(r.z_dd).label }}</span>
|
:class="dirCls(r.direction)">{{ zBand(r.z_dd).label }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sig-none" v-if="!SIG.metrics.length">今天没有资金异动</div>
|
<div class="sig-none" v-if="!SIG.metrics.length">今天没有资金异动</div>
|
||||||
<div class="muted" style="font-size:11px;margin-top:6px">
|
<div class="muted" style="font-size:11px;margin-top:6px">
|
||||||
|
|
@ -633,7 +648,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<div class="srcb" v-show="srcOpen.decision">
|
<div class="srcb" v-show="srcOpen.decision">
|
||||||
<div class="sigrow2" v-for="(r,i) in SIG.decision.slice(0,16)" :key="i">
|
<div class="sigrow2" v-for="(r,i) in SIG.decision.slice(0,16)" :key="i">
|
||||||
<span class="nm">{{ nm(r.ts_code) }}</span><span class="sp"></span>
|
<span class="nm">{{ nm(r.ts_code) }}</span><span class="sp"></span>
|
||||||
<span class="disp" :class="r.action==='SELL'?'deny':'done'">{{ r.action==='SELL'?'建议卖出':'建议买入' }}</span>
|
<span class="disp" :class="r.action==='SELL'?'sig-down':'sig-up'">{{ r.action==='SELL'?'建议卖出':'建议买入' }}</span>
|
||||||
<span class="mono muted" style="font-size:11px">{{ hm(r.time) }}</span></div>
|
<span class="mono muted" style="font-size:11px">{{ hm(r.time) }}</span></div>
|
||||||
<div class="sig-none" v-if="!SIG.decision.length">今天没有买卖广播</div>
|
<div class="sig-none" v-if="!SIG.decision.length">今天没有买卖广播</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -646,7 +661,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<div class="srcb" v-show="srcOpen.qrs">
|
<div class="srcb" v-show="srcOpen.qrs">
|
||||||
<div class="sigrow2" v-for="(r,i) in SIG.qrs.slice(0,16)" :key="i" :class="{old:r.stale}">
|
<div class="sigrow2" v-for="(r,i) in SIG.qrs.slice(0,16)" :key="i" :class="{old:r.stale}">
|
||||||
<span class="nm">{{ nm(r.ts_code) }}</span>
|
<span class="nm">{{ nm(r.ts_code) }}</span>
|
||||||
<span v-if="r.direction && r.direction!=='—'" :class="dirUp(r.direction)?'clr-up':'clr-down'"
|
<span v-if="r.direction && r.direction!=='—'" :class="dirCls(r.direction)"
|
||||||
style="font-weight:600">{{ r.direction }}</span>
|
style="font-weight:600">{{ r.direction }}</span>
|
||||||
<span class="sp"></span>
|
<span class="sp"></span>
|
||||||
<span class="disp" :class="lvClass(r.level)" style="padding:0 6px">{{ lvLabel(r.level) }}</span>
|
<span class="disp" :class="lvClass(r.level)" style="padding:0 6px">{{ lvLabel(r.level) }}</span>
|
||||||
|
|
@ -661,7 +676,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<div class="srcb" v-show="srcOpen.capital">
|
<div class="srcb" v-show="srcOpen.capital">
|
||||||
<div class="sigrow2" v-for="(r,i) in SIG.capital.slice(0,16)" :key="i" :class="{old:r.stale}">
|
<div class="sigrow2" v-for="(r,i) in SIG.capital.slice(0,16)" :key="i" :class="{old:r.stale}">
|
||||||
<span class="nm">{{ nm(r.ts_code) }}</span>
|
<span class="nm">{{ nm(r.ts_code) }}</span>
|
||||||
<span v-if="r.direction && r.direction!=='—'" :class="dirUp(r.direction)?'clr-up':'clr-down'"
|
<span v-if="r.direction && r.direction!=='—'" :class="dirCls(r.direction)"
|
||||||
style="font-weight:600">{{ r.direction }}</span>
|
style="font-weight:600">{{ r.direction }}</span>
|
||||||
<span class="sp"></span>
|
<span class="sp"></span>
|
||||||
<span class="disp" :class="lvClass(r.level)" style="padding:0 6px">{{ lvLabel(r.level) }}</span>
|
<span class="disp" :class="lvClass(r.level)" style="padding:0 6px">{{ lvLabel(r.level) }}</span>
|
||||||
|
|
@ -676,7 +691,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<div class="srcb" v-show="srcOpen.price">
|
<div class="srcb" v-show="srcOpen.price">
|
||||||
<div class="sigrow2" v-for="(r,i) in SIG.price_notice.slice(0,16)" :key="i" :class="{old:r.stale}">
|
<div class="sigrow2" v-for="(r,i) in SIG.price_notice.slice(0,16)" :key="i" :class="{old:r.stale}">
|
||||||
<span class="nm">{{ nm(r.ts_code) }}</span>
|
<span class="nm">{{ nm(r.ts_code) }}</span>
|
||||||
<span v-if="r.direction && r.direction!=='—'" :class="dirUp(r.direction)?'clr-up':'clr-down'"
|
<span v-if="r.direction && r.direction!=='—'" :class="dirCls(r.direction)"
|
||||||
style="font-weight:600">{{ r.direction }}</span>
|
style="font-weight:600">{{ r.direction }}</span>
|
||||||
<span class="sp"></span>
|
<span class="sp"></span>
|
||||||
<span class="disp" :class="lvClass(r.level)" style="padding:0 6px">{{ lvLabel(r.level) }}</span>
|
<span class="disp" :class="lvClass(r.level)" style="padding:0 6px">{{ lvLabel(r.level) }}</span>
|
||||||
|
|
@ -693,21 +708,22 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<div class="muted" style="margin:4px 0 3px" v-if="SIG.sell.length">决策系统建议卖出</div>
|
<div class="muted" style="margin:4px 0 3px" v-if="SIG.sell.length">决策系统建议卖出</div>
|
||||||
<div class="sigrow2" v-for="(r,i) in SIG.sell.slice(0,12)" :key="'s'+i"
|
<div class="sigrow2" v-for="(r,i) in SIG.sell.slice(0,12)" :key="'s'+i"
|
||||||
style="flex-wrap:wrap;cursor:pointer" @click="toggleSell(r.ts_code+'_'+i)"
|
style="flex-wrap:wrap;cursor:pointer" @click="toggleSell(r.ts_code+'_'+i)"
|
||||||
|
:class="{old: r.today===false}"
|
||||||
:title="r.reason ? '点一下看理由' : '这条没有理由'">
|
:title="r.reason ? '点一下看理由' : '这条没有理由'">
|
||||||
<span class="muted mono" style="font-size:10px;width:9px;flex:none"
|
<span class="muted mono" style="font-size:10px;width:9px;flex:none"
|
||||||
:style="{visibility: r.reason ? 'visible' : 'hidden'}">{{ sellOpen[r.ts_code+'_'+i] ? '▾' : '▸' }}</span>
|
:style="{visibility: r.reason ? 'visible' : 'hidden'}">{{ sellOpen[r.ts_code+'_'+i] ? '▾' : '▸' }}</span>
|
||||||
<span class="nm">{{ nm(r.ts_code) }}</span>
|
<span class="nm">{{ nm(r.ts_code) }}</span>
|
||||||
<span class="disp deny">{{ r.dominant_signal==='take_profit'?'止盈':'建议卖出' }}</span>
|
<span class="disp sig-down">{{ r.dominant_signal==='take_profit'?'止盈':'建议卖出' }}</span>
|
||||||
<span class="muted" style="font-size:11.5px" v-if="r.confidence!=null">把握 {{ r.confidence }}</span>
|
<span class="muted" style="font-size:11.5px" v-if="confPct(r.confidence)!=null">把握 {{ confPct(r.confidence) }}</span>
|
||||||
<span class="sp"></span>
|
<span class="sp"></span>
|
||||||
<span class="mono muted" style="font-size:11px">{{ hm(r.time) }}</span>
|
<span class="mono muted" style="font-size:11px">{{ r.today===false ? ((r.ymd||'').slice(5)+' ') : '' }}{{ hm(r.time) }}</span>
|
||||||
<div style="flex-basis:100%;font-size:12px;line-height:1.45" class="muted"
|
<div style="flex-basis:100%;font-size:12px;line-height:1.45" class="muted"
|
||||||
v-if="r.reason && sellOpen[r.ts_code+'_'+i]" @click.stop>{{ r.reason }}</div>
|
v-if="r.reason && sellOpen[r.ts_code+'_'+i]" @click.stop>{{ r.reason }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="muted" style="margin:8px 0 3px" v-if="SIG.multi.length">多个来源同时报警</div>
|
<div class="muted" style="margin:8px 0 3px" v-if="SIG.multi.length">多个来源同时报警</div>
|
||||||
<div class="sigrow2" v-for="(r,i) in SIG.multi.slice(0,10)" :key="'m'+i" :class="{old:r.stale}">
|
<div class="sigrow2" v-for="(r,i) in SIG.multi.slice(0,10)" :key="'m'+i" :class="{old:r.stale}">
|
||||||
<span class="nm">{{ nm(r.ts_code) }}</span>
|
<span class="nm">{{ nm(r.ts_code) }}</span>
|
||||||
<span v-if="r.direction && r.direction!=='—'" :class="dirUp(r.direction)?'clr-up':'clr-down'"
|
<span v-if="r.direction && r.direction!=='—'" :class="dirCls(r.direction)"
|
||||||
style="font-weight:600">{{ r.direction }}</span>
|
style="font-weight:600">{{ r.direction }}</span>
|
||||||
<span class="sp"></span>
|
<span class="sp"></span>
|
||||||
<span class="disp" :class="lvClass(r.level)" style="padding:0 6px">{{ lvLabel(r.level) }}</span>
|
<span class="disp" :class="lvClass(r.level)" style="padding:0 6px">{{ lvLabel(r.level) }}</span>
|
||||||
|
|
@ -716,12 +732,12 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<div class="sigrow2" v-for="(r,i) in SIG.timing.slice(0,10)" :key="'t'+i">
|
<div class="sigrow2" v-for="(r,i) in SIG.timing.slice(0,10)" :key="'t'+i">
|
||||||
<span class="nm">{{ nm(r.ts_code) }}</span><span class="sp"></span>
|
<span class="nm">{{ nm(r.ts_code) }}</span><span class="sp"></span>
|
||||||
<span class="muted" style="font-size:11.5px">参考价 <span class="mono" style="color:var(--ink)">{{ r.price==null?'—':r.price }}</span></span>
|
<span class="muted" style="font-size:11.5px">参考价 <span class="mono" style="color:var(--ink)">{{ r.price==null?'—':r.price }}</span></span>
|
||||||
<span class="muted" style="font-size:11.5px">把握 <span class="mono" style="color:var(--ink)">{{ r.confidence==null?'—':r.confidence }}</span></span>
|
<span class="muted" style="font-size:11.5px">把握 <span class="mono" style="color:var(--ink)">{{ confPct(r.confidence)==null?'—':confPct(r.confidence) }}</span></span>
|
||||||
<span class="mono muted" style="font-size:11px">{{ hm(r.time) }}</span></div>
|
<span class="mono muted" style="font-size:11px">{{ hm(r.time) }}</span></div>
|
||||||
<div class="muted" style="margin:8px 0 3px" v-if="SIG.money_flow.length">资金流强度</div>
|
<div class="muted" style="margin:8px 0 3px" v-if="SIG.money_flow.length">资金流强度</div>
|
||||||
<div class="sigrow2" v-for="(r,i) in SIG.money_flow.slice(0,10)" :key="'f'+i" :class="{old:r.stale}">
|
<div class="sigrow2" v-for="(r,i) in SIG.money_flow.slice(0,10)" :key="'f'+i" :class="{old:r.stale}">
|
||||||
<span class="nm">{{ nm(r.ts_code) }}</span>
|
<span class="nm">{{ nm(r.ts_code) }}</span>
|
||||||
<span v-if="r.direction && r.direction!=='—'" :class="dirUp(r.direction)?'clr-up':'clr-down'"
|
<span v-if="r.direction && r.direction!=='—'" :class="dirCls(r.direction)"
|
||||||
style="font-weight:600">{{ r.direction }}</span>
|
style="font-weight:600">{{ r.direction }}</span>
|
||||||
<span class="sp"></span>
|
<span class="sp"></span>
|
||||||
<span class="disp" :class="lvClass(r.level)" style="padding:0 6px">{{ lvLabel(r.level) }}</span>
|
<span class="disp" :class="lvClass(r.level)" style="padding:0 6px">{{ lvLabel(r.level) }}</span>
|
||||||
|
|
@ -820,7 +836,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<template v-if="heldSigOf(s.row.ts_code).length">
|
<template v-if="heldSigOf(s.row.ts_code).length">
|
||||||
<span style="cursor:default"
|
<span style="cursor:default"
|
||||||
:title="heldSigOf(s.row.ts_code).slice(0,3).map(g=>(g.time||'')+' '+g.cat+' '+(g.direction||'')).join('\n')">
|
:title="heldSigOf(s.row.ts_code).slice(0,3).map(g=>(g.time||'')+' '+g.cat+' '+(g.direction||'')).join('\n')">
|
||||||
<span :class="dirUp(heldSigOf(s.row.ts_code)[0].direction)?'clr-up':'clr-down'">
|
<span :class="dirCls(heldSigOf(s.row.ts_code)[0].direction)">
|
||||||
{{ heldSigOf(s.row.ts_code)[0].cat }} {{ heldSigOf(s.row.ts_code)[0].direction || '' }}</span>
|
{{ heldSigOf(s.row.ts_code)[0].cat }} {{ heldSigOf(s.row.ts_code)[0].direction || '' }}</span>
|
||||||
<br><span class="muted mono" style="font-size:11px">{{ (heldSigOf(s.row.ts_code)[0].time||'').slice(0,5) }}<template
|
<br><span class="muted mono" style="font-size:11px">{{ (heldSigOf(s.row.ts_code)[0].time||'').slice(0,5) }}<template
|
||||||
v-if="heldSigOf(s.row.ts_code).length>1"> +{{ heldSigOf(s.row.ts_code).length-1 }}</template></span>
|
v-if="heldSigOf(s.row.ts_code).length>1"> +{{ heldSigOf(s.row.ts_code).length-1 }}</template></span>
|
||||||
|
|
@ -2571,6 +2587,16 @@ createApp({
|
||||||
};
|
};
|
||||||
const zBars = z => { const on = zBand(z).on; return [1, 2, 3, 4].map(i => i <= on); };
|
const zBars = z => { const on = zBand(z).on; return [1, 2, 3, 4].map(i => i <= on); };
|
||||||
const dirUp = d => ['看涨', '涨', 'inflow', '流入'].includes(d);
|
const dirUp = d => ['看涨', '涨', 'inflow', '流入'].includes(d);
|
||||||
|
// 信号方向配色 (2026-08-28): A股口径 —— 买入/看涨/流入类一律红, 卖出/看跌/流出类一律绿。
|
||||||
|
// 原来模板里全写 dirUp(x)?'clr-up':'clr-down', 而词表里没有「买入/卖出」: 买入落到
|
||||||
|
// else 分支被涂成绿色, 方向色恰好用反。认不出的词给中性灰, 不硬猜方向。
|
||||||
|
const DIR_UP_SET = new Set(['看涨', '涨', 'inflow', '流入', '买入', '建议买入', 'BUY', '转多']);
|
||||||
|
const DIR_DOWN_SET = new Set(['看跌', '跌', 'outflow', '流出', '卖出', '建议卖出', 'SELL', '减仓', '清仓']);
|
||||||
|
const dirCls = d => DIR_UP_SET.has(String(d || '')) ? 'clr-up'
|
||||||
|
: (DIR_DOWN_SET.has(String(d || '')) ? 'clr-down' : 'muted');
|
||||||
|
// 把握值统一显示成百分比: 上游有的发 0.72、有的发 72, 折成同一口径, 不再出现「72(满分 1)」。
|
||||||
|
const confPct = v => { const n = Number(v); if (v == null || v === '' || !isFinite(n)) return null;
|
||||||
|
return Math.round((n > 1 ? n / 100 : n) * 100) + '%'; };
|
||||||
// 一处组装: 各信号当前条目 (告警按级别筛选后的口径, 与筛选条联动)。
|
// 一处组装: 各信号当前条目 (告警按级别筛选后的口径, 与筛选条联动)。
|
||||||
// 已下线的 volume(放量异动) 不再单列 —— 若流里还有历史残留键, 归入 other。
|
// 已下线的 volume(放量异动) 不再单列 —— 若流里还有历史残留键, 归入 other。
|
||||||
const SIG = computed(() => {
|
const SIG = computed(() => {
|
||||||
|
|
@ -2639,7 +2665,8 @@ createApp({
|
||||||
const s = SIG.value;
|
const s = SIG.value;
|
||||||
for (const r of (s.sell || [])) out.push({
|
for (const r of (s.sell || [])) out.push({
|
||||||
ts_code: r.ts_code, time: r.time || '', cat: '建议卖出',
|
ts_code: r.ts_code, time: r.time || '', cat: '建议卖出',
|
||||||
direction: '卖出', level: 'DANGER', held: held.has(code6(r.ts_code)) });
|
direction: '卖出', level: 'DANGER', held: held.has(code6(r.ts_code)),
|
||||||
|
ymd: r.ymd || '', today: r.today !== false });
|
||||||
for (const r of (s.decision || [])) out.push({
|
for (const r of (s.decision || [])) out.push({
|
||||||
ts_code: r.ts_code, time: r.time || '', cat: '决策系统',
|
ts_code: r.ts_code, time: r.time || '', cat: '决策系统',
|
||||||
direction: r.action === 'SELL' ? '卖出' : '买入', level: 'WARNING',
|
direction: r.action === 'SELL' ? '卖出' : '买入', level: 'WARNING',
|
||||||
|
|
@ -2650,11 +2677,12 @@ createApp({
|
||||||
out.sort((a, b) => (b.held - a.held) || String(b.time).localeCompare(String(a.time)));
|
out.sort((a, b) => (b.held - a.held) || String(b.time).localeCompare(String(a.time)));
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
const sigBriefHeldCount = computed(() => sigBrief.value.filter(x => x.held).length);
|
// 「今天的信号」列与持仓角标都只数今天的 (2026-08-28): 昨天的风控卖出不再冒充今天
|
||||||
|
const sigBriefHeldCount = computed(() => sigBrief.value.filter(x => x.held && x.today !== false).length);
|
||||||
const heldSigMap = computed(() => {
|
const heldSigMap = computed(() => {
|
||||||
const m = {};
|
const m = {};
|
||||||
for (const x of sigBrief.value) {
|
for (const x of sigBrief.value) {
|
||||||
if (!x.held) continue;
|
if (!x.held || x.today === false) continue;
|
||||||
const k = code6(x.ts_code);
|
const k = code6(x.ts_code);
|
||||||
(m[k] || (m[k] = [])).push(x);
|
(m[k] || (m[k] = [])).push(x);
|
||||||
}
|
}
|
||||||
|
|
@ -3096,9 +3124,11 @@ createApp({
|
||||||
goLabel:'去我的策略', go:'strat' });
|
goLabel:'去我的策略', go:'strat' });
|
||||||
}
|
}
|
||||||
for (const r of (SIG.value.sell || [])) {
|
for (const r of (SIG.value.sell || [])) {
|
||||||
|
if (r.today === false) continue; // 昨天的风控卖出不再冒充今天的消息 (2026-08-28)
|
||||||
out.push({ kind:'strat', time:_hm(r.time), sort:_hms(r.time), ts_code:r.ts_code,
|
out.push({ kind:'strat', time:_hm(r.time), sort:_hms(r.time), ts_code:r.ts_code,
|
||||||
cat:(r.dominant_signal === 'take_profit' ? '决策系统建议止盈' : '决策系统建议卖出')
|
cat:(r.dominant_signal === 'take_profit' ? '决策系统建议止盈' : '决策系统建议卖出')
|
||||||
+ (r.confidence != null ? (',把握 ' + r.confidence + '(满分 1)') : ''),
|
+ (confPct(r.confidence) != null ? (',把握 ' + confPct(r.confidence)) : ''),
|
||||||
|
dir:'卖出',
|
||||||
strong:true, level:'DANGER', why:r.reason || '', goLabel:'去处理', go:'strat' });
|
strong:true, level:'DANGER', why:r.reason || '', goLabel:'去处理', go:'strat' });
|
||||||
}
|
}
|
||||||
for (const o of (opLog.value || [])) {
|
for (const o of (opLog.value || [])) {
|
||||||
|
|
@ -3138,8 +3168,14 @@ createApp({
|
||||||
const srcOpen = reactive({ metrics:true, mr:false, decision:false, qrs:false,
|
const srcOpen = reactive({ metrics:true, mr:false, decision:false, qrs:false,
|
||||||
capital:false, price:false, rare:false });
|
capital:false, price:false, rare:false });
|
||||||
function toggleSrc(k) { srcOpen[k] = !srcOpen[k]; }
|
function toggleSrc(k) { srcOpen[k] = !srcOpen[k]; }
|
||||||
const sigBriefHeld = computed(() => sigBrief.value.filter(x => x.held));
|
// 默认只看今天 (2026-08-28): 风控卖出流不按日期分键, 清晨没有新信号时最近 N 条全是
|
||||||
const sigBriefOther = computed(() => sigBrief.value.filter(x => !x.held));
|
// 昨天以前的, 页面又只显示 HH:MM —— 昨天 14:49 的旧条目会挂在今天 13:20 的页面上冒充
|
||||||
|
// 新消息。今天的照常按持仓分两段; 更早的收进底部折叠区, 带日期、置灰 —— 不丢信息也不吵人。
|
||||||
|
const sigToday = computed(() => sigBrief.value.filter(x => x.today !== false));
|
||||||
|
const sigOld = computed(() => sigBrief.value.filter(x => x.today === false));
|
||||||
|
const showOldSig = ref(false);
|
||||||
|
const sigBriefHeld = computed(() => sigToday.value.filter(x => x.held));
|
||||||
|
const sigBriefOther = computed(() => sigToday.value.filter(x => !x.held));
|
||||||
|
|
||||||
// 三、左右两栏: 宽度够就常驻, 不够就收成窄条; 窄条点开是浮层。
|
// 三、左右两栏: 宽度够就常驻, 不够就收成窄条; 窄条点开是浮层。
|
||||||
// 临界值按「主区要放得下持仓表」倒推 —— 表最小 940 加面板内边距 32 等于 972:
|
// 临界值按「主区要放得下持仓表」倒推 —— 表最小 940 加面板内边距 32 等于 972:
|
||||||
|
|
@ -3228,8 +3264,8 @@ createApp({
|
||||||
showAlerts, alertItems, alertLatest, alertLevelClass, alertGroups, alertStripItems,
|
showAlerts, alertItems, alertLatest, alertLevelClass, alertGroups, alertStripItems,
|
||||||
alertStripCount, alertTotalCount, catShown, toggleCat, ageText, alertRowClass,
|
alertStripCount, alertTotalCount, catShown, toggleCat, ageText, alertRowClass,
|
||||||
alertItemsF, levelShown, toggleLevel, sellOpen, toggleSell, levelCount, ALERT_LEVELS, lvLabel, lvClass, alertGridFixed, passLevel, flowText, flowClass,
|
alertItemsF, levelShown, toggleLevel, sellOpen, toggleSell, levelCount, ALERT_LEVELS, lvLabel, lvClass, alertGridFixed, passLevel, flowText, flowClass,
|
||||||
sigBrief, sigBriefHeldCount, heldSigOf, openSigCenter,
|
sigBrief, sigBriefHeldCount, heldSigOf, openSigCenter, sigToday, sigOld, showOldSig,
|
||||||
SIG, moneyCn, zBand, zBars, dirUp, eventsQuiet,
|
SIG, moneyCn, zBand, zBars, dirUp, dirCls, confPct, eventsQuiet,
|
||||||
MSG_KINDS, kindLabel, hm, msgOpen, msgKind, msgFeed, msgCounts, msgShown,
|
MSG_KINDS, kindLabel, hm, msgOpen, msgKind, msgFeed, msgCounts, msgShown,
|
||||||
msgLatest, msgLevelClass, setMsgKind, msgGo,
|
msgLatest, msgLevelClass, setMsgKind, msgGo,
|
||||||
sigTab, srcOpen, toggleSrc, sigBriefHeld, sigBriefOther,
|
sigTab, srcOpen, toggleSrc, sigBriefHeld, sigBriefOther,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,14 @@
|
||||||
# tradingSystem (PMS) 容器编排
|
# tradingSystem (PMS) 容器编排
|
||||||
# 用法见 README「Docker 部署」。.env 放在本文件同目录 (服务器上手工维护, 不入库)。
|
# 用法见 README「Docker 部署」。.env 放在本文件同目录 (服务器上手工维护, 不入库)。
|
||||||
|
#
|
||||||
|
# 双实例 (模拟/正式) 隔离 (2026-08-28): 容器名前缀、镜像标签、页面宿主机端口这三样
|
||||||
|
# 从 .env 取值、默认值与从前完全一致 —— 现有部署一个字不用改。另一套实例 (通常在
|
||||||
|
# 另一台服务器; 同机双栈也行) 在自己的 .env 里加:
|
||||||
|
# PMS_STACK_PREFIX=real- → 容器名变成 real-pms-web / real-pms-ws ...
|
||||||
|
# PMS_IMAGE=pms-real:latest → 镜像标签分开, 互不覆盖
|
||||||
|
# PMS_WEB_PORT_HOST=38200 → 页面宿主机端口 (容器内固定 38100, 不用改)
|
||||||
|
# COMPOSE_PROJECT_NAME=pms-real → compose 项目名 (网络/卷前缀), docker 原生支持
|
||||||
|
# 这些键进容器环境也无害: settings 是 extra="ignore", 不声明就当不存在。
|
||||||
# 三组服务:
|
# 三组服务:
|
||||||
# 默认 pms-web 管理页面 (38100)
|
# 默认 pms-web 管理页面 (38100)
|
||||||
# --profile sched pms-beat / pms-worker Celery 调度与执行
|
# --profile sched pms-beat / pms-worker Celery 调度与执行
|
||||||
|
|
@ -11,7 +20,7 @@
|
||||||
|
|
||||||
x-pms-base: &pms-base
|
x-pms-base: &pms-base
|
||||||
build: .
|
build: .
|
||||||
image: pms:latest
|
image: ${PMS_IMAGE:-pms:latest}
|
||||||
env_file: .env
|
env_file: .env
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
|
|
@ -36,10 +45,10 @@ x-pms-base: &pms-base
|
||||||
services:
|
services:
|
||||||
pms-web:
|
pms-web:
|
||||||
<<: *pms-base
|
<<: *pms-base
|
||||||
container_name: pms-web
|
container_name: ${PMS_STACK_PREFIX:-}pms-web
|
||||||
command: uvicorn app.web.main:app --host 0.0.0.0 --port 38100
|
command: uvicorn app.web.main:app --host 0.0.0.0 --port 38100
|
||||||
ports:
|
ports:
|
||||||
- "38100:38100"
|
- "${PMS_WEB_PORT_HOST:-38100}:38100"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:38100/health', timeout=3)"]
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:38100/health', timeout=3)"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
|
@ -49,13 +58,13 @@ services:
|
||||||
|
|
||||||
pms-beat:
|
pms-beat:
|
||||||
<<: *pms-base
|
<<: *pms-base
|
||||||
container_name: pms-beat
|
container_name: ${PMS_STACK_PREFIX:-}pms-beat
|
||||||
profiles: ["sched"]
|
profiles: ["sched"]
|
||||||
command: celery -A app.scheduler.celery_app beat -l info
|
command: celery -A app.scheduler.celery_app beat -l info
|
||||||
|
|
||||||
pms-worker:
|
pms-worker:
|
||||||
<<: *pms-base
|
<<: *pms-base
|
||||||
container_name: pms-worker
|
container_name: ${PMS_STACK_PREFIX:-}pms-worker
|
||||||
profiles: ["sched"]
|
profiles: ["sched"]
|
||||||
command: celery -A app.scheduler.celery_app worker -l info -c 2
|
command: celery -A app.scheduler.celery_app worker -l info -c 2
|
||||||
|
|
||||||
|
|
@ -66,7 +75,7 @@ services:
|
||||||
# 只是让 QMT 多留一会儿, 重连后按 last_seq 补发)。
|
# 只是让 QMT 多留一会儿, 重连后按 last_seq 补发)。
|
||||||
pms-ws:
|
pms-ws:
|
||||||
<<: *pms-base
|
<<: *pms-base
|
||||||
container_name: pms-ws
|
container_name: ${PMS_STACK_PREFIX:-}pms-ws
|
||||||
profiles: ["ws"]
|
profiles: ["ws"]
|
||||||
command: python -m app.ws.runner
|
command: python -m app.ws.runner
|
||||||
stop_grace_period: 25s
|
stop_grace_period: 25s
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue