import { Injectable, NotFoundException } from '@nestjs/common'; import { MoexClientService } from '../moex-client/moex-client.service'; import { CacheService } from '../cache/cache.service'; @Injectable() export class BondsService { constructor( private readonly moexClient: MoexClientService, private readonly cache: CacheService, ) {} async getBond(secid: string) { const { data: bond, fromCache, cachedAt, } = await this.cache.getOrFetch( 'bond', [secid], () => this.moexClient.getBondData(secid), 'securityTtl', ); if (!bond) { throw new NotFoundException(`Bond ${secid} not found`); } const { data: mkt } = await this.cache.getOrFetch( 'marketdata', ['bonds', secid], () => this.moexClient.getBondMarketData(secid), 'marketDataTtl', ); return { data: { secid: bond.secid, isin: bond.isin, name: bond.shortName, shortName: bond.shortName, latName: null, listLevel: bond.listLevel, issueSize: bond.issueSize, faceValue: bond.faceValue, faceUnit: bond.isin.startsWith('XS') ? 'USD' : 'RUB', matDate: bond.matDate, couponValue: bond.couponValue ?? 0, couponPercent: bond.couponPercent, couponPeriod: bond.couponPeriod, nextCoupon: bond.nextCoupon, accruedInt: bond.accruedInt ?? 0, bondType: bond.bondType, bondSubType: bond.bondSubType, offerDate: bond.offerDate, buybackDate: bond.buybackDate, marketData: { price: mkt?.last ?? bond.prevPrice ?? 0, yieldToMaturity: mkt?.yield ?? bond.yieldAtPrevWaprice ?? null, duration: mkt?.duration ?? null, accruedInt: bond.accruedInt ?? 0, couponValue: bond.couponValue ?? 0, couponPercent: bond.couponPercent, nextCouponDate: bond.nextCoupon, open: mkt?.open ?? 0, high: mkt?.high ?? null, low: mkt?.low ?? null, volume: mkt?.volume ?? 0, updatedAt: mkt?.updateTime ? new Date().toISOString().split('T')[0] + 'T' + mkt.updateTime : new Date().toISOString(), }, }, meta: { fromCache, cachedAt }, }; } async getMarketData(secid: string) { const { data: mkt, fromCache, cachedAt, } = await this.cache.getOrFetch( 'marketdata', ['bonds', secid], () => this.moexClient.getBondMarketData(secid), 'marketDataTtl', ); if (!mkt) { throw new NotFoundException(`Market data for bond ${secid} not found`); } return { data: { price: mkt.last ?? 0, yieldToMaturity: mkt.yield ?? null, duration: mkt.duration ?? null, accruedInt: 0, couponValue: 0, couponPercent: null, nextCouponDate: null, open: mkt.open ?? 0, high: mkt.high ?? null, low: mkt.low ?? null, volume: mkt.volume ?? 0, updatedAt: mkt.updateTime ? new Date().toISOString().split('T')[0] + 'T' + mkt.updateTime : new Date().toISOString(), }, meta: { fromCache, cachedAt }, }; } async getHistory(secid: string, from: string, till: string) { const { data, fromCache, cachedAt } = await this.cache.getOrFetch( 'history', ['bonds', secid, from, till], () => this.moexClient.getBondHistory(secid, from, till), 'historyTtl', ); return { data: data.map((h) => ({ date: h.tradeDate, closePrice: h.legalClosePrice ?? h.close ?? 0, yieldClose: h.yieldClose ?? null, duration: h.duration ?? null, })), meta: { fromCache, cachedAt }, }; } }