关于 asset-management
asset-management 是面向个人投资者的命令行工具,用于跟踪以美元计价的股票和 ETF 持仓。它从交易记录计算可审计数据,将 AI 叙述与计算隔离,并为建议标注对应规则。
主要功能
- 01侧重回撤的投资组合简报
- 02基于规则的买卖建议
- 03受约束的可选 AI 叙述
- 04与基准比较回测
- 05导入 Ghostfolio CSV 和 JSON
- 06离线价格缓存
适用场景
- 01查看投资组合回撤与恢复情况
- 02准备附有规则说明的再平衡建议
- 03将资产配置与基准进行测试
- 04从 Ghostfolio 导入持仓
MCP 相关
项目包含面向 Claude Desktop 的只读 MCP 插件,可基于同一验证核心进行投资组合对话。插件可使用本地价格缓存。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
我们还没有获取到这个服务的安装包或远程地址,请按项目 README 中的说明安装。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
资产管理
简而言之: uvx --from git+https://github.com/disin7c9/asset-management asset-management --demo — 基于内置示例投资组合生成一份以回撤为先的投资组合简报,只需一条命令,无需设置。仅支持美元计价、仅做多的股票/ETF;你需要自行保留交易记录。
跟踪个人股票/ETF 投资组合,并获取可审计的建议。所有数字均由 Python 计算;可选的 AI 只负责叙述——它不会收到这些数字,而且它引用的每个数值都会从计算结果中替换得出。
🔢 数字防护栏
许多 AI 金融工具会让模型生成数字。在这里,模型只能放置 {{token}} 占位符:确定性渲染器会从经过验证的核心中替换出数值,并且如果模型自行输入了任何数字,就会拒绝整段叙述——涵盖任何 Unicode 数字类别,因此 ½ 和 3.2 一样会触发拒绝:
the model wrote │ the reader gets
─────────────────────────────────────────────┼─────────────────────────────────────────────
"Your deepest stretch fell │ "Your deepest stretch fell -9.84% from
{{max_drawdown}} from its peak, and an │ its peak, and an ulcer index of 2.38%
ulcer index of {{ulcer}} says the ride │ says the ride stayed shallow."
stayed shallow." │ ✔ every figure substituted from the
│ validated core
─────────────────────────────────────────────┼─────────────────────────────────────────────
"Your portfolio fell 12% but recovered │ (no summary at all)
nicely — don't worry." │ ✘ REFUSED — one model-typed numeral voids
│ the entire note; the brief prints
│ without it
亲自运行两端流程——它会驱动真实的生产防护栏,无需 API 密钥:uv run python scripts/demo_fence.py
🎁 你将获得
- 一份针对真实持仓的回撤优先简报——你从峰值下跌了多少、在水下持续了多久、收复了多少——每项经抽样得出的风险统计数据都附有 Bootstrap 置信区间(收益是会计恒等式,因此如实地不附置信区间)。
- 确定性的买入/卖出建议,帮助你朝所选目标配置调整;每条建议都配有生成它的明确规则——你学会规则,而不是盲目信任机器人。
- 可选的、带防护栏的 AI 叙述,以及一个只读的 Claude Desktop 插件(“与你的投资组合对话”),二者使用同一个经过验证的核心。
持仓是根据只追加的交易记录(每行包含日期、股票代码、操作、数量、价格和费用)推导得出,而非存储——因此相同输入始终会产生相同输出。价格通过备用数据源获取(主要使用 Yahoo Finance;次要使用 Tiingo,需免费的 API 密钥),并采用磁盘缓存;每个显示的数字都可追溯到其来源,而无法如实计算的数值(时间窗口太短、没有真实解)会显示 n/a,而不是编造数字。股票拆分会自动调整(股数会与经过拆分调整的历史价格相匹配),因此持有期间发生的拆分不会扭曲收益。如果拆分数据源不可用,不匹配检测器会捕捉到 2:1 或更大的比例。
📑 目录
🚀 从这里开始
⚡ 60 秒上手(内置模拟投资组合,无需设置)
uvx --from git+https://github.com/disin7c9/asset-management asset-management --demo
也可以从克隆的仓库运行:uv sync && uv run python -m app --demo(需要 Python 3.12 和 uv)。
完整体验(仍使用内置示例组合,可在线体验约 1 分钟)——下面的两条命令将完整展示该工具的特色功能:先提出一个预设目标,再用已知的 60-40 基准进行验证,并给出基于留出近期窗口的判定;同时列出按阈值区间生成的再平衡建议,每条建议都注明所依据的规则,并附上各基金的具体信息:
uvx --from git+https://github.com/disin7c9/asset-management asset-management --demo --allocate moderate --allocate-out demo_target.csv
uvx --from git+https://github.com/disin7c9/asset-management asset-management --demo --backtest --target demo_target.csv --benchmark 60-40 --rebalance bands --metadata
注意它不会怎么说:当证据不足时,判定会写成“与 60-40 相比,回撤差异不明确;配对自助法未能确认这一差距”之类的表述,而绝不会说“跑赢基准”。当输出值得信任时,下面的四个步骤会引导你将它用于自己的资金。
💵 用它管理自己的资金 — 四个步骤
步骤 1 — 你的账本(输入)
所有内容都源自一份交易日志。CSV 格式采用 Ghostfolio 自有的 CSV 导入架构(因此,你在这里维护的账本也可直接导入 Ghostfolio)——列为 Date, Code, DataSource, Currency, Price, Quantity, Action, Fee, Note。Action 可取 buy、sell、dividend、fee、interest、deposit、withdraw 之一。现金流(deposit/withdraw)使用 CASH 代码,并将金额填入 Price 列。此工具仅支持 USD(仅做多股票/ETF):Currency 必须为 USD——非 USD 行会被拒绝,并明确指出行号,不会悄悄按 1:1 记为美元。数值列中的空单元格按零处理。非 ISO 格式的日期会被拒绝,并显示清晰的错误信息。UTF-8 BOM 可正常处理。捆绑示例(data/sample_data/transactions.csv)展示了所有行类型。
已经在使用 Ghostfolio? 直接将输入指向 Ghostfolio 的 JSON 导出文件(Portfolio → Activities → ⋯ → Export)——加载器会自动识别并直接读取,无需转换步骤。它会读取活动(股息现金额 = quantity × unitPrice;Ghostfolio 的 UTC 时间戳会转换回你的本地日期),并跳过非 USD、加密货币和非证券(ITEM/LIABILITY)行,同时发出警告(目前仅支持 USD 计价、仅做多的股票/ETF)。没有原生 Ghostfolio 账户的券商用户,可以先运行社区转换器,例如 Export-To-Ghostfolio(支持将 26 家券商的数据转换为 Ghostfolio JSON)。
uv run python -m app --book your.csv # or ghostfolio-export.json — auto-detected
uv run python -m app --book your.csv --dry-run # preview an import BEFORE trusting it:
# format, events, skipped rows with reasons,
# derived holdings — fetches nothing
只需设置一次默认值:在仓库根目录的 .env 文件中设置(该文件已被 git 忽略)——ASSET_BOOK=path/to/your.csv(还可选设置 ASSET_TARGET=path/to/target.csv)——之后直接运行 python -m app 就能生成简报。显式传入的标志始终优先。没有内置的静默默认值:若未指定 --book 或 ASSET_BOOK,依赖账本的操作会报错,直接运行则会显示提示——捆绑示例需通过 --demo 显式启用,不会被默认使用。
步骤 2 — 预热缓存(一次即可)
核心采用离线优先设计:价格来自磁盘缓存,联网运行时会刷新缓存。全新克隆后,先填充一次缓存:
uv run python -m app --book your.csv --warm # your tickers + the benchmark references
uv run python -m app --book your.csv --warm full # + the ~375-ETF discovery universe (slow) —
# only needed for offline --discover
之后,--offline 运行和 Claude Desktop 插件都完全使用此缓存(可通过 --cache-dir / ASSET_CACHE_DIR 指定其他位置)。
可选但推荐: 在 .env 中添加免费的 Tiingo API 密钥——TIINGO_API_KEY=...——以启用第二个价格来源。Yahoo Finance 有时会限制短时间内的大量请求;添加密钥后,获取价格失败时会改用 Tiingo,而不是报告缺少股票代码。未添加密钥时,工具只会从 Yahoo 获取价格。
步骤 3 — 选择目标
大多数决策功能(--rebalance、--backtest、保留样本检查)都围绕目标配置展开——这是一个由 你 持有并编辑的小型 CSV 文件(Ticker,Weight)。根据你的起点,有三种获取方式:
- 你已经持有一个投资组合 →
--dump-target target.csv会写入你当前的配置;编辑它,让配置趋向你想要的组合。(目标是一个完整规格——有关退出语义,请参阅再平衡模式。) - 从风险取向开始 →
--onboard会在终端中提出三个简单问题(投资期限 / 对亏损的反应 / 现金缓冲),并生成匹配的预设;或者使用--allocate conservative|moderate|aggressive自行选择。两者都可在没有任何投资组合的情况下运行——在你进行第一笔交易之前,每个角色都会从精选的标的库中填入标的。这些是战略性的角色分类模板(按风险取向划分股票 / 债券 / 分散投资标的,并在每个类别中采用核心-卫星配置):你已经持有的角色会对应到你在该角色中持有的最大基金;缺少的角色则会从精选标的库中选取合适的默认 ETF 来填充。 - 自行探索并构建配置 →
--discover会为配置不足的角色推荐经过筛选的 ETF;--screen QQQM,SCHD会根据你的投资组合评估任意候选标的(成本、流动性、成立时间、重叠程度,以及它是否确实分散了你最严重的回撤)。然后手动编写 CSV 文件。
还有两种更简单的重新加权规则——--allocate equal_weight(1/N;稳健的基准)和 --allocate inverse_vol(让每项持仓贡献大致相同的风险,而非相同金额;使用 --allocate-cap 0.30 限制任一权重的上限)。我们刻意不包含基于收益预测的优化器(均值-方差 / 最大夏普比率)——它们容易过拟合,因此会留到后续版本,并设置一道优势门槛。
跟随目标之前,请先验证它:
uv run python -m app --backtest --target target.csv --benchmark 60-40 # or all-weather | permanent
模拟你的目标配置和基准配置的共同历史表现(名义金额 $10k,优先考察回撤的区间,并附置信区间),并加入一项保留的近期窗口判定:shallower(回撤较浅)/ deeper(回撤较深)/ inconclusive(无法判定)/ insufficient(数据不足)——绝不会称为“跑赢”。判定依据是Ulcer 指数(整个窗口内的回撤损失——包括回撤深度和持续时间);同时要求 CDaR(最差尾部区间的平均值)不与判定相矛盾,并通过配对自助法置信区间确认;最大回撤仅作为背景信息报告,不作为判定依据——在较短的历史记录中,单次最严重事件太过嘈杂,不足以据此做出任何判断。当无法判定时,会返回 inconclusive,并准确说明是哪道门槛阻止了判定。
提议、模拟和执行是强制分开的步骤:--allocate/--onboard 只能提议(并可选择写入文件),且不能在同一条命令中与 --backtest/--rebalance 组合使用。你先检查文件,再模拟配置,然后请求生成订单——每一步都使用单独的命令。策略绝不会悄然变成交易。
Step 4 — 每周简报
一个命令即可完成例行检查——状态简报、触及区间时的建议操作、回测与基准判定,以及基金信息:
uv run python -m app --book your.csv --backtest --target target.csv --benchmark permanent --rebalance bands --metadata # + --narrate --save --send to taste
- 选择与你的投资姿态最接近的
--benchmark参考基准(60-40/all-weather/permanent)。 - 只有某项持仓偏离区间(“5/25 规则”)时,
bands才会触发,并重新平衡整个投资组合——如果这周你要存入资金且不想卖出,请改用--rebalance cash_flow_only --new-cash 500(仅买入低于目标配置的资产;更利于税务规划)。 - 面板可以组合使用——每个标志都会添加一个面板,并且可以叠加使用(参见参考)。
同一份报告通过一次构建提供三种输出:stdout 上的纯文本(始终提供)、保存到 reports/<asof>.md 的 Markdown(--save),以及 HTML 邮件(--send;在 .env 中设置 RESEND_API_KEY 和 REPORT_TO)。输出目标失败不会导致运行崩溃——简报仍会打印,并记录失败信息——但如果你指定的输出目标失败,进程会以非零状态退出,以便调度程序发现问题。同一规则也适用于无法完成工作的运行:如果没有任何持仓能获取价格(数据提供方无法访问、离线时缓存为空),简报仍会根据你的账本打印,但退出状态为非零,并用一行文字说明如何修复;只有一个股票代码缺失时,运行状态仍为 partial,并以 0 退出。每周一的简报只需将此命令加入 cron:
# 08:00 every Monday — email the brief and archive the markdown.
# cron's PATH is minimal and won't find uv — point PATH at it (see `which uv`):
PATH=/home/you/.local/bin:/usr/bin:/bin
0 8 * * 1 cd /path/to/asset-management && uv run python -m app --book your.csv --backtest --target target.csv --benchmark permanent --rebalance bands --metadata --save --send >> reports/cron.log 2>&1
(cron 只会在机器当时处于开机状态时触发——在 WSL 上,可通过 Windows 任务计划程序运行 wsl.exe,确保任务始终触发。)该工具读取的所有键都列在配置中。
在没有 WSL 的 Windows 上,以上操作均可照常进行(uv 会自带 Python——运行 irm https://astral.sh/uv/install.ps1 | iex 安装,然后克隆项目、编写 .env 并运行)。只有调度方式不同:
# the Windows counterpart of the cron line above, from your clone.
# -StartWhenAvailable: a Monday the machine was asleep runs on wake instead of skipping.
Register-ScheduledTask -TaskName "asset-brief" `
-Trigger (New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 8:00am) `
-Settings (New-ScheduledTaskSettingsSet -StartWhenAvailable) `
-Action (New-ScheduledTaskAction -Execute "$env:USERPROFILE\.local\bin\uv.exe" `
-Argument "run python -m app --book your.csv --backtest --target target.csv --benchmark permanent --rebalance bands --metadata --save --send" `
-WorkingDirectory "C:\path\to\asset-management")
🔍 为什么你可以信任这些数字
🚫 不构成财务建议——从输出形式上体现,而非只在细则中声明
输出的形式使其成为描述而非建议:
- 每条建议都会对应触发它的明确规则(“5/25 区间被突破——减持 X”),绝不会只说“买入 X”;
- 每项采样风险指标都附有置信区间,数据太少时会明确说明(
inconclusive、insufficient、n/a),而不是装作结论可靠; - 基准判定只会使用
shallower/deeper/inconclusive/insufficient——这些用语中没有“胜过”; - 任何声称存在优势的结论,都必须通过样本外检验才能给出建议——设计上不会采用仅基于样本内数据的数值;
- 该工具是只读的:它绝不会进行交易,AI 也无法写入你的账本。
这些设计塑造了简报。可选的 AI 解说是自然语言文本:防护机制确保其中的数字来自核心程序,但不限制措辞——不会阻止带有建议或预测意味的句子。app/narrate.py 的模块文档字符串准确列出了哪些内容会被强制执行,哪些不会。
✅ 正确性有据可查
- 检查门会在每次推送到
main和每个 PR 时运行完整测试套件、mypy --strict和 ruff——顶部的徽章就是该检查门的实时状态; - 持仓、市场价值和盈亏已与 Ghostfolio 精确核对到分,Sharpe、Sortino 和最大回撤已与 quantstats 精确核对到小数点后 4 位——这是记录于 2026-06-02 的快照,并非实时 CI 检查 → reconcile/RESULTS.md;
- 所有公式都记录在 MATH.md 中;
- 数字防护机制可通过脚本自行测试:
scripts/demo_fence.py。
🔬 验证 — 与两个独立工具核对
这些数字已通过两个独立工具交叉核对(测试框架位于 reconcile/)。记录的运行结果是 2026-06-02 快照,基于一个包含 11 个事件的账本——也就是现金跟踪添加 CASH 行之前的内置示例;因此应将其视为某一时点的核验,而不是 CI 每次重新运行的门槛:
- ghostfolio 根据同一份交易日志重建持仓、市值和盈亏,结果精确到分。
- quantstats 根据收益率序列独立计算 Sharpe、Sortino 和最大回撤,结果匹配到小数点后 4 位。
该工具计算的每个公式——收益率、回撤系列(Ulcer / CDaR)、风险调整比率、引导法置信区间,以及配置和筛选计算——都集中定义在一个位置:MATH.md。
两者共同验证了整个流程:ghostfolio 确认持仓和市值重建结果;quantstats 确认风险/收益公式。完整对比见 reconcile/RESULTS.md。
uv run --with quantstats python reconcile/reconcile_quantstats.py # metric cross-check
# end-to-end (ghostfolio): see reconcile/RESULTS.md
💬 与你的投资组合聊天 — Claude Desktop 插件(只读 MCP)
将你的投资组合作为只读工具提供给 AI 助手(Claude Desktop、Claude Code 等),供其调用——这样你就能“与投资组合聊天”,而每个工具结果中的数字都由 CLI 使用的同一确定性核心计算得出。请明确边界:助手自身的文字内容不受约束。这里没有任何机制可以阻止模型在聊天中自行输入数字,因此任何无法从工具结果中清晰追溯的数字都应视为未经验证——服务在其 portfolio://guarantees 资源中对此有明确说明。(CLI 的 --narrate 路径不同:它会以机械方式替换数字,并拒绝包含模型输入数字的叙述。)该服务为只读——没有写入工具,绑定到你的 ASSET_BOOK 账本(不接受文件路径参数)——并且离线运行,只有两种范围受限、可选择退出的数据获取行为:缓存为空时,会自动预热核心数据集一次(你的股票代码 + 基准参考,约 30–60s);筛选或提议未缓存的股票代码时,会按需获取数据。设置 ASSET_MCP_OFFLINE=1 可禁用这两种行为(适用于已预热的缓存;如果指向空缓存,则只会如实退化为 n/a)。八种工具:
portfolio_summary— 持仓、盈亏和年化收益率。risk_report— 以回撤为核心的风险报告:最大回撤(深度/日期/恢复)、Ulcer、CDaR、Sharpe/Sortino/Calmar,均附带引导法置信区间。rebalance_check— 根据你的ASSET_TARGET提供买入/卖出/持有建议(只提供建议,不会交易;如果账本只有部分数据已缓存,则拒绝计算仓位规模)。securities_facts— 每项持仓的已公布基金事实(费用比率、AUM、成交量、成立年限、类别)。discover_gaps— 为你持有比例不超过 3% 的角色推荐新 ETF;可选的role/flavor参数可进一步筛选某个类别(仅提议;使用screen_candidate评估候选项)。screen_candidate— 根据你的账本评估新的候选股票代码(分散效果/成本/流动性/成立年限/重叠度,逐项说明原因)。propose_allocation— 根据你的账本和投资范围,为某种投资取向(conservative/moderate/aggressive)制定战略目标,并使用相同的近期留出窗口和 Ulcer 优先判定方法与参考方案进行验证——仅供提议,数字来自核心引擎,绝非投资建议。starter_allocation— 新手?回答三个简单的风险问题 → 获得一个起始投资取向及其经验证的方案(进入propose_allocation的引导流程)。
**安装(Claude Desktop):**前往 Settings → Developer → Edit Config,并添加以下内容(如果从未修改过 claude_desktop_config.json,也可以直接替换):
{
"mcpServers": {
"asset-management": {
"command": "uvx",
"args": ["--from",
"https://github.com/disin7c9/asset-management/releases/download/v2.12.2/asset_management-2.12.2-py3-none-any.whl",
"asset-management-mcp"],
"env": {
"ASSET_BOOK": "C:\\path\\to\\your\\transactions.csv",
"ASSET_TARGET": "C:\\path\\to\\your\\target.csv",
"TIINGO_API_KEY": "your-free-tiingo-key(optional_secondary_source)"
}
}
}
}
重启 Claude Desktop。你只需要确保 uv 位于 PATH 中,无需其他操作——
uvx 会自行解析 Python 和锁定的依赖项。首次启动较慢(它会构建一个约 500 MB 的环境:pandas / NumPy / PyArrow),所以请耐心等一分钟;之后每次启动都只需几秒。首次调用工具时,价格缓存会预热一次(约 30–60 秒);缓存位于你的主目录中,因此重新安装后仍会保留。免费套餐也能使用。URL 固定了一个发行版——每次启动之间内容不会变化;要升级,请将两个版本号都替换为最新发行版。
所有 env 条目都是可选的——不用的可以删除。 删除 ASSET_BOOK,即可先用模拟数据探索内置演示投资组合。ASSET_TARGET 是 rebalance_check 用来对比你持仓的目标;如果不设置,只有这个工具会报错并给出提示。TIINGO_API_KEY(免费账户)可在 Yahoo 限流时启用第二个价格来源。较少用到的选项:ASSET_CACHE_DIR(更改价格缓存位置)、ASSET_MCP_OFFLINE=1(不获取任何数据——用于缓存已预热的情况)。这里不需要 LLM 密钥:读取这些工具的助手就是解说者;服务本身从不调用模型。
.mcpb 捆绑包——目前仅适用于 Claude Code。 使用 uv run python scripts/build_mcpb.py 构建,生成 dist/asset-management-<version>.mcpb,然后通过 Settings → Extensions 一键安装。其工具可在 Claude Code 中使用。它们无法在 Claude Desktop 的聊天窗口中使用:Desktop 不会向模型提供侧载扩展中的工具——这些工具会显示在工具菜单中,保留各自的权限开关,但就是不会被调用。通过目录安装的扩展和通过配置注册的服务都能正常使用,因此聊天场景应使用上文的配置方式。
在聊天中: 打开 + 菜单,选择现成的开场提示——投资组合检查、我的回撤是多少?、我该再平衡吗?、填补我的缺口、帮我找到起始资产配置、提出一个投资策略——每个提示都会预先加载仅使用数据的分析框架。服务还会发布 portfolio://guarantees(其四项强制保证,带版本信息并随代码一起发布):可从同一个 + 菜单添加;或者,如果客户端允许模型自行读取资源,只需询问“我可以相信这些数字吗?”,它就会根据清单作答,而不是临场发挥。
🛟 如果出了问题
- Claude 提示只能看到一个工具,或者一个都看不到。 前往 Settings → Connectors → asset-management,然后打开工具。新注册的服务会默认禁用其工具;在权限提示中批准某个工具,只会启用该工具。Claude 会如实报告它所获得的简短列表——它无从得知其他工具的存在,因此会告诉你该服务“只有一个工具”。实际上它有八个。
- 工具出现在菜单中,但 Claude 从不调用它们。 你安装了
.mcpb捆绑包。Claude Desktop 的聊天窗口不会向模型提供侧载扩展中的工具——菜单和权限开关由扩展本身生成,因此一切看起来都已连接,但模型根本不知道这些工具存在。请移除扩展,并使用上文的配置方式。 - 服务完全没有出现。 Claude Desktop 会在 PATH 中解析
"command",但找不到uvx。请安装 uv 并重启 Desktop,或在"command"中填写绝对路径(Windows:C:\Users\<you>\.local\bin\uvx.exe)。 - 首次启动时一直显示未连接。 这次启动正在构建环境(约 500 MB)。稍等一分钟,然后重启 Claude Desktop。之后启动只需几秒。
- 首次启动时出现
os error 32(“另一个进程正在使用该文件”)并退出。 Windows:uv 构建环境时,另一个进程(通常是杀毒软件扫描程序)会短暂占用 uv 缓存中刚写入的文件,导致安装无法及时完成。在终端中手动运行一次配置里的uvx命令(如果再次失败,只需重新运行——重试不需要付出任何代价)。等它不再显示任何信息时,环境就构建好了:按 Ctrl-C,然后重启 Desktop。之后每次启动都会复用已构建的环境,不会再发生竞争。 - 所有数值都显示为
n/a。 价格缓存尚未预热,而ASSET_MCP_OFFLINE=1禁止了为预热缓存而进行的抓取。请移除该变量,或通过 CLI 预热一次缓存:uvx --from git+https://github.com/disin7c9/asset-management asset-management --book your.csv --warm。 - (仅限
.mcpb)出现No MCP config found for extension … skipping。 Desktop 不会启动服务,直到你保存 Configure 表单——而且只有更改内容后,Save 才会启用。切换任意字段,保存,然后重启。
或者直接运行服务 / 将其注册到 Claude Code:
uv run python -m app.mcp_server # serve over stdio (set ASSET_BOOK in .env)
asset-management-mcp # the same server as an installed entry point
claude mcp add asset-management -- uv run python -m app.mcp_server # register, then /mcp to use it
服务本身不运行大模型——由助手调用它;这不是财务建议。
📖 参考
报告采用可组合面板,而非互斥模式——可以组合标志,各面板会依次叠加。每项操作所需条件如下:
| 操作 | 所需条件 | 功能 |
|---|---|---|
| 状态简报(默认) | --book | 你的持仓、回报以及回撤/风险 |
--rebalance MODE | --book + --target | 提供朝目标调整的买入/卖出建议(--new-cash 用于指定存入资金的金额) |
--allocate RULE | 预设规则无需参数;重新加权规则需要 --book | 提出一个目标——重新加权你的持仓(equal_weight/inverse_vol,需要账本),或构建战略角色模板(conservative/moderate/aggressive,无需账本);使用 --allocate-out 将其写入文件 |
--onboard | 无(账本用于根据你当前持有的资产确定角色) | 新用户的步骤 0:在终端回答 3 个简单的风险问题 → 匹配的投资姿态会自动生成相应的 --allocate 预设(仅提供建议;使用 --allocate-out 保存) |
--dry-run | --book(或 --demo) | 在信任导入数据前预览:检测到的格式、解析的事件、跳过/标记的行及原因,以及这些行推导出的持仓——不获取任何数据,也不计算简报 |
--metadata | --book | 每项持仓已发布的基金信息(费用率、AUM、成交量、成立年限、类别),缓存 7 天 |
--screen TICKERS | --book + 价格 | 根据你的账本评估新候选标的:分散效果(包括你的亏损日和最大回撤)、成本、流动性、成立年限、集中度、自动拒绝杠杆/反向产品、去重持仓重叠——每项结论均附理由。添加 --target 可进行留出角色检查:在留出窗口中,5% 的配置是否降低了回撤痛苦程度(Ulcer,并检查 CDaR)?“无法确定”会指出未通过的门槛。仅提供建议;PASS 表示“合理、便宜、流动性好、确实不同”,绝非预测 |
--discover [roles] | --book + 价格 | 为你持仓占比不超过 3% 的角色推荐新 ETF,并使用同一筛选流程——仅提供建议(参见发现) |
--backtest | --target | 模拟再平衡与买入并持有的比较——无需 --book;仅输出模拟结果 |
--backtest --benchmark REF | --target | 对照标准参考组合(60-40 / all-weather / permanent)验证目标——以回撤为先的阶段 + 留出的近期窗口,并给出以 Ulcer 为先的结论 |
--narrate | --book + 一个 LLM 密钥 | 在简报顶部提供通俗易懂的摘要;模型只撰写文字,所有数字均由核心程序填入并验证(可选,默认关闭——参见叙述) |
所有标志,按组列出:
# input & cache
uv run python -m app --demo # zero-setup test drive on the bundled example book
uv run python -m app --book your.csv # your book (CSV or Ghostfolio JSON, auto-detected)
uv run python -m app --book your.csv --dry-run # preview an import — fetches nothing
uv run python -m app --book your.csv --warm # ONE-TIME: fill the offline cache (add `full` for the universe)
uv run python -m app --book your.csv --offline # serve from the on-disk cache; no network
uv run python -m app --book your.csv --no-prices # holdings + realized P&L only (no network)
uv run python -m app --book your.csv --cache-dir /some/dir # override the cache location
# panels (stack freely)
uv run python -m app --book your.csv --no-risk # skip the holdings drawdown/risk panel
uv run python -m app --book your.csv --metadata # + SECURITIES panel (fund facts)
uv run python -m app --book your.csv --screen QQQM,SCHD # judge NEW candidates
uv run python -m app --book your.csv --screen SCHD --target t.csv # + the held-out role check
uv run python -m app --book your.csv --discover # + DISCOVERY panel (gap-filling ETFs)
uv run python -m app --book your.csv --narrate # + the fenced plain-language SUMMARY
# targets (propose-only)
uv run python -m app --book your.csv --dump-target target.csv # write your CURRENT allocation
uv run python -m app --book your.csv --allocate moderate --allocate-out t.csv # strategic preset
uv run python -m app --book your.csv --allocate inverse_vol --allocate-out t.csv # re-weight holdings
uv run python -m app --demo --onboard # 3-question quiz → a preset
# act & validate (separate commands, by design)
uv run python -m app --book your.csv --rebalance to_total --target target.csv # suggestions toward the target
uv run python -m app --book your.csv --rebalance cash_flow_only --target target.csv --new-cash 1000
uv run python -m app --backtest --target target.csv # notional backtest — target-only, no --book
uv run python -m app --backtest --target target.csv --benchmark 60-40 # + the reference comparison & verdict
uv run python -m app --book your.csv --rebalance bands --backtest --target target.csv # panels stack
# delivery
uv run python -m app --book your.csv --save # also write reports/<asof>.md (markdown)
uv run python -m app --book your.csv --save --reports-dir /elsewhere # ...somewhere other than reports/
uv run python -m app --book your.csv --send # also email the brief as HTML via Resend
target.csv 是使用 --dump-target / --allocate-out 创建的文件(也可以使用 data/sample_data/target.csv)。--allocate 仅提供建议,不能与 --rebalance/--backtest 同时使用。
💻 核心简报
示例输出 — 完整的 --demo 简报(回撤、比率、回报、持仓)
$ uv run python -m app --demo
=== DRAWDOWN (investment, time-weighted) ===
Max drawdown: -9.84% (95% CI -17.16% .. -6.44%)
peak 2025-02-19 → trough 2025-04-08 → 2025-06-10 (111 days)
Ulcer index: 2.40% (95% CI 1.60% .. 6.32%)
CDaR (worst 5%): 6.67% (95% CI 4.51% .. 14.09%)
You've spent 81% of this period below a previous high.
Gains given back: -$1,240 — peak profit $3,336 (2025-02-19) → $2,096 (2025-04-08) → 2025-06-10
(dollars of profit given back from a peak; flow-neutral — funding & transfers don't distort it)
=== RISK-ADJUSTED (annualized, 252-day basis, risk-free 0%, ± bootstrap CI) ===
Sharpe: +1.55 (95% CI +0.52 .. +2.49)
Sortino: +2.33 (95% CI +0.73 .. +3.85)
Calmar: +1.73 (95% CI +0.49 .. +3.56)
=== RETURNS (annualized, 252-day basis) ===
Period: 2023-01-05 → 2026-07-30 (1302 days, ~3.56y)
Time-weighted (true TWR): +17.04%
Money-weighted (IRR): +15.92%
Modified Dietz (approx TWR): +15.65%
(point figures: accounting identities over your cash flows, not sampled statistics → no band; see RISK-ADJUSTED for bootstrapped CIs)
=== HOLDINGS ===
ticker shares avg cost price mkt value unreal realized
-------------------------------------------------------------------------
BND 50.000 72.26 72.50 3624.75 +11.75 +45.10
IAU 25.000 37.23 75.34 1883.50 +952.88 +220.62
VEA 15.000 40.27 69.01 1035.22 +431.22 +0.00
VOO 12.000 375.13 675.28 8103.36 +3601.76 +295.80
-------------------------------------------------------------------------
Total cost basis (held): $9,649.23
Market value (priced): $14,646.84
Unrealized P&L: $+4,997.61
Realized P&L (sells+div): $+549.37
Fees paid (informational): $33.00
Net P&L (unrealized + realized): $+5,546.99
Prices: 4 cache (age: 6m .. 6m old as of 2026-07-29 15:20 UTC)
Generated by asset-management. Figures are computed by a deterministic core; this is not financial advice.
置信区间来自移动区块自助法。回撤是投资(时间加权)回撤,而非账户余额回撤。该面板还会报告已回吐收益——累计市场利润中以美元计的最大跌幅(直观地说,就是“我眼看着多少收益蒸发了”);它不受资金流影响,因此存款、提款和转账不会使其失真。每次运行还会在 stderr 上输出一行结构化 JSON 日志(run_summary):{date, source, n_events_replayed, n_prices_fetched, n_prices_missing, n_series_fetched, n_series_missing, fallbacks_used, status, report_saved, email_sent, rebalance, backtest, allocate, dump_target, metadata, screen, narrate, discover, discover_narrate, benchmark_narrate, warm, onboard, dry_run}(相关时会包含 email_detail/error)。
🔁 再平衡模式
目标是一份完整的规格(--target path,列为 Ticker,Weight;权重为相对值,系统会进行归一化):任何已持有但未列出的代码都会被视为退出,并卖出至 $0。因此,--rebalance 时必须指定 --target(不会静默采用默认值),运行时会警告并列出目标中遗漏的已持有代码。要有意平仓,请将其权重设为 0——这是明确且不会触发警告的退出;遗漏它也会产生相同结果,但会触发安全警告(工具无法判断你是“忘了”还是“有意为之”)。模式:
to_total— 卖出 + 买入,使组合精确达到目标(现金中性;也会投入--new-cash)cash_flow_only— 将--new-cash投入低配资产;绝不卖出(税务友好)fixed_dca— 使用--new-cash按目标配比买入,不考虑偏离情况bands—to_total,但由偏离触发条件控制:如果没有代码超出其区间,就不进行任何交易;如果有任一代码超出区间,则每个交易项都会回到目标(包括--new-cash)。只交易超出区间的项目会导致有卖出而无买入,因为抵消偏离的部分位于仍在区间内的项目中。区间取绝对值--band(默认 5pp)与--band-rel× 该代码目标权重(“5/25 规则”,默认 25%)中的较小值——因此,小仓位不会获得远超自身规模的区间;目标权重为 0% → 区间为 0 → 始终退出
示例输出 — 再平衡建议(演示组合,附带目标 VOO 45 / VEA 20 / BND 25 / IAU 10)
以下两个代码块均为针对 data/sample_data/target.csv 运行所示命令的原样输出。价格会变动,因此这些数字只是某一时点的快照;输出形式不变。
to_total — 卖出 + 买入,使组合精确达到目标:
$ uv run python -m app --demo --rebalance to_total --target data/sample_data/target.csv
=== SUGGESTED ACTIONS (rebalance to target) ===
ticker cur% tgt% action $ shares why
------------------------------------------------------------------------------
VEA 7.1 20.0 BUY +1894.14 +27.445 7.1% vs 20.0% target
VOO 55.3 45.0 SELL -1512.28 -2.239 55.3% vs 45.0% target
IAU 12.9 10.0 SELL -418.82 -5.559 12.9% vs 10.0% target
BND 24.7 25.0 BUY +36.96 +0.510 24.7% vs 25.0% target
Buy $1,931.10 · Sell $1,931.10 · net +$0.00 (cash-neutral)
bands(5/25 规则)— 相同目标。区间是一项触发条件:VEA 超出其区间 12.9pp,因此整个组合都会回到目标。BND 位于其自身区间内,仍然会交易,因为抵消偏离的部分就在这里——该行会说明原因:
$ uv run python -m app --demo --rebalance bands --target data/sample_data/target.csv
=== SUGGESTED ACTIONS (threshold-band rebalance) ===
ticker cur% tgt% action $ shares why
------------------------------------------------------------------------------
VEA 7.1 20.0 BUY +1894.14 +27.445 drift -12.9pp exceeds 5.00pp band
VOO 55.3 45.0 SELL -1512.28 -2.239 drift +10.3pp exceeds 5.00pp band
IAU 12.9 10.0 SELL -418.82 -5.559 drift +2.9pp exceeds 2.50pp band
BND 24.7 25.0 BUY +36.96 +0.510 24.7% vs 25.0% target (rebalance triggered)
Buy $1,931.10 · Sell $1,931.10 · net +$0.00 (cash-neutral)
如果没有任何代码超出其区间,就完全不会进行交易。仅生成建议——不会执行任何交易;每一行都会注明触发的规则。
--backtest --target T.csv 会对该目标进行名义金额为 $10,000 的历史模拟,并打印一个 BACKTEST 面板,比较再平衡(通过 --rebalance-every {monthly,quarterly,annually} 设置周期,默认按季度)与买入并持有——以回撤为先(最大回撤、Ulcer 指数、CDaR——每项都附带 bootstrap 置信区间),并报告夏普比率、Sortino 比率和收益率。这是名义模拟:从所有股票代码都有价格的最早日期开始,按目标权重投入一笔全新的 $10k(可通过 --backtest-start 覆盖),因此测试的是策略,不受你实际买入时机的影响。明确标注为历史模拟,而非预测。
固定的再平衡策略不拟合任何参数,因此整个历史数据都是样本外数据,不存在过拟合。滚动前推训练/测试选择机制——只有在策略进行搜索(调优参数或从候选项中挑选,例如优化器或择时策略)时才需要——会暂缓引入;纪律型策略与优势策略的门槛会确保未来任何优势策略在提供建议前,都必须通过滚动前推回测。当前的再平衡模式都属于纪律型策略,因此可以自由给出建议。
使用 --benchmark 时,样本外判定会通过三个具名门槛得出:Ulcer 指数改善幅度必须超过噪声边界,CDaR(最差尾部的平均值)可以持平,但不能与改善方向相悖;并且配对移动区块 bootstrap 置信区间必须确认这一结论——否则结果为 inconclusive,原因中会指出未通过的门槛。最大回撤和波动率仅作为背景数据报告,不参与判定:单次最糟事件属于极值统计量,在较短历史数据上噪声太大,无法用于判断。
示例输出 — 样本外判定与 60-40 比较
$ uv run python -m app --backtest --target data/sample_data/target.csv --benchmark 60-40
=== BENCHMARK (preset vs 60-40 · 2023-01-06 → 2026-07-29 — propose-only) ===
preset 60-40
--------------------------------------------------------------------------
Max drawdown -10.40% -11.22%
95% CI -16.5% .. -5.7% -17.5% .. -5.6%
Ulcer index 2.20% 2.24%
95% CI 1.5% .. 5.5% 1.4% .. 6.9%
CDaR (worst 5%) 6.43% 6.59%
95% CI 4.1% .. 13.0% 4.1% .. 14.7%
Sharpe (252-basis) +1.54 +1.45
95% CI +0.57 .. +2.51 +0.46 .. +2.45
Sortino +2.30 +2.16
95% CI +0.81 .. +4.04 +0.65 .. +3.82
Annualized return +16.47% +14.06%
Final value $17,144 $15,924
prices: cache · oldest fetch 2026-07-29
Held-out recent-window: OOS (2025-07-08→2026-07-29, 266d): Ulcer 1.90% vs 1.33% 60-40; CDaR 6.06% vs 4.08%; max DD -8.0% vs -5.8% (context); return +16.3% vs +12.6%/yr (+3.7pp, context) — no clear drawdown difference from 60-40; 95% CI of the Ulcer gain [-2.36pp, +0.27pp] (the paired bootstrap does not confirm the gap)
这里,Ulcer 指数差距超过了噪声边界,因此判定会继续进入 bootstrap 阶段——但置信区间跨过了零,所以结果仍为 inconclusive,并会说明是哪个门槛未通过。以 5% 资金份额筛选的候选策略则恰好相反:其效果小得多,通常仅第 1 个门槛就足以得出结论(MATH.md §12.7)。
还要留意它没有说什么。该预设的回撤损失略低于 60-40(Ulcer 指数为 5.57%,而 60-40 为 5.74%),收益率也略低于 60-40(+9.42%/年,而 60-40 为 +9.81%/年)——这是取舍,不是胜利。即使回撤方面有优势,也无法在样本外窗口中站稳:判定结果为**“回撤差异不明确”**,因为 Ulcer 指数的改善幅度仍在噪声边界内。样本外一行还会同时列出收益率代价和回撤改善,因此不会只告诉你路径更平稳,却不说明代价。
--discover 会将你的持仓映射到各个角色(美国大盘、新兴市场、TIPS、REITs,……),找出你持仓占比 ≤3% 的角色,从精选范围(随附的 app/data/universe.csv,约 375 只低成本 ETF)中为每个角色选出规模最大的核心基金,并使用与 --screen 相同的筛选流程进行检查——输出一个 DISCOVERY 面板,其中列出每个候选项的判断结果(PASS/WARN/FAIL)和原因。--discover reit,tips 可限制角色范围;添加 --narrate 可生成一个代码围栏注释,按角色匹配度为候选项排序。PASS 表示“合理、便宜、流动性好,而且与你持有的资产确实不同”,绝不代表预测。
四条诚实原则可避免面板言过其实。缺口表示没有专门的基金——你持有的宽基基金可能已经按市值权重涵盖了该角色(全市场基金已持有中小盘股;综合债券基金已持有国债和投资级公司债),因此面板会明确说明,而不会暗示存在空缺。候选项按近似替代品货架分组——范围中的每一行都带有机器可读的 flavor(即其货架:国债久期、行业、REITs 地理区域),每个菜单都会显示某一货架上可比较的基金(≥3 只,构成真正的选择),同时列出该角色其他货架的名称和数量,而不是将它们隐藏或排序;默认货架是预设方案已有的选择,因此不会带来新观点,只需一个标志即可查看其他货架(--discover treasury:long)。优先展示核心基金——core 标志(普通混合型/多元化/投资级,而非成长型/价值型风格、单一区域或高收益倾向)可防止垃圾债基金因资产管理规模偶然较大而填补“公司债缺口”;没有核心基金的货架只会显示为索引条目,除非你明确指定(--discover corporate-bond:high-yield 表示你同意查看垃圾债,结果也会明确标为垃圾债)。此外,行业/主题区域绝不会被标记为缺口——不持有科技类押注是一种立场,而非空缺;--discover sector-equity 会提供货架地图(科技 · 半导体 · 清洁能源 · ……),但不会替你选择行业,因为这是你的决定;指定某个货架即可筛选其中的基金。使用不含新增列的自定义范围时,功能会退化为普通行为。
示例输出——主货架、卫星指数、指定货架
逐字输出:每段分别取自命令打印的面板中的一个角色(完整面板会涵盖所有缺口)。
默认(--demo --discover)——展示主货架上的可比较基金,并列出角色的其他货架,而非为其排序:
corporate-bond — you currently hold 0%
· investment-grade — the standard sleeve
VCIT PASS Vanguard Intermediate-Term Corp (0 fail / 0 warn / 6 pass / 2 n-a)
Corporate Bond ETF.
LQD PASS iShares iBoxx $ Investment Grad (0 fail / 0 warn / 8 pass / 0 n-a)
Corporate Bond ETF.
IGIB PASS iShares 5-10 Year Investment Gr (0 fail / 0 warn / 6 pass / 2 n-a)
Corporate Bond ETF.
also here: high-yield — junk — equity-like drawdowns (19) — drill with --discover corporate-bond:<shelf>
卫星(--demo --discover sector-equity)——工具会映射各个货架,但不会替你选择:
sector-equity — you currently hold 0%
a tactical bet, not a hole — picking the sector/theme is the decision here, and it is yours. This maps the shelves; it won't rank them.
tech (6) · semis (3) · software (1) · ai (2) · cyber (1) · innovation (3) · quantum (1) · health (5) · biotech (4) · devices (1) · financial (6) · banks (3) · bdc (1) · brokers (1) · utilities (5) · energy (6) · explorers (1) · oil-services (1) · miners (4) · nat-resources (2) · materials (3) · uranium (3) · battery-metals (1) · water (2) · infrastructure (5) · grid (1) · clean-energy (2)
name one to see its screened funds, e.g. --discover sector-equity:tech
指定一个货架(--demo --discover treasury:long)——货架标签会预先说明风险:
treasury — you currently hold 0%
· long — 20y+ — rate-sensitive, equity-scale drawdowns
TLT PASS iShares 20+ Year Treasury Bond (0 fail / 0 warn / 6 pass / 2 n-a)
Long Government ETF.
VGLT PASS Vanguard Long-Term Treasury ETF (0 fail / 0 warn / 6 pass / 2 n-a)
Long Government ETF.
TLH PASS iShares 10-20 Year Treasury Bon (0 fail / 0 warn / 6 pass / 2 n-a)
Long Government ETF.
SPTL PASS State Street SPDR Portfolio Lon (0 fail / 0 warn / 6 pass / 2 n-a)
Long Government ETF.
GOVI PASS Invesco Equal Weight 0-30 Year (0 fail / 0 warn / 6 pass / 2 n-a)
Long Government ETF.
XTEN PASS BondBloxx Bloomberg Ten Year Ta (0 fail / 0 warn / 6 pass / 2 n-a)
Long Government ETF.
also here: intermediate — the standard sleeve (~3-10y) (6) · short — 1-3y — cash-like (4) — drill with --discover treasury:<shelf>
候选范围会自动构建(并可刷新),无需手动维护:
uv run python scripts/build_universe.py --auto --out app/data/universe.csv
它会从筛选器中按资产类别取规模最大的美国 ETF(依据基金规模,而非过往回报——追逐业绩正是此功能要避免的陷阱),排除杠杆型/反向型基金,并为少数筛选器无法单独筛出的类别保留一小组精选基金。在 .env 中设置 ASSET_UNIVERSE=path/to/universe.csv,即可让 --discover 使用你自己的列表。
使用 --target(或在 .env 中设置 ASSET_TARGET)时,每个发现的候选项还会接受持仓外角色检查——将其视为 5% 的配置进行重放,并使用筛选器决策时未采用的近期窗口进行评估。面板会注明运行模式,因为两种模式的结果看起来完全相同。每四个候选项大约需要一秒钟。
🔍 筛选候选标的
--screen TICKER 会逐项评估一个新标的是否适合你的投资组合:成本、流动性、持有年限、集中度、与现有持仓的重叠度、是否能分散你过去的回撤,以及它自身的最大回撤;每项都会说明原因。它只给出建议;PASS 表示“合理、低成本、流动性好且确实不同”,绝不代表预测。
自身回撤这一行会与你已持有的、跌幅最深的基金比较,而不是与你的投资组合最大跌幅比较。组合的跌幅按构成必然小于其组成部分,因此“比你的组合跌得更深”这道门槛几乎所有单只股票基金都能越过——在附带的示例中,这会标记该组合持有的四只基金中的三只。通过同类基准门槛,表示“不会比你现有持仓更糟”;此外还有一个固定的股票类基准门槛(跌幅超过 −30%),无论如何都会发出警告。
添加 --target 后,还会运行留出样本角色检查:为候选标的配置 5% 的投资组合份额,回放投资组合,并且只根据近期一段检查决策时未查看过的窗口进行评判。关于这项评判,有两点需要注意。它衡量的是回撤带来的痛苦,而非收益——即使没有收益,不相关基金也能让投资过程更平稳,因此输出会将收益成本与回撤改善并列显示,由你权衡。此外,执行角色检查时,筛选中的基于收益的检查(相关性、回撤窗口收益、自身回撤)仅使用样本内窗口计算,并标注为 [in-sample through …],这样就不会根据随后被留出的窗口来选择候选标的;结构性检查(成本、流动性、持有年限、重叠度)不包含收益信息,仍会读取完整历史数据。详情:MATH.md §12.3。
输出示例 — 相同角色,不同风险(IEF 与 TLT)
两者都能承担国债角色,且相关性检查都通过——但 own-drawdown 显示它们的风险并不相同:
$ uv run python -m app --demo --screen IEF,TLT
IEF — PASS (0 fail / 0 warn / 6 pass / 2 n-a)
[pass] diversifier: ρ=+0.22 vs your book; red-day ρ=+0.06; during your worst drawdown (2025-02-19→2025-04-08, -9.8%) it returned +2.9%
[pass] own-drawdown: worst fall -10.2% (2023-05-03→2023-10-19) in 3.6y; shallower than IAU, which you already hold (-26.4%)
TLT — PASS (0 fail / 0 warn / 6 pass / 2 n-a)
[pass] diversifier: ρ=+0.25 vs your book; red-day ρ=+0.09; during your worst drawdown (2025-02-19→2025-04-08, -9.8%) it returned +1.0%
[pass] own-drawdown: worst fall -22.4% (2023-04-06→2023-10-19) in 3.6y; shallower than IAU, which you already hold (-26.4%)
这里两者都通过了同类基准门槛——该组合已持有 IAU,跌幅为 -26.4%,因此门槛依据的是你现有持仓的承受水平,而不是抽象的阈值。但差异仍然是重点:TLT 的最大跌幅超过 IEF 的两倍;对于最大持仓跌幅不到 22% 的组合,同一条 TLT 结果会发出警告,而 IEF 不会。仅看相关性,会让两者毫无区别地通过。
--narrate 会在简报顶部添加一段简短的英文 SUMMARY,用句子说明你的回撤、风险和回报情况。此功能需主动启用,默认关闭;其设计确保语言模型绝不会在简报中填入错误数字:模型只撰写带有 {{placeholder}} 占位符的文字,工具会将占位符替换为自身核心中经过验证的数据;如果模型试图自行写入任何数字,或提及不存在的数据,整段摘要都会被拒绝。措辞由模型撰写;所有数据都由工具提供,并且区块会标明生成它的模型。同样的防护也适用于发现面板的旁白(--discover --narrate:模型按角色契合度为筛选出的选项排序并解释,绝不进行预测)和基准评估结论的旁白(--backtest --benchmark … --narrate)。这只是描述,不构成财务建议。
你需要在 .env 中提供自己的 LLM 密钥——除非你启用此功能,否则不会向任何地方发送数据:
ASSET_NARRATE_PROVIDER=anthropic # or: openai (any OpenAI-compatible endpoint)
ASSET_NARRATE_MODEL=claude-haiku-4-5
ASSET_NARRATE_KEY=sk-...
# for provider=openai, also set the endpoint (https required; http only for localhost):
ASSET_NARRATE_BASE_URL=https://api.groq.com/openai/v1
ASSET_NARRATE_TIER=paid # free | paid | local — privacy dial (see below); default: free
隐私调节项。 tier 控制哪些信息会离开你的设备。使用 free(默认值——适用于免费套餐可能会用你的输入训练模型的服务提供商)时,只会发送粗略的定性等级(“中等”“稳健”);具体金额、回报和日期会保留在本地,并在本地填入。使用 paid(与用户签有不使用其数据训练模型的合同的服务提供商)时,会发送具体数据,以生成更丰富的措辞。第三种等级 local 适用于在你自己的设备上运行的模型(http://localhost 上的 Ollama / llama.cpp——也支持 ::1 或 host.docker.internal):由于数据不会离开设备,这种模式也会发送具体数据;但只有连接到真正的本地端点时才会启用,否则会回退到 free。此调节项采用安全失败机制:只有明确设置为 paid 或 local 时才会发送具体数值——等级为空或拼写错误时都会使用 free;使用 free 时,工具会记录一行提示,提醒你服务提供商可能会使用所发送的数据进行训练。如果未配置旁白,或模型调用失败,简报仍会正常生成,只是不包含 SUMMARY。
🔑 配置——所有密钥一处管理
所有配置都通过环境变量设置。从代码仓库检出目录运行时,在仓库根目录下的 .env 文件中设置一次即可;该文件已加入 Git 忽略规则。通过 uvx 运行时(没有代码仓库可供读取 .env),请将同样的变量名设置为系统环境变量,或设置在 Claude Desktop 配置的 env 区块中。显式传入的标志始终优先。
# your book + target (Steps 1 & 3)
ASSET_BOOK=data/my_data/transactions.csv
# ASSET_CSV=... # legacy alias for ASSET_BOOK, still honored
ASSET_TARGET=data/my_data/target.csv
# prices — optional free second source for when Yahoo throttles (Step 2)
TIINGO_API_KEY=...
# email delivery for --send (Step 4)
RESEND_API_KEY=...
[email protected]
# [email protected] # optional; defaults to Resend's onboarding sender
# narration for --narrate (optional — your own LLM key; see Narration above)
ASSET_NARRATE_PROVIDER=anthropic # or: openai (any OpenAI-compatible endpoint)
ASSET_NARRATE_MODEL=claude-haiku-4-5
ASSET_NARRATE_KEY=sk-...
# ASSET_NARRATE_BASE_URL=https://... # provider=openai only
# ASSET_NARRATE_TIER=free # free (default: only coarse bands leave) | paid (exact figures) | local
# MCP addon only
# ASSET_MCP_OFFLINE=1 # never fetch — for an already-warmed cache
# rarely needed
# ASSET_CACHE_DIR=/somewhere/else # move the price cache
# ASSET_UNIVERSE=path/to/universe.csv # your own discovery universe
这里没有任何必填项:每项需要密钥的功能都会在调用时说明,并且即使没有密钥,也会正常降级运行。
🔧 项目
🧪 开发
uv run pytest # unit + property-based + regression tests
uv run mypy app/ # strict type-checking
uv run ruff check app/ tests/ # lint
uv run python scripts/build_mcpb.py # package the Claude-Desktop addon → dist/asset-management-<v>.mcpb
📁 目录结构
asset-management/
├── app/
│ ├── events.py CSV / Ghostfolio-JSON → typed event list
│ ├── derive.py events → holdings + cost basis + realized P&L
│ ├── corporate_actions.py split-adjust raw share counts (stock splits)
│ ├── prices.py multi-source price fetch (latest + history + splits) with provenance + cache
│ ├── metadata.py published fund facts (expense ratio, AUM, volume, age, holdings) + cache
│ ├── returns.py events + prices → equity curve, true TWR, MWR (XIRR), Modified Dietz
│ ├── risk.py drawdown family (max-DD / Ulcer / CDaR) + Sharpe/Sortino/Calmar with bootstrap CIs
│ ├── strategy.py holdings + target → named buy/sell suggestions (rebalance modes) + edge gate
│ ├── allocate.py choose a target: equal_weight / inverse_vol + risk-posture presets + per-asset caps
│ ├── onboard.py step-0 risk quiz (pure): 3 questions → a conservative / moderate / aggressive posture
│ ├── screen.py judge NEW candidate tickers (diversifier / cost / liquidity / age / overlap)
│ ├── backtest.py notional simulation + the held-out role/benchmark verdicts (Ulcer-first)
│ ├── pipeline.py the shared book→prices→returns→risk bundle (cli + mcp_server) + cache warm + the --demo book
│ ├── report.py suggestions + backtest + state + prices + returns + risk → ReportData → text/markdown/HTML
│ ├── narrate.py fenced narration: validated figures → {{token}} prose → SUMMARY (pure; no number can be the model's)
│ ├── llm.py optional narrator backend (OpenAI-compatible + Anthropic); fail-closed, opt-in
│ ├── email.py send the HTML brief via Resend (--send)
│ ├── cli.py argparse + entry composition + delivery routing + structured run log
│ ├── mcp_server.py read-only stdio MCP server: 8 tools over the core (offline; one-time cold-call auto-warm, ASSET_MCP_OFFLINE=1 opts out)
│ ├── universe.py curated ETF universe loader (Candidate + roles); app/data/universe.csv, auto-built
│ ├── discover.py book → role gaps → shelf menus: lead-shelf default / full menu / index / drill (--discover, propose-only)
│ ├── log_config.py logging setup
│ ├── http_safe.py the one no-redirect URL opener — a 3xx must not carry an API key to another host
│ ├── __main__.py python -m app
│ └── __init__.py
├── tests/ automated suite (unit, property, regression) — offline, run on every change
├── reconcile/ manual cross-validation against external tools (ghostfolio, quantstats)
├── scripts/ build_universe.py (refresh the ETF universe) · build_mcpb.py (Claude-Desktop bundle) · demo_fence.py (poke the fence)
├── .github/ the gate: ruff + mypy --strict + pytest on every push to main + every PR
├── pyproject.toml dependencies, mypy/ruff config
└── README.md
🔒 隐私政策
此工具完全在你自己的机器上运行。它没有后端、没有账户,也没有遥测数据——作者永远不会收到任何信息。
- **收集哪些信息。**什么都不收集。你的交易记录、计算得出的持仓和价格缓存只会在你的电脑上读取和写入,路径由你传入(
--book、--cache-dir或ASSET_BOOK/ASSET_CACHE_DIR)。不会上传任何内容,也不会发送使用数据、崩溃报告或分析数据。 - 哪些信息会离开你的机器,且仅限于这些。(1)价格和基金数据请求——股票代码会发送到 Yahoo Finance,如果你设置了
TIINGO_API_KEY,也会发送到 Tiingo,以获取报价、历史数据、拆分信息和已发布的基金事实数据。仅发送股票代码,绝不会发送你的数量、成本基础或余额。(2)叙述,--narrate,默认关闭:如果你开启此功能并提供自己的大模型密钥,投资组合的数字会发送给你选择的提供商(兼容 OpenAI 的提供商或 Anthropic),以生成文字说明。ASSET_NARRATE_TIER设置项控制发送的详细程度;local会将其保留在你的机器上。关闭此功能后,不会联系任何大模型。(3)电子邮件发送,--send,默认关闭:如果你开启此功能并提供自己的 Resend API 密钥,呈现后的 HTML 简报——其中包含你的持仓、份额数量、成本基础和盈亏——将通过 POST 请求发送到 Resend,再由其发送到你设置的地址。不会向其发送其他内容;不使用--send时,绝不会联系 Resend。 - **存储与保留。**从克隆的仓库运行时,价格缓存位于
data/prices;将工具作为软件包安装时,价格缓存位于你的主文件夹中的.asset-management/prices(可通过ASSET_CACHE_DIR覆盖)。缓存会一直保留,直到你将其删除。这两个目录相互独立——从克隆的仓库预热缓存,不会预热 wheel 安装版桌面附加组件读取的缓存。使用--save写入的报告位于reports/。所有内容都是归你所有的普通文件;删除这些文件夹,数据便会消失。作者不持有副本,也无法获取副本。 - **与第三方共享。**作者不会分享任何内容,也不会收到可供分享的内容。涉及的第三方仅限于你选择使用的上述服务——Yahoo、Tiingo、你选择的大模型提供商以及 Resend——每个服务均受其自身隐私政策约束。
- **MCP 服务。**它从不写入你的账本,也不进行交易;它绑定到你配置的账簿,不接受文件路径参数,也不提供写入工具。它会在
ASSET_CACHE_DIR下写入自己的价格缓存(若未配置账簿,还会写入一个演示账簿)。它会执行两次有界、可选择退出的网络获取(一次性预热冷缓存,以及筛选未缓存股票代码时按需获取);ASSET_MCP_OFFLINE=1可禁用两者。它不会向其他任何地方发送信息。 - **联系方式。**请在 github.com/disin7c9/asset-management/issues 提交问题。
📜 许可证
AGPL-3.0-or-later。可免费使用、修改和自托管,也可用于商业用途。唯一的条件是:如果你分发修改后的版本,或将其作为网络服务提供给他人使用,就必须依据相同许可证发布修改后的源代码。(与 Ghostfolio 使用相同的许可证。)
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。