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

143 lines
4.0 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 SharesService {
constructor(
private readonly moexClient: MoexClientService,
private readonly cache: CacheService,
) {}
async getShare(secid: string) {
const desc = await this.moexClient.getSecurityDescription(secid);
if (
!desc ||
!(
desc.group === 'stock_shares' ||
desc.type === 'common_share' ||
desc.type === 'preferred_share'
)
) {
throw new NotFoundException(`Share ${secid} not found`);
}
const { data: marketData } = await this.cache.getOrFetch(
'marketdata',
['shares', secid],
() => this.moexClient.getShareMarketData(secid),
'marketDataTtl',
);
const price = marketData?.last ?? (marketData ? null : 0);
const change = marketData?.lastChange ?? 0;
const changePercent = marketData?.lastChangePrcnt ?? 0;
return {
secid: desc.secid,
isin: desc.isin,
name: desc.name,
shortName: desc.shortName,
latName: desc.latName,
listLevel: desc.listLevel,
issueSize: desc.issueSize,
faceValue: desc.faceValue,
faceUnit: desc.faceUnit === 'SUR' ? 'RUB' : desc.faceUnit,
type: desc.type,
marketData: {
price: price ?? 0,
change,
changePercent,
open: marketData?.open ?? 0,
high: marketData?.high ?? null,
low: marketData?.low ?? null,
volume: marketData?.volume ?? 0,
value: marketData?.value ?? 0,
issueCapitalization: marketData?.issueCapitalization ?? null,
updatedAt: marketData?.updateTime
? new Date().toISOString().split('T')[0] + 'T' + marketData.updateTime
: new Date().toISOString(),
},
};
}
async getMarketData(secid: string) {
const {
data: marketData,
fromCache,
cachedAt,
} = await this.cache.getOrFetch(
'marketdata',
['shares', secid],
() => this.moexClient.getShareMarketData(secid),
'marketDataTtl',
);
if (!marketData) {
throw new NotFoundException(`Market data for ${secid} not found`);
}
return new ApiEnvelopePayload(
{
price: marketData.last ?? 0,
change: marketData.lastChange ?? 0,
changePercent: marketData.lastChangePrcnt ?? 0,
open: marketData.open ?? 0,
high: marketData.high ?? null,
low: marketData.low ?? null,
volume: marketData.volume ?? 0,
value: marketData.value ?? 0,
issueCapitalization: marketData.issueCapitalization ?? null,
updatedAt: marketData.updateTime
? new Date().toISOString().split('T')[0] + 'T' + marketData.updateTime
: new Date().toISOString(),
},
fromCache,
cachedAt,
);
}
async getDividends(secid: string) {
const { data, fromCache, cachedAt } = await this.cache.getOrFetch(
'dividends',
[secid],
() => this.moexClient.getDividends(secid),
'dividendsTtl',
);
return new ApiEnvelopePayload(
data.map((d) => ({
registryCloseDate: d.registryCloseDate,
value: d.value,
currency: d.currencyId,
})),
fromCache,
cachedAt,
);
}
async getHistory(secid: string, from: string, till: string) {
const { data, fromCache, cachedAt } = await this.cache.getOrFetch(
'history',
['shares', secid, from, till],
() => this.moexClient.getHistory(secid, from, till),
'historyTtl',
);
return new ApiEnvelopePayload(
data.map((h) => ({
date: h.tradeDate,
open: h.open ?? 0,
high: h.high ?? 0,
low: h.low ?? 0,
close: h.close ?? 0,
volume: h.volume,
value: h.value,
})),
fromCache,
cachedAt,
);
}
}