diff --git a/Memory.md b/Memory.md index 456cd1e..e8041c0 100644 --- a/Memory.md +++ b/Memory.md @@ -1,5 +1,11 @@ # Omniledger 架构与开发记忆 (Memory) +## 全局修复多币种聚合漏洞,强制叠加汇率乘数 (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` 间接推导导致的精度损失。 diff --git a/src/actions/portfolio.ts b/src/actions/portfolio.ts index fda81f6..21e09ed 100644 --- a/src/actions/portfolio.ts +++ b/src/actions/portfolio.ts @@ -24,8 +24,12 @@ interface Position { realizedPnlCny: string; avgCost: string; dilutedCost: string; + dilutedCostCny: string; floatingPnl: string; + floatingPnlCny: string; accumulatedPnl: string; + accumulatedPnlCny: string; + marketValueCny: string; holdingDays: number; exchange: string; accumulatedDividendsCny: string; @@ -326,6 +330,17 @@ export async function getPortfolioPositions(): Promise { holding.latestPrice ); + // 获取资产对人民币的汇率 + const fxRate = new Big( + getRate(rateMap, holding.baseCurrency, 'CNY') || '1' + ); + + // 将引擎返回的原生币种金额折算为 CNY + const marketValueCny = new Big(metrics.marketValue).times(fxRate).toString(); + const floatingPnlCny = new Big(metrics.floatingPnl).times(fxRate).toString(); + const accumulatedPnlCny = new Big(metrics.accumulatedPnl).times(fxRate).toString(); + const dilutedCostCny = new Big(metrics.dilutedCost).times(fxRate).toString(); + const holdingNative = new Big(metrics.holdings); const avgCostNative = new Big(metrics.averageCost); const dilutedCostNative = new Big(metrics.dilutedCost); @@ -370,8 +385,12 @@ export async function getPortfolioPositions(): Promise { realizedPnlCny: holding.realizedPnlCny.toString(), avgCost: metrics.averageCost, dilutedCost: metrics.dilutedCost, + dilutedCostCny, floatingPnl: metrics.floatingPnl, + floatingPnlCny, accumulatedPnl: metrics.accumulatedPnl, + accumulatedPnlCny, + marketValueCny, holdingDays, exchange: holding.exchange, accumulatedDividendsCny: holding.accumulatedDividendsCny.toString(), diff --git a/src/actions/snapshots.ts b/src/actions/snapshots.ts index e527f33..e6734e9 100644 --- a/src/actions/snapshots.ts +++ b/src/actions/snapshots.ts @@ -333,8 +333,16 @@ export async function reconstructPortfolioHistory() { const metrics = calculateAssetMetrics(assetTxs, cnyPrice); - totalValueCny = totalValueCny.plus(metrics.marketValue); - totalCostCny = totalCostCny.plus(metrics.totalInvested); + // 获取资产对人民币的汇率 + const assetFxRate = new Big(getRate(baseCurrency, 'CNY') || '1'); + + // marketValue 已使用 CNY 价格计算,直接取用 + // totalInvested 基于原始币种价格,需乘以汇率折算为 CNY + const posValueCny = new Big(metrics.marketValue); + const posCostCny = new Big(metrics.totalInvested).times(assetFxRate); + + totalValueCny = totalValueCny.plus(posValueCny); + totalCostCny = totalCostCny.plus(posCostCny); } const existing = await db