133 lines
3.7 KiB
TypeScript
133 lines
3.7 KiB
TypeScript
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 },
|
|
};
|
|
}
|
|
}
|