feat(ledger): 接入计算引擎,实现 Dashboard 指标的精确数据对齐
This commit is contained in:
parent
d60659df18
commit
9ce398efb1
@ -187,3 +187,9 @@
|
|||||||
- 引入 `big.js` 用于高精度计算,编写并导出 `calculateAssetMetrics` 函数。
|
- 引入 `big.js` 用于高精度计算,编写并导出 `calculateAssetMetrics` 函数。
|
||||||
- 核心算法:强制按时间升序排序流水,遍历推演 BUY/SELL/DIVIDEND 三种交易类型,支持加权均价计算与清仓重置逻辑。
|
- 核心算法:强制按时间升序排序流水,遍历推演 BUY/SELL/DIVIDEND 三种交易类型,支持加权均价计算与清仓重置逻辑。
|
||||||
- 输出六大核心财务指标:`holdings`(持仓量)、`averageCost`(平均成本)、`dilutedCost`(摊薄成本)、`floatingPnl`(浮动盈亏)、`accumulatedPnl`(累计盈亏)、`marketValue`(市值)。
|
- 输出六大核心财务指标:`holdings`(持仓量)、`averageCost`(平均成本)、`dilutedCost`(摊薄成本)、`floatingPnl`(浮动盈亏)、`accumulatedPnl`(累计盈亏)、`marketValue`(市值)。
|
||||||
|
|
||||||
|
## 打通 Dashboard 与 finance utils 的数据链路 (Task 56b)
|
||||||
|
- 在 `src/actions/portfolio.ts` 顶部引入 `calculateAssetMetrics` 工具函数,实现财务引擎接入。
|
||||||
|
- 重构 `getPortfolioPositions` 的第二个循环:对每个资产调用 `calculateAssetMetrics(transactions, latestPrice)`,将返回的 `holdings`、`averageCost`、`dilutedCost`、`floatingPnl`、`accumulatedPnl`、`marketValue` 映射到 Position 对象的 Native 币种字段。
|
||||||
|
- Dashboard 表格字段精确对齐:现價→`latestPrice`、市值→`metrics.marketValue`、攤薄/成本→`metrics.dilutedCost / metrics.averageCost`、浮動盈虧→`metrics.floatingPnl`、累計盈虧→`metrics.accumulatedPnl`。
|
||||||
|
- 累计盈亏验证公式:`accumulatedPnl = marketValue + 卖出/分红现金 - 总投入`,确保有卖出或分红记录的资产(如英特尔、分红ETF)数据精确。
|
||||||
@ -4,6 +4,7 @@ import { db } from '@/db';
|
|||||||
import { transactions, assets, exchangeRates } from '@/db/schema';
|
import { transactions, assets, exchangeRates } from '@/db/schema';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { asc, eq } from 'drizzle-orm';
|
import { asc, eq } from 'drizzle-orm';
|
||||||
|
import { calculateAssetMetrics } from '@/utils/finance';
|
||||||
|
|
||||||
interface Position {
|
interface Position {
|
||||||
assetId: string;
|
assetId: string;
|
||||||
@ -312,54 +313,35 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
|||||||
// 总盈亏 (CNY)
|
// 总盈亏 (CNY)
|
||||||
const totalPnlCny = unrealizedPnlCny.plus(holding.realizedPnlCny).plus(holding.accumulatedDividendsCny);
|
const totalPnlCny = unrealizedPnlCny.plus(holding.realizedPnlCny).plus(holding.accumulatedDividendsCny);
|
||||||
|
|
||||||
// Native 原生币种计算
|
const metrics = calculateAssetMetrics(
|
||||||
const marketValueNative = new Big(holding.latestPrice).times(holding.quantity);
|
holding.transactions.map(tx => ({
|
||||||
const currentNativeValue = marketValueNative;
|
date: tx.executedAt ?? new Date(),
|
||||||
|
txType: tx.txType,
|
||||||
|
quantity: tx.quantity,
|
||||||
|
price: tx.price,
|
||||||
|
fee: tx.fee,
|
||||||
|
})),
|
||||||
|
holding.latestPrice
|
||||||
|
);
|
||||||
|
|
||||||
// 平均成本 (Native) = 总买入成本 (Native) / 总买入数量
|
const holdingNative = new Big(metrics.holdings);
|
||||||
let avgCostNative = new Big('0');
|
const avgCostNative = new Big(metrics.averageCost);
|
||||||
if (holding.totalBuyQuantity.gt(0)) {
|
const dilutedCostNative = new Big(metrics.dilutedCost);
|
||||||
avgCostNative = holding.totalBuyCostNative.div(holding.totalBuyQuantity);
|
const floatingPnlNative = new Big(metrics.floatingPnl);
|
||||||
}
|
const cumulativePnlNative = new Big(metrics.accumulatedPnl);
|
||||||
|
const marketValueNative = new Big(metrics.marketValue);
|
||||||
|
|
||||||
// 摊薄成本 (Native) = (总买入成本 - 已实现盈亏 - 累计分红) / 当前持仓数量
|
|
||||||
let dilutedCostNative = new Big('0');
|
|
||||||
if (holding.quantity.gt(0)) {
|
|
||||||
dilutedCostNative = holding.totalBuyCostNative.minus(holding.realizedPnlNative).minus(holding.accumulatedDividendsNative).div(holding.quantity);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 浮动盈亏 (Native) = 市值 - (平均成本 * 当前持仓数量)
|
|
||||||
const floatingPnlNative = marketValueNative.minus(avgCostNative.times(holding.quantity));
|
|
||||||
|
|
||||||
// 浮动盈亏百分比 (Native)
|
|
||||||
let floatingPnlPercent = new Big('0');
|
let floatingPnlPercent = new Big('0');
|
||||||
const avgCostBasisNative = avgCostNative.times(holding.quantity);
|
const avgCostBasisNative = avgCostNative.times(holdingNative);
|
||||||
if (avgCostBasisNative.gt(0)) {
|
if (avgCostBasisNative.gt(0)) {
|
||||||
floatingPnlPercent = floatingPnlNative.div(avgCostBasisNative).times(new Big('100'));
|
floatingPnlPercent = floatingPnlNative.div(avgCostBasisNative).times(new Big('100'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 累计盈亏 (Native) = 浮动盈亏 + 已实现盈亏 + 累计分红
|
|
||||||
const cumulativePnlNative = floatingPnlNative.plus(holding.realizedPnlNative).plus(holding.accumulatedDividendsNative);
|
|
||||||
|
|
||||||
// 累计盈亏百分比 (Native) = 累计盈亏 / 总买入成本 * 100
|
|
||||||
let cumulativePnlPercent = new Big('0');
|
let cumulativePnlPercent = new Big('0');
|
||||||
if (holding.totalBuyCostNative.gt(0)) {
|
if (holding.totalBuyCostNative.gt(0)) {
|
||||||
cumulativePnlPercent = cumulativePnlNative.div(holding.totalBuyCostNative).times(new Big('100'));
|
cumulativePnlPercent = cumulativePnlNative.div(holding.totalBuyCostNative).times(new Big('100'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 平均成本 (CNY) 保留兼容
|
|
||||||
let avgCost = new Big('0');
|
|
||||||
if (holding.totalBuyQuantity.gt(0)) {
|
|
||||||
avgCost = holding.totalBuyCostCny.div(holding.totalBuyQuantity);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 摊薄成本 (CNY) 保留兼容
|
|
||||||
let dilutedCost = new Big('0');
|
|
||||||
if (holding.quantity.gt(0)) {
|
|
||||||
dilutedCost = holding.totalBuyCostCny.minus(holding.realizedPnlCny).minus(holding.accumulatedDividendsCny).div(holding.quantity);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 持仓天数
|
|
||||||
let holdingDays = 0;
|
let holdingDays = 0;
|
||||||
if (holding.firstBuyDate) {
|
if (holding.firstBuyDate) {
|
||||||
const diffMs = today.getTime() - holding.firstBuyDate.getTime();
|
const diffMs = today.getTime() - holding.firstBuyDate.getTime();
|
||||||
@ -374,7 +356,7 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
|||||||
symbol: holding.symbol,
|
symbol: holding.symbol,
|
||||||
name: holding.name,
|
name: holding.name,
|
||||||
type: holding.type,
|
type: holding.type,
|
||||||
quantity: holding.quantity.toString(),
|
quantity: holdingNative.toString(),
|
||||||
baseCurrency: holding.baseCurrency,
|
baseCurrency: holding.baseCurrency,
|
||||||
cnyValue: cnyValue.toString(),
|
cnyValue: cnyValue.toString(),
|
||||||
totalCostCny: holding.totalBuyCostCny.toString(),
|
totalCostCny: holding.totalBuyCostCny.toString(),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user