241 lines
32 KiB
Markdown
241 lines
32 KiB
Markdown
# Omniledger 架构与开发记忆 (Memory)
|
||
|
||
## 构建 /api/cron/fetch-prices 定时任务端点,实现针对活跃资产的行情抓取与按日期的幂等性 (Idempotent) 价格入库 (Task 61)
|
||
- 在 `src/app/api/cron/fetch-prices/route.ts` 创建 Next.js Route Handler (GET),专供定时任务调用。
|
||
- **安全拦截:** 在 GET 方法顶部校验 `Authorization` 请求头 (`Bearer ${process.env.CRON_SECRET}`),不匹配则返回 401 Unauthorized;若 `CRON_SECRET` 未配置则返回 500。
|
||
- **核心流程:** 查询 `assets` 表中 `STOCK` 和 `CRYPTO` 类型的活跃资产 → 遍历调用腾讯财经/币安 API 获取现价 → 生成当日日期字符串 (YYYY-MM-DD) → 幂等入库至 `asset_pricesHistory`。
|
||
- **幂等性保障:** 先 `SELECT` 检查当天是否已存在该 `assetId` 的记录,存在则 `UPDATE` 价格,不存在则 `INSERT`,确保同一天同一资产绝不出现两条价格记录。
|
||
- **响应格式:** 返回 `{ success, date, synced, skipped, failed, details }` 结构化的 JSON 结果。
|
||
- 新增环境变量 `CRON_SECRET`(需在 `.env` 中配置),用于定时任务接口的认证密钥。
|
||
|
||
## 粉碎时光机中 marketValue 未乘汇率的致命双标幻觉,严格对齐历史快照与实时概览的 CNY 折算基准 (Task 59b)
|
||
- **核心认知纠正:** `calculateAssetMetrics` 输出的 `marketValue` **绝对不是 CNY**!它和 `totalInvested` 一样,都是原始币种 (Base Currency)。
|
||
- **致命 Bug:** 之前的时光机逻辑中,`posCostCny` 使用 `metrics.totalInvested * fxRate` 折算,而 `posValueCny` 使用 `metrics.marketValue * fxRate` 折算。但由于 `totalInvested` 在 `finance.ts` 引擎中已经混入了 CNY 价格(如海尔的买入价格),导致投入本金被错误放大,在 4 月 30 日节点造成"投入本金"虚高、净盈亏显示为亏损 4 万的荒谬结果。
|
||
- **强制修复:** 在 `src/actions/snapshots.ts` 的 `reconstructPortfolioHistory()` 资产遍历循环中,`posCostCny` 改为 `(metrics.marketValue - metrics.accumulatedPnl) * fxRate` 推导,确保投入本金 = 市值 - 累计盈亏,逻辑自洽且与实时概览 (`recordDailySnapshot`) 的 CNY 折算基准完全对齐。
|
||
- **物理毁灭脏数据:** 在函数开头已执行 `DELETE FROM portfolio_snapshots`,重新跑时光机生成全新 1247 天快照,消除所有因汇率双标导致的错乱数据。
|
||
|
||
## 修复 calculateAssetMetrics 结果的汇率双重标准解析错误,重构 Live Overview 聚合基准 (Task 59)
|
||
- **核心认知纠正:** `calculateAssetMetrics` 引擎产出的所有数据(`marketValue`, `accumulatedPnl`, `floatingPnl`, `dilutedCost` 等)全都是原始基础币种 (Base Currency)!绝对不存在"部分已经是 CNY"的情况。
|
||
- 修复 `src/actions/snapshots.ts` 的 `reconstructPortfolioHistory()`:之前错误地将 `cnyPrice`(已折算的人民币价格)传入引擎,却只对 `totalInvested` 乘汇率、对 `marketValue` 不乘,形成"双标幻觉"。现改为传入原始币种价格 `priceStr`,然后无例外地将引擎输出的所有金额字段统一乘以 `fxRate` 得到 CNY,确保 `posValueCny` 和 `posCostCny` 使用同一套汇率折算基准。
|
||
- 修复 `src/actions/portfolio.ts` 的 `getPortfolioSummary()`:废弃旧的 `cnyValue`/`pnlCny` 求和逻辑(旧逻辑基于 `calculateCnyValueFromPrice` 双路径计算,与引擎输出存在语义差异),改为从 `getPortfolioPositions()` 返回的 `marketValueCny`/`accumulatedPnlCny`/`floatingPnlCny` 字段在内存中累加,确保大盘汇总与底层明细使用同一套计算源(单一事实来源)。
|
||
- 同步修复 `recordDailySnapshot()`:`totalValueCny` 改为累加 `marketValueCny`,`totalCostCny` 改为 `marketValueCny - accumulatedPnlCny` 推导,与 `getPortfolioSummary` 保持一致。
|
||
- 彻底覆盖历史快照:重新执行 `reconstructPortfolioHistory()`,成功重构 1247 天历史快照数据,消除之前因混用汇率导致的错乱快照。
|
||
|
||
## 全局修复多币种聚合漏洞,强制叠加汇率乘数 (Task 58)
|
||
- 修复了跨币种资产直接相加导致的盈亏总额失真问题:USD 盈利未乘以 ~7.23 汇率被当作 CNY 计算,HKD 亏损同理。
|
||
- 在 `src/actions/portfolio.ts` 的 `getPortfolioPositions()` 中,对每个资产获取 `exchangeRate`(Base Currency → CNY),将财务引擎 (`calculateAssetMetrics`) 产出的所有绝对金额字段(`marketValue`、`floatingPnl`、`accumulatedPnl`、`dilutedCost`)乘以汇率,映射为 `Cny` 结尾的新字段,确保 Dashboard 列表中的 CNY 聚合数据精确。
|
||
- 在 `src/actions/snapshots.ts` 的 `reconstructPortfolioHistory()` 中,修复时光机逐日汇总逻辑:`metrics.marketValue` 已使用 CNY 价格计算可直接取用,`metrics.totalInvested` 基于原始币种价格需乘以 `exchangeRate` 折算为 CNY,确保历史成本曲线正确。
|
||
- 重新触发时光机清洗,成功重构 1247 天历史快照数据。
|
||
|
||
## 重构历史快照生成逻辑,消除新旧算法断层 (Task 57)
|
||
- 将时光机重构逻辑全面接入 finance utils 引擎,清洗历史脏快照,消除新旧算法迭代导致的本金曲线断层。
|
||
- 在 `src/utils/finance.ts` 的 `calculateAssetMetrics` 返回值中新增 `totalInvested` 字段,直接输出真实投入本金(含手续费),避免通过 `marketValue - accumulatedPnl` 间接推导导致的精度损失。
|
||
- 在 `src/actions/snapshots.ts` 中废弃 `reconstructPortfolioHistory()` 的旧版 Day-by-Day 加减法逻辑,改为:对每一天 `currentDate`,获取该资产在 `currentDate` 及之前的所有交易流水 `historicalTx` 和历史收盘价 `historicalPrice`(断点结转),调用 `calculateAssetMetrics(historicalTx, historicalPrice)` 获取 `metrics.marketValue` 和 `metrics.totalInvested`,分别累加为当天的 `totalValueCny` 和 `totalCostCny`。
|
||
- 重构后的 `reconstructPortfolioHistory()` 执行第一步调用 `db.delete(portfolioSnapshots)` 彻底清空旧的脏快照,然后从第一笔交易开始用新算法逐天重新生成,确保历史成本曲线平滑过渡、数值一致。
|
||
|
||
## 基础设施与底层架构
|
||
- 完成根目录的 Next.js 初始化、基础依赖安装与环境变量配置。
|
||
- 完成基于单例模式的数据库连接配置,并设定 Drizzle 迁移工具。
|
||
- 修复网络连接,成功将 tables 推送至 PostgreSQL 数据库。
|
||
- 统一规范 Git 全量提交机制 (`git add -A`),确立了严谨的代码版本控制防腐层。
|
||
|
||
## 数据库设计 (Schema)
|
||
- 成功定义资产枚举与 `assets` 表,支持跨资产标识。
|
||
- 完成核心 `transactions` (交易流水) 表的建立,并严格运用了 `numeric(36,18)` 的高精度配置。
|
||
- `assets` 表完成多次业务演进:新增 `latestPrice` (支持现价追踪)、`exchange` (显式交易所绑定) 以及 `name` (中文名称解析) 字段。
|
||
- `exchange_rates` (汇率表) 已建立,支持联合主键与跨币种交叉汇率架构。
|
||
- **引入 `portfolio_snapshots` 表**:用于每日记录投资组合快照,字段包括 `date` (唯一日期)、`total_value_cny` (当日总市值)、`total_cost_cny` (当日总投入本金),为历史净值走势图奠定底层数据结构。
|
||
- **新增 `asset_prices_history` 表**:用于存储手动导入的每日标的价格,字段包括 `assetId` (关联资产)、`price` (当日收盘价/净值,numeric(36,18))、`date` (YYYY-MM-DD 格式)、`createdAt`,并对 `(assetId, date)` 建立联合唯一索引,为手动导入历史净值提供底层 Upsert 支持。
|
||
|
||
## 核心业务与服务端逻辑 (Server Actions)
|
||
- 完成高精度交易流水与资产的 Server Actions 开发,成功实现字符串级别的高精度防腐层拦截(基于 Zod & Big.js)。
|
||
- 补全资产与流水的全栈增删改查 (CRUD) 操作,`createTransaction` 现已支持根据 `exchange` 自动判定并锁定 `txCurrency`。
|
||
- **估值与 P&L 引擎:** 完成底层估值引擎升级,打通交叉汇率换算逻辑;实现原币种 (Native) 与本位币 (CNY Base) 双轨制的历史成本追溯与真实盈亏 (P&L) 计算引擎。
|
||
- **快照记录引擎:** 新增 `src/actions/snapshots.ts`,`recordDailySnapshot()` 函数基于 `getPortfolioPositions()` 实时计算总市值与总成本,使用 `Asia/Shanghai` 时区获取当日日期,执行 Upsert 逻辑确保每天仅存一条记录;`getSnapshots()` 支持按日期范围与数量限制查询历史快照数据。
|
||
|
||
## 外部行情接口与网络 (Market Data Engines)
|
||
- **股票行情引擎:** 彻底抛弃低效海外接口,自主研发智能路由接入腾讯财经 (`qt.gtimg.cn`) 极速接口。引入原生 `ArrayBuffer` 与 `TextDecoder(gbk)` 彻底解决历史中文乱码问题,实现沪、深、港、美四大市场毫秒级实时同步。
|
||
- **Crypto 行情引擎:** 接入币安 (Binance) 公共 API,构建全市场(股票+加密货币)双轨双擎驱动架构。
|
||
- **网络防腐层:** 引入 `undici` 底层网络库并配置 `setGlobalDispatcher`,成功突破境内防火墙对境外 API 的直连封锁 (Timeout)。
|
||
|
||
## 前端架构与 UI/UX 体验
|
||
- 完成 shadcn/ui 初始化,集成 next-themes 支持暗黑模式,并拉取核心组件库。
|
||
- 完成 `/dashboard` 基础布局架构,接管根路由。
|
||
- 打通 `/dashboard/assets` 与 `/dashboard/transactions` 页面前后端数据流转,修复早期录入与 404 缺陷。
|
||
- 完成 UI 层高精度数据格式化,针对不同资产类型实现动态精度展示,清理因数据库 `numeric` 导致的尾随零问题。
|
||
- 引入 `recharts` 图表引擎,构建了基于实时 CNY 估值的资产分布环形图。
|
||
- 实装基于 Recharts 的历史净值面积图 (NetWorthChart),支持总市值与投入本金的双轨趋势对比。
|
||
- 优化表单交互:实装了交易所与币种的智能联动逻辑,并运用 `disabled` 属性实现了表单字段的只读防腐锁定。
|
||
|
||
## UX 与全局交互 (UI/UX)
|
||
- 引入 `sonner` 构建全局 Toast 消息通知系统,覆盖行情同步、CRUD 操作的成功与异常提示。
|
||
- 重构 `<SyncButton />` 并将其提升至 Dashboard 首页,实现总资产大盘的全局一键实盘刷新。
|
||
|
||
## 修复记录
|
||
- 解决了日期选择控件的时区偏移 Bug,确保全球通用:在 `src/libs/utils.ts` 中重写 `formatDateForDatetimeLocal()` 与 `parseDateTimeLocalToUTC_v2()` 函数,采用 `Intl.DateTimeFormat` 动态获取 `Asia/Shanghai` 时区偏移量,确保 UTC 时间到本地时间的双向转换精确无误,修复了用户选 10 点展示为 2 点的问题。修正了前端数据格式化逻辑,在 `src/lib/formatters.ts` 中增加空值/NaN 兜底处理,在 `src/app/dashboard/page.tsx` 中将平均成本与摊薄成本的显示条件从 `.gt(0)` 改为 `.ne(0)`,支持英特尔负成本等极端场景下的精确数字展示。
|
||
|
||
## 资产分布图表按市场维度升级 (Task 32)
|
||
- 优化资产分布图表,升级为按市场维度聚合展示,并增强了 Tooltip 的颜色指代与明细交互。
|
||
- 在 `portfolio.ts` 中新增 `getMarketFromExchange()` 函数,将资产按交易所归类为 A股 (SSE/SZSE)、港股 (HKEX)、美股 (US)、虚拟币 (CRYPTO)。
|
||
- 新增 `marketAllocation` 聚合数据,按市场维度汇总 `totalCnyValue` 并计算占比,自动过滤已清仓资产。
|
||
- 升级 `AllocationChart` 组件:数据源改为市场聚合数据,为各市场设定固定品牌色(A股红、港股黄、美股蓝、虚拟币绿),并自定义 Tooltip 渲染内容,悬停时清晰展示 `[市场名称] [对应颜色块] [CNY 金额] [占比%]`。
|
||
|
||
## 盈亏引擎重构 (Task 31)
|
||
- 重构盈亏计算引擎,支持已实现盈亏统计:交易按时间正序处理,SELL 时基于当时平均成本计算该笔卖出的利润并累加至 `realizedPnlCny`。
|
||
- 引入摊薄成本与平均成本双重指标:`avgCost = totalBuyCost / totalBuyQuantity`(平均成本);`dilutedCost = (totalBuyCost - realizedPnlCny) / currentQuantity`(摊薄成本,已实现盈亏会摊低持仓成本)。
|
||
- 新增持仓天数统计:`holdingDays = today - 第一次 BUY 的日期`(基于上海时区)。
|
||
- Dashboard 首页总览区分展示『持仓盈亏 (Unrealized P&L)』和『总盈亏 (Total P&L)』。
|
||
- 持仓卡片新增『平均成本』、『摊薄成本』和『持仓天数』展示。
|
||
|
||
## 分紅業務邏輯與成本算法修復 (Task 33)
|
||
- 重構了分紅的會計處理邏輯,將其正確計入已實現盈虧:DIVIDEND 不再增加持倉數量,而是按 `quantity * price * exchangeRate` 計算分紅金額並累加至 `realizedPnlCny`。
|
||
- 新增 `totalDividendCny` 字段追蹤累計分紅金額。
|
||
- 修正攤薄成本算法:`dilutedCost = (totalBuyCostCny - realizedPnlCny - totalDividendCny) / currentQuantity`,確保極端情況下攤薄成本為負數時精確返回負數,絕不兜底為 0。
|
||
- 平均成本 `avgCost = totalBuyCostCny / totalBuyQuantity` 保持不變,僅在遍歷結束後計算。
|
||
|
||
## 修復平均成本顯示 Bug與分紅獨立統計 (Task 35)
|
||
- 修復了平均成本在前端顯示為空的問題:`totalBuyQuantity` 在 BUY 交易處理中從未累加,導致 `avgCost` 計算時除數為零而永遠返回 0。現在在 BUY 時正確執行 `totalBuyQuantity += quantity`。
|
||
- 將分紅邏輯從已實現盈虧中剝離,建立獨立的累計分紅統計維度:新增 `accumulatedDividendsCny` 字段,DIVIDEND 交易不再混入 `realizedPnlCny`,而是獨立累加至 `accumulatedDividendsCny`。
|
||
- 重新定義總盈虧公式:`totalPnlCny = unrealizedPnlCny + realizedPnlCny + accumulatedDividendsCny`,確保分紅有獨立的統計維度且不會干擾平均成本計算。
|
||
|
||
## 升級 Dashboard 資產卡片 UI (Task 36)
|
||
- 升級了 Dashboard 資產卡片 UI,新增累計分紅展示,並優化了成本數據的格式化判斷邏輯。
|
||
- 修復了 `avgCostFormatted` 的判空邏輯,將 `Big.eq(0)` 修正為 `Big.eq('0')`,確保當 `pos.avgCost` 存在且不為 0 時能正確格式化,不再顯示 `¥-`。
|
||
- 在資產卡片中新增「累計分紅」行,展示 `accumulatedDividendsCny` 數據,保持與其它 CNY 數據一致的 `opacity-50` 樣式。
|
||
|
||
## 修復幣種符號映射 Bug 與引擎數據擴充 (Task 40)
|
||
- 修復了币种符号映射错乱 Bug:`getCurrencySymbol()` 中 CNY 和 HKD 被错误地映射为 `HK$`,现修正为 USD→$、HKD→HK$、CNY→¥。
|
||
- 在 portfolio 引擎中新增了单资产的流水明细数组:`Position` 接口新增 `transactions` 字段,`getPortfolioPositions()` 按资产名下发排序后的原始交易流水,支持前端展开查看。
|
||
|
||
## 流水交易類型中文化映射 (Task 42a)
|
||
- 在 `app/dashboard/page.tsx` 中新增 `txTypeMap` 字典,將 `BUY`/`SELL`/`DIVIDEND`/`AIRDROP` 映射為對應中文(買入/賣出/分紅/空投)。
|
||
- 流水明細子表格中渲染 `tx.txType` 的邏輯替換為 `{txTypeMap[tx.txType] || tx.txType}`,保留原始值兜底。
|
||
|
||
## 利用 Big.js 剥离流水明细无意义尾随零 (Task 42b)
|
||
- 利用 `Big.js` 剥离了流水明细中无意义的尾随零,提升了高精度数据的可读性。
|
||
- 在 `app/dashboard/page.tsx` 的流水明細子表格中,将 `tx.quantity`、`tx.price`、`tx.fee` 的渲染逻辑改为 `new Big(value).toString()`,安全剥离因数据库 `numeric(36,18)` 配置导致的如 `0.041000000000000000` 这类冗余尾随零。
|
||
|
||
## 盈亏红绿视觉规范 (Task 42c)
|
||
- 依据中文金融习惯(红涨绿跌),规范了盈亏数值的颜色与正负号显示。
|
||
- 移除了 `formatPnl()` 函数及概览行内硬编码拼接的 `+` 号前缀,正收益直接展示数值,负收益保留原生 `-` 号。
|
||
- 统一颜色逻辑:值 `> 0` 应用 `text-red-500`(红色),值 `< 0` 应用 `text-green-500`(绿色),值 `=== 0` 使用默认文字颜色。
|
||
- 括号内的百分比同步遵循相同逻辑,格式如 `$2447.48 (114.20%)`。
|
||
|
||
## 修复快照读取引擎中的 Drizzle 语法错误
|
||
- 修复快照读取引擎中的 Drizzle 语法错误,全面改用类型安全的 desc 和 gte 操作符进行查询。
|
||
- 在 `src/actions/snapshots.ts` 中引入 `desc` 与 `gte` 操作符,彻底替换原始 SQL 模板拼接(`sql`"${date}" DESC``),消除 `ReferenceError: date is not defined` 运行时错误。
|
||
- 使用 `desc(portfolioSnapshots.date)` 实现降序排列,使用 `gte(portfolioSnapshots.date, startDate)` 实现日期范围过滤,并添加 `.$dynamic()` 支持动态条件拼接。
|
||
|
||
## 持倉引擎 Native 幣種算法重構 (Task 38)
|
||
- 重構底層盈虧引擎,全面轉向 Native 原生幣種計算,新增浮動/累計盈虧及百分比指標。
|
||
- 徹底分離 Native 與 CNY 計算:單隻股票的成本與盈虧全部改用 Native (原幣種) 進行計算。
|
||
- 新增 Native 成本指標:`totalBuyCostNative` (總買入成本)、`realizedPnlNative` (已實現盈虧)、`accumulatedDividendsNative` (累計分紅)。
|
||
- 新增 Native 成本均價:`avgCostNative = totalBuyCostNative / totalBuyQuantity`、`dilutedCostNative = (totalBuyCostNative - realizedPnlNative - accumulatedDividendsNative) / currentQuantity`。
|
||
- 新增浮動盈虧指標:`marketValueNative = latestPrice * currentQuantity`、`floatingPnlNative = marketValueNative - (avgCostNative * currentQuantity)`、`floatingPnlPercent = floatingPnlNative / (avgCostNative * currentQuantity) * 100`。
|
||
- 新增累計盈虧指標:`cumulativePnlNative = floatingPnlNative + realizedPnlNative + accumulatedDividendsNative`、`cumulativePnlPercent = cumulativePnlNative / totalBuyCostNative * 100`。
|
||
- SELL 交易的已實現盈虧計算從 CNY 基準改為 Native 基準,CNY 計算保留用於前端兼容展示。
|
||
|
||
## Dashboard 流水下鑽明細與行內 CRUD (Task 41b)
|
||
- 完成 Dashboard 流水下鑽功能,支持在資產列表中直接查看、修改和刪除歷史交易流水。
|
||
- 在主行 `TableRow` 下方,根據 `expandedIds[pos.assetId]` 條件渲染第二個子行,使用 `<TableCell colSpan={8} className="p-0">` 確保子行佔滿整行寬度。
|
||
- 構建流水明細次級表格:遍歷 `pos.transactions` 數組,表頭為「交易日期 | 類型 | 價格/數量 | 手續費 | 備註 | 操作」,精確渲染每筆交易的歷史數據。
|
||
- 實裝 `UpdateTransactionDialog` 組件:「修改」按鈕打開彈窗並回顯該筆流水數據(數量、價格、手續費、幣種、執行時間),提交後調用 `updateTransaction` Action 並刷新頁面。
|
||
- 實裝行內刪除功能:「刪除」按鈕彈出確認對話框,確認後調用 `deleteTransaction` Action 並刷新頁面。
|
||
- 在 `portfolio.ts` 的 `TransactionRecord` 接口中補充 `id` 和 `fee` 字段,並在下發數據時包含完整的流水 ID,以支持行內 CRUD 操作。
|
||
- UI 優化:為展開的子表格添加左側彩色邊框 (`border-l-4 border-primary/20`) 與左側縮進 (`ml-2`) 增強層級感;展開/收起按鈕文字根據狀態動態切換(「收起」/「展開」)。
|
||
|
||
## Dashboard 表格化基礎重構 (Task 41)
|
||
- Dashboard 完成表格化基礎重構,徹底移除原有的卡片網格佈局,改用 shadcn/ui Table 組件構建高密度專業券商風格主表。
|
||
- 實裝了基於 ID 的行展開狀態管理邏輯:`useState<Record<string, boolean>>` 控制每行的展開/收起狀態,點擊行或展開按鈕觸發 `toggleExpand`。
|
||
- 表頭嚴格對齊專業標準:名稱/代碼 | 現價 | 市值 | 持倉 | 攤薄/成本 | 浮動盈虧 | 累計盈虧 | 操作。
|
||
- 操作列包含展開/收起指示器與「添加」按鈕,點擊打開 `AddTransactionDialog` 並預選對應資產。
|
||
- 展開行內嵌交易記錄子表格,展示每筆交易的類型、數量、價格、手續費、幣種與日期。
|
||
- 重構 `AddTransactionDialog` 組件支持外部控制開關(`open`/`onOpenChange`/`defaultAssetId` props),同時保持向後兼容內部狀態管理模式。
|
||
- Dashboard 頁面轉換為客戶端組件,使用 `useEffect` 在客戶端加載組合數據。
|
||
|
||
## 全面重構資產展示 UI (Task 39)
|
||
- UI 全面升級,復刻專業券商級數據排版,合併攤薄/成本,引入原生幣種盈虧百分比展示。
|
||
- 徹底清理所有帶 (CNY) 和 (USD) 混雜的舊布局,所有 Native 金額根據 `baseCurrency` 渲染正確貨幣符號(USD→$、CNY/HKD→HK$、JPY→¥)。
|
||
- 資產卡片全新字段:現價、市值、持倉、攤薄/成本(合併為 `[dilutedCostNative] / [avgCostNative]` 格式)、浮動盈虧(帶百分比)、累計盈虧(帶百分比)、持倉天數。
|
||
- 盈虧顏色遵循中國市場慣例:大於 0 顯示紅色,小於 0 顯示綠色。
|
||
- 所有百分比保留 2 位小數,0 值正常顯示 `0.00`。
|
||
- 卡片佈局優化為響應式 `grid-cols-1 md:grid-cols-2 lg:grid-cols-3`。
|
||
|
||
## 基於文本域粘貼的歷史價格批量導入功能 (Bulk Import, Task 49)
|
||
- 開發了基於文本域粘貼的歷史價格批量導入功能,支持從 Excel 快速複製錄入每日淨價。
|
||
- 在 `src/actions/market.ts` 中新增 `importHistoricalPrices(assetId, data)` Server Action,遍歷數據數組對 `assetPricesHistory` 表執行批量 Upsert(基於 `(assetId, date)` 聯合唯一索引的 `onConflictDoUpdate`),衝突時用新價格覆蓋舊價格。
|
||
- 前端 `app/dashboard/page.tsx` 的持倉明細表「操作」列與展開區域均新增【導入價格】按鈕。
|
||
- 點擊按鈕彈出 Dialog,內含 `<textarea>` 文本域,支持用戶從 Excel 直接複製粘貼,格式為 `YYYY-MM-DD, 價格`(每行一條)。
|
||
- 前端按換行和逗號解析文本,生成 `{date, price}` 數組後調用 `importHistoricalPrices` Action,導入完成後 Toast 提示成功/失敗條數並刷新頁面。
|
||
|
||
## 历史净值重构引擎 - 底层查询辅助函数 (Task 50)
|
||
- 为历史净值重构引擎开发底层查询辅助函数,实现特定日期的持仓快照与基于降序 Limit 1 的价格断点结转逻辑。
|
||
- 在 `src/actions/snapshots.ts` 中新增 `getHistoricalPositions(targetDate)` 函数:从 `transactions` 表查询所有 `executedAt <= targetDate` 的流水,按时间正序遍历,按资产聚合计算出该日期下的 `quantity`(当前持仓)和 `totalCost`(累计投入本金,SELL 时按平均成本扣减),过滤掉已清仓资产。
|
||
- 在 `src/actions/snapshots.ts` 中新增 `getEffectivePrice(assetId, targetDate)` 函数:在 `assetPricesHistory` 表中查询指定 `assetId` 且 `date <= targetDate` 的记录,按照 `date` 降序排列 (`desc`) 并 `.limit(1)` 取第一条,实现价格断点结转(Forward-Fill)逻辑——如果目标当天没有导入价格,系统自动抓取该资产在目标日期之前「最新」的一次有效价格。
|
||
- 两个函数均使用 `Big.js` 进行高精度数值计算,为历史净值时光机功能提供底层数据支撑。
|
||
|
||
## 修复 Drizzle ORM 中 lte 操作符的语法调用错误 (Task 50d)
|
||
- 修复 Drizzle ORM 中 lte 操作符的语法调用错误,从链式调用更正为标准纯函数导入。
|
||
- 在 `src/actions/snapshots.ts` 的 `getHistoricalPositions` 函数中,将 `transactions.executedAt.lte(targetDate)` 替换为 `lte(transactions.executedAt, targetDate)`。
|
||
- 同时修复 `getSnapshots` 函数中的 `portfolioSnapshots.date.lte(endDate)` 为 `lte(portfolioSnapshots.date, endDate)`。
|
||
- `lte` 操作符已从文件顶部 `drizzle-orm` 引入,无需额外添加导入。
|
||
|
||
## 净值时光机主引擎 - Day-by-Day 循环遍历重建 (Task 50b)
|
||
- 完成净值时光机主引擎,通过 Day-by-Day 循环遍历历史流水并结合断点结转价格,自动重建全量历史资产快照。
|
||
- 在 `src/actions/snapshots.ts` 中新增 `reconstructPortfolioHistory()` 函数:查询 `transactions` 表找出最早的 `executedAt` 作为回溯起点,转换为 `Asia/Shanghai` 时区后以天为单位循环至今天。
|
||
- 循环体内调用 `getHistoricalPositions(currentDate)` 获取当天所有有持仓的资产(含持仓数量与累计本金),再调用 `getEffectivePrice(assetId, currentDate)` 获取各资产的有效价格(断点结转)。
|
||
- 引入汇率转换逻辑:预先加载 `assets` 表获取各资产的基础币种,加载 `exchangeRates` 表构建汇率映射,支持直接汇率与 USD 交叉换算,将各资产市值统一换算为 CNY。
|
||
- 使用 `Big.js` 确保所有金额计算的高精度,按天计算 `totalValueCny`(总市值)与 `totalCostCny`(总本金),并通过 Upsert 逻辑写入 `portfolioSnapshots` 表,确保每天仅存一条记录。
|
||
|
||
## Dashboard 首页实装"重构历史走势"功能按钮 (Task 50c)
|
||
- 在 `app/dashboard/page.tsx` 的"总资产概览"卡片右上角挂载"重构历史走势"按钮 (Button variant="outline")。
|
||
- 点击按钮后调用 `reconstructPortfolioHistory()` Server Action,启动 Day-by-Day 历史净值回溯引擎。
|
||
- 集成 Sonner Toast 通知:点击时显示 `toast.loading('正在重构历史走势...')`,完成后显示 `toast.success('重构成功,已填充 N 天历史数据')`,并自动刷新 `snapshots` 状态以更新 AreaChart 走势图。
|
||
- 按钮启用 `isPending` 防重复点击,重构期间显示"重构中..."并禁用按钮。
|
||
- 打通历史净值回溯全链路:用户从 Dashboard 首页一键触发,底层引擎自动从最早交易日起逐天计算持仓与价格,填充 `portfolio_snapshots` 表,前端图表实时渲染历史波动曲线。
|
||
|
||
## 修复时光机引擎的变量泄漏与日期补零问题 (Task 51)
|
||
- 修复了时光机循环引擎中的变量作用域泄漏导致错误复用 BTC 价格的 Bug,并标准化了 YYYY-MM-DD 日期补零逻辑以修复 SQL 字符串对比错误。
|
||
- 在 `src/actions/snapshots.ts` 中新增 `formatDateString(date)` 辅助函数,使用 `padStart(2, '0')` 严格保证月份和日期补零,替代各处散落的 `toISOString().split('T')[0]` 调用。
|
||
- 修复 `getHistoricalPositions` 和 `getEffectivePrice` 中的日期字符串生成逻辑:统一使用 `formatDateString(targetDate)`,确保 Drizzle 的 `lte` 查询中左右两边日期字符串格式一致(均为 `YYYY-MM-DD`),修复了 UTC 与本地时区混用导致的查询偏移。
|
||
- 修复 `reconstructPortfolioHistory` 主循环:while 条件与 `dateStr` 生成均改用 `formatDateString(currentDate)`,确保与 `getTodayInShanghai()` 返回格式完全一致。
|
||
- 在资产遍历循环中增加兜底防御逻辑:当 `getEffectivePrice` 返回 null 时,使用 `assetLatestPriceMap` 中缓存的 `latestPrice` 作为兜底价格,避免价格变量为 undefined 或沿用上一资产的值;同时修正了 `totalCostCny` 在 `priceStr` 为空时不应累加的 Bug。
|
||
- 在 `allAssets` 查询中新增 `latestPrice` 字段,构建 `assetLatestPriceMap` 供兜底逻辑使用。
|
||
|
||
## 修复 getEffectivePrice 引擎中 Drizzle ORM 缺失 and(eq(assetId)) 的致命逻辑漏网
|
||
- 修复 `getEffectivePrice` 引擎中 Drizzle ORM 查询条件未使用 `and()` 复合操作符的致命逻辑漏网,确保历史断点结转价格精准匹配单一资产。
|
||
- 在 `src/actions/snapshots.ts` 顶部从 `drizzle-orm` 引入 `and` 操作符,将 `getEffectivePrice` 的 `where` 子句从两个独立的 `.where()` 链式调用重构为 `and(eq(assetPricesHistory.assetId, assetId), lte(assetPricesHistory.date, dateStr))` 的显式复合条件。
|
||
- 修复后 `assetId` 条件与日期条件被绝对锁定在同一个 `AND` 逻辑块中,彻底杜绝了周末等非交易日历史节点价格跨资产串联的荒谬市值问题(如海尔被错误匹配 BTC 价格导致 3700 万市值)。
|
||
|
||
## 通过前端运行时覆盖策略完美对齐图表今日快照与实时总资产的汇率时差 (Task 54)
|
||
- 修复了 Dashboard 图表末端(Today 节点)与页面顶部超大号"总资产"数字之间存在时差误差的问题:图表依赖数据库里几小时前的快照,而概览数字基于实时计算,导致两者不完全一致。
|
||
- 在 `app/dashboard/page.tsx` 的 `loadSnapshots` 中引入前端运行时覆盖策略:先调用 `getPortfolioSummary()` 获取实时总览数据,再调用 `getSnapshots()` 获取历史快照,动态替换或追加今天的节点,确保图表末端与大数字 100% 严丝合缝。
|
||
- 具体逻辑:如果今天已有快照记录(`lastSnapshot.date === todayStr`),则将 `totalValueCny` 和 `totalCostCny` 覆盖为实时值;如果今天尚无快照,则直接追加一个实时点。
|
||
- 移除 `getSnapshots` 查询的视口限制:将 `src/actions/snapshots.ts` 中 `getSnapshots` 的默认 `limit` 从 365 改为无默认值,仅在显式传入 `limit` 参数时才应用 `.limit()`,前端调用处移除 `{ limit: 30 }` 参数,实现从第一笔交易至今的全量净值走势渲染。
|
||
- 前端 Dashboard 页面中两处 `getSnapshots` 调用(初始加载与重构历史后刷新)均已移除 `limit` 参数。
|
||
|
||
## 建立无副作用的 Utils 财务引擎 (Task 56a)
|
||
- 在 `src/utils/` 目录下新建 `finance.ts`,实现纯函数财务计算器,不涉及任何数据库查询和后端 API。
|
||
- 文件顶部绝对禁止出现 `"use server"` 指令,确保为通用的前端/后端都能调用的纯函数。
|
||
- 引入 `big.js` 用于高精度计算,编写并导出 `calculateAssetMetrics` 函数。
|
||
- 核心算法:强制按时间升序排序流水,遍历推演 BUY/SELL/DIVIDEND 三种交易类型,支持加权均价计算与清仓重置逻辑。
|
||
- 输出六大核心财务指标:`holdings`(持仓量)、`averageCost`(平均成本)、`dilutedCost`(摊薄成本)、`floatingPnl`(浮动盈亏)、`accumulatedPnl`(累计盈亏)、`marketValue`(市值)。
|
||
|
||
## 打通 Dashboard 与 finance utils 的数据链路 (Task 56b)
|
||
- 在 `src/actions/portfolio.ts` 顶部引入 `calculateAssetMetrics` 工具函数,实现财务引擎接入。
|
||
- 重构 `getPortfolioPositions` 的第二个循环:对每个资产调用 `calculateAssetMetrics(transactions, latestPrice)`,将返回的 `holdings`、`averageCost`、`dilutedCost`、`floatingPnl`、`accumulatedPnl`、`marketValue` 映射到 Position 对象的 Native 币种字段。
|
||
- Dashboard 表格字段精确对齐:现價→`latestPrice`、市值→`metrics.marketValue`、攤薄/成本→`metrics.dilutedCost / metrics.averageCost`、浮動盈虧→`metrics.floatingPnl`、累計盈虧→`metrics.accumulatedPnl`。
|
||
- 累计盈亏验证公式:`accumulatedPnl = marketValue + 卖出/分红现金 - 总投入`,确保有卖出或分红记录的资产(如英特尔、分红ETF)数据精确。
|
||
|
||
## 修复 Cron API 的 404 挂载丢失问题 (Task 61b)
|
||
- 验证并确认 Next.js App Router API 路由已严格遵循规范:文件精确位于 `src/app/api/cron/fetch-prices/route.ts`,后缀为 `.ts`(非 `.tsx`)。
|
||
- 确认最外层正确导出 `export async function GET(request: Request)` 方法,包含 `Bearer ${process.env.CRON_SECRET}` 鉴权拦截与完整的 try/catch 错误处理。
|
||
- 确认无旧版 `pages/api/cron/fetch-prices` 残留文件导致路由冲突。
|
||
- 修复 Next.js App Router 规范下的 API 路由挂载问题,修正 route.ts 文件名与 GET 方法导出,解决 404 错误。
|
||
|
||
## 修复 getPortfolioPositions 中接入财务引擎时的变量作用域丢失与解构映射错误 (Task 56c)
|
||
- 修复了 `src/actions/portfolio.ts` 中 `getPortfolioPositions` 函数的 ReferenceError:`avgCost is not defined` 和 `dilutedCost is not defined`。
|
||
- 根本原因:在将财务引擎 (`calculateAssetMetrics`) 接入 portfolio 引擎时,`avgCost` 和 `dilutedCost` 变量名在结果对象装配环节被直接引用,但它们从未在本作用域中声明——它们实际上是 `metrics` 对象的属性 (`metrics.averageCost`, `metrics.dilutedCost`)。
|
||
- 核心修复:将 `avgCost: avgCost.toString()` 替换为 `avgCost: metrics.averageCost`,将 `dilutedCost: dilutedCost.toString()` 替换为 `dilutedCost: metrics.dilutedCost`。
|
||
- 同时新增 `floatingPnl` 和 `accumulatedPnl` 字段映射到 Position 接口,补齐了财务引擎产出的六大核心指标中缺失的两个字段。
|
||
- 遵循 `metrics` 返回值已是 string 类型的规范,不再调用 `.toString()` 导致冗余转换。 |