moex-vibe/apps/backend/src/modules/bonds/bonds.service.ts

137 lines
3.8 KiB
TypeScript

import { Injectable, NotFoundException } from '@nestjs/common';
import { MoexClientService } from '../moex-client/moex-client.service';
import { CacheService } from '../cache/cache.service';
import { ApiEnvelopePayload } from '../../common/dto/api-response.dto';
@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 new ApiEnvelopePayload(
{
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(),
},
},
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 new ApiEnvelopePayload(
{
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(),
},
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 new ApiEnvelopePayload(
data.map((h) => ({
date: h.tradeDate,
closePrice: h.legalClosePrice ?? h.close ?? 0,
yieldClose: h.yieldClose ?? null,
duration: h.duration ?? null,
})),
fromCache,
cachedAt,
);
}
}