feat(web): 对账中心上线——恒等式B5只读端点+前端恒等式区块,双轨对账页升级为对账中心(issue#42-A) [nas]
- API: GET /paper/reconcile/identity?date&refresh——缺省读 supervisor 15:10/23:27
已存 identity_reconcile 行,无存行现算并落库(与 build/save 同套幂等);
路由置于 /paper/{aid} 前,与既有 reconcile 端点同组
- 前端: Reconcile.vue 顶部新增恒等式卡片(账户级红绿/未归因±/占比vs容忍0.5%/
可展开逐票缺口+实例市值拆分),页面与菜单更名为「对账中心」;
api/paper.ts 补 IdentityReport 类型族
- 测试: 绿线(50/10050=0.497%pass)+存行复用+refresh强制重算(200=1.96%红)+
snapshot_missing 如实标注,12/12 绿
- 附: docs/session_prompts/ 两份 session 移交提示词入库(fundamentals 18min→
数据session / 统一ex定寸→策略session,用户08-26拍板)
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
# 移交:策略 session(small_cap 选股 fundamentals 步骤 18 分钟瓶颈——08-26 巡检新发现)
|
||||
|
||||
## 现象(VPS 实测,live_20.log 三日对照)
|
||||
|
||||
| 日期 | 候选池 | 选股2/3 fundamentals | 下单时刻 |
|
||||
|------|--------|---------------------|---------|
|
||||
| 08-24 | 30 只(max_pool bug 日) | 13.6s | 09:30:14 |
|
||||
| 08-25 | 3226 只 | 335.1s | 09:35:40 |
|
||||
| 08-26 | 3226 只 | **1081.4s (18分钟)** | 09:48:04 |
|
||||
|
||||
- 09:30:00 任务启动 → 09:48 才下单,`[delay=+1084s]`,开盘 18 分钟敞口
|
||||
- 每日 `download_financial_data 超时 120s (3226 只),跳过读缓存` 必现
|
||||
- shadow_61(影子侧同代码)同步 09:48 —— 双轨一致,纯属共同慢
|
||||
|
||||
## 根因(已定位,无需再查)
|
||||
|
||||
`get_fundamentals_df(3226只)`(sanguo_portfolio/providers/sanguo_fundamentals.py:460-470):
|
||||
1. `xt.download_financial_data(3226)` 卡满 120s 超时被弃(daemon 线程仍在后台抢 miniQMT 数据目录)
|
||||
2. `xt.get_financial_data(3226)` 逐股读本地缓存 ≈0.3s/股 —— 3226×0.3 ≈ 16min 主体
|
||||
3. 08-25→08-26 从 335s 恶化到 1081s:疑与残留 download 线程争锁 + 开盘 miniQMT 客户端负载有关
|
||||
|
||||
## 关键事实:策略实际只用 2 个字段
|
||||
|
||||
`small_cap_ex.py` 选股2/3 全部消费 = **eps 过滤 + market_cap 升序排序取 top100**(:233-247)。
|
||||
而 **market_cap 已有本地 unified 估值 parquet 批量覆盖**(`_apply_local_market_caps`,:489,秒级、全池同口径)——08-24 P0 就是为绕开盘 Capital 下载加的。
|
||||
|
||||
## 建议方向(你们拍板)
|
||||
|
||||
**先用本地 parquet 按 market_cap 全池升序 → 截 bottom ~300 → 只对这 300 只走 xt fundamentals(eps 过滤)→ 再取 top100**。
|
||||
- xt 查询量 3226→300,18min→~2min 内
|
||||
- 语义差异仅当 bottom300 中 >200 只 eps≤0 才可见(实际市场几乎不可能);如要保守可截 500
|
||||
- 动量评分(选股3/3)已经很快(2.8s)不用动
|
||||
- 备选:问数据 session unified parquet 有没有 eps/roe 列,有则整步全本地化
|
||||
|
||||
## 测试与验收
|
||||
|
||||
- 测试:bottom-N 截断后选股结果与全池版对齐(同一历史日 top100 集合 diff 应为空或个位数且可解释)
|
||||
- 验收:明早 live_20 选股2/3 <180s,下单回到 09:3x
|
||||
- commit 打 [vps],随下次同车上 VPS(当前 gap 里已有 919fa50 迟到成交根因修在等车)
|
||||
|
||||
## 环境事实
|
||||
|
||||
- 昨日引擎 16s 同步等待弃跟踪导致迟到成交漏账(-18060)由前后端 session 今晚修账止血,你们已交的 919fa50(live_reconcile a+b)是根因修——两者不冲突
|
||||
- VPS 只读探针模板:`ssh 49.232.102.198 'C:\Python310\python.exe -X utf8 -' < script.py`
|
||||
- 另请顺手确认:all_weather_ex(live#17)自 08-19 创建起零成交、常驻 500k 现金空仓——是择时门关着的预期行为还是配置问题?(非今日回归,不急)
|
||||
@@ -0,0 +1,35 @@
|
||||
# 移交:策略 session(定寸口径统一 _ex——用户 08-26 拍板「都要统一ex的」)
|
||||
|
||||
## 拍板内容
|
||||
|
||||
用户 08-26 拍板:**回测与实盘定寸口径全部统一到 _ex 语义**(per_value = 总资产/N,即 JQ 原版 `stock_value/len` 语义)。
|
||||
|
||||
## 现状(已核实)
|
||||
|
||||
| 文件 | 现口径 | 位置 |
|
||||
|------|--------|------|
|
||||
| momentum_timing.py | `cash = _available_cash(context); per_value = cash/target_num` | :229-232 |
|
||||
| value_selection.py | 同上 | :200-203 |
|
||||
| small_cap.py | 同上 | :385-388 |
|
||||
| all_weather.py | `cash = _available_cash(context)/n_pick`(两处) | :204, :273 |
|
||||
|
||||
- _ex 副本(841ea15 已上 VPS)已全部用 `_total_value`;helper 就在 `all_weather.py:587`(`_total_value(context)`,portfolio.total_value 优先,回退 cash+Σ持仓)
|
||||
- 聚宽原版语义本来就是 stock_value/len(总资产口径)——统一同时对齐 JQ 原版
|
||||
- 前端无需动:PortfolioBacktest.vue `max_pool` 默认已是 0(=全市场不限,与实盘 env 一致;记忆里「默认30」已过时)
|
||||
|
||||
## 要做的
|
||||
|
||||
1. 四个原版策略的 per_value 换 `_total_value`(带 _ex 同款 `total<=0` 守卫);all_weather 两处同改
|
||||
2. **顺手带上低优尾巴**:value_selection 选股 `set` → `sorted`(顺序确定化,你们 08-15 就记了待办)
|
||||
3. 测试锚点可镜像 `tests/portfolio/test_sizing_total_value.py`(stale_cash=1000 vs total=10500 那组)
|
||||
4. 注意:改完**历史回测结果会变**(这正是目的——回测↔实盘对齐);如果有对照基线需求跑一次前后对比留档
|
||||
|
||||
## 验收
|
||||
|
||||
- 回测侧同参数下原版与 _ex 副本选股+定寸一致(TET 副本对照天然就是验收)
|
||||
- commit 打 [vps],随下次同车
|
||||
|
||||
## 环境事实
|
||||
|
||||
- VPS tag=vps-deployed@919fa50(08-26 19:50 推,含你们的 live_reconcile 根因修——**明天 15:05 EOD 对账首次实跑**,15:10 恒等式观察 #38 第 1 绿日)
|
||||
- 08-26 恒等式已修账至 0.000%(当日买侧 4 笔迟到漏记也手动补齐,002159/002576/002633/002862 共 700 股)——此后同类由 live_reconcile 自动兜
|
||||
@@ -222,3 +222,41 @@ export async function getReconcilePairs(date?: string): Promise<ReconcilePair[]>
|
||||
'/paper/reconcile', { params: date ? { date } : {} })
|
||||
return data.pairs
|
||||
}
|
||||
|
||||
// ===== 账户恒等式(B5):全账户市值 = Σ实例账本市值 + 未归因(容忍 ≤0.5%) =====
|
||||
|
||||
export interface IdentityUnattrPosition {
|
||||
symbol: string
|
||||
snapshot_volume: number
|
||||
instance_volume: number
|
||||
diff: number
|
||||
}
|
||||
|
||||
export interface ReconcileIdentityRow {
|
||||
account: string
|
||||
date: string
|
||||
status: string // pass | unattributed_over_tol | snapshot_missing | no_instances
|
||||
snapshot_mv: number | null
|
||||
instance_mv_total: number
|
||||
unattributed_mv: number | null
|
||||
unattributed_pct: number | null
|
||||
tolerance_pct: number
|
||||
instances: { id: number; name: string; mv: number }[]
|
||||
unattributed_positions: IdentityUnattrPosition[]
|
||||
}
|
||||
|
||||
export interface ReconcileIdentityReport {
|
||||
date: string
|
||||
rows: ReconcileIdentityRow[]
|
||||
identity_passed: boolean
|
||||
}
|
||||
|
||||
export async function getReconcileIdentity(
|
||||
date?: string,
|
||||
refresh = false,
|
||||
): Promise<ReconcileIdentityReport> {
|
||||
const { data } = await apiClient.get<ReconcileIdentityReport>(
|
||||
'/paper/reconcile/identity',
|
||||
{ params: { ...(date ? { date } : {}), ...(refresh ? { refresh: true } : {}) } })
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ const PAGE_TITLE: Array<{ match: RegExp; group: string; title: string }> = [
|
||||
{ match: /^\/paper\/new$/, group: '模拟盘', title: '新建模拟盘' },
|
||||
{ match: /^\/paper\/result\//, group: '模拟盘', title: '模拟盘结果' },
|
||||
{ match: /^\/paper\/live\//, group: '模拟盘', title: '实走监控' },
|
||||
{ match: /^\/paper\/reconcile/, group: '模拟盘', title: '双轨对账' },
|
||||
{ match: /^\/paper\/reconcile/, group: '模拟盘', title: '对账中心' },
|
||||
{ match: /^\/paper$/, group: '模拟盘', title: '模拟盘' },
|
||||
{ match: /^\/live\/new$/, group: '实盘', title: '新建实盘' },
|
||||
{ match: /^\/live\/monitor\//, group: '实盘', title: '实盘监控' },
|
||||
@@ -73,7 +73,7 @@ const CMD_ITEMS: CmdItem[] = [
|
||||
{ label: '批量评估', group: '投研', path: '/factor/batch' },
|
||||
{ label: '因子分析', group: '投研', path: '/factor/new' },
|
||||
{ label: '模拟盘列表', group: '模拟', path: '/paper' },
|
||||
{ label: '双轨对账', group: '模拟', path: '/paper/reconcile' },
|
||||
{ label: '对账中心', group: '模拟', path: '/paper/reconcile' },
|
||||
{ label: '新建模拟盘', group: '模拟', path: '/paper/new' },
|
||||
{ label: '实盘列表', group: '实盘', path: '/live' },
|
||||
{ label: '新建实盘', group: '实盘', path: '/live/new' },
|
||||
@@ -182,7 +182,7 @@ function onLogout(): void {
|
||||
<el-sub-menu index="paper">
|
||||
<template #title><span class="nav-label">模拟盘</span></template>
|
||||
<el-menu-item index="/paper">模拟盘列表</el-menu-item>
|
||||
<el-menu-item index="/paper/reconcile">双轨对账</el-menu-item>
|
||||
<el-menu-item index="/paper/reconcile">对账中心</el-menu-item>
|
||||
<el-menu-item index="/paper/new">新建模拟盘</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="live">
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getReconcilePairs, type ReconcilePair } from '@/api/paper'
|
||||
import {
|
||||
getReconcilePairs, getReconcileIdentity,
|
||||
type ReconcilePair, type ReconcileIdentityRow,
|
||||
} from '@/api/paper'
|
||||
|
||||
const pairs = ref<ReconcilePair[]>([])
|
||||
const identityRows = ref<ReconcileIdentityRow[]>([])
|
||||
const identityPassed = ref<boolean | null>(null)
|
||||
const loading = ref(false)
|
||||
const date = ref<string>(new Date().toISOString().slice(0, 10))
|
||||
|
||||
async function refresh(): Promise<void> {
|
||||
loading.value = true
|
||||
try {
|
||||
pairs.value = await getReconcilePairs(date.value || undefined)
|
||||
const [ps, ident] = await Promise.all([
|
||||
getReconcilePairs(date.value || undefined),
|
||||
getReconcileIdentity(date.value || undefined),
|
||||
])
|
||||
pairs.value = ps
|
||||
identityRows.value = ident.rows
|
||||
identityPassed.value = ident.identity_passed
|
||||
} catch (e: unknown) {
|
||||
ElMessage.error(e instanceof Error ? e.message : '加载失败')
|
||||
} finally {
|
||||
@@ -25,6 +36,23 @@ function num(v: number | null | undefined): string {
|
||||
return Math.round(v).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
function signed(v: number | null | undefined): string {
|
||||
if (v == null || !Number.isFinite(v)) return '—'
|
||||
const s = Math.round(v).toLocaleString('zh-CN')
|
||||
return v > 0 ? '+' + s : s
|
||||
}
|
||||
|
||||
function identityStatus(row: ReconcileIdentityRow): { text: string; type: 'success' | 'danger' | 'info' } {
|
||||
if (row.status === 'pass') return { text: '恒等式通过', type: 'success' }
|
||||
if (row.status === 'unattributed_over_tol') return { text: '未归因超容忍', type: 'danger' }
|
||||
if (row.status === 'snapshot_missing') return { text: '无账户快照', type: 'info' }
|
||||
return { text: '无运行实例', type: 'info' }
|
||||
}
|
||||
|
||||
function overTol(row: ReconcileIdentityRow): boolean {
|
||||
return row.unattributed_pct != null && row.unattributed_pct > row.tolerance_pct
|
||||
}
|
||||
|
||||
function bps(v: number | null | undefined): string {
|
||||
if (v == null || !Number.isFinite(v)) return '—'
|
||||
return v.toFixed(1) + ' bps'
|
||||
@@ -44,8 +72,8 @@ function sideText(s: string): string {
|
||||
<div v-loading="loading" class="page paper-reconcile">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h2 class="page-title">双轨对账</h2>
|
||||
<p class="page-subtitle">影子柜台 vs 实盘模拟 · 同策略同参数并跑,差异即真实滑点成本</p>
|
||||
<h2 class="page-title">对账中心</h2>
|
||||
<p class="page-subtitle">恒等式(全账户市值 = Σ实例账本 + 未归因 ≤0.5%)+ 双轨对比(影子 vs 实盘) · 每交易日 15:10 / 23:27 自动结算</p>
|
||||
</div>
|
||||
<div class="head-right">
|
||||
<el-date-picker v-model="date" type="date" value-format="YYYY-MM-DD"
|
||||
@@ -54,6 +82,66 @@ function sideText(s: string): string {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== 恒等式(B5)===== -->
|
||||
<el-card shadow="never" class="identity-card">
|
||||
<template #header>
|
||||
<div class="section-head">
|
||||
<span class="section-title">账户恒等式</span>
|
||||
<el-tag v-if="identityPassed === true" type="success" effect="dark">全部通过</el-tag>
|
||||
<el-tag v-else-if="identityPassed === false" type="danger" effect="dark">存在缺口</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="identityRows" size="small" empty-text="当日无恒等式数据(15:10 首次结算)">
|
||||
<el-table-column type="expand">
|
||||
<template #default="{ row }">
|
||||
<div class="expand-body">
|
||||
<div class="muted inst-line">
|
||||
实例市值拆分:
|
||||
<span v-for="i in row.instances" :key="i.id" class="mono inst-item">
|
||||
#{{ i.id }} {{ i.name }} {{ num(i.mv) }}</span>
|
||||
</div>
|
||||
<el-table v-if="row.unattributed_positions.length" :data="row.unattributed_positions"
|
||||
size="small" class="inner-table">
|
||||
<el-table-column prop="symbol" label="未归因标的" min-width="110" class-name="mono" />
|
||||
<el-table-column prop="snapshot_volume" label="账户实际持仓" width="130" class-name="num" />
|
||||
<el-table-column prop="instance_volume" label="Σ实例账本" width="130" class-name="num" />
|
||||
<el-table-column label="缺口(股)" width="110" class-name="num">
|
||||
<template #default="{ row: p }">
|
||||
<span :class="p.diff !== 0 ? 'down' : ''">{{ signed(p.diff) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-else class="muted">逐票无分歧</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="account" label="QMT 账户" min-width="120" class-name="mono" />
|
||||
<el-table-column label="状态" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="identityStatus(row).type">{{ identityStatus(row).text }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="账户市值(快照)" width="140" class-name="num">
|
||||
<template #default="{ row }">{{ num(row.snapshot_mv) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Σ实例账本市值" width="140" class-name="num">
|
||||
<template #default="{ row }">{{ num(row.instance_mv_total) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="未归因" width="130" class-name="num">
|
||||
<template #default="{ row }">
|
||||
<span :class="overTol(row) ? 'down' : ''">{{ signed(row.unattributed_mv) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="占市值 / 容忍" width="150" class-name="num">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.unattributed_pct != null" :class="overTol(row) ? 'down' : ''">
|
||||
{{ pct(row.unattributed_pct) }} / {{ row.tolerance_pct }}%</span>
|
||||
<span v-else class="muted">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-alert v-if="!loading && pairs.length === 0" type="info" :closable="false"
|
||||
title="暂无双轨配对"
|
||||
description="需要一对同策略的「实盘 + 影子」账户同时运行中才产生配对;数据从并跑首个交易日起累积。" />
|
||||
@@ -156,6 +244,13 @@ function sideText(s: string): string {
|
||||
.paper-reconcile { display: flex; flex-direction: column; gap: 16px; }
|
||||
.head-right { display: flex; align-items: center; gap: 12px; }
|
||||
|
||||
.section-head { display: flex; align-items: center; gap: 10px; }
|
||||
.identity-card :deep(.el-card__body) { padding: 8px 12px; }
|
||||
.expand-body { padding: 6px 24px; display: flex; flex-direction: column; gap: 8px; }
|
||||
.inst-line { font-size: 12px; }
|
||||
.inst-item { margin-right: 12px; }
|
||||
.inner-table { max-width: 560px; }
|
||||
|
||||
.pair-block { display: flex; flex-direction: column; gap: 12px; }
|
||||
.pair-head { display: flex; align-items: center; gap: 12px; }
|
||||
.pair-title { font-size: 15px; font-weight: 700; color: var(--text); }
|
||||
|
||||
@@ -308,6 +308,31 @@ def get_reconcile(live_id: int, shadow_id: int, date: str | None = None,
|
||||
return report
|
||||
|
||||
|
||||
@router.get("/paper/reconcile/identity", dependencies=[Depends(verify_token)])
|
||||
def get_identity_reconcile(date: str | None = None, refresh: bool = False):
|
||||
"""账户恒等式报告(B5):全账户市值 = Σ实例账本市值 + 未归因(容忍 ≤0.5%)。
|
||||
|
||||
缺省读 shadow_supervisor 15:10/23:27 已存行;无存行(当日未跑或 refresh=true)
|
||||
按当前库现算并落库——与 supervisor 同一套 build/save,同日幂等覆盖。
|
||||
"""
|
||||
from datetime import datetime as _dt
|
||||
|
||||
from sanguo_trader.shadow.reconcile_report import (
|
||||
build_identity_report, load_identity_report, save_identity_report,
|
||||
)
|
||||
|
||||
d = date or _dt.now().strftime("%Y-%m-%d")
|
||||
db = _db_path["path"]
|
||||
rows = [] if refresh else load_identity_report(db, d)
|
||||
if rows:
|
||||
passed = bool(rows) and all(
|
||||
r.get("status") in ("pass", "no_instances") for r in rows)
|
||||
return {"date": d, "rows": rows, "identity_passed": passed}
|
||||
report = build_identity_report(db, d)
|
||||
save_identity_report(db, report)
|
||||
return report
|
||||
|
||||
|
||||
@router.get("/paper/{aid}", dependencies=[Depends(verify_token)])
|
||||
def get_paper(aid: int):
|
||||
db = _db_path["path"]
|
||||
|
||||
@@ -240,3 +240,84 @@ def test_reconcile_routes(tmp_path):
|
||||
r3 = c.get("/api/v1/paper/39", headers=_auth(token))
|
||||
assert r3.status_code == 200
|
||||
assert r3.json()["mode"] == "shadow"
|
||||
|
||||
|
||||
def test_identity_reconcile_route(tmp_path):
|
||||
"""GET /paper/reconcile/identity:现算→落库→二次读存行;超容忍标红。"""
|
||||
import sqlite3
|
||||
|
||||
c, token = _client(tmp_path)
|
||||
db = os.path.join(str(tmp_path), "p.db")
|
||||
|
||||
def _seed(snap_mv: float) -> None:
|
||||
with sqlite3.connect(db) as conn:
|
||||
conn.execute("DELETE FROM live_accounts")
|
||||
conn.execute("DELETE FROM live_balance")
|
||||
conn.execute("DELETE FROM live_positions")
|
||||
conn.execute("DELETE FROM qmt_account_snapshot")
|
||||
conn.execute(
|
||||
"INSERT INTO live_accounts (id,name,account,vt_symbol,"
|
||||
"strategy_class,strategy_name,status) VALUES "
|
||||
"(5,'live','66639661','pool','channel_test',"
|
||||
"'portfolio_channel_test','running')")
|
||||
conn.execute(
|
||||
"INSERT INTO live_balance (account_id,date,cash,market_value,"
|
||||
"total) VALUES (5,'2026-08-15',1000.0,10000.0,11000.0)")
|
||||
conn.execute(
|
||||
"INSERT INTO live_positions (account_id,symbol,volume,avg_price) "
|
||||
"VALUES (5,'510300.XSHG',1000,4.0)")
|
||||
conn.execute(
|
||||
"INSERT INTO qmt_account_snapshot (account,mini_path,cash,"
|
||||
"market_value,total,positions,updated_at) VALUES "
|
||||
"('66639661','',1000.0,?,10100.0,"
|
||||
"'[{\"symbol\": \"510300.SH\", \"volume\": 1000}]',"
|
||||
"'2026-08-15 15:10:00')", (snap_mv,))
|
||||
conn.commit()
|
||||
|
||||
# 绿线:快照 10050 − 账本 10000 = 50(0.497% < 0.5%) → pass
|
||||
_seed(10050.0)
|
||||
r = c.get("/api/v1/paper/reconcile/identity?date=2026-08-15",
|
||||
headers=_auth(token))
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["identity_passed"] is True
|
||||
row = data["rows"][0]
|
||||
assert row["account"] == "66639661"
|
||||
assert row["status"] == "pass"
|
||||
assert abs(row["unattributed_mv"] - 50.0) < 1e-6
|
||||
assert row["unattributed_positions"] == [] # 逐票 1000=1000 无分歧
|
||||
|
||||
# 二次调用走已存行(identity_reconcile 同日已落库),结果一致
|
||||
r2 = c.get("/api/v1/paper/reconcile/identity?date=2026-08-15",
|
||||
headers=_auth(token))
|
||||
assert r2.json()["rows"][0]["unattributed_mv"] == row["unattributed_mv"]
|
||||
|
||||
# 超红线(改库后 refresh=true 强制重算):快照 10200 − 账本 10000 = 200(1.96%)
|
||||
_seed(10200.0)
|
||||
r3 = c.get("/api/v1/paper/reconcile/identity?date=2026-08-15&refresh=true",
|
||||
headers=_auth(token))
|
||||
d3 = r3.json()
|
||||
assert d3["identity_passed"] is False
|
||||
assert d3["rows"][0]["status"] == "unattributed_over_tol"
|
||||
assert abs(d3["rows"][0]["unattributed_mv"] - 200.0) < 1e-6
|
||||
|
||||
|
||||
def test_identity_reconcile_snapshot_missing(tmp_path):
|
||||
"""无快照账号如实标 snapshot_missing,不算通过也不崩。"""
|
||||
import sqlite3
|
||||
|
||||
c, token = _client(tmp_path)
|
||||
db = os.path.join(str(tmp_path), "p.db")
|
||||
with sqlite3.connect(db) as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO live_accounts (id,name,account,vt_symbol,strategy_class,"
|
||||
"strategy_name,status) VALUES (6,'l2','88888888','pool',"
|
||||
"'all_weather','portfolio_all_weather_ex','running')")
|
||||
conn.commit()
|
||||
|
||||
r = c.get("/api/v1/paper/reconcile/identity?date=2026-08-15",
|
||||
headers=_auth(token))
|
||||
assert r.status_code == 200
|
||||
row = r.json()["rows"][0]
|
||||
assert row["status"] == "snapshot_missing"
|
||||
assert r.json()["identity_passed"] is False
|
||||
|
||||
Reference in New Issue
Block a user