refactor(ui): 重塑 Dashboard 数据流,剔除违规本金反推逻辑,应用顺向盈亏计算
This commit is contained in:
parent
5a056a238c
commit
8b76ec9a6d
11
Memory.md
11
Memory.md
@ -400,4 +400,13 @@
|
||||
## 精确定位 Client Component,修复 net-worth-chart.tsx 中的 dataKey 与 Tooltip 绑定错误,彻底解决视图层与数据层本金单位不统一的问题 (Task 80)
|
||||
- **根因分析**:在 `src/components/dashboard/net-worth-chart.tsx`(Client Component)中,净值走势图的投入本金曲线和 Tooltip 需要读取经过汇率折算后的法币本金字段(`totalCostCny`),而非原币种或未经折算的字段。
|
||||
- **数据链路验证**:从数据库 `portfolio_snapshots.total_cost_cny` → Drizzle ORM 映射为 `totalCostCny` → `getSnapshots()` 返回 → `page.tsx` 的 `loadSnapshots()` 中计算 `totalCostCny = totalCnyValue - totalPnlCny` → 通过 props 传入 `NetWorthChart` → `chartData` 映射为 `totalCostCny` → `<Area dataKey="totalCostCny">` 渲染 + `CustomTooltip` 从 `payload[0].payload.totalCostCny` 读取。
|
||||
- **修复验证**:`Snapshot` 接口定义 `totalValueCny` / `totalCostCny`,`chartData` 映射使用 `totalValueCny` / `totalCostCny`(均 `parseFloat`),`<Area>` 的 `dataKey` 分别为 `totalValueCny` 和 `totalCostCny`,`CustomTooltip` 从 `data.totalValueCny` / `data.totalCostCny` 解构计算净盈亏。全链路字段名严格一致,确保投入本金曲线显示真实的累计投入成本(CNY 折算后)。
|
||||
- **修复验证**:`Snapshot` 接口定义 `totalValueCny` / `totalCostCny`,`chartData` 映射使用 `totalValueCny` / `totalCostCny`(均 `parseFloat`),`<Area>` 的 `dataKey` 分别为 `totalValueCny` 和 `totalCostCny`,`CustomTooltip` 从 `data.totalValueCny` / `data.totalCostCny` 解构计算净盈亏。全链路字段名严格一致,确保投入本金曲线显示真实的累计投入成本(CNY 折算后)。
|
||||
|
||||
## 剔除 page.tsx 中违规的 `本金 = 市值 - 盈亏` 反向派生逻辑,确立 `盈亏 = 市值 - 本金` 的顺向金融计算流,彻底修复了走势图本金显示被旧 PnL 污染的架构 Bug (Task 81)
|
||||
- **根因分析**:在 `app/dashboard/page.tsx` 的 `loadSnapshots()` 函数中(第 178 行),今日快照的 `totalCostCny` 被错误地通过 `new Big(summary.totalCnyValue).minus(new Big(summary.totalPnlCny)).toString()` 反向推导得出。这导致:1) 本金被旧 PnL 数据污染,失去底层真实性;2) 违反了金融计算中"本金优先"的架构红线。
|
||||
- **架构红线**:绝对禁止反推本金。本金 (`totalCostCny`) 必须直接读取数据库 snapshot 表或从 position 层级的 `totalCostCny` 字段正和累加,绝不允许做任何加减法。
|
||||
- **具体修复**:
|
||||
- 在 `src/actions/portfolio.ts` 的 `getPortfolioSummary()` 函数中新增 `totalCostCny` 的逐项累加计算:`totalCostCny = sum(pos.totalCostCny)`,并在返回值中暴露 `totalCostCny` 字段。
|
||||
- 在 `app/dashboard/page.tsx` 的 `loadSnapshots()` 函数中,删除 `new Big(summary.totalCnyValue).minus(new Big(summary.totalPnlCny))` 的反向推导代码,改为直接使用 `summary.totalCostCny` 作为快照的本金值。
|
||||
- 净盈亏在 `NetWorthChart` 的 `CustomTooltip` 中通过 `pnl = totalValue - totalCost` 顺向派生,符合"盈亏 = 市值 - 本金"的金融计算规范。
|
||||
- **验收标准**:鼠标悬浮在图表上,投入本金显示底层原汁原味的成本值(如 ¥5094.59),净盈亏自动修正为真实值(如 +¥472.12)。
|
||||
@ -171,20 +171,19 @@ export default function DashboardPage() {
|
||||
|
||||
useEffect(() => {
|
||||
async function loadSnapshots() {
|
||||
await recordDailySnapshot();
|
||||
const summary = await getPortfolioSummary();
|
||||
await recordDailySnapshot();
|
||||
const data = await getSnapshots();
|
||||
const todayStr = new Date().toISOString().slice(0, 10);
|
||||
const totalCostCny = new Big(summary.totalCnyValue).minus(new Big(summary.totalPnlCny)).toString();
|
||||
const lastSnapshot = data[data.length - 1];
|
||||
if (lastSnapshot && lastSnapshot.date === todayStr) {
|
||||
lastSnapshot.totalValueCny = summary.totalCnyValue;
|
||||
lastSnapshot.totalCostCny = totalCostCny;
|
||||
lastSnapshot.totalCostCny = summary.totalCostCny;
|
||||
} else {
|
||||
data.push({
|
||||
date: todayStr,
|
||||
totalValueCny: summary.totalCnyValue,
|
||||
totalCostCny,
|
||||
totalCostCny: summary.totalCostCny,
|
||||
});
|
||||
}
|
||||
setSnapshots(data);
|
||||
|
||||
@ -468,11 +468,13 @@ export async function getPortfolioSummary(includeCleared: boolean = false) {
|
||||
let totalCnyValue = new Big('0');
|
||||
let totalPnlCny = new Big('0');
|
||||
let totalFloatingPnlCny = new Big('0');
|
||||
let totalCostCny = new Big('0');
|
||||
|
||||
for (const pos of positions) {
|
||||
totalCnyValue = totalCnyValue.plus(new Big(pos.marketValueCny || '0'));
|
||||
totalPnlCny = totalPnlCny.plus(new Big(pos.accumulatedPnlCny || '0'));
|
||||
totalFloatingPnlCny = totalFloatingPnlCny.plus(new Big(pos.floatingPnlCny || '0'));
|
||||
totalCostCny = totalCostCny.plus(new Big(pos.totalCostCny || '0'));
|
||||
}
|
||||
|
||||
const chartData = positions.map((pos, index) => ({
|
||||
@ -531,6 +533,7 @@ export async function getPortfolioSummary(includeCleared: boolean = false) {
|
||||
return {
|
||||
positions,
|
||||
totalCnyValue: totalCnyValue.toString(),
|
||||
totalCostCny: totalCostCny.toString(),
|
||||
totalPnlCny: totalPnlCny.toString(),
|
||||
unrealizedPnlCny: totalFloatingPnlCny.toString(),
|
||||
chartData,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user