nav-review-cost-report
SKILL_712700669 · vv1.1 · Olive 海外运营 · Owner:Olive 基金运营 · 发布于 2026-08-10
调用 20
下载 0
点赞 0
浏览 0
- 简介
- 基于第三方凭证独立重算NAV投资成本,生成交互式HTML报告与验证JSON。
- 触发词
- 投资成本复核,Cost阶段复核,生成成本报告
- 分发渠道
- ARK Engine
- 功能测试
- ✅ 通过 · 业务评审:✅ 通过
- 技能包文件
- nav-review-cost-report/SKILL.md、nav-review-cost-report/references/cost_calculation_rules.md、nav-review-cost-report/references/inv_cost_review_html_template.html、nav-review-cost-report/references/inv_cost_review_template.md、nav-review-cost-report/references/mmf_reinvestment_period_rules.md
使用示例:请对本基金执行Stage 3.2成本复核并生成报告。
SKILL.md 全文
Frontmatter
| name | nav-review-cost-report |
|---|---|
| description | | |
NAV Review Stage 3.2 — Investment Cost Review 报告生成与上传 (HTML 版)
⛔ 模板强制遵循规则(最高优先级,不可跳过)
生成 HTML 报告时,必须先read_file("references/inv_cost_review_html_template.html") 加载模板,然后严格按照模板的完整结构生成报告。禁止自行设计布局、样式或交互方式。
具体要求:
1. 三栏布局不可变更:左侧导航栏(240px, #1E293B 深色背景) + 中间复核面板(margin-left:240px, margin-right:520px) + 右侧 Supporting 证据面板(520px, fixed, #F1F5F9),三者缺一不可
2. CSS/JS 必须从模板复制:不得自行编写替代样式或脚本,必须使用模板中定义的完整 CSS 和 Vanilla JS 交互逻辑(含 Lightbox)
3. Section 结构必须与模板一致:TB vs N4 → Continuity → Cost Movement → MMF Detail → Individual Investments → Distribution Impact → Fund Statements → Conclusion,不得省略、合并或重新排序
4. 橙色主题色(#ff9800)不可变更:导航栏标题、Section 标题下划线、交互高亮等均使用模板定义的橙色主题
5. 交互行为必须保留:所有数据行必须是 .interactive-row + data-supporting-id,右侧面板默认可见并联动切换
6. 禁止降级为单栏/双栏布局:任何情况下不得生成简单的单页报告或传统文档式布局
违反上述任何一条即视为报告生成失败,必须重新生成。
---
⛔ Supporting 证据来源限制(强制,最高优先级)
只有第三方/外部独立文件才能作为 Supporting 证据。基金自身内部系统生成的文件严禁作为 Supporting。 | 禁止作为 Supporting | 允许作为 Supporting | |---|---| | ❌ Investment Position Appraisal (IPA/N4) | ✅ 银行对账单(DBS Bank Statement 等) | | ❌ Trial Balance (TB) / TB Detail | ✅ 基金行政管理人报表(Citco MV Statement、Citco Capital Account Statement) | | ❌ Purchases and Sales Report | ✅ 交易合同/确认函(Contract Note) | | ❌ Nominal Ledger / Detailed General Ledger | ✅ 银行付款确认(Cash Payment Confirmation) | | ❌ Statement of Operations (SOFI) | ✅ 托管人报告 | | ❌ Change in Unrealized Gains Losses Report | ✅ Payment Log(独立第三方核对记录) | 核对逻辑:中间面板的复核数据可引用 IPA/TB 数字进行对比展示,但右侧 Supporting 面板只展示第三方证据截图来佐证这些数字的正确性。对于 Investment Cost 核对,典型第三方证据包括:Citco Capital Account Statement(确认份额/金额)、Contract Note(确认交易)、Cash Payment Confirmation(确认资金流)。如果某个核对项无对应第三方文件,则该行不设置 Supporting 证据。 ---⛔ Supporting 面板截图与计算过程规则(强制)
规则 A:高清截图 + 红框高亮关键数据
每个.supporting-item 必须包含对应源文件的高清截图(base64 内嵌),且用红框高亮标注关键数字/字段:
1. 截图范围 — 按数据密度决定:
- 数据集中在 1-2 行的(如某个持仓的成本行、单笔交易金额)→ 使用
clip_box裁剪,只截取文档标题行 + 关键数据行区域,无需整页 - 数据分布跨多行或多页的(如完整 IPA 表、完整 TB)→ 保留整页或多页展示
- 判断标准:如果关键数据能在 PDF 页面高度的 30% 以内完整展示,则裁剪;否则整页
- 使用
page.get_text("words")精确定位目标金额/文字坐标 - 红色描边 3px(PIL ImageDraw),无填充,边距外扩 3px
- 只标注与当前核对项直接相关的 1-3 个关键数字(如成本金额、份额数、单价),禁止圈整行或大面积区域
- 截图渲染 DPI ≥ 150(裁剪截图用 4x zoom 确保清晰)
<div class="sup-section-label"> 标注来源文件名和取数用途,如 "IPA — SR13 Cost (May-31)"
规则 B:计算过程 Supporting(涉及测算时强制输出)
当复核过程中涉及数值测算(而非简单读取对比)时,必须在该行对应的.supporting-item 最下方新增一个计算过程区块。典型场景包括但不限于:
- 赎回交易中测算赎回 cost(= 赎回份额 × 平均成本单价)
- 期初成本推算(= 上期 IPA 成本 − 赎回扣减 + 新增申购)
- 多币种换算加总(= 各币种余额 × 汇率 后求和)
- UGL 验证(= Market Value − Verified Cost)
- Change in UGL 计算(= 期末 UGL − 期初 UGL)
<div class="calc-block">
<div class="calc-title">📐 Calculation: [计算目的,如 "Redemption Cost for SR13"]</div>
<pre class="calc-steps">
Step 1: Identify redeemed units
Redeemed Units = 4,176.4832 (from Contract Note)
Step 2: Determine average cost per unit
Prior Period Avg Cost = Prior Total Cost / Prior Total Units
= 17,054,611.89 / 18,222.8478
= 935.9221 per unit
Step 3: Calculate redemption cost
Redemption Cost = Redeemed Units × Avg Cost Per Unit
= 4,176.4832 × 935.9221
= 3,908,724.05
</pre>
<div class="calc-result">✓ Result: USD 3,908,724.05 — matches TB credit</div>
</div>
CSS 样式(须写入 HTML <style> 中):
.calc-block {
margin-top: 12px;
border: 1px solid #E2E8F0;
border-radius: 6px;
overflow: hidden;
background: #FAFBFC;
}
.calc-title {
padding: 8px 12px;
font-size: 12px;
font-weight: 600;
color: #1E293B;
background: #F1F5F9;
border-bottom: 1px solid #E2E8F0;
}
.calc-steps {
padding: 10px 14px;
font-size: 11px;
font-family: 'Consolas', 'Courier New', monospace;
line-height: 1.6;
color: #334155;
white-space: pre-wrap;
margin: 0;
background: #FAFBFC;
}
.calc-result {
padding: 8px 12px;
font-size: 12px;
font-weight: 600;
color: #0A8754;
background: #E6F4ED;
border-top: 1px solid #B7E4C7;
}
执行规则:
- 简单读取对比(TB 余额 = Bank 余额)→ 不需要计算过程,只需截图
- 涉及乘除运算、汇率换算、多步推导 → 必须输出计算过程
- 计算过程中引用的每个输入值须标注来源文件(如 "from Contract Note"、"from Prior IPA")
- 最终结果行必须标注验证状态(matches / differs from 目标值)
When to Use
- 由
nav-review-report调度 skill 在 Stage 3.2 自动调用 - 用户单独说"生成 Investment Cost Review"、"出成本复核报告"
- 已存在
<target_dir>/.cortex/nav_profile.json与<target_dir>/.cortex/nav_data/与<target_dir>/.cortex/screenshots/inv_cost/ - 前置条件:Cash Review 已完成(Cash 核对结果已闭环)
输入
target_dir:基金期间文件夹绝对路径- profile:
<target_dir>/.cortex/nav_profile.json - 数据:
<target_dir>/.cortex/nav_data/sofi.json(或 ipa.json)、prior_cost.json、investment_movement.json、investment_support.json、mmf_reinvestment.json、payment_log.json、distribution_je.json - 截图:
<target_dir>/.cortex/screenshots/inv_cost/*.png
输出
| 报告 | HTML 文件名 | iGowin Subject | |------|------------|----------------| | Investment Cost Review |Investment Cost Review - [基金名] [YYYYMMDD].html | Investments Long 中的 Cost 子科目 |
额外强制产物:<target_dir>/.cortex/nav_data/verified_cost.json(供 UGL Review 消费)
HTML 交互式报告输出至 <target_dir>/ 根目录(用户双击可直接在浏览器中打开),并作为 iGowin 系统上传的最终交付物。中间脚本放 <target_dir>/.cortex/。
---
外置文件索引
| 文件 | 路径 | 加载时机 | |------|------|---------| | Investment Cost Review 内容模板 |references/inv_cost_review_template.md | 启动时 read_file(Section 结构 + 核对逻辑) |
| Investment Cost Review HTML 模板 | references/inv_cost_review_html_template.html | 启动时 read_file(固定 HTML/CSS/JS 三栏结构) |
| 投资成本变动计算规则 | references/cost_calculation_rules.md | 启动时 read_file — 定义期初成本、赎回/转出扣减、转入增加的取值规则与限制性条件 |
| MMF Reinvestment 取数期间规则 | references/mmf_reinvestment_period_rules.md | 启动时 read_file — 定义 MMF 红利转投的跨期取数逻辑(季度=M2+M3+M4,月度=M+1),含 Posn 验证和 TB 交叉验证。违反将导致 Reinvestment 与 TB Dividend Income 不匹配 |
| Distribution 五步框架 + JE 矩阵 | ../nav-review-report/references/distribution_framework.md | 按需:仅当存在 Distribution 时 read_file |
| iGowin 上传流程 | ../nav-review-report/references/igowin_upload.md | 上传阶段 read_file |
所有路径相对于 skill 目录:c:\Users\syy10787\.cortex\skills\nav-review-cost-report\
---
UI 与格式约束
HTML 报告为最终交付物,必须采用响应式、现代化双栏交互式设计,提供极佳的屏幕核对体验:1. 结构与排版布局(三栏固定布局)
- 最左侧 - 导航栏 (
.sidebar): - 固定在左侧 (
position: fixed; width: 240px; height: 100vh; background: #1E293B; color: #CBD5E1;)。 - 包含导航标题(基金名称和 NAV 日期)以及各 Section 的快速定位链接。
- 中间 - 复核结果主面板 (
.main): - 位于导航右侧、Supporting 面板左侧 (
margin-left: 240px; margin-right: 520px; padding: 24px 28px; min-width: 600px;)。 - 展示逐项复核表格、逻辑分析段落和 PASS/FAIL 结论。
- ⚠️ 强制规则:所有数据行必须与 Supporting 一一对应:报告中每一个 Section 的每一行数据(包括但不限于 Portfolio Overview、交易明细、Cost Movement、差异分析、TB 验证等),都必须添加
.interactive-row类,并设置data-supporting-id="[ID]"属性,指向右侧 Supporting 面板中对应的证据项。不允许出现无 Supporting 映射的数据行。 - 最右侧 - Supporting 证据面板 (
.supporting-panel): - 宽度
520px,固定在右侧 (position: fixed; right: 0; top: 0; width: 520px; height: 100vh; overflow-y: auto; background: #F1F5F9; border-left: 2px solid #CBD5E1; padding: 16px;)。 - ⚠️ 默认可见:Supporting 面板始终展示在右侧,不需要点击按钮切换显示/隐藏。用户点击左侧数据行时,面板内容切换到对应证据。
- 集中展示复核截图。每个截图容器为
.supporting-item,并带有唯一的id(与主面板行的data-supporting-id对应)。 - 默认隐藏其他
.supporting-item,仅激活当前点击行对应的 Supporting 截图(.active类控制显示)。 - 面板顶部显示标题"📋 Supporting 证据",初始状态显示提示文字"← 点击左侧数据行查看对应证据"。
2. 交互体验设计 (Vanilla JS)
- 页面加载时默认激活并高亮第一个有 Supporting 的表格行,并在右侧面板展示对应的截图。
- 当用户点击主面板中的
.interactive-row时,通过 JS 移除先前的激活状态,在当前点击行添加.highlighted(浅蓝色背景与蓝色边框),并在右侧面板中平滑滚动并展示对应的 Supporting 截图 (scrollIntoView({ behavior: 'smooth' }))。 - ⚠️ 全行覆盖强制规则:报告中所有 Section(计算规则除外)的每一行数据都必须是
.interactive-row,包括但不限于: - Section: 期初平均成本表 → 映射到上期 IPA 截图
- Section: 交易明细表(赎回/转出/转入各行及合计行)→ 映射到对应确认书截图
- Section: Portfolio Overview 各行及合计行 → 映射到 IPA 对比截图
- Section: Cost Movement 各行及合计行 → 映射到该持仓的完整证据
- Section: 差异分析各行 → 映射到 Realized G/L + Purchases and Sales 截图
- Section: TB 交叉验证各行 → 映射到 Trial Balance 截图
- 双击图片 → 全屏 Lightbox 预览(含 −/倍数/+/Reset 工具栏 + 滚轮缩放 + 拖拽平移):Supporting 面板中的截图默认以正常尺寸内嵌显示,hover 时光标变为
zoom-in提示可放大。双击任一截图弹出全屏半透明遮罩 lightbox(position: fixed; inset: 0; background: rgba(0,0,0,0.88)),图片居中展示。遮罩顶部固定一个工具栏(<div class="lightbox-toolbar">),含−、当前倍数(如100%)、+、Reset、✕五个元素。按钮+/−以 0.25× 步进调节 lightbox 内图片大小(范围 0.5×~4×,初始 1×);Reset恢复 1× 并清空平移偏移;✕、点击遮罩空白处或按Esc关闭 lightbox。在遮罩内使用鼠标 滚轮缩放(步进 0.1×,以光标点为锚点保持视觉中心);图片放大超出可视区时,按住左键可上下左右拖拽平移(cursor 变为grab/grabbing)。
3. 颜色与视觉风格
- 主题背景:
#F8FAFC。 - 文字字体:
"Segoe UI", "Microsoft YaHei", Arial, sans-serif。 - 结论横幅:
- PASS:背景
#E2EFDA,文字#375623,左边框 5px 实线#548235 - FAIL:背景
#FCE4D6,文字#C65911,左边框 5px 实线#C65911 - 表格样式:边框
#E2E8F0,表头背景#F1F5F9加粗,奇偶行交替背景。
执行流程
Step 1:加载 profile + 数据 + 模板 + 计算规则
编写 Python 脚本加载数据:import os, json, sys BASE = r"<target_dir>" PROFILE = json.load(open(os.path.join(BASE, ".cortex", "nav_profile.json"), encoding="utf-8"))加载
<target_dir>/.cortex/nav_data/ 下对应的所有核对 JSON 数据。
⚠️ 必须先 read_file 加载 references/cost_calculation_rules.md,确保投资成本变动的计算逻辑严格遵守其中定义的规则(期初成本取上期IPA、赎回/转出扣减用上期平均成本×份额、转入用当期交易金额、当期Report仅作比对)。
⚠️ 必须 read_file 加载 references/mmf_reinvestment_period_rules.md,确保 MMF Reinvestment 取数期间正确:季度频率=M2+M3+M4(不含首月M1),月度频率=次月M+1(不含当月)。M1 reinvestment 属于上季度末月红利,计入当季是严重错误。
⚠️ 必须 read_file 加载 references/inv_cost_review_html_template.html,生成报告时严格遵循此 HTML 模板的三栏布局结构、CSS 样式(橙色主题色 #ff9800)、交互式 JS 脚本。模板定义了完整的 Section 结构(TB vs N4 → Continuity → Cost Movement → MMF Detail → Individual Investments → Distribution Impact → Fund Statements → Conclusion),不得省略或改变顺序。
Step 2:生成交互式 HTML 报告
使用 Python 脚本拼装 HTML 字符串,将数据填入 HTML/CSS 模板中: 1. 渲染左侧导航栏:显示 5 个 Section 链接:- Section 1: Portfolio Overview
- Section 2: MMF Cost Movement (若有)
- Section 3: Partnership / HF Cost Movement (若有)
- Section 4: Subscriptions In Advance (若有)
- Section 5: Conclusion
investment_movement.json 逐持仓循环生成表格行。行中插入 class="interactive-row" data-supporting-id="support-[持仓名]-overview"。
3. 渲染右侧 Supporting 面板:在 .supporting-panel 中,针对每个持仓的截图,渲染 <div id="support-[持仓名]" class="supporting-item">,内部包含来自 .cortex/screenshots/inv_cost/ 目录的图片。
⚠️ 截图内嵌强制规则:所有截图必须以 base64 data URI 内嵌到 HTML 文件中,禁止使用相对路径或外部文件引用。实现方式:在 Python 脚本中,对每个 PNG 文件读取二进制内容并转为 base64 编码,然后以 data:image/png;base64,<base64_string> 格式写入 <img src="...">。
import base64
def embed_image(base_dir, folder, filename):
"""读取截图文件并返回 base64 data URI"""
path = os.path.join(base_dir, ".cortex", "screenshots", folder, filename)
if os.path.exists(path):
with open(path, "rb") as f:
data = base64.b64encode(f.read()).decode("utf-8")
return f"data:image/png;base64,{data}"
return "" # 文件不存在时返回空字符串
def embed_all_pages(base_dir, folder, prefix):
"""读取所有匹配 prefix_pageN.png 的截图并返回 base64 data URI 列表"""
import os as _os
shot_dir = _os.path.join(base_dir, ".cortex", "screenshots", folder)
pages = []
if _os.path.isdir(shot_dir):
for f in sorted(_os.listdir(shot_dir)):
if f.startswith(prefix) and f.endswith(".png"):
pages.append((f, embed_image(base_dir, folder, f)))
return pages
HTML 中使用:<img src="{embed_image(BASE, 'inv_cost', 'prior_ipa.png')}" alt="Prior IPA">
> 理由:内嵌截图使 HTML 文件完全自包含,双击即可在任何位置打开查看,不依赖 .cortex/ 目录结构。上传至 iGowin 后也能直接在浏览器中完整展示,无需额外文件。
⚠️ 截图清晰度强制规则:使用 PyMuPDF (fitz) 渲染 PDF 页面为 PNG 时,必须使用 4x zoom(fitz.Matrix(4, 4)),确保文字和数字在浏览器中清晰可读。2x zoom 不足以保证小字清晰度。
⚠️ 多页截图完整展示强制规则:当源 PDF 被转换为多页 PNG 截图时(如 prior_ipa_page1.png、prior_ipa_page2.png),必须全部嵌入到 Supporting 面板中,禁止只展示第一页而遗漏后续页面。使用 embed_all_pages() 函数遍历所有匹配页面并逐一嵌入。这是确保 Supporting 证据完整性的硬性要求。
⚠️ Portfolio Overview(Section 1)Supporting 强制规则:Portfolio Overview 每行的 Supporting 面板必须同时展示两类截图,缺一不可:
- 期末数据取值截图:上期 IPA/SOFI(Prior Cost 列取值来源)+ 当期 IPA/SOFI(Current Cost 列取值来源)
- 期间发生额取数截图:Investment Movement Summary + 各项活动的原始支持文件截图(Capital Call Notice、Distribution Notice、MMF Holdings/Reinvestment 等)
.supporting-item,内部用分段标题(<div class="sup-section-label">标题</div>)将多张截图分组展示,每段标题清晰标注取数用途。所有 <img> 标签的 src 均使用 embed_image() 或 embed_all_pages() 返回的 base64 data URI。
> 分段标题样式:padding:6px 10px; font-size:11px; color:#475569; background:#F8FAFC; border-bottom:1px solid #E2E8F0; border-top:1px solid #E2E8F0; font-weight:600; margin-top:8px;(首段无 border-top)。
⚠️ 所有 Section 的 Supporting 均须完整展示多页截图:不仅限于 Portfolio Overview,其他 Section(交易明细、Cost Movement、差异分析、TB 验证等)的 Supporting 面板同样必须使用 embed_all_pages() 完整嵌入所有页面,不允许只展示第一页。
4. 嵌入交互式 JS 脚本(双击截图 → 全屏 Lightbox 预览 + 工具栏缩放 + 滚轮缩放 + 拖拽平移):
CSS 新增(在 <style> 末尾添加):
/ ── Supporting 图片悬停提示 ── /
.supporting-item img {{
cursor: zoom-in;
transition: opacity 0.15s;
}}
.supporting-item img:hover {{ opacity: 0.9; }}
/ ── Lightbox 全屏遮罩 ── /
.lightbox-overlay {{
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.88);
z-index: 9999;
overflow: hidden;
}}
.lightbox-overlay.active {{ display: block; }}
.lightbox-toolbar {{
position: fixed;
top: 12px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
box-shadow: 0 4px 14px rgba(0,0,0,0.35);
font-size: 12px;
color: #1E293B;
z-index: 10000;
user-select: none;
}}
.lightbox-btn {{
border: 1px solid #CBD5E1;
background: #F8FAFC;
color: #1E293B;
width: 30px;
height: 26px;
line-height: 1;
font-size: 14px;
border-radius: 4px;
cursor: pointer;
padding: 0;
}}
.lightbox-btn:hover {{ background: #E2E8F0; }}
.lightbox-btn.reset,
.lightbox-btn.close {{ width: auto; padding: 0 10px; font-size: 11px; }}
.lightbox-btn.close {{ border-color: #FCA5A5; color: #B91C1C; background: #FEF2F2; }}
.lightbox-level {{ min-width: 46px; text-align: center; font-variant-numeric: tabular-nums; }}
.lightbox-stage {{
position: absolute;
inset: 0;
overflow: auto;
cursor: grab;
display: flex;
align-items: center;
justify-content: center;
}}
.lightbox-stage.grabbing {{ cursor: grabbing; }}
.lightbox-stage img {{
max-width: 95vw;
max-height: 95vh;
transform-origin: center center;
user-select: none;
-webkit-user-drag: none;
transition: transform 0.05s linear;
box-shadow: 0 0 30px rgba(0,0,0,0.5);
}}
HTML 结构(在 </body> 之前注入一次即可):
<div class="lightbox-overlay" id="lightbox" aria-hidden="true">
<div class="lightbox-toolbar">
<button class="lightbox-btn" data-zoom="out" title="缩小">−</button>
<span class="lightbox-level">100%</span>
<button class="lightbox-btn" data-zoom="in" title="放大">+</button>
<button class="lightbox-btn reset" data-zoom="reset" title="重置">Reset</button>
<button class="lightbox-btn close" data-zoom="close" title="关闭">✕ 关闭</button>
</div>
<div class="lightbox-stage" id="lightbox-stage">
<img id="lightbox-img" src="" alt="Zoomed view" />
</div>
</div>
JS 完整脚本:
<script>
document.addEventListener('DOMContentLoaded', function() {
const rows = document.querySelectorAll('.interactive-row');
const items = document.querySelectorAll('.supporting-item');
const placeholder = document.getElementById('no-supporting-placeholder');
if (items.length > 0) {
activateSupporting(items[0].id);
const firstRow = document.querySelector('.interactive-row[data-supporting-id="' + items[0].id + '"]');
if (firstRow) firstRow.classList.add('highlighted');
}
rows.forEach(row => {
row.addEventListener('click', function() {
rows.forEach(r => r.classList.remove('highlighted'));
this.classList.add('highlighted');
activateSupporting(this.getAttribute('data-supporting-id'));
});
});
function activateSupporting(id) {
if (!id) return;
const targetItem = document.getElementById(id);
if (targetItem) {
if (placeholder) placeholder.style.display = 'none';
items.forEach(i => i.classList.remove('active'));
targetItem.classList.add('active');
targetItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
// ── 双击 Supporting 截图 → 全屏 Lightbox 预览 ──
const ZOOM_MIN = 0.5, ZOOM_MAX = 4.0, STEP_BTN = 0.25, STEP_WHEEL = 0.1;
const overlay = document.getElementById('lightbox');
const stage = document.getElementById('lightbox-stage');
const lbImg = document.getElementById('lightbox-img');
const lbLevel = overlay ? overlay.querySelector('.lightbox-level') : null;
let scale = 1;
const apply = () => {
if (lbImg) lbImg.style.transform = 'scale(' + scale + ')';
if (lbLevel) lbLevel.textContent = Math.round(scale * 100) + '%';
};
const setScale = v => { scale = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, v)); apply(); };
function openLightbox(src, alt) {
if (!overlay || !lbImg) return;
lbImg.src = src;
lbImg.alt = alt || 'Zoomed view';
scale = 1;
apply();
if (stage) { stage.scrollLeft = 0; stage.scrollTop = 0; }
overlay.classList.add('active');
overlay.setAttribute('aria-hidden', 'false');
}
function closeLightbox() {
if (!overlay) return;
overlay.classList.remove('active');
overlay.setAttribute('aria-hidden', 'true');
if (lbImg) lbImg.src = '';
}
document.querySelectorAll('.supporting-item img').forEach(img => {
img.addEventListener('dblclick', function(e) {
e.preventDefault();
e.stopPropagation();
openLightbox(this.src, this.alt);
});
});
if (overlay) {
overlay.querySelectorAll('.lightbox-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
const action = btn.getAttribute('data-zoom');
if (action === 'in') setScale(scale + STEP_BTN);
if (action === 'out') setScale(scale - STEP_BTN);
if (action === 'reset') {
if (stage) { stage.scrollLeft = 0; stage.scrollTop = 0; }
setScale(1);
}
if (action === 'close') closeLightbox();
});
});
overlay.addEventListener('click', e => {
if (e.target === overlay || e.target === stage) closeLightbox();
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && overlay.classList.contains('active')) closeLightbox();
});
stage.addEventListener('wheel', e => {
if (!overlay.classList.contains('active')) return;
e.preventDefault();
const rect = stage.getBoundingClientRect();
const px = e.clientX - rect.left + stage.scrollLeft;
const py = e.clientY - rect.top + stage.scrollTop;
const oldScale = scale;
setScale(scale + (e.deltaY < 0 ? STEP_WHEEL : -STEP_WHEEL));
const ratio = scale / oldScale;
stage.scrollLeft = px * ratio - (e.clientX - rect.left);
stage.scrollTop = py * ratio - (e.clientY - rect.top);
}, { passive: false });
let dragging = false, sx = 0, sy = 0, ssl = 0, sst = 0;
stage.addEventListener('mousedown', e => {
if (e.button !== 0) return;
if (e.target.closest('.lightbox-toolbar')) return;
dragging = true;
sx = e.clientX; sy = e.clientY;
ssl = stage.scrollLeft; sst = stage.scrollTop;
stage.classList.add('grabbing');
e.preventDefault();
});
document.addEventListener('mousemove', e => {
if (!dragging) return;
stage.scrollLeft = ssl - (e.clientX - sx);
stage.scrollTop = sst - (e.clientY - sy);
});
document.addEventListener('mouseup', () => {
if (!dragging) return;
dragging = false;
stage.classList.remove('grabbing');
});
}
});
</script>
5. 将完整 HTML 内容写入 <target_dir>/Investment Cost Review - [基金名] [YYYYMMDD].html。
Step 3:产出 verified_cost.json
报告生成时,必须同时将逐持仓的核对结果输出至<target_dir>/.cortex/nav_data/verified_cost.json。
格式必须与之前保持一致,以便 downstream UGL Review 消费:
{
"generated_by": "nav-review-cost-report",
"nav_date": "YYYY-MM-DD",
"investments": [
{
"name": "持仓名",
"type": "Partnership Interest / Money Market Instrument / Hedge Fund",
"verified_cost": 12345.67,
"source": "IPA / SOFI / CAS"
}
]
}
Step 4:上传 iGowin
read_file("../nav-review-report/references/igowin_upload.md") 获取上传流程,然后执行 6 步(上传 HTML 文件本身,不再做 PDF 转换):
1. get_nav_packs(fund_name) → 获取 applicationNo
2. get_subject_names(application_no) → 找到 Investment Cost 类 subject_name
3. get_upload_url(file_name, content_type="text/html") → 拿到 uploadUrl + objectName
4. ⚠️ 上传前确认关卡(强制):调用 ask_questions 弹窗向用户展示完整上传摘要并取得明确确认。摘要必须包含:基金名、NAV 日期、流程单号、目标 subject、文件名、文件大小(MB)、报告核对结论(PASS / FAIL,及差异金额)。选项至少包含「确认上传」「跳过该报告」「全部取消」。未取得确认前,不得执行 Step 5 的 PUT 上传或 Step 6 的 save_check_result。即便此前在主 skill 入口已确认整体计划,此处仍须再次确认(理由见 igowin_upload.md 的"强制原则"段)。
5. PowerShell 上传(仅在 Step 4 通过后执行):Invoke-WebRequest -UseBasicParsing -Method Put -Uri $uploadUrl -InFile $htmlPath -ContentType "text/html"
6. save_check_result(object_name, file_name, subject_name, application_no)(仅在 Step 5 PUT 返回 200 OK 后执行)
---
完成检查表
- [ ] HTML 交互式报告已落盘,能双击打开且最左侧包含清晰导航栏
- [ ] 三栏固定布局:左侧导航(240px) + 中间数据面板 + 右侧 Supporting 面板(520px,默认可见)
- [ ] 所有数据行与 Supporting 一一对应:报告中每个 Section 的每一行数据都有
.interactive-row+data-supporting-id,点击后右侧面板切换到对应证据 - [ ] 中间复核行与右侧 Supporting 截图具备联动点击高亮、平滑滚动体验
- [ ] 双击 Supporting 截图打开全屏 Lightbox 预览:遮罩顶部含
−/ 倍数 /+/Reset/✕工具栏(按钮 0.25× 步进、范围 0.5×~4×);遮罩内支持鼠标滚轮缩放(步进 0.1×、以光标点为锚点)与上下左右拖拽平移;点击遮罩空白处、✕或按Esc关闭 - [ ] Portfolio Overview(Section 1)每行的 Supporting 面板同时包含期末数据取值截图和期间发生额取数截图,分段标题清晰标注取数用途
- [ ] 多页截图完整展示:所有 Supporting 项中的多页 PNG 截图(如
prior_ipa_page1.png+prior_ipa_page2.png)全部嵌入,不允许只展示第一页 - [ ]
verified_cost.json已正确写入且字段完整 - [ ] MMF 期末 Cost 收口口径校验:MMF cost 列出"Closing = statement holdings + next-period reinvestment"拆分;Subscription 行使用凭证原值(如 +3,000,000.00);公式末尾含
+ Next-period Reinvestment Accrual行;最终 Closing Cost = IPA/SOFI/TB 一致;严禁通过反推 Subscription 数字(如 +2,984,177.47)来强行平账 - [ ] Cost Review 总 Cost 与 TB 闭合:
verified_cost.json.total_verified_cost等于 TBInvestments Long, Equity, Cost+Investments Long, Fund, Cost+Investments Long, PE, Cost三项 ClosingBalance 之和;MMF 行的 verified_cost 与 UGL Review 中 Book Cost 列保持完全一致 - [ ] MMF 期末 Cost 收口口径校验:MMF cost 列出"Closing = statement holdings + next-period reinvestment"拆分;Subscription 行使用凭证原值(如 +3,000,000.00);公式末尾含
+ Next-period Reinvestment Accrual行;最终 Closing Cost = IPA/SOFI/TB 一致;严禁通过反推 Subscription 数字(如 +2,984,177.47)来强行平账 - [ ] Cost Review 总 Cost 与 TB 闭合:
verified_cost.json.total_verified_cost等于 TBInvestments Long, Equity, Cost+Investments Long, Fund, Cost+Investments Long, PE, Cost三项 ClosingBalance 之和;MMF 行的 verified_cost 与 UGL Review 中 Book Cost 列保持完全一致 - [ ] 所有截图以 base64 data URI 内嵌到 HTML 文件中,HTML 文件完全自包含,双击即可在任何位置打开查看;截图渲染使用 4x zoom 确保清晰度
- [ ] 上传前确认关卡已执行:在 PUT 上传前已用
ask_questions向用户展示上传摘要(基金名 / NAV 日期 / 流程单号 / subject / 文件名 / 大小 / 核对结论)并取得明确「确认上传」回应;如用户选择跳过则未调用save_check_result,并在交付总结中标注"用户选择不上传" - [ ] iGowin applicationNo 匹配当期,并且 save_check_result 返回成功(HTML 文件已成功上传)
- [ ] 成本计算规则已遵守:期初成本取上期IPA、赎回/转出扣减用上期平均成本×份额、转入用当期交易金额、当期Report数据仅作比对对象
- [ ] 平均成本法 vs Tax Lot 法差异已标注:若差异 > 1 USD,已在报告中明确标注差异金额和原因
截图质量与标注规则
截图清晰度要求(最高质量)
清晰度为第一优先级。所有内嵌 HTML 的截图必须确保数字、文字完全清晰可读。 | 参数 | 值 | 说明 | |------|-----|------| | DPI | 150 | HTML 内嵌 JPEG 渲染分辨率 | | JPEG quality | 85 | 高品质,文字无锯齿 | | 红框线宽 | 3px | PIL ImageDraw,高清图上清晰可见 | | 落盘 PNG | 300 DPI | 独立 PNG 文件最高清晰度 | | 宽表 zoom | 4× | IPA 等宽表裁剪用 4x zoom | 不设单文件体积上限。如需控制体积,通过clip_box 裁剪区域(只截关键数据行),禁止降低 DPI 或 quality。
import fitz, io, base64
from PIL import Image, ImageDraw
def screenshot_for_embed(pdf_path, page_num, red_rects=None, clip_box=None, dpi=150, quality=85):
"""高清 JPEG 截图用于 HTML 内嵌"""
doc = fitz.open(pdf_path)
page = doc[page_num]
mat = fitz.Matrix(dpi/72, dpi/72)
pix = page.get_pixmap(matrix=mat, clip=fitz.Rect(*clip_box) if clip_box else None)
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
if red_rects:
draw = ImageDraw.Draw(img)
scale = dpi / 72.0
ox, oy = (clip_box[0], clip_box[1]) if clip_box else (0, 0)
for box in red_rects:
draw.rectangle([(box[0]-ox)scale-1, (box[1]-oy)scale-1,
(box[2]-ox)scale+1, (box[3]-oy)scale+1], outline='red', width=3)
buf = io.BytesIO()
img.save(buf, format='JPEG', quality=quality, optimize=True)
doc.close()
return f"data:image/jpeg;base64,{base64.b64encode(buf.getvalue()).decode()}"
截图红框标注规则
所有 Supporting 截图必须用红框高亮关键数字: 1. 定位方法:使用page.get_text("words") 精确提取数字坐标
2. 标注内容:仅标注与当前核对项直接相关的金额(如 proceeds、NAV/share、quantity、cost basis)
3. 红框样式:红色描边 3px(PIL),无填充,略扩展 3px 边距
4. 禁止:圈整行/大区域、不透明填充、标注非关键信息
数据层级标注强制规则 (Data Hierarchy)
报告中必须严格区分 Reference(被核对对象) 与 Supporting(第三方证据): | Category | Documents | 标签色 | |----------|-----------|--------| | Reference(被核对) | TB, IPA, Realized GL Report, P&S Report, Change in UGL, Maples NAV Per Share, DGL | 紫色#f3e5f5 |
| Supporting(第三方证据) | Contract Note, SS&C/GP Investor Statement, Bank Statement, Redemption Confirmation | 绿色 #e8f5e9 |
强制规则:
- FS Pack 内部报告(TB/IPA/Realized GL/P&S/DGL 等)永远不能标记为 Supporting——它们是被核对对象,不能自证
- Cost Movement 的验证逻辑必须是:Contract Note(第三方)确认 proceeds → Bank Statement(第三方)确认到账 → 反推 cost basis → 与 TB Credits 比对
- PASS 结论必须基于 Supporting 文件验证 Reference 数字
关键注意事项
1. 本 skill 只负责 Investment Cost Review,不涉及 Cash 或 UGL。 2. HTML 即最终交付物:不再生成 PDF,所有上传与展示统一基于 HTML 文件。 3. 所有截图必须以 base64 data URI 内嵌到 HTML 文件中,使 HTML 完全自包含,不依赖外部文件。禁止使用相对路径或外部文件引用。 4. 不要使用任何第三方繁重 JS 库,交互动作必须全部由精简的原生 Vanilla JS 编写。 5. verified_cost.json 是下游 UGL Review 的强制依赖,必须产出且字段完整。 6. Distribution 必走五步框架:只有 Recallable 减少 Cost。 7. MMF 校验公式硬性:Cost = MV = (A) End-month Closing MV + (B) Next-month Reinvestment。- 🔒 期末 Cost 收口口径(与 IPA/SOFI/TB 对齐,强制):
- MMF 期末 cost = 当期末 statement Holdings + 次期 reinvestment(已 ex-div 但未 settle)。这是 IPA/SOFI/TB 三处一致采用的 ex-dividend date 会计口径。
- 示例(Q4-2025):12-31 statement 持有 3,000,000 units × NAV 1.00 = 3,000,000.00 (Holdings);2026-01-05 settle 的 5,211.36 单位转投 dividend (ex-div 02-Jan-2026) → 期末 cost = 3,005,211.36 ✓ 与 IPA/SOFI/TB 完全闭合。
- Subscription 行规则:用凭证原值(statement 上的 Settlement Amount,如 3,000,000.00),不能反推。如果发现等式不平衡,禁止通过反推 Subscription 数字来强行平账(这会出现像 +2,984,177.47 这类无凭证支撑的"假数字")。
- 公式末尾必须有"+ Next-period Reinvestment Accrual"行,用 ex-div date 标注,让等式以 IPA cost 收口:
Opening + In-period Reinvestment − Redemption + Subscription + Next-period Reinvestment Accrual = Closing Cost (=IPA/SOFI/TB)。 - TB 双轨核对:DividendsReceivable 期末 = next-period reinvestment 金额(如 5,211.36)。这与 MMF cost 内含的同一笔金额不是双重计算,而是会计准则下的"应计 + 待 settle"双轨记录,settle 后会冲销。
- 常见错误(已踩坑):把 Closing Cost 改成 statement holdings 单值(如 3,000,000.00 而非 3,005,211.36)会导致总 Investment Cost 与 TB 期末 Investments Long Fund Cost 出现 5,211.36 缺口。绝不这样改。
- 跨期取数范围(强制):quarterly 报告 Reinvestment 仅取 M2 + M3 + M4 三期(不含季度首月 M1,因为 M1 reinvestment 属于上一季度末持仓的尾巴);monthly 报告 Reinvestment 仅取 M+1(次月) 一期(不含当月)。任何包含首月 / 当月 reinvestment 的合计都视为错误。
- Supporting 逐月对应(强制):MMF Reinvestment Detail 表格每月行的 Supporting 只展示该月的
mmf_holdings_<month>.png+mmf_reinvestment_<month>.png,不允许把整季所有月份的截图都展示在每行下面。汇总行可聚合所有计入月份(首月除外)。每月行使用独立data-supporting-id(如support-mmf-2025-11),汇总行使用support-mmf-summary。 - Cost Review 与 UGL Review 一致性(强制):
verified_cost.json中 MMF 的verified_cost必须与本期末 cost 收口口径一致(即 statement holdings + next-period reinvestment)。UGL Review 直接消费此值,严禁 Cost Review 与 UGL Review 在 MMF cost 上出现两个不同的口径。如发现两份报告对同一持仓 cost 给出不同值,必须立即停止并按 IPA/SOFI/TB 口径反查纠正 - 示例(Q4-2025):12-31 statement 持有 3,000,000 units × NAV 1.00 = 3,000,000.00 (Holdings);2026-01-05 settle 的 5,211.36 单位转投 dividend (ex-div 02-Jan-2026) → 期末 cost = 3,005,211.36 ✓ 与 IPA/SOFI/TB 完全闭合。
- Subscription 行规则:用凭证原值(statement 上的 Settlement Amount,如 3,000,000.00),不能反推。如果发现等式不平衡,禁止通过反推 Subscription 数字来强行平账(这会出现像 +2,984,177.47 这类无凭证支撑的"假数字")。
- 公式末尾必须有"+ Next-period Reinvestment Accrual"行,用 ex-div date 标注,让等式以 IPA cost 收口:
Opening + In-period Reinvestment − Redemption + Subscription + Next-period Reinvestment Accrual = Closing Cost (=IPA/SOFI/TB)。 - TB 双轨核对:DividendsReceivable 期末 = next-period reinvestment 金额(如 5,211.36)。这与 MMF cost 内含的同一笔金额不是双重计算,而是会计准则下的"应计 + 待 settle"双轨记录,settle 后会冲销。
- 常见错误(已踩坑):把 Closing Cost 改成 statement holdings 单值(如 3,000,000.00 而非 3,005,211.36)会导致总 Investment Cost 与 TB 期末 Investments Long Fund Cost 出现 5,211.36 缺口。绝不这样改。
- 跨期取数范围(强制):quarterly 报告 Reinvestment 仅取 M2 + M3 + M4 三期(不含季度首月 M1,因为 M1 reinvestment 属于上一季度末持仓的尾巴);monthly 报告 Reinvestment 仅取 M+1(次月) 一期(不含当月)。任何包含首月 / 当月 reinvestment 的合计都视为错误。
- Supporting 逐月对应(强制):MMF Reinvestment Detail 表格每月行的 Supporting 只展示该月的
mmf_holdings_<month>.png+mmf_reinvestment_<month>.png,不允许把整季所有月份的截图都展示在每行下面。汇总行可聚合所有计入月份(首月除外)。每月行使用独立data-supporting-id(如support-mmf-2025-11),汇总行使用support-mmf-summary。 - Cost Review 与 UGL Review 一致性(强制):
verified_cost.json中 MMF 的verified_cost必须与本期末 cost 收口口径一致(即 statement holdings + next-period reinvestment)。UGL Review 直接消费此值,严禁 Cost Review 与 UGL Review 在 MMF cost 上出现两个不同的口径。如发现两份报告对同一持仓 cost 给出不同值,必须立即停止并按 IPA/SOFI/TB 口径反查纠正。
- 当交易通过 Nominee 账户执行(赎回 Redemption、认购 Subscription、Stock Deposit 等),必须从 monthlyStatement 的"交易明細 Transaction Details"页获取交易确认截图
- 截图来源:Ark/Noah monthlyStatement → 找到"交易明細 Transaction Details"标题的页面(通常紧随 Holdings 页之后)
- 红框 highlight 目标:定位该交易行的 Transaction Amount(交易金額) 列数字,用红框标注。例如:赎回 (1,500,000.00)、Stock Deposit 各 Series 金额 (418,393.62) / (445,221.37) / (266,957.79) 等
- 截图文件名:
nominee_txn_details_<month>.png,按交易发生月份命名 - 使用场景:
- MMF Redemption → highlight 赎回金额
- 新基金 Subscription(通过 Nominee 结算)→ highlight Stock Deposit / Subscription Settlement 金额
- Fund Withdrawal(向标的基金出款)→ highlight 提款金额
- 定位方法:使用
page.get_text("words")搜索交易金额数字(如 "1,500,000" 或 "418,393.62"),取其坐标画红框 - 与 Cashflow Statement 的区别:Cashflow Statement(现金账项)显示净现金流;Transaction Details 显示具体交易明细(含份额、价格、金额),后者提供更完整的交易确认证据
references/cost_calculation_rules.md,核心规则如下:
- 期初成本:取自上期 IPA 的 Cost 列(不使用 TB 或其他来源)
- 赎回/转出扣减成本 = 上期平均成本(上期 IPA Cost ÷ Shares)× 交易份额
- 转入成本 = 当期交易金额(Confirmation of Transfer / Liquidity Event 中记录的金额,不使用平均成本 × 份额)
- 当期 Report 数据仅作比对:当期 IPA / TB / Purchases and Sales 等不能作为计算依据,只能作为最终比对对象
- Non-recallable Distribution 不影响成本:本金返还和收益分配均不影响投资本金
- 平均成本法 vs Tax Lot 法差异:系统按 tax lot 实际成本扣减,当平均成本 ≠ 初始认购价时会产生差异,需在报告中标注原因,结论仍为 PASS(附说明)
.interactive-row 并设置 data-supporting-id,点击后右侧 Supporting 面板切换到对应证据。不允许出现无 Supporting 映射的数据行。
11. ⚠️ Supporting 面板默认可见:右侧 Supporting 面板始终展示(position: fixed; right: 0),不需要点击按钮切换显示/隐藏。中间数据面板需预留右侧空间(margin-right: 520px)。
12. ⚠️ 多页截图必须完整展示:当源 PDF 被转换为多页 PNG 时(如 xxx_page1.png + xxx_page2.png),必须使用 embed_all_pages() 全部嵌入到 Supporting 面板中,禁止只展示第一页而遗漏后续页面。
13. 截图清晰度必须使用 4x zoom 渲染:PyMuPDF 渲染 PDF 页面时使用 fitz.Matrix(4, 4)(最高清晰度),2x zoom 不足以保证小字清晰度。双击 Supporting 截图打开 Lightbox 预览(顶部 −/倍数/+/Reset/✕ 工具栏 + 滚轮缩放 + 拖拽平移)让审阅人可进一步查看细节。
14. iGowin 上传前必须经过 ask_questions 强制确认:PUT 上传与 save_check_result 都是不可逆操作,因此在调用 get_upload_url 之后、PUT 之前必须用 ask_questions 弹窗向用户确认(含基金名 / NAV 日期 / 流程单号 / subject / 文件名 / 文件大小 / 核对结论)。即使本 skill 被 nav-review 主 skill 调用、用户已在主入口确认过整体计划,此处仍须再次确认;本 skill 单独运行时同样适用。用户选择「跳过」则不上传该报告,「全部取消」则终止。不允许通过任何"批处理静默上传"绕过此关卡。