import type { Money } from '#src/common/money.types'; import { add, decimalFromString, formatDecimal, isZero, multiply, subtract, } from '#src/common/decimal'; import { decimal, rubMoney } from '#src/integrations/tbank/decimal'; import { metadataKey, type TbankInstrument, type TbankMoney, TbankPortfolio, } from '#src/integrations/tbank/tbank.api'; import type { Classification } from '../application/position-classification.service'; import type { PortfolioDto, PortfolioPositionDto } from '../presentation/portfolio.dto'; import { portfolioStructure } from '../domain/portfolio-structure'; const RUB = 'RUB'; const types: Record = { share: 'STOCK', bond: 'BOND', etf: 'FUND', currency: 'CURRENCY', futures: 'FUTURE', option: 'OPTION', sp: 'STRUCTURED_NOTE', dfa: 'DIGITAL_FINANCIAL_ASSET', }; function mapPortfolio( upstream: NonNullable, cashBalances: TbankMoney[], metadata: ReadonlyMap, lastSuccessfulAt: string, classification: ReadonlyMap = 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); let cashTotal = decimal({}); for (const value of cashBalances) { if (value.currency?.trim().toUpperCase() !== RUB) continue; cashTotal = add(cashTotal, decimal(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, totals, positions, cash, structure: portfolioStructure(positions, [cash]), }; } function mapPosition( position: NonNullable[number], metadata: null | TbankInstrument, classification?: Classification, ): null | PortfolioPositionDto { const quantity = decimal(position.quantity ?? {}); if (isZero(quantity)) return null; const figi = position.figi?.trim() || null; const instrumentUid = position.instrumentUid?.trim() || null; const currentPrice = rubMoney(position.currentPrice); const averagePurchasePrice = rubMoney(position.averagePositionPrice); const currentValue = currentPrice && { currency: RUB, value: formatDecimal(multiply(quantity, decimalFromString(currentPrice.value))), }; const absoluteReturn = currentPrice && averagePurchasePrice ? { currency: RUB, value: formatDecimal(multiply( subtract(decimalFromString(currentPrice.value), decimalFromString(averagePurchasePrice.value)), quantity, )), } : null; return { figi, instrumentUid, displayName: metadata?.name?.trim() || metadata?.ticker?.trim() || figi || 'Unknown instrument', type: types[position.instrumentType?.toLowerCase() ?? ''] ?? 'OTHER', quantity: formatDecimal(quantity), currentValue, averagePurchasePrice, absoluteReturn, isin: metadata?.isin?.trim() || null, ticker: metadata?.ticker?.trim() || null, logoName: metadata?.brand?.logoName?.trim() || null, sector: classification?.sector ?? null, issuer: classification?.issuer ?? null, }; } export { mapPortfolio };