Doodle 子系统 · 孩子(coco)的真实财富记账与计算工具
财富管理系统 — 项目交接
目标:给孩子(Cordelia / coco)建立一套记录真实财富的系统,锻炼财务管理能力——知道自己有多少钱、
钱从哪来花到哪去、借钱有成本、投资有风险和收益。doodle 只负责记录/计算/展示,不碰真实资金转移
(银行转账、股票下单仍由家长在真实世界完成)。与积分系统(services/rewards.py)
完全独立、互不兑换。需求文档是仓库根目录的 WEALTH_SPEC.md / WEALTH_IMPL.md,
开发顺序按 SPEC §5 的 6 个优先级走,2026-08-25 一天内后端全部做完;2026-10-21 第二次 session 做完前端两个页面
(个人资产页 + 投资页),孩子可以自己查股票、自己买卖(按实时价成交、联动现金),看组合收益和盈亏;行情数据源换成 Nasdaq。
数据模型 + 现金收支 + 余额总览
汇率折算(双币种展示)
家庭借贷(自动计息)
投资 - 家庭项目
投资 - 美股
每日快照 + 趋势(折线图数据源)
前端:个人资产页 + 投资页(10-21)
孩子自助买卖美股,联动现金(10-21)
行情换 Nasdaq API,yfinance 已移除(10-21)
自选股 + 每 15 分钟定时刷新报价(10-21)
借贷/家庭项目没有家长操作界面,也没有权限校验
已做 / 未做
✅ 已完成(全部用真实 HTTP 请求 + 手算数值核对过)
- 数据模型:
storage/models.py 里 8 张 Wealth* 表,一次迁移(tools/migrate_v22.py)全部建好,覆盖全部 6 个优先级的表结构,避免以后再补迁移。
- 现金收支:孩子自己记收入(压岁钱/劳动所得)/支出,
GET/POST /transactions、DELETE /transactions/{id}。
- 余额总览:
GET /summary 按 WEALTH_IMPL.md §3 结构返回,现金 + 借贷净值 + 投资市值,CNY/THB 双币种同时展示。
- 汇率折算:
services/wealth_fx.py,按日缓存(open.er-api.com),命中当天缓存不重复调接口,接口失败兜底最近一次成功缓存;从只服务 CNY/THB 泛化到支持 USD(美股要用)。
- 家庭借贷:
GET/POST /loans、POST /loans/{id}/settle,应计利息实时按天数算(单利 Actual/365),不依赖定时任务;已结清的贷款冻结在结清时点。
- 投资-家庭项目:
POST /investments/family_project,收益按约定 end_date 到期封顶(不论是否手动结算),本金×(1+年化×天数/365)。
- 投资-美股:
POST /investments/us_stock,services/wealth_stock.py 接 yfinance,按 (ticker, 日期) 缓存 + 失败兜底最近缓存;卖出走 sell_price 冻结。
- 每日快照 + 趋势:
services/wealth_accrual.py(tools/wealth_daily_accrual.py 定时调用)每天落 wealth_loan_accruals/wealth_investment_valuations/wealth_daily_snapshots 三张历史表,幂等;GET /trend?days=30 读快照给折线图用。
- 前端(10-21):
/doodle/wealth/index.html 个人资产页(总资产 THB/CNY 切换、两种现金实际持有量、资产构成条、总资产趋势折线、记一笔收入/支出、流水、借贷只读)+ /doodle/wealth/invest.html 投资页(组合市值/浮动盈亏/今日涨跌/已实现盈亏/可用现金、搜索 + 热门代码、报价 + 52 周区间 + 1月/3月/1年走势、买入两步确认、持仓按股票合并可展开卖出、已卖出、家庭项目只读)。图表手写内联 SVG(doodle 前端没有引图表库,同 deficit 做法)。入口:quiz hub 顶栏"💰 我的财富"。Playwright 桌面 + 手机宽度全流程验证,无 JS 报错。
- 孩子自助买卖(10-21):
POST /stock/buy、/stock/{id}/sell,按实时价成交(允许 0.0001 股碎股),买入从所选币种现金扣款、余额不足拒绝,卖出回款同币种;联动流水带 investment_id,不可手动删除。总资产在现金↔投资市值之间搬家,不重复计算。迁移 tools/migrate_v23.py。
- 自选(10-21):投资页"⭐ 自选"卡 + 报价卡"☆ 加自选";
tools/wealth_watchlist_refresh.py 由 crontab */15 * * * * 调用,脚本内按美东时间只在工作日 04:00-20:00 ET 工作,刷新所有人自选 + 持仓报价写入 wealth_stock_quotes;页面开着每 60 秒读库刷新。迁移 tools/migrate_v24.py。
- 行情数据源(10-21):复测 yfinance 在本 VPS(=生产)仍被限流,直连 Yahoo chart 也 429 →
services/wealth_stock.py 改用 Nasdaq 官方公开接口(报价/日线/搜索/ETF 全可用),进程内 TTL + 缓存表 + 兜底三层保护。
- crontab:新增 1 条(曼谷时间 21:00 跑每日计息/快照),已按"VPS 系统时区是东京不是曼谷"的规则换算成
0 23 * * *。
🚫 被阻塞 / 没做
- 借贷 / 家庭项目没有家长操作界面:发起、结清仍只能调 API;而且
POST /loans、/investments/family_project 等写接口没有家长权限校验,任何登录账号(包括孩子)都能调。需要先定"家长 = 哪个账号"(Harrow 用的是 username == "dad")。
- 借贷 / 家庭项目本金不动现金:如果以后孩子真的拿手上的现金借出去或投家庭项目,本金会同时算在现金和借贷/投资里(和美股原来的问题一样),需要另定规则。
- 卖出只能整笔卖:每次买入是一笔持仓,卖出按笔整卖,不支持只卖一部分。
- owner 硬编码 "coco":
tools/wealth_daily_accrual.py 只处理一个孩子账号,若后续 cordelia(用户名待定)也要用,需要改成从 users 表按角色查询。
- 快照不支持精确历史补算:
wealth_ledger.loan_net_cny/investment_value_cny 内部按"今天"实时算,run_daily_accrual 传 target_date 只对明细表(计息/估值)生效,快照汇总值本质是"跑任务那一刻"的状态——这是有意的简化(同 harrow_task_rollover.py 的既有约定),不是 bug。
环境信息
| 线上入口 | /doodle/wealth/index.html(个人资产)、/doodle/wealth/invest.html(投资);静态文件 /var/www/edmund-home/doodle/wealth/,仓库副本 frontend/wealth/ |
| 后端路由 | routers/wealth.py,前缀 /doodle/api/wealth,main.py 已挂载 |
| 核心服务模块 | services/wealth_ledger.py(现金/借贷/投资的增删改查 + 余额计算)、wealth_fx.py(汇率)、wealth_stock.py(美股行情,Nasdaq API)、wealth_accrual.py(每日计息/折算/快照) |
| 数据表 | storage/models.py 里所有 Wealth* 开头的类;迁移脚本 tools/migrate_v22.py(一次建好全部 8 张表) |
| 依赖 | 无新增(行情、汇率都走已有的 httpx);yfinance 已于 10-21 从 requirements.txt 移除 |
| owner 约定 | 现金收支/趋势查看:owner=当前登录账号(孩子自查自记);借贷/家长录入投资:owner 由调用方显式传入("家长发起",操作者可能是 dad 账号);孩子自助买卖 /stock/*、/portfolio:owner=当前登录账号 |
API 一览
GET /doodle/api/wealth/summary 总资产总览(双币种折算)
GET /doodle/api/wealth/trend?days=30 总资产历史趋势
GET /doodle/api/wealth/transactions 收支流水
POST /doodle/api/wealth/transactions 记一笔收入/支出
DELETE /doodle/api/wealth/transactions/{id}
GET /doodle/api/wealth/loans 借贷列表(含实时应计利息)
POST /doodle/api/wealth/loans 发起借贷协议
POST /doodle/api/wealth/loans/{id}/settle 结清借贷
GET /doodle/api/wealth/investments 投资列表(家庭项目+美股)
POST /doodle/api/wealth/investments/family_project 发起家庭项目
POST /doodle/api/wealth/investments/us_stock 买入美股
POST /doodle/api/wealth/investments/{id}/settle 结算/卖出投资(家长录入)
GET /doodle/api/wealth/stock/search?q=apple 搜美股/ETF ← 10-21 新增
GET /doodle/api/wealth/stock/quote/{ticker} 实时报价
GET /doodle/api/wealth/stock/history/{ticker}?days=90 日线(走势图)
POST /doodle/api/wealth/stock/buy 孩子买入(扣现金)
POST /doodle/api/wealth/stock/{id}/sell 孩子卖出(回现金)
GET /doodle/api/wealth/portfolio 投资组合汇总
GET /doodle/api/wealth/watchlist 自选+最新报价(读库)
POST /doodle/api/wealth/watchlist 加自选 {ticker}
DELETE /doodle/api/wealth/watchlist/{ticker} 删自选
crontab(共 2 条)
0 23 * * * wealth_daily_accrual.py # 曼谷 21:00(已按东京+2h 换算)计息/折算/生成快照
*/15 * * * * wealth_watchlist_refresh.py # 自选+持仓报价;脚本内按美东判断,仅工作日 04:00-20:00 ET 工作
常用命令
cd /home/ubuntu/doodle
git status --short
pm2 restart doodle && pm2 logs doodle --lines 20 --nostream
# 查某个孩子的总资产
curl -s "http://127.0.0.1:8090/doodle/api/wealth/summary" -H "Cookie: doodle_session=..."
# 手动补跑一次每日计息/快照
.venv/bin/python tools/wealth_daily_accrual.py
# 查快照历史
sqlite3 data/doodle.db "SELECT snapshot_date, total_cny, total_thb FROM wealth_daily_snapshots ORDER BY snapshot_date DESC LIMIT 10;"
已知坑(详见 LESSONS.md 的 "财富管理模块" 章节)
| yfinance / Yahoo 被限流 → 已换 Nasdaq | yfinance 先取 Yahoo crumb,这一步对云服务商 IP 段直接限流;直连 Yahoo v8 chart 也是第二次请求就 429。10-21 改用 Nasdaq 官方公开接口(需浏览器 UA),按 assetclass=stocks/etf 分开查(传错类别返回 null 不报错),搜索是前缀匹配(搜 "micro" 出不来 Microsoft)。 |
| 孩子买股票必须联动现金 | 原 create_us_stock 是"家长录入持仓",不动现金;孩子自助买如果不扣现金,钱会同时算在现金和投资里。已改走 buy_stock/sell_stock,联动流水不可删。 |
| 汇率表泛化 | services/wealth_fx.py 原本只服务 CNY/THB,接入美股后加了 quote_currency 参数(默认 THB,旧调用点不用改),一次接口调用顺手把 THB 和 USD 都缓存,不多打请求。 |
| VPS 系统时区是东京不是曼谷(重要,同 Harrow 那次踩的坑) | 系统时区 Asia/Tokyo(UTC+9),应用逻辑用 Asia/Bangkok(UTC+7),差 2 小时,且这个 cron 版本不支持 CRON_TZ。本模块的 crontab 一开始也写错了(0 21 * * *,以为是曼谷 21:00,实际是曼谷 19:00),同日随 Harrow 那次时区排查一起发现并修正为 0 23 * * *。新增任何要求"曼谷几点触发"的定点任务,先 timedatectl 确认系统时区再换算。 |
家庭项目到期封顶用 end_date 而非结算时间戳 | WealthInvestment 表没有类似 WealthLoan.settled_at 的实际结算时间字段,家庭项目收益按约定 end_date 封顶(min(end_date, today)),不论是否已手动点结算,到期后金额就不再继续增长——这样更贴合"收益按合同条款算"的语义。 |
下一步建议
- 让 coco 真用起来:现在 coco 账上全是 0,先由家长把现有压岁钱 / 劳动所得记进去,再让孩子自己试买一小份股票。
- 家长操作界面 + 权限校验:定好家长账号后,给借贷、家庭项目做发起 / 结清界面,写接口加家长校验。
- 借贷 / 家庭项目本金是否从现金扣:定规则后按美股同款做法联动现金。
- 观察真实使用一段时间后,看
WEALTH_SPEC.md §6 里的"临时默认规则"(每日结算时点、汇率更新频率)是否需要调整。