refactor: centralize provider projections
This commit is contained in:
@@ -1,16 +1,110 @@
|
||||
import type { Money } from '../../../../common/money.types';
|
||||
import type { PortfolioDto, PortfolioPositionDto } from '../presentation/portfolio.dto';
|
||||
|
||||
import {
|
||||
add,
|
||||
decimalFromString,
|
||||
formatDecimal,
|
||||
isZero,
|
||||
multiply,
|
||||
subtract,
|
||||
} from '../../../../common/decimal';
|
||||
import { decimal } from '../../../../integrations/tbank/decimal';
|
||||
import {
|
||||
metadataKey,
|
||||
type TbankInstrument,
|
||||
type TbankMoney,
|
||||
TbankPortfolio,
|
||||
TbankPositions,
|
||||
} from '../../../../integrations/tbank/tbank.api';
|
||||
import { portfolioStructure } from '../domain/portfolio-structure';
|
||||
|
||||
const types: Record<string, string> = {
|
||||
share: 'STOCK',
|
||||
bond: 'BOND',
|
||||
etf: 'FUND',
|
||||
currency: 'CURRENCY',
|
||||
futures: 'FUTURE',
|
||||
option: 'OPTION',
|
||||
sp: 'STRUCTURED_NOTE',
|
||||
dfa: 'DIGITAL_FINANCIAL_ASSET',
|
||||
};
|
||||
|
||||
function mapPortfolio(
|
||||
upstream: NonNullable<TbankPortfolio['positions']>,
|
||||
cash: TbankMoney[],
|
||||
metadata: ReadonlyMap<string, { data?: null | TbankInstrument }>,
|
||||
lastSuccessfulAt: string,
|
||||
): PortfolioDto {
|
||||
const positions = upstream
|
||||
.map((position) => mapPosition(position, metadata.get(metadataKey(position))?.data ?? null))
|
||||
.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));
|
||||
}
|
||||
const cashBalances = [...balances].map(([currency, value]) => ({
|
||||
currency,
|
||||
value: formatDecimal(value),
|
||||
}));
|
||||
return {
|
||||
lastSuccessfulAt,
|
||||
isStale: false,
|
||||
totalsByCurrency: totalsByCurrency(upstream, cash),
|
||||
positions,
|
||||
cashBalances,
|
||||
structure: portfolioStructure(positions, cashBalances),
|
||||
};
|
||||
}
|
||||
|
||||
function mapPosition(
|
||||
position: NonNullable<TbankPortfolio['positions']>[number],
|
||||
metadata: null | TbankInstrument,
|
||||
): 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 = money(position.currentPrice);
|
||||
const averagePurchasePrice = money(position.averagePositionPrice);
|
||||
const currentValue = currentPrice && {
|
||||
currency: currentPrice.currency,
|
||||
value: formatDecimal(multiply(quantity, decimalFromString(currentPrice.value))),
|
||||
};
|
||||
const absoluteReturn =
|
||||
currentPrice && averagePurchasePrice && currentPrice.currency === averagePurchasePrice.currency
|
||||
? {
|
||||
currency: currentPrice.currency,
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
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']>,
|
||||
@@ -44,4 +138,4 @@ function totalsByCurrency(
|
||||
.map(([currency, value]) => ({ currency, value: formatDecimal(value) }));
|
||||
}
|
||||
|
||||
export { totalsByCurrency };
|
||||
export { mapPortfolio, totalsByCurrency };
|
||||
|
||||
Reference in New Issue
Block a user