fix(ledger): 修复汇率转换的双标幻觉,统一大盘与快照的财务聚合基准
This commit is contained in:
+13
-21
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user