总资产公式修复+数据模型文档
bugfix: price_monitor写total_assets时漏算frozen_cash 公式修正: total_assets = market_value + cash + frozen_cash 影响: price_monitor两处公式 + stale_push_wlin fallback路径 docs: portfolio-data-model.md 新增 数据模型字段说明 现金流更新规则 常见错误清单
This commit is contained in:
+14
-6
@@ -651,15 +651,15 @@
|
||||
],
|
||||
"cash": 92678.85,
|
||||
"total_market_value": 835552.6,
|
||||
"total_assets": 928231.45,
|
||||
"total_assets": 967712.85,
|
||||
"total_pl": 0,
|
||||
"position_pct": 90.02,
|
||||
"updated_at": "2026-06-29 22:23:47",
|
||||
"updated_at": "2026-06-29 22:28",
|
||||
"source": "/home/hmo/stocks/holding.xls",
|
||||
"frozen_cash": 39481.4,
|
||||
"available_cash": 92678.85,
|
||||
"frozen": 39481.4,
|
||||
"total_cash": 132145.6,
|
||||
"total_cash": 132160.25,
|
||||
"cash_updated_at": "2026-06-29 12:34",
|
||||
"recent_trades": [
|
||||
{
|
||||
@@ -675,10 +675,10 @@
|
||||
"total_mv": 835552.6,
|
||||
"note": "cash fixed from screenshot 6/29, prices=CNY",
|
||||
"currency": "CNY",
|
||||
"last_verified_at": "2026-06-29 22:20",
|
||||
"last_verified_at": "2026-06-29 22:28",
|
||||
"_total_mv": 835552.6,
|
||||
"_total_cash": 113240.25,
|
||||
"_total_assets": 948792.85,
|
||||
"_total_cash": 132160.25,
|
||||
"_total_assets": 967712.85,
|
||||
"cash_history": [
|
||||
{
|
||||
"time": "2026-06-29 22:23:47",
|
||||
@@ -686,6 +686,14 @@
|
||||
"frozen": 39481.4,
|
||||
"source": "manual:post-法拉电子-sell-100shares@189.2",
|
||||
"formula": "初始可用73758.85+法拉电子18920"
|
||||
},
|
||||
{
|
||||
"time": "2026-06-29 22:28",
|
||||
"cash": 92678.85,
|
||||
"frozen": 39481.4,
|
||||
"total_mv": 835552.6,
|
||||
"total_assets": 967712.85,
|
||||
"source": "price_monitor_auto"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
# portfolio.json 数据模型
|
||||
|
||||
## 一句话
|
||||
|
||||
**总资产 = 持仓市值 + 可用现金 + 冻结资金**,三个数字必须全对,缺少任何一项数字就不对。
|
||||
|
||||
## 字段说明
|
||||
|
||||
```jsonc
|
||||
{
|
||||
// 持仓列表(price_monitor 每2分钟更新价格)
|
||||
"holdings": [
|
||||
{
|
||||
"code": "01888", // 股票代码
|
||||
"name": "建滔积层板", // 股票名称
|
||||
"shares": 500, // 持股数
|
||||
"price": 83.59, // 最新价(CNY!所有股票统一人民币计价)
|
||||
"cost": 88.23, // 成本价(CNY)
|
||||
"market_value": 41795.0 // 市值 = shares × price
|
||||
}
|
||||
// ...
|
||||
],
|
||||
|
||||
// 现金(从 Dad 截图来源更新,price_monitor 不碰!)
|
||||
"cash": 92678.85, // 可用资金(人民币)
|
||||
"frozen_cash": 39481.40, // 冻结资金(T+2未交收/挂单占用)
|
||||
|
||||
// 汇总(price_monitor 每2分钟自动重算)
|
||||
"total_mv": 835552.6, // 持仓市值 = Σ(shares × price)
|
||||
"total_assets": 967712.85, // 总资产 = total_mv + cash + frozen_cash
|
||||
"position_pct": 86.3, // 仓位% = total_mv / total_assets × 100
|
||||
|
||||
// 元数据
|
||||
"currency": "CNY", // 本文件所有价格均为人民币
|
||||
"updated_at": "2026-06-29 22:33:00", // 最近一次更新
|
||||
"source": "holding.xls / manual", // 持仓来源
|
||||
"data_model_version": "2" // 数据模型版本(防止新旧数据混淆)
|
||||
}
|
||||
```
|
||||
|
||||
## 现金流
|
||||
|
||||
### 现金如何被更新?
|
||||
|
||||
| 渠道 | 操作 | 更新字段 | 频次 |
|
||||
|------|------|---------|------|
|
||||
| holding.xls 导入 | `import_holding_xls.py` | cash, frozen_cash | Dad更新后 |
|
||||
| 成交截图 | 手动解析 + 对比旧数据计算变更 | cash(+/-), frozen_cash | Dad发送时 |
|
||||
| price_monitor | 只更新价格,不动现金 | 不更新 | 每2分钟 |
|
||||
| 手动修正 | Dad告知准确数字 | cash, frozen_cash | Dad告知时 |
|
||||
|
||||
### 现金变更追踪
|
||||
|
||||
portfolio.json 的 `cash_history` 数组记录了每次现金变更:
|
||||
```jsonc
|
||||
"cash_history": [
|
||||
{
|
||||
"time": "2026-06-29 22:23:47",
|
||||
"cash": 92678.85, // 更新后的可用资金
|
||||
"frozen": 39481.40, // 更新后的冻结资金
|
||||
"source": "manual:post-法拉电子-sell", // 变更来源
|
||||
"formula": "初始73758.85 + 法拉电子18920" // 计算过程
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 规则
|
||||
|
||||
1. **price_monitor 绝不能修改 cash / frozen_cash** — 它只更新 price 字段和 total_mv / total_assets
|
||||
2. 现金变更只能来自 Dad 截图、holding.xls 导入、或 Dad 手动告知
|
||||
3. 每次现金变更必须记录到 cash_history,注明来源和计算过程
|
||||
4. 所有报告脚本读总资产时,从 `portfolio.json.total_assets` 取,不要自己算
|
||||
|
||||
## 币种
|
||||
|
||||
- portfolio.json 全部存 CNY(港股价格 × HK_RATE 转人民币)
|
||||
- decisions.json 港股价格存 HKD 原值(带 `currency: "HKD"` 标记)
|
||||
- price_monitor 比较 decisions.json 中的价格和止损时:同币种(都是 HKD),直接比较
|
||||
- 报告输出港股价格时显示 HKD 并标注「(HKD)」
|
||||
|
||||
## 常见错误
|
||||
|
||||
### ❌ total_assets 漏了冻结资金
|
||||
|
||||
```python
|
||||
# WRONG — 只加了可用现金,冻结资金漏了
|
||||
total_assets = market_value + cash
|
||||
|
||||
# RIGHT — 可用 + 冻结
|
||||
total_assets = market_value + cash + frozen_cash
|
||||
```
|
||||
|
||||
### ❌ 港股硬编码 ×0.866
|
||||
|
||||
```python
|
||||
# WRONG — 价格本身已经是 CNY(price_monitor在写入时就转了)
|
||||
mv = shares * price * 0.866
|
||||
|
||||
# RIGHT — price 已经是 CNY
|
||||
mv = shares * price
|
||||
```
|
||||
|
||||
### ❌ LLM 报告自己算总资产
|
||||
|
||||
```
|
||||
WRONG: 报告里写 "总资产 = 持仓市值 + 现金"
|
||||
RIGHT: 报告里写 "总资产 = portfolio.json.total_assets"
|
||||
```
|
||||
|
||||
## 版本记录
|
||||
|
||||
| 版本 | 日期 | 变更 |
|
||||
|------|------|------|
|
||||
| 1 | 2026-06-29以前 | 无规范,cash字段含义模糊 |
|
||||
| 2 | 2026-06-29 | 明确 cash=可用, frozen_cash=冻结, total_assets=市值+可用+冻结 |
|
||||
+10
-3
@@ -167,7 +167,12 @@ def refresh_data_prices():
|
||||
pf['total_mv'] = round(sum(
|
||||
h.get('shares',0) * h.get('price',0) for h in pf.get('holdings',[])
|
||||
), 2)
|
||||
pf['total_assets'] = round(pf['total_mv'] + pf.get('cash',0), 2)
|
||||
# total_assets = 持仓市值 + 可用现金 + 冻结资金(缺一不可!2026-06-29 bugfix)
|
||||
# cash = 可用资金(从截图/导入/成交记录来的,price_monitor不动它)
|
||||
# frozen_cash = 冻结资金(T+2未交收/挂单占用)
|
||||
available = float(pf.get('cash', 0) or 0)
|
||||
frozen = float(pf.get('frozen_cash', 0) or 0)
|
||||
pf['total_assets'] = round(pf['total_mv'] + available + frozen, 2)
|
||||
json.dump(pf, open(PORTFOLIO_PATH, 'w'), ensure_ascii=False, indent=2)
|
||||
elif pf.get('updated_at'):
|
||||
# 即使价格无变化,每10分钟刷新一次updated_at,防健康检查误报
|
||||
@@ -214,8 +219,10 @@ def refresh_data_prices():
|
||||
if abs(old_mv - live_market_value) > 0.01:
|
||||
pf['total_mv'] = round(live_market_value, 2)
|
||||
|
||||
total_cash = pf.get('cash', 0) # 保留上次的现金值,不重算
|
||||
pf['total_assets'] = round(live_market_value + total_cash, 2)
|
||||
# total_assets = 持仓市值 + 可用现金 + 冻结资金(重复!同步上一处公式)
|
||||
available = float(pf.get('cash', 0) or 0)
|
||||
frozen = float(pf.get('frozen_cash', 0) or 0)
|
||||
pf['total_assets'] = round(live_market_value + available + frozen, 2)
|
||||
if pf['total_assets'] > 0:
|
||||
pf['position_pct'] = round(live_market_value / pf['total_assets'] * 100, 2)
|
||||
pf['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M')
|
||||
|
||||
Reference in New Issue
Block a user