fix(ledger): 补全汇率折算乘数,修复跨币种直接相加导致的盈亏总额失真
This commit is contained in:
parent
a3b5563db2
commit
52a94a9ffa
@ -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` 间接推导导致的精度损失。
|
||||
|
||||
@ -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<Position[]> {
|
||||
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<Position[]> {
|
||||
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(),
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user