← 交接首页 工具首页 财富管理系统 · Project Handoff
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 被限流 → 已换 Nasdaqyfinance 先取 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)),不论是否已手动点结算,到期后金额就不再继续增长——这样更贴合"收益按合同条款算"的语义。

下一步建议

  1. 让 coco 真用起来:现在 coco 账上全是 0,先由家长把现有压岁钱 / 劳动所得记进去,再让孩子自己试买一小份股票。
  2. 家长操作界面 + 权限校验:定好家长账号后,给借贷、家庭项目做发起 / 结清界面,写接口加家长校验。
  3. 借贷 / 家庭项目本金是否从现金扣:定规则后按美股同款做法联动现金。
  4. 观察真实使用一段时间后,看 WEALTH_SPEC.md §6 里的"临时默认规则"(每日结算时点、汇率更新频率)是否需要调整。
本页对应 2026-08-25(后端)+ 2026-10-21(前端两页、孩子自助买卖、行情换 Nasdaq)两次 session;首次(从 WEALTH_SPEC.md/WEALTH_IMPL.md 起步,按优先级 1→6 一路做完数据模型、现金收支、汇率、借贷、家庭项目投资、美股投资、每日快照/趋势,全部部署验证)。 详细过程记录在仓库根目录 CODING-LOG.md(搜"财富管理系统")和 LESSONS.md。