处理页面渲染不完全的问题
This commit is contained in:
parent
ceaf4eef12
commit
e217f2c817
28
README.md
28
README.md
|
|
@ -58,10 +58,11 @@ scripts/
|
|||
test_batch4_units.py 动作引擎 四类自主动作触发与数量口径 11 例
|
||||
test_batch5_units.py 决策系统信号流解析与消化口径 8 例
|
||||
test_batch6_units.py ws 通道: 测试向量/签名/公钥形态/水位/DDL体检 51 例
|
||||
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) 32 例
|
||||
init_db.py 建表 (应用 ddl_pms_v1.sql, 幂等, 默认演练)
|
||||
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) 33 例
|
||||
init_db.py 建表 (应用 ddl_pms_v1.sql, 幂等, 默认演练; 含 DDL 体检)
|
||||
check_db.py 实机连通性与表结构自检 (需真实 .env)
|
||||
gen_keys.py ws 通道密钥: 生成 / 只取公钥(--pubkey) / 自检(--check)
|
||||
gen_keys.py ws 通道密钥: 生成 / 只取公钥(--pubkey) / PEM(--pem) / 自检(--check)
|
||||
reset_ledger.py 清空账本并把回放游标对齐到当前 (影子运行期重来一次; 不碰下游表)
|
||||
```
|
||||
|
||||
## 三条铁律
|
||||
|
|
@ -101,7 +102,12 @@ docker compose logs -f pms-ws
|
|||
git pull && docker compose build && docker compose up -d
|
||||
```
|
||||
|
||||
基础镜像 `python:3.11-slim` 拉取慢时,先给服务器 Docker 配置 registry 镜像加速。日志落 `./logs`(已挂载卷);容器时区 Asia/Shanghai。管理页面的前端资源(Vue3 / ElementPlus / axios)走 unpkg CDN,浏览器需能访问外网;若内网隔离,把页面头部三行 `<script>/<link>` 换成本地文件即可(页面本身无构建步骤)。
|
||||
基础镜像 `python:3.11-slim` 拉取慢时,先给服务器 Docker 配置 registry 镜像加速。日志落 `./logs`(已挂载卷);容器时区 Asia/Shanghai。
|
||||
|
||||
**管理页面的两个坑**(都踩过,写在这里):
|
||||
|
||||
1. **前端资源走 unpkg CDN**,浏览器需能访问外网。上不了外网的表现不是报错,而是**只剩一个头部、下面全空**——`el-*` 组件全部渲染不出来。离线办法:把 Vue3 / ElementPlus / axios 三个库放进 `app/web/static/vendor/`,再把 `index.html` 头部那 5 个 URL 换成 `/static/vendor/xxx`(后端已把 `static` 挂在 `/static`,页面本身无构建步骤)。
|
||||
2. **模板里不能用自闭合的自定义标签**。`<el-alert ... />` 这种写法浏览器 HTML 解析器**不认**——只有 `br`/`img`/`input` 这类 void 元素和 SVG 才认自闭合。写成 `<el-xxx/>` 会被当成开标签,**后面所有内容都变成它的子节点**;若它带 `v-if` 且条件为假,整页就跟着消失。2026-07-29 页面白屏即此故(75 处自闭合,第一处 `<el-alert v-if="err"/>` 把整页吞掉了)。一律写成 `<el-xxx ...></el-xxx>`。
|
||||
|
||||
**依赖版本坑**:`redis` 必须锁在 5.x。redis-py 6.x 默认按 RESP3 握手(先发 `HELLO 3`),而 208/150 两台 Redis 是 6.0 以前的版本,会回 `unknown command HELLO`,行情与 Celery 总线一起断。`requirements.txt` 已锁版本,代码侧另有 `protocol=2` 双保险;升级依赖时别把这条放开。
|
||||
|
||||
|
|
@ -155,6 +161,20 @@ QMT ──trade/order_update──▶ pms-ws ──落 pms_qmt_inbox──▶
|
|||
|
||||
影子模式下的完整闭环:页面下命令 → 方案落表 → 方案转指令 → 择时按日配额给出「今天该出多少、什么价、挂到几点」→ 你照着在 QMT 下单 → 5 分钟一次的回放把成交认领回批次账本 → 命令进度自动推进。整条链路除了「人手下单」这一步,其余与实盘接管后完全一致。
|
||||
|
||||
### 回放游标:首次启动不追认历史
|
||||
|
||||
`trading_order` 里躺着旧系统多年的成交。PMS 刚上线时一条在途指令都没有,若从表头开始回放,每条历史成交都会被判成「外部成交」并入 BASE 批次——账本上凭空长出一堆早已清掉的持仓,**摊薄成本与安全垫全错**,而补仓、盈利加仓、保垫减仓都挂在安全垫上,一错就是整条纪律链。
|
||||
|
||||
所以 `PMS_REPLAY_CURSOR` 为空时,`replay_fills` **只把游标对齐到当前最新成交、一条都不入账**,并打一条 WARNING 说明。要补历史(页面「参数设置」改这个参数):
|
||||
|
||||
| 游标值 | 行为 |
|
||||
|---|---|
|
||||
| 留空 | 首次启动自动对齐到最新成交,不追认历史 |
|
||||
| 某个 `order_id` | 从它之后开始回放 |
|
||||
| `ALL` | 从头全量回放 |
|
||||
|
||||
已经被历史成交污染的账本,用 `scripts/reset_ledger.py` 清掉重来(只删 PMS 自有表,绝不碰下游 `trading_*`),然后跑一次 `POST /api/ops/reconcile?apply_fix=true` 让对账以下游为准把真实持仓补回来。
|
||||
|
||||
## 自主提议的分流(设计 §6 / §7)
|
||||
|
||||
动作引擎每分钟扫一遍持仓,产出 FILL / ADD / DCA / TRIM 四类候选,然后依次过三道:
|
||||
|
|
|
|||
|
|
@ -135,6 +135,18 @@ def fetch_filled_orders(*, since_id=None, since_time=None, limit: int = 500) ->
|
|||
return [_norm_order(r) for r in rows]
|
||||
|
||||
|
||||
def latest_filled_order_id():
|
||||
"""当前已成交单里最大的 order_id —— 回放游标冷启动时的锚点。
|
||||
|
||||
取 MAX 而不是"最新时间", 是为了跟游标推进用同一把尺子: `fetch_filled_orders` 走的是
|
||||
`order_id > :sid` + `ORDER BY order_id ASC`, 即**字典序** (order_id 形如
|
||||
`SELL_601956.SH_1778549400`, 并非时间序)。锚点若按时间取, 会跟字典序对不上而漏行。
|
||||
"""
|
||||
r = fetch_one("SELECT MAX(order_id) AS mx FROM trading_order WHERE order_status IN ('"
|
||||
+ "', '".join(FILLED_STATUSES) + "')")
|
||||
return (r or {}).get("mx")
|
||||
|
||||
|
||||
def _norm_order(r: dict) -> dict:
|
||||
side = str(r.get("order_side") or "").strip().lower()
|
||||
if side in ("1", "b", "买入", "买"):
|
||||
|
|
|
|||
|
|
@ -27,15 +27,49 @@ from app.services import market, param_store, portfolio
|
|||
logger = logging.getLogger("pms.ledger")
|
||||
|
||||
CURSOR_KEY = "PMS_REPLAY_CURSOR"
|
||||
CURSOR_ALL = "ALL" # 游标设成这个值 = 显式要求从头全量回放 (见 _seed_cursor)
|
||||
STREAK_KEY = "PMS_RECON_STREAK"
|
||||
LIVE_INSTR = ("DISPATCHED", "JUDGE_PASSED", "RULE_PASSED")
|
||||
|
||||
|
||||
# ================================================================ 回放
|
||||
def _seed_cursor(out: dict) -> dict:
|
||||
"""首次回放: 把游标对齐到当前最新成交, **不追认历史**。
|
||||
|
||||
`trading_order` 里躺着旧系统多年的成交记录。PMS 刚上线时一条在途指令都没有, 若从头
|
||||
回放, 每一条历史成交都会被判成「外部成交」并入 BASE 批次 —— 账本上凭空长出一堆早就
|
||||
清掉的持仓, 摊薄成本与安全垫全错, 日志还刷几百条 EXTERNAL_FILL 告警。而安全垫是补仓、
|
||||
盈利加仓、保垫减仓共同的判断依据, 它一错整条纪律链跟着错。**账本必须从干净的起点开始。**
|
||||
|
||||
要补历史有两条路 (页面「参数设置」改 PMS_REPLAY_CURSOR):
|
||||
* 设成某个 order_id → 从它之后开始回放
|
||||
* 设成 ALL → 从头全量回放
|
||||
"""
|
||||
try:
|
||||
anchor = downstream_repo.latest_filled_order_id()
|
||||
except Exception as e:
|
||||
out.update({"ok": False, "errors": [f"读下游最新成交失败: {type(e).__name__}: {e}"]})
|
||||
return out
|
||||
if not anchor:
|
||||
out["note"] = "下游暂无已成交单, 游标待下次再对齐"
|
||||
return out
|
||||
pms_repo.set_param(CURSOR_KEY, anchor, updated_by="system")
|
||||
out.update({"cursor": anchor, "seeded": True})
|
||||
out["note"] = (f"首次回放: 游标已对齐到当前最新成交 {anchor}, **不追认历史** —— "
|
||||
f"账本从现在起跟踪。需要补历史请在页面把 PMS_REPLAY_CURSOR 改成某个 "
|
||||
f"order_id (从它之后开始) 或 {CURSOR_ALL} (从头全量)")
|
||||
logger.warning("[replay_fills] %s", out["note"])
|
||||
return out
|
||||
|
||||
|
||||
def replay_fills(*, limit: int = 500) -> dict:
|
||||
"""增量回放 trading_order 已成交单 → 批次入账 (每 5 分钟一跳, 幂等)。"""
|
||||
out = {"ok": True, "fills": 0, "actions": 0, "alerts": [], "errors": [], "cursor": None}
|
||||
cursor = pms_repo.get_param(CURSOR_KEY) or None
|
||||
cursor = pms_repo.get_param(CURSOR_KEY)
|
||||
if not cursor: # 从未设过 (或被清空) —— 冷启动, 只对齐游标不入账
|
||||
return _seed_cursor(out)
|
||||
if str(cursor).strip().upper() == CURSOR_ALL:
|
||||
cursor = None # 显式要求从头全量回放
|
||||
try:
|
||||
fills = downstream_repo.fetch_filled_orders(since_id=cursor, limit=limit)
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ RUNTIME_EXTRA = {
|
|||
"PMS_GLOBAL_EXEC_HALT": (False, bool, "全局暂停执行 / 休假模式"),
|
||||
"PMS_BRAKE_UNTIL": (0, int, "组合刹车解除日 YYYYMMDD, 0=未刹车"),
|
||||
"PMS_HIGH_WATER": (0.0, float, "组合市值高水位 (刹车判定基准)"),
|
||||
"PMS_REPLAY_CURSOR": ("", str, "成交回放游标 (trading_order.order_id)"),
|
||||
"PMS_REPLAY_CURSOR": ("", str, "成交回放游标 (trading_order.order_id)。留空=首次启动时自动对齐到当前最新成交且不追认历史; 填某个 order_id=从它之后开始; 填 ALL=从头全量回放"),
|
||||
"PMS_RECON_STREAK": (0, int, "连续对账不一致天数"),
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ DESC = {
|
|||
"PMS_EOD_FORCE_TIME": "当日配额兜底时点", "PMS_EOD_FORCE_DISCOUNT": "兜底限价系数 (卖出)",
|
||||
"PMS_MIN_LOT_MERGE": "一手检查: 批次自动合并",
|
||||
"PMS_DISPATCH_EXPIRE_MIN": "指令下发后未被接受的过期时间 (分钟)",
|
||||
"PMS_DISPATCH_MODE": "下发通道: shadow=只记账待人工 / ws=WebSocket 直连 QMT (待实现)",
|
||||
"PMS_DISPATCH_MODE": "下发通道: shadow=只记账待人工 / ws=WebSocket 直连 QMT (需先起 pms-ws)",
|
||||
"PMS_EXEC_SLICES": "当日配额分几笔出手",
|
||||
"PMS_ORDER_TTL_MIN": "单笔挂单有效期 (交易分钟), 到点下游自动撤; 兜底单不受限",
|
||||
"PMS_RISK_WARN_ENTRY": "单笔敞口告警线 (占规模)", "PMS_RISK_WARN_PORTFOLIO": "组合敞口告警线",
|
||||
|
|
|
|||
|
|
@ -35,6 +35,18 @@ STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
|
|||
|
||||
app = FastAPI(title="PMS 持仓管理系统", version=VERSION)
|
||||
|
||||
# 把 static 目录挂到 /static —— 内网隔离时的离线兜底:
|
||||
# 页面默认从 unpkg 取 Vue3 / ElementPlus / axios, 浏览器上不了外网就会白屏 (更准确地说是
|
||||
# 只剩一个光秃秃的头部, 因为 el-* 组件全渲染不出来)。把三个库放进 static/vendor/ 再把
|
||||
# index.html 头部那五个 URL 换成 /static/vendor/xxx 即可离线运行, 无需构建步骤。
|
||||
# 具体做法见 README「管理页面」一节。
|
||||
try:
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
if os.path.isdir(STATIC_DIR):
|
||||
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
||||
except Exception as _e: # 挂不上不影响任何 API, 页面照常从 CDN 取
|
||||
logger.warning("静态目录挂载失败 (不影响接口): %s", _e)
|
||||
|
||||
|
||||
def ok(fn, *args, **kw):
|
||||
"""统一出参包装: 成功 {"ok":true, ...}; 失败 {"ok":false,"error":...} 且 HTTP 200。"""
|
||||
|
|
|
|||
|
|
@ -58,17 +58,17 @@
|
|||
</div>
|
||||
|
||||
<el-alert v-if="err" class="banner" :title="err" type="error" effect="dark" show-icon
|
||||
@close="err=''"/>
|
||||
@close="err=''"></el-alert>
|
||||
<el-alert v-if="ov.sector_ready===false" class="banner" type="warning" effect="dark" show-icon
|
||||
:closable="false"
|
||||
title="行业划分数据源未配置 —— 行业集中度硬拦截已停用, 行业类命令置灰 (设计 §5)"/>
|
||||
title="行业划分数据源未配置 —— 行业集中度硬拦截已停用, 行业类命令置灰 (设计 §5)"></el-alert>
|
||||
<el-alert v-if="ov.price_missing && ov.price_missing.length" class="banner" type="warning"
|
||||
effect="dark" show-icon :closable="false"
|
||||
:title="'以下持仓取不到实时价, 已用摊薄成本兜底: ' + ov.price_missing.join(', ')"/>
|
||||
:title="'以下持仓取不到实时价, 已用摊薄成本兜底: ' + ov.price_missing.join(', ')"></el-alert>
|
||||
<el-alert v-if="health.calendar_degraded" class="banner" type="warning" effect="dark" show-icon
|
||||
:closable="false" title="交易日历降级: 未安装 chinesecalendar, 节假日不可辨"/>
|
||||
:closable="false" title="交易日历降级: 未安装 chinesecalendar, 节假日不可辨"></el-alert>
|
||||
<el-alert v-if="dm.shadow" class="banner" type="info" effect="dark" show-icon :closable="false"
|
||||
:title="'影子运行中 —— ' + (dm.hint || '')"/>
|
||||
:title="'影子运行中 —— ' + (dm.hint || '')"></el-alert>
|
||||
|
||||
<div class="metrics">
|
||||
<div class="m"><div class="k">总规模</div><div class="v">{{ money(ov.scale) }}</div></div>
|
||||
|
|
@ -96,30 +96,30 @@
|
|||
<el-button size="small" @click="loadParams">放弃修改</el-button>
|
||||
</div>
|
||||
<el-table :data="params" size="small" height="560" border>
|
||||
<el-table-column prop="key" label="参数" width="240" class-name="mono"/>
|
||||
<el-table-column prop="key" label="参数" width="240" class-name="mono"></el-table-column>
|
||||
<el-table-column label="当前值" width="200">
|
||||
<template #default="s">
|
||||
<el-select v-if="s.row.key==='PMS_AUTONOMY'" v-model="s.row.value" size="small">
|
||||
<el-option v-for="o in ['full','propose_only','off']" :key="o" :label="o" :value="o"/>
|
||||
<el-option v-for="o in ['full','propose_only','off']" :key="o" :label="o" :value="o"></el-option>
|
||||
</el-select>
|
||||
<el-select v-else-if="s.row.key==='PMS_SECTOR_SOURCE'" v-model="s.row.value" size="small">
|
||||
<el-option label="(空) 停用行业约束" value=""/>
|
||||
<el-option label="custom_table" value="custom_table"/>
|
||||
<el-option label="gp_stock_category" value="gp_stock_category"/>
|
||||
<el-option label="(空) 停用行业约束" value=""></el-option>
|
||||
<el-option label="custom_table" value="custom_table"></el-option>
|
||||
<el-option label="gp_stock_category" value="gp_stock_category"></el-option>
|
||||
</el-select>
|
||||
<el-switch v-else-if="s.row.type==='bool'" v-model="s.row.value"/>
|
||||
<el-input v-else v-model="s.row.value" size="small"/>
|
||||
<el-switch v-else-if="s.row.type==='bool'" v-model="s.row.value"></el-switch>
|
||||
<el-input v-else v-model="s.row.value" size="small"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="file_default" label="文件初值" width="130"/>
|
||||
<el-table-column prop="file_default" label="文件初值" width="130"></el-table-column>
|
||||
<el-table-column label="来源" width="80">
|
||||
<template #default="s">
|
||||
<el-tag size="small" :type="s.row.source==='table'?'success':'info'" effect="dark">
|
||||
{{ s.row.source==='table' ? '页面' : '文件' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="desc" label="说明" min-width="260"/>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="160" class-name="muted"/>
|
||||
<el-table-column prop="desc" label="说明" min-width="260"></el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="160" class-name="muted"></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
|
|
@ -149,7 +149,7 @@
|
|||
<div v-for="(f,name) in curSpec.fields" :key="name" class="row">
|
||||
<span class="muted">{{ fieldLabel(name) }}</span>
|
||||
<el-select v-if="f.type==='enum'" v-model="form.params[name]" size="small" style="width:150px">
|
||||
<el-option v-for="o in f.choices" :key="o" :label="o" :value="o"/>
|
||||
<el-option v-for="o in f.choices" :key="o" :label="o" :value="o"></el-option>
|
||||
</el-select>
|
||||
<el-input v-else v-model="form.params[name]" size="small" style="width:150px"
|
||||
:placeholder="f.required===false ? '可留空(默认)' : ''">
|
||||
|
|
@ -158,7 +158,7 @@
|
|||
<template v-else-if="f.type==='price'" #append>元</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<el-input v-model="form.note" size="small" style="width:200px" placeholder="备注(可选)"/>
|
||||
<el-input v-model="form.note" size="small" style="width:200px" placeholder="备注(可选)"></el-input>
|
||||
<el-button type="primary" size="small" :loading="issuing" @click="issue(false)">下达</el-button>
|
||||
</div>
|
||||
<div class="muted" style="margin-top:8px" v-if="curSpec && curSpec.cls==='task'">
|
||||
|
|
@ -179,22 +179,22 @@
|
|||
<el-button size="small" style="margin:8px 0" @click="loadPlans(s.row.command_id)">
|
||||
查看方案明细</el-button>
|
||||
<el-table v-if="plansOf===s.row.command_id" :data="plans" size="small" border>
|
||||
<el-table-column prop="ts_code" label="股票" width="110" class-name="mono"/>
|
||||
<el-table-column prop="action" label="动作" width="80"/>
|
||||
<el-table-column prop="qty" label="数量" width="90"/>
|
||||
<el-table-column prop="ts_code" label="股票" width="110" class-name="mono"></el-table-column>
|
||||
<el-table-column prop="action" label="动作" width="80"></el-table-column>
|
||||
<el-table-column prop="qty" label="数量" width="90"></el-table-column>
|
||||
<el-table-column label="金额" width="110">
|
||||
<template #default="p">{{ money(p.row.amount) }}</template></el-table-column>
|
||||
<el-table-column prop="priority" label="优先级" width="80"/>
|
||||
<el-table-column prop="status" label="状态" width="90"/>
|
||||
<el-table-column prop="deadline" label="截止" width="110"/>
|
||||
<el-table-column prop="reason" label="理由" min-width="260"/>
|
||||
<el-table-column prop="priority" label="优先级" width="80"></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="90"></el-table-column>
|
||||
<el-table-column prop="deadline" label="截止" width="110"></el-table-column>
|
||||
<el-table-column prop="reason" label="理由" min-width="260"></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="command_id" label="命令号" width="160" class-name="mono"/>
|
||||
<el-table-column prop="cmd_type" label="类型" width="160"/>
|
||||
<el-table-column prop="ts_code" label="标的" width="110" class-name="mono"/>
|
||||
<el-table-column prop="command_id" label="命令号" width="160" class-name="mono"></el-table-column>
|
||||
<el-table-column prop="cmd_type" label="类型" width="160"></el-table-column>
|
||||
<el-table-column prop="ts_code" label="标的" width="110" class-name="mono"></el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="s">
|
||||
<el-tag size="small" effect="dark" :type="stTag(s.row.status)">{{ s.row.status }}</el-tag>
|
||||
|
|
@ -204,7 +204,7 @@
|
|||
<template #default="s">
|
||||
<div v-if="s.row.progress && s.row.progress.target_amount">
|
||||
<el-progress :percentage="progPct(s.row.progress)" :stroke-width="10"
|
||||
:text-inside="true" status="success"/>
|
||||
:text-inside="true" status="success"></el-progress>
|
||||
<span class="muted">{{ money(s.row.progress.done_amount) }} /
|
||||
{{ money(s.row.progress.target_amount) }}
|
||||
<template v-if="s.row.progress.gap"> · 缺口 {{ money(s.row.progress.gap) }}</template>
|
||||
|
|
@ -213,7 +213,7 @@
|
|||
<span v-else class="muted">{{ (s.row.progress||{}).plan_count || 0 }} 条方案</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="issued_at" label="下达时间" width="160" class-name="muted"/>
|
||||
<el-table-column prop="issued_at" label="下达时间" width="160" class-name="muted"></el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="s">
|
||||
<el-button size="small" :disabled="!canCancel(s.row)"
|
||||
|
|
@ -237,23 +237,23 @@
|
|||
<el-button size="small" @click="loadLots(s.row.ts_code)">查看批次</el-button>
|
||||
<el-table v-if="lotsOf===s.row.ts_code" :data="lots" size="small" border
|
||||
style="margin-top:8px">
|
||||
<el-table-column prop="lot_type" label="批次" width="90"/>
|
||||
<el-table-column prop="qty" label="剩余" width="90"/>
|
||||
<el-table-column prop="closed_qty" label="已核销" width="90"/>
|
||||
<el-table-column prop="open_price" label="开仓价" width="100"/>
|
||||
<el-table-column prop="close_avg_price" label="核销均价" width="100"/>
|
||||
<el-table-column prop="realized_pnl" label="已实现" width="100"/>
|
||||
<el-table-column prop="open_date" label="开仓日" width="110"/>
|
||||
<el-table-column prop="lot_type" label="批次" width="90"></el-table-column>
|
||||
<el-table-column prop="qty" label="剩余" width="90"></el-table-column>
|
||||
<el-table-column prop="closed_qty" label="已核销" width="90"></el-table-column>
|
||||
<el-table-column prop="open_price" label="开仓价" width="100"></el-table-column>
|
||||
<el-table-column prop="close_avg_price" label="核销均价" width="100"></el-table-column>
|
||||
<el-table-column prop="realized_pnl" label="已实现" width="100"></el-table-column>
|
||||
<el-table-column prop="open_date" label="开仓日" width="110"></el-table-column>
|
||||
<el-table-column prop="instruction_id" label="来源指令" min-width="180"
|
||||
class-name="mono"/>
|
||||
<el-table-column prop="note" label="备注" min-width="160"/>
|
||||
class-name="mono"></el-table-column>
|
||||
<el-table-column prop="note" label="备注" min-width="160"></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="ts_code" label="股票" width="110" class-name="mono"/>
|
||||
<el-table-column prop="sector" label="行业" width="100"/>
|
||||
<el-table-column prop="status" label="状态" width="90"/>
|
||||
<el-table-column prop="ts_code" label="股票" width="110" class-name="mono"></el-table-column>
|
||||
<el-table-column prop="sector" label="行业" width="100"></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="90"></el-table-column>
|
||||
<el-table-column label="冻结" width="110">
|
||||
<template #default="s">
|
||||
<el-tag v-if="s.row.frozen_reason && s.row.frozen_reason!=='NONE'" size="small"
|
||||
|
|
@ -261,13 +261,13 @@
|
|||
<span v-else class="muted">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="total_qty" label="持仓" width="90"/>
|
||||
<el-table-column prop="avail_qty" label="可卖" width="90"/>
|
||||
<el-table-column prop="total_qty" label="持仓" width="90"></el-table-column>
|
||||
<el-table-column prop="avail_qty" label="可卖" width="90"></el-table-column>
|
||||
<el-table-column label="批次(底/补/加/补仓/T)" width="170">
|
||||
<template #default="s"><span class="mono nowrap">{{ s.row.base_qty }}/{{ s.row.fill_qty }}/{{ s.row.add_qty }}/{{ s.row.dca_qty }}/{{ s.row.t0_qty }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="avg_cost" label="摊薄成本" width="100"/>
|
||||
<el-table-column prop="price" label="现价" width="90"/>
|
||||
<el-table-column prop="avg_cost" label="摊薄成本" width="100"></el-table-column>
|
||||
<el-table-column prop="price" label="现价" width="90"></el-table-column>
|
||||
<el-table-column label="安全垫" width="130">
|
||||
<template #default="s">
|
||||
<el-tag size="small" effect="dark" :type="cuTag(s.row.cushion_state)">
|
||||
|
|
@ -311,33 +311,33 @@
|
|||
<el-table v-if="((s.row.progress||{}).children||[]).length"
|
||||
:data="s.row.progress.children" size="small" border
|
||||
style="margin-top:6px">
|
||||
<el-table-column prop="ymd" label="日期" width="100"/>
|
||||
<el-table-column prop="at" label="时点" width="90"/>
|
||||
<el-table-column prop="qty" label="数量" width="90"/>
|
||||
<el-table-column prop="limit" label="限价" width="90"/>
|
||||
<el-table-column prop="mode" label="通道" width="100"/>
|
||||
<el-table-column prop="ymd" label="日期" width="100"></el-table-column>
|
||||
<el-table-column prop="at" label="时点" width="90"></el-table-column>
|
||||
<el-table-column prop="qty" label="数量" width="90"></el-table-column>
|
||||
<el-table-column prop="limit" label="限价" width="90"></el-table-column>
|
||||
<el-table-column prop="mode" label="通道" width="100"></el-table-column>
|
||||
<el-table-column label="兜底" width="70">
|
||||
<template #default="c">{{ c.row.forced ? '是' : '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reason" label="出手依据" min-width="260"/>
|
||||
<el-table-column prop="reason" label="出手依据" min-width="260"></el-table-column>
|
||||
</el-table>
|
||||
<div v-else class="muted" style="margin-top:6px">尚无出手记录</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="instruction_id" label="指令号" width="230" class-name="mono"/>
|
||||
<el-table-column prop="ts_code" label="股票" width="110" class-name="mono"/>
|
||||
<el-table-column prop="action" label="动作" width="80"/>
|
||||
<el-table-column prop="side" label="方向" width="70"/>
|
||||
<el-table-column prop="qty" label="数量" width="80"/>
|
||||
<el-table-column prop="exec_qty" label="已成交" width="80"/>
|
||||
<el-table-column prop="instruction_id" label="指令号" width="230" class-name="mono"></el-table-column>
|
||||
<el-table-column prop="ts_code" label="股票" width="110" class-name="mono"></el-table-column>
|
||||
<el-table-column prop="action" label="动作" width="80"></el-table-column>
|
||||
<el-table-column prop="side" label="方向" width="70"></el-table-column>
|
||||
<el-table-column prop="qty" label="数量" width="80"></el-table-column>
|
||||
<el-table-column prop="exec_qty" label="已成交" width="80"></el-table-column>
|
||||
<el-table-column label="进度" width="130">
|
||||
<template #default="s">
|
||||
<el-progress :percentage="insPct(s.row)" :stroke-width="10" :text-inside="true"/>
|
||||
<el-progress :percentage="insPct(s.row)" :stroke-width="10" :text-inside="true"></el-progress>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="115"/>
|
||||
<el-table-column prop="origin_type" label="来源" width="80"/>
|
||||
<el-table-column prop="status" label="状态" width="115"></el-table-column>
|
||||
<el-table-column prop="origin_type" label="来源" width="80"></el-table-column>
|
||||
<el-table-column label="操作" width="90">
|
||||
<template #default="s">
|
||||
<el-button size="small" @click="cancelIns(s.row.instruction_id)">撤销</el-button>
|
||||
|
|
@ -352,20 +352,20 @@
|
|||
<div class="panel">
|
||||
<h3>评审账本 (规则闸 / 研判闸 / 人工裁决全量留痕)</h3>
|
||||
<el-table :data="ledger" size="small" border max-height="260">
|
||||
<el-table-column prop="decided_at" label="时间" width="160"/>
|
||||
<el-table-column prop="ts_code" label="股票" width="110" class-name="mono"/>
|
||||
<el-table-column prop="action" label="动作" width="90"/>
|
||||
<el-table-column prop="arbiter" label="评审方" width="90"/>
|
||||
<el-table-column prop="decided_at" label="时间" width="160"></el-table-column>
|
||||
<el-table-column prop="ts_code" label="股票" width="110" class-name="mono"></el-table-column>
|
||||
<el-table-column prop="action" label="动作" width="90"></el-table-column>
|
||||
<el-table-column prop="arbiter" label="评审方" width="90"></el-table-column>
|
||||
<el-table-column label="结论" width="90">
|
||||
<template #default="s">
|
||||
<el-tag size="small" effect="dark" :type="s.row.verdict==='PASS'?'success':'danger'">
|
||||
{{ s.row.verdict }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="price_at" label="评审价" width="90"/>
|
||||
<el-table-column prop="price_at" label="评审价" width="90"></el-table-column>
|
||||
<el-table-column label="未通过项" min-width="240">
|
||||
<template #default="s"><span class="muted">{{ (s.row.failed_checks||[]).join(' | ') }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reason" label="理由" min-width="200"/>
|
||||
<el-table-column prop="reason" label="理由" min-width="200"></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
|
@ -374,7 +374,7 @@
|
|||
<el-tab-pane name="prop">
|
||||
<template #label>
|
||||
提议确认
|
||||
<el-badge v-if="proposals.length" :value="proposals.length" class="ml"/>
|
||||
<el-badge v-if="proposals.length" :value="proposals.length" class="ml"></el-badge>
|
||||
</template>
|
||||
<div class="panel">
|
||||
<h3>待确认队列 (自主档位 {{ ov.autonomy }})</h3>
|
||||
|
|
@ -384,17 +384,17 @@
|
|||
</div>
|
||||
<el-alert v-if="dm.judge && !dm.judge.available" type="warning" effect="dark" show-icon
|
||||
:closable="false" style="margin-bottom:10px"
|
||||
:title="'研判闸未接通 —— ' + dm.judge.reason + ' (补足/加仓/补仓一律降级为人工确认)'"/>
|
||||
:title="'研判闸未接通 —— ' + dm.judge.reason + ' (补足/加仓/补仓一律降级为人工确认)'"></el-alert>
|
||||
<el-table :data="proposals" size="small" border max-height="420">
|
||||
<el-table-column prop="proposal_id" label="提议号" width="230" class-name="mono"/>
|
||||
<el-table-column prop="ts_code" label="股票" width="110" class-name="mono"/>
|
||||
<el-table-column prop="action" label="动作" width="90"/>
|
||||
<el-table-column prop="qty" label="数量" width="90"/>
|
||||
<el-table-column prop="proposal_id" label="提议号" width="230" class-name="mono"></el-table-column>
|
||||
<el-table-column prop="ts_code" label="股票" width="110" class-name="mono"></el-table-column>
|
||||
<el-table-column prop="action" label="动作" width="90"></el-table-column>
|
||||
<el-table-column prop="qty" label="数量" width="90"></el-table-column>
|
||||
<el-table-column label="硬数字" min-width="320">
|
||||
<template #default="s"><span class="mono muted">{{ JSON.stringify(s.row.hard_numbers) }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="judge_verdict" label="研判" width="100"/>
|
||||
<el-table-column prop="expire_at" label="失效时间" width="160"/>
|
||||
<el-table-column prop="judge_verdict" label="研判" width="100"></el-table-column>
|
||||
<el-table-column prop="expire_at" label="失效时间" width="160"></el-table-column>
|
||||
<el-table-column label="裁决" width="170">
|
||||
<template #default="s">
|
||||
<el-button size="small" type="success" @click="decide(s.row,'ACCEPTED')">采纳</el-button>
|
||||
|
|
@ -444,7 +444,7 @@
|
|||
<div v-if="(report.report||{}).attention">
|
||||
<h3 style="color:#409EFF;font-size:14px">关注区</h3>
|
||||
<el-table :data="report.report.attention" size="small" border>
|
||||
<el-table-column prop="type" label="事项" width="130"/>
|
||||
<el-table-column prop="type" label="事项" width="130"></el-table-column>
|
||||
<el-table-column label="内容">
|
||||
<template #default="s"><span class="mono muted">{{ JSON.stringify(s.row) }}</span></template>
|
||||
</el-table-column>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
清空 PMS 账本并把回放游标对齐到当前 (影子运行期的重来一次)
|
||||
===========================================================
|
||||
运行:
|
||||
docker compose run --rm pms-web python scripts/reset_ledger.py # 演练, 只统计不删
|
||||
docker compose run --rm pms-web python scripts/reset_ledger.py --yes # 实际执行
|
||||
docker compose run --rm pms-web python scripts/reset_ledger.py --yes --keep-commands
|
||||
|
||||
什么时候用
|
||||
----------
|
||||
账本被脏数据污染、需要从干净起点重来时。典型场景就是 2026-07-29 那次: 首次启动时回放游标
|
||||
为空, `replay_fills` 从 `trading_order` 表头开始扫, 把旧系统多年的历史成交**全部**当成
|
||||
「外部成交」并入 BASE 批次 —— 账本上凭空长出一堆早已清掉的持仓, 摊薄成本与安全垫全错。
|
||||
(该 bug 已修: 游标为空时只对齐不追认, 见 ledger_service._seed_cursor。)
|
||||
|
||||
**只动 PMS 自有表, 绝不碰下游的 trading_*** —— 那三张表归下游系统维护, PMS 只读。
|
||||
|
||||
清完之后账本是空的, 而真实持仓在 QMT 那边。要让账本反映真实持仓, 有两条路:
|
||||
* 让日终对账把下游持仓补进来 (以下游为准, 会生成 RECON 批次), 或
|
||||
* 在页面用建仓命令把已有持仓登记成 BASE 批次
|
||||
影子运行期一般选前者: 跑一次 `POST /api/ops/reconcile?apply_fix=true`。
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# 账本类表 —— 会被清空
|
||||
LEDGER_TABLES = ["pms_lot", "pms_position", "pms_instruction", "pms_action_ledger",
|
||||
"pms_qmt_order", "pms_qmt_inbox"]
|
||||
# 命令与方案 —— 默认也清 (方案指向的指令都没了, 留着只会是孤儿); --keep-commands 可保留
|
||||
COMMAND_TABLES = ["pms_plan", "pms_command", "pms_proposal"]
|
||||
# 绝不碰: 下游三表 / 行业映射 / 参数表 / 日报
|
||||
NEVER_TOUCH = ["trading_order", "trading_position", "trading_buy_plan",
|
||||
"pms_industry_map", "pms_runtime_param", "pms_daily_report"]
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--yes", action="store_true", help="确认执行 (缺省只演练)")
|
||||
ap.add_argument("--keep-commands", action="store_true",
|
||||
help="保留命令/方案/提议, 只清账本与指令")
|
||||
ap.add_argument("--no-seed", action="store_true",
|
||||
help="不重置回放游标 (默认会把它对齐到当前最新成交)")
|
||||
args = ap.parse_args()
|
||||
|
||||
from app.db.session import execute, fetch_one
|
||||
from app.repo import downstream_repo, pms_repo
|
||||
from app.services.ledger_service import CURSOR_KEY
|
||||
|
||||
tables = list(LEDGER_TABLES) + ([] if args.keep_commands else COMMAND_TABLES)
|
||||
|
||||
print("将清空以下表:")
|
||||
total = 0
|
||||
for t in tables:
|
||||
try:
|
||||
n = int((fetch_one(f"SELECT COUNT(*) AS n FROM {t}") or {}).get("n") or 0)
|
||||
total += n
|
||||
print(f" {t:<22} {n} 行")
|
||||
except Exception as e:
|
||||
print(f" {t:<22} 读取失败: {type(e).__name__}: {e}")
|
||||
print(f"\n合计 {total} 行。以下表**不会**被动: {', '.join(NEVER_TOUCH)}")
|
||||
|
||||
cur = pms_repo.get_param(CURSOR_KEY)
|
||||
print(f"\n当前回放游标: {cur or '(空)'}")
|
||||
if not args.no_seed:
|
||||
try:
|
||||
anchor = downstream_repo.latest_filled_order_id()
|
||||
print(f"将把游标对齐到当前最新成交: {anchor or '(下游暂无成交)'}")
|
||||
print(" → 此后只跟踪新成交, 不再追认历史")
|
||||
except Exception as e:
|
||||
anchor = None
|
||||
print(f"读下游最新成交失败 (游标将清空): {type(e).__name__}: {e}")
|
||||
else:
|
||||
anchor = None
|
||||
print("--no-seed: 游标保持不变")
|
||||
|
||||
if not args.yes:
|
||||
print("\n[演练模式] 未删除任何数据。确认后加 --yes 重跑:")
|
||||
print(" docker compose run --rm pms-web python scripts/reset_ledger.py --yes")
|
||||
return
|
||||
|
||||
print()
|
||||
failed = []
|
||||
for t in tables:
|
||||
try:
|
||||
n = execute(f"DELETE FROM {t}")
|
||||
print(f" OK {t:<22} 删除 {n} 行")
|
||||
except Exception as e:
|
||||
print(f" FAIL {t:<22} {type(e).__name__}: {e}")
|
||||
failed.append(t)
|
||||
|
||||
if not args.no_seed:
|
||||
try:
|
||||
pms_repo.set_param(CURSOR_KEY, anchor or "", updated_by="reset_ledger")
|
||||
print(f" OK 回放游标 → {anchor or '(空, 下次回放自动对齐)'}")
|
||||
except Exception as e:
|
||||
print(f" FAIL 游标重置: {type(e).__name__}: {e}")
|
||||
failed.append(CURSOR_KEY)
|
||||
|
||||
# ws 通道状态一并归零 —— 指令与 inbox 都清了, 留着旧水位会让补发对不上
|
||||
try:
|
||||
execute("UPDATE pms_ws_state SET last_seq = 0, acked_seq = 0, server_seq = 0, "
|
||||
"resync_flag = 0, conn_state = 'INIT', updated_at = NOW() WHERE id = 1")
|
||||
print(" OK pms_ws_state 水位归零 (inbox 已清空, 水位必须跟着回零)")
|
||||
except Exception as e:
|
||||
print(f" WARN pms_ws_state 归零失败 (表可能还没建): {type(e).__name__}: {e}")
|
||||
|
||||
print("\n" + "-" * 62)
|
||||
if failed:
|
||||
print(f"FAILED: {', '.join(failed)}")
|
||||
sys.exit(1)
|
||||
print("账本已清空。下一步:")
|
||||
print(" 1. 让对账把下游真实持仓补进账本 (以下游为准):")
|
||||
print(" curl -X POST 'http://127.0.0.1:38100/api/ops/reconcile?apply_fix=true'")
|
||||
print(" 2. 或在页面用建仓命令把已有持仓登记成 BASE 批次")
|
||||
print(" 3. 核对: python scripts/check_db.py")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -392,6 +392,12 @@ def install_fakes(prices=None, positions=None, params=None, high5=None):
|
|||
downstream_repo.fetch_positions = lambda: {"rows": [], "columns": {"qty": None},
|
||||
"raw_count": 0}
|
||||
downstream_repo.fetch_refs = lambda c: None
|
||||
downstream_repo.latest_filled_order_id = lambda: "ANCHOR_0"
|
||||
# 回放游标预置成非空 —— 否则 replay_fills 会走「冷启动只对齐不追认」那条路 (见
|
||||
# ledger_service._seed_cursor), 下面那几个回放用例就测不到入账。冷启动本身另有专门用例。
|
||||
# 用 "0" 而不是随便一个字符串: next_cursor 只进不退, 且非数字 id 会退化成字典序比较,
|
||||
# 占位值若比真实 order_id 大 (比如 "SEED"), 游标就永远推不动了。
|
||||
fake.params.setdefault("PMS_REPLAY_CURSOR", "0")
|
||||
param_store._cache.update({"at": 0.0, "data": {}, "error": None, "loaded": False})
|
||||
portfolio.save_neg_streak({})
|
||||
return fake
|
||||
|
|
@ -700,6 +706,42 @@ def _():
|
|||
downstream_repo.fetch_filled_orders = orig
|
||||
|
||||
|
||||
@case("账本服务·首次回放只对齐游标, 不把下游历史当外部成交追认")
|
||||
def _():
|
||||
from app.repo import downstream_repo
|
||||
from app.services import ledger_service as ls
|
||||
fake = install_fakes()
|
||||
fake.params.pop("PMS_REPLAY_CURSOR", None) # 冷启动: 从没设过游标
|
||||
hist = [{"order_id": f"BUY_X_{i}", "ts_code": "600000.SH", "side": "buy", "qty": 100,
|
||||
"price": 10.0, "done_time": "2020-01-01 09:40:00"} for i in range(500)]
|
||||
orig = downstream_repo.fetch_filled_orders
|
||||
try:
|
||||
downstream_repo.fetch_filled_orders = lambda **kw: hist
|
||||
downstream_repo.latest_filled_order_id = lambda: "SELL_ZZZ_999"
|
||||
r = ls.replay_fills()
|
||||
# 关键: 一条都不能入账。旧系统多年的历史成交若被并入 BASE, 摊薄成本与安全垫全错,
|
||||
# 而补仓/加仓/保垫减仓都挂在安全垫上 —— 一错就是整条纪律链。
|
||||
assert r.get("seeded") and r["fills"] == 0 and r["actions"] == 0, r
|
||||
assert r["cursor"] == "SELL_ZZZ_999", r
|
||||
assert not fake.lots and not fake.positions, "冷启动不该产生任何批次或持仓"
|
||||
assert fake.params["PMS_REPLAY_CURSOR"] == "SELL_ZZZ_999"
|
||||
|
||||
# 游标就位后, 新成交照常入账
|
||||
new = [{"order_id": "ZZZ_NEW", "ts_code": "600000.SH", "side": "buy", "qty": 100,
|
||||
"price": 10.0, "done_time": "2026-07-29 09:40:00"}]
|
||||
downstream_repo.fetch_filled_orders = lambda **kw: new
|
||||
r2 = ls.replay_fills()
|
||||
assert r2["ok"] and r2["actions"] == 1, r2
|
||||
|
||||
# 显式全量: 游标设 ALL 才追认历史
|
||||
fake.params["PMS_REPLAY_CURSOR"] = ls.CURSOR_ALL
|
||||
downstream_repo.fetch_filled_orders = lambda **kw: hist[:3]
|
||||
r3 = ls.replay_fills()
|
||||
assert r3["fills"] == 3, r3
|
||||
finally:
|
||||
downstream_repo.fetch_filled_orders = orig
|
||||
|
||||
|
||||
@case("账本服务·对账以下游为准 + 连续不一致升级")
|
||||
def _():
|
||||
from app.services import ledger_service as ls
|
||||
|
|
|
|||
Loading…
Reference in New Issue