import { Injectable } from '@nestjs/common'; import type { TbankBond } from '../../../../../integrations/tbank/tbank.api'; import { add, decimalFromString, divide, formatDecimal, multiply, subtract, } from '../../../../../common/decimal'; import { ISO_DATE_LENGTH, ISO_DATE_START } from '../../../../../common/time'; import { decimal } from '../../../../../integrations/tbank/decimal'; import { CandleService } from '../../application/candle.service'; import { CatalogEnrichmentService, type CatalogPayload, } from '../../application/catalog-enrichment.service'; import { timestamp } from '../../application/instrument-market'; import { money } from '../infrastructure/bond-money.mapper'; import { BondCandlesQueryDto, BondQuoteDto, BondQuoteResourceDto, } from '../presentation/bond.dto'; const ZERO_AMOUNT = 0n; const PRICE_DECIMAL_PLACES = 9; @Injectable() export class BondMarketService { constructor( private readonly history: CandleService, private readonly sources: CatalogEnrichmentService, ) {} async quote( userId: string, uid: string, observe = false, ): Promise { const result = await this.sources.section( userId, 'bond', uid, 'quote', observe, ); return { data: result.values.instrument ? bondQuote(result.values, result.unavailable, result.observedAt) : null, meta: result.meta, }; } candles( userId: string, uid: string, period: BondCandlesQueryDto['period'] = 'month', observe = false, ) { return this.history.read( userId, { uid, instrumentType: 'bond', period }, observe, ); } } export function bondQuote( values: CatalogPayload, unavailable: string[], observedAt: string, ): BondQuoteDto { const { price: last, candle: previous, tradingStatus = null } = values; const instrument = values.instrument as TbankBond | undefined; const priceAt = timestamp(last?.time); const pricePercent = last?.price ? formatDecimal(decimal(last.price)) : null; const nominal = money(instrument?.nominal); const aci = money(instrument?.aciValue); const cleanPrice = pricePercent !== null && nominal && decimalFromString(nominal.value).value > ZERO_AMOUNT ? { currency: nominal.currency, value: formatDecimal( divide( multiply( decimalFromString(pricePercent), decimalFromString(nominal.value), ), decimalFromString('100'), PRICE_DECIMAL_PLACES, ), ), } : null; const dirtyPrice = cleanPrice && aci && aci.currency === cleanPrice.currency ? { currency: cleanPrice.currency, value: formatDecimal( add( decimalFromString(cleanPrice.value), decimalFromString(aci.value), ), ), } : null; return { pricePercent, cleanPrice, aci, dirtyPrice, priceAt, tradingStatus, changePercentagePoints: last?.price && previous?.close ? formatDecimal(subtract(decimal(last.price), decimal(previous.close))) : null, previousCloseDate: timestamp(previous?.time)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null, unavailable, observedAt, }; }