refactor!: support RUB only (WealthLedger/main#55) (#204)
BREAKING CHANGE: per-currency API fields replaced with single RUB Money.
This commit was merged in pull request #204.
This commit is contained in:
@@ -8,13 +8,12 @@ import {
|
||||
multiply,
|
||||
subtract,
|
||||
} from '#src/common/decimal';
|
||||
import { decimal } from '#src/integrations/tbank/decimal';
|
||||
import { decimal, rubMoney } from '#src/integrations/tbank/decimal';
|
||||
import {
|
||||
metadataKey,
|
||||
type TbankInstrument,
|
||||
type TbankMoney,
|
||||
TbankPortfolio,
|
||||
TbankPositions,
|
||||
} from '#src/integrations/tbank/tbank.api';
|
||||
|
||||
import type { Classification } from '../application/position-classification.service';
|
||||
@@ -22,6 +21,8 @@ import type { PortfolioDto, PortfolioPositionDto } from '../presentation/portfol
|
||||
|
||||
import { portfolioStructure } from '../domain/portfolio-structure';
|
||||
|
||||
const RUB = 'RUB';
|
||||
|
||||
const types: Record<string, string> = {
|
||||
share: 'STOCK',
|
||||
bond: 'BOND',
|
||||
@@ -35,32 +36,36 @@ const types: Record<string, string> = {
|
||||
|
||||
function mapPortfolio(
|
||||
upstream: NonNullable<TbankPortfolio['positions']>,
|
||||
cash: TbankMoney[],
|
||||
cashBalances: TbankMoney[],
|
||||
metadata: ReadonlyMap<string, { data?: null | TbankInstrument }>,
|
||||
lastSuccessfulAt: string,
|
||||
classification: ReadonlyMap<string, Classification> = new Map(),
|
||||
): PortfolioDto {
|
||||
const positions = upstream
|
||||
// Currency positions mirror cash balances and are non-RUB holdings by definition.
|
||||
.filter((position) => types[position.instrumentType?.toLowerCase() ?? ''] !== 'CURRENCY' &&
|
||||
(!position.currentPrice?.currency?.trim() || position.currentPrice.currency.trim().toUpperCase() === RUB))
|
||||
.map((position) => mapPosition(position, metadata.get(metadataKey(position))?.data ?? null, classification.get(metadataKey(position))))
|
||||
.filter((position): position is PortfolioPositionDto => position !== null);
|
||||
const balances = new Map<string, ReturnType<typeof decimal>>();
|
||||
for (const value of cash) {
|
||||
const currency = value.currency?.trim().toUpperCase();
|
||||
const amount = decimal(value);
|
||||
if (!currency || isZero(amount)) continue;
|
||||
balances.set(currency, add(balances.get(currency) ?? decimal({}), amount));
|
||||
let cashTotal = decimal({});
|
||||
for (const value of cashBalances) {
|
||||
if (value.currency?.trim().toUpperCase() !== RUB) continue;
|
||||
cashTotal = add(cashTotal, decimal(value));
|
||||
}
|
||||
const cashBalances = [...balances].map(([currency, value]) => ({
|
||||
currency,
|
||||
value: formatDecimal(value),
|
||||
}));
|
||||
const cash: Money = { currency: RUB, value: formatDecimal(cashTotal) };
|
||||
let total = cashTotal;
|
||||
for (const position of positions) {
|
||||
if (!position.currentValue) continue;
|
||||
total = add(total, decimalFromString(position.currentValue.value));
|
||||
}
|
||||
const totals: Money = { currency: RUB, value: formatDecimal(total) };
|
||||
return {
|
||||
lastSuccessfulAt,
|
||||
isStale: false,
|
||||
totalsByCurrency: totalsByCurrency(upstream, cash),
|
||||
totals,
|
||||
positions,
|
||||
cashBalances,
|
||||
structure: portfolioStructure(positions, cashBalances),
|
||||
cash,
|
||||
structure: portfolioStructure(positions, [cash]),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,16 +78,16 @@ function mapPosition(
|
||||
if (isZero(quantity)) return null;
|
||||
const figi = position.figi?.trim() || null;
|
||||
const instrumentUid = position.instrumentUid?.trim() || null;
|
||||
const currentPrice = money(position.currentPrice);
|
||||
const averagePurchasePrice = money(position.averagePositionPrice);
|
||||
const currentPrice = rubMoney(position.currentPrice);
|
||||
const averagePurchasePrice = rubMoney(position.averagePositionPrice);
|
||||
const currentValue = currentPrice && {
|
||||
currency: currentPrice.currency,
|
||||
currency: RUB,
|
||||
value: formatDecimal(multiply(quantity, decimalFromString(currentPrice.value))),
|
||||
};
|
||||
const absoluteReturn =
|
||||
currentPrice && averagePurchasePrice && currentPrice.currency === averagePurchasePrice.currency
|
||||
currentPrice && averagePurchasePrice
|
||||
? {
|
||||
currency: currentPrice.currency,
|
||||
currency: RUB,
|
||||
value: formatDecimal(multiply(
|
||||
subtract(decimalFromString(currentPrice.value), decimalFromString(averagePurchasePrice.value)),
|
||||
quantity,
|
||||
@@ -106,44 +111,4 @@ function mapPosition(
|
||||
};
|
||||
}
|
||||
|
||||
function money(value: TbankMoney | undefined): Money | null {
|
||||
const currency = value?.currency?.trim().toUpperCase();
|
||||
return currency && value
|
||||
? { currency, value: formatDecimal(decimal(value)) }
|
||||
: null;
|
||||
}
|
||||
|
||||
function totalsByCurrency(
|
||||
positions: NonNullable<TbankPortfolio['positions']>,
|
||||
money: NonNullable<TbankPositions['money']>,
|
||||
): Money[] {
|
||||
const totals = new Map<string, ReturnType<typeof decimal>>();
|
||||
const addTotal = (
|
||||
currency: string | undefined,
|
||||
amount: ReturnType<typeof decimal>,
|
||||
) => {
|
||||
const normalizedCurrency = currency?.trim().toUpperCase();
|
||||
if (!normalizedCurrency || isZero(amount)) return;
|
||||
totals.set(
|
||||
normalizedCurrency,
|
||||
add(totals.get(normalizedCurrency) ?? decimal({}), amount),
|
||||
);
|
||||
};
|
||||
|
||||
for (const value of money) addTotal(value.currency, decimal(value));
|
||||
for (const position of positions) {
|
||||
const price = position.currentPrice;
|
||||
// CURRENCY positions mirror the cash balances already added above.
|
||||
if (!price || types[position.instrumentType?.toLowerCase() ?? ''] === 'CURRENCY') continue;
|
||||
addTotal(
|
||||
price.currency,
|
||||
multiply(decimal(position.quantity ?? {}), decimal(price)),
|
||||
);
|
||||
}
|
||||
|
||||
return [...totals]
|
||||
.filter(([, value]) => !isZero(value))
|
||||
.map(([currency, value]) => ({ currency, value: formatDecimal(value) }));
|
||||
}
|
||||
|
||||
export { mapPortfolio, totalsByCurrency };
|
||||
export { mapPortfolio };
|
||||
|
||||
Reference in New Issue
Block a user