fix(ledger): 修复汇率转换的双标幻觉,统一大盘与快照的财务聚合基准

This commit is contained in:
2026-05-01 05:52:09 +08:00
parent 52a94a9ffa
commit b4f21e7cd6
4 changed files with 46 additions and 33 deletions
+13 -21
View File
@@ -416,28 +416,20 @@ export async function getPortfolioPositions(): Promise<Position[]> {
export async function getPortfolioSummary() {
const positions = await getPortfolioPositions();
const totalCnyValue = positions.reduce(
(sum, pos) => sum.plus(new Big(pos.cnyValue)),
new Big('0')
);
// 单一事实来源:复用 getPortfolioPositions 已汇率折算的结果
let totalCnyValue = new Big('0');
let totalPnlCny = new Big('0');
let totalFloatingPnlCny = new Big('0');
const totalPnlCny = positions.reduce(
(sum, pos) => sum.plus(new Big(pos.pnlCny)),
new Big('0')
);
const unrealizedPnlCny = positions.reduce(
(sum, pos) => {
const totalPnl = new Big(pos.pnlCny);
const realized = new Big(pos.realizedPnlCny);
return sum.plus(totalPnl.minus(realized));
},
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'));
}
const chartData = positions.map((pos, index) => ({
name: pos.symbol,
value: new Big(pos.cnyValue).toNumber(),
value: new Big(pos.marketValueCny || '0').toNumber(),
fill: [
'#3b82f6',
'#8b5cf6',
@@ -458,11 +450,11 @@ export async function getPortfolioSummary() {
const market = getMarketFromExchange(pos.exchange);
const existing = marketMap.get(market);
if (existing) {
existing.totalCnyValue = existing.totalCnyValue.plus(new Big(pos.cnyValue));
existing.totalCnyValue = existing.totalCnyValue.plus(new Big(pos.marketValueCny || '0'));
} else {
marketMap.set(market, {
market,
totalCnyValue: new Big(pos.cnyValue),
totalCnyValue: new Big(pos.marketValueCny || '0'),
});
}
}
@@ -492,7 +484,7 @@ export async function getPortfolioSummary() {
positions,
totalCnyValue: totalCnyValue.toString(),
totalPnlCny: totalPnlCny.toString(),
unrealizedPnlCny: unrealizedPnlCny.toString(),
unrealizedPnlCny: totalFloatingPnlCny.toString(),
chartData,
marketAllocation,
};
+13 -12
View File
@@ -26,13 +26,19 @@ function getTodayInShanghai(): string {
export async function recordDailySnapshot() {
const positions = await getPortfolioPositions();
// 统一使用 engine 输出的 marketValueCny / accumulatedPnlCny
const totalValueCny = positions.reduce(
(sum, pos) => sum.plus(pos.cnyValue || '0'),
(sum, pos) => sum.plus(new Big(pos.marketValueCny || '0')),
new Big(0)
).toString();
// 推导真实投入本金 CNY = 市值 - 累计盈亏
const totalCostCny = positions.reduce(
(sum, pos) => sum.plus(pos.totalCostCny || '0'),
(sum, pos) => {
const mv = new Big(pos.marketValueCny || '0');
const ap = new Big(pos.accumulatedPnlCny || '0');
return sum.plus(mv.minus(ap));
},
new Big(0)
).toString();
@@ -324,21 +330,16 @@ export async function reconstructPortfolioHistory() {
const priceStr = await getEffectivePrice(assetId, currentDate);
const baseCurrency = assetBaseCurrencyMap.get(assetId) || 'USD';
let cnyPrice: string;
if (!priceStr) {
cnyPrice = convertPriceToCny(assetLatestPriceMap.get(assetId) || '0', baseCurrency);
} else {
cnyPrice = convertPriceToCny(priceStr, baseCurrency);
}
const priceStrForMetrics = priceStr || assetLatestPriceMap.get(assetId) || '0';
const metrics = calculateAssetMetrics(assetTxs, cnyPrice);
const metrics = calculateAssetMetrics(assetTxs, priceStrForMetrics);
// 获取资产对人民币的汇率
const assetFxRate = new Big(getRate(baseCurrency, 'CNY') || '1');
// marketValue 已使用 CNY 价格计算,直接取用
// totalInvested 基于原始币种价格,需乘以汇率折算为 CNY
const posValueCny = new Big(metrics.marketValue);
// 统一汇率折算边界:calculateAssetMetrics 输出全部为 Base Currency
// 必须无例外地将所有金额字段乘以 fxRate 得到 CNY
const posValueCny = new Big(metrics.marketValue).times(assetFxRate);
const posCostCny = new Big(metrics.totalInvested).times(assetFxRate);
totalValueCny = totalValueCny.plus(posValueCny);