feat: add candles module and share history endpoint
This commit is contained in:
parent
45042d9cc7
commit
d850f3f4fd
28
apps/backend/src/modules/candles/candles.controller.ts
Normal file
28
apps/backend/src/modules/candles/candles.controller.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Controller, Get, Param, Query, ValidationPipe } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||||
|
import { CandlesService } from './candles.service';
|
||||||
|
import { CandlesQueryDto } from './dto/candles-query.dto';
|
||||||
|
|
||||||
|
@ApiTags('Candles')
|
||||||
|
@Controller('securities')
|
||||||
|
export class CandlesController {
|
||||||
|
constructor(private readonly candlesService: CandlesService) {}
|
||||||
|
|
||||||
|
@Get('shares/:secid/candles')
|
||||||
|
@ApiOperation({ summary: 'Получить свечи акции' })
|
||||||
|
async getShareCandles(
|
||||||
|
@Param('secid') secid: string,
|
||||||
|
@Query(ValidationPipe) query: CandlesQueryDto,
|
||||||
|
) {
|
||||||
|
return this.candlesService.getCandles('shares', secid, query.interval, query.from, query.till);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('bonds/:secid/candles')
|
||||||
|
@ApiOperation({ summary: 'Получить свечи облигации' })
|
||||||
|
async getBondCandles(
|
||||||
|
@Param('secid') secid: string,
|
||||||
|
@Query(ValidationPipe) query: CandlesQueryDto,
|
||||||
|
) {
|
||||||
|
return this.candlesService.getCandles('bonds', secid, query.interval, query.from, query.till);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/backend/src/modules/candles/candles.module.ts
Normal file
10
apps/backend/src/modules/candles/candles.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { CandlesController } from './candles.controller';
|
||||||
|
import { CandlesService } from './candles.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [CandlesController],
|
||||||
|
providers: [CandlesService],
|
||||||
|
exports: [CandlesService],
|
||||||
|
})
|
||||||
|
export class CandlesModule {}
|
||||||
44
apps/backend/src/modules/candles/candles.service.spec.ts
Normal file
44
apps/backend/src/modules/candles/candles.service.spec.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { CandlesService } from './candles.service';
|
||||||
|
import { MoexClientService } from '../moex-client/moex-client.service';
|
||||||
|
import { CacheService } from '../cache/cache.service';
|
||||||
|
import configuration from '../../config/configuration';
|
||||||
|
import { CandleInterval } from './dto/candles-query.dto';
|
||||||
|
|
||||||
|
describe('CandlesService', () => {
|
||||||
|
let service: CandlesService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [ConfigModule.forRoot({ load: [configuration] })],
|
||||||
|
providers: [
|
||||||
|
CandlesService,
|
||||||
|
MoexClientService,
|
||||||
|
{
|
||||||
|
provide: 'CACHE_MANAGER',
|
||||||
|
useValue: { get: () => undefined, set: () => Promise.resolve(), del: () => Promise.resolve() },
|
||||||
|
},
|
||||||
|
CacheService,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<CandlesService>(CandlesService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return daily candles for SBER', async () => {
|
||||||
|
const result = await service.getCandles(
|
||||||
|
'shares',
|
||||||
|
'SBER',
|
||||||
|
CandleInterval.DAY,
|
||||||
|
'2026-05-01',
|
||||||
|
'2026-06-01',
|
||||||
|
);
|
||||||
|
expect(result.data.length).toBeGreaterThan(0);
|
||||||
|
expect(result.data[0].open).toBeDefined();
|
||||||
|
}, 15000);
|
||||||
|
});
|
||||||
47
apps/backend/src/modules/candles/candles.service.ts
Normal file
47
apps/backend/src/modules/candles/candles.service.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { MoexClientService } from '../moex-client/moex-client.service';
|
||||||
|
import { CacheService } from '../cache/cache.service';
|
||||||
|
import { CandleInterval } from './dto/candles-query.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CandlesService {
|
||||||
|
constructor(
|
||||||
|
private readonly moexClient: MoexClientService,
|
||||||
|
private readonly cache: CacheService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private mapInterval(interval: CandleInterval): 60 | 24 {
|
||||||
|
return interval === CandleInterval.HOUR ? 60 : 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCandles(
|
||||||
|
market: 'shares' | 'bonds',
|
||||||
|
secid: string,
|
||||||
|
interval: CandleInterval,
|
||||||
|
from: string,
|
||||||
|
till: string,
|
||||||
|
) {
|
||||||
|
const moexInterval = this.mapInterval(interval);
|
||||||
|
const { data, fromCache, cachedAt } = await this.cache.getOrFetch(
|
||||||
|
'candles',
|
||||||
|
[market, secid, String(moexInterval), from, till],
|
||||||
|
() =>
|
||||||
|
this.moexClient.getCandles('stock', market, secid, moexInterval, from, till),
|
||||||
|
'candlesTtl',
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data.map((c) => ({
|
||||||
|
open: c.open,
|
||||||
|
high: c.high,
|
||||||
|
low: c.low,
|
||||||
|
close: c.close,
|
||||||
|
volume: c.volume,
|
||||||
|
value: c.value,
|
||||||
|
begin: c.begin,
|
||||||
|
end: c.end,
|
||||||
|
})),
|
||||||
|
meta: { fromCache, cachedAt },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
21
apps/backend/src/modules/candles/dto/candles-query.dto.ts
Normal file
21
apps/backend/src/modules/candles/dto/candles-query.dto.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsString, IsEnum, IsDateString } from 'class-validator';
|
||||||
|
|
||||||
|
export enum CandleInterval {
|
||||||
|
HOUR = '1h',
|
||||||
|
DAY = '24h',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CandlesQueryDto {
|
||||||
|
@ApiProperty({ enum: CandleInterval })
|
||||||
|
@IsEnum(CandleInterval)
|
||||||
|
interval!: CandleInterval;
|
||||||
|
|
||||||
|
@ApiProperty({ format: 'date', example: '2025-06-13' })
|
||||||
|
@IsDateString()
|
||||||
|
from!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ format: 'date', example: '2026-06-13' })
|
||||||
|
@IsDateString()
|
||||||
|
till!: string;
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { Controller, Get, Param } from '@nestjs/common';
|
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||||
import { SharesService } from './shares.service';
|
import { SharesService } from './shares.service';
|
||||||
|
|
||||||
@ -25,4 +25,14 @@ export class SharesController {
|
|||||||
async getDividends(@Param('secid') secid: string) {
|
async getDividends(@Param('secid') secid: string) {
|
||||||
return this.sharesService.getDividends(secid);
|
return this.sharesService.getDividends(secid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get(':secid/history')
|
||||||
|
@ApiOperation({ summary: 'Получить дневную историю торгов акции' })
|
||||||
|
async getHistory(
|
||||||
|
@Param('secid') secid: string,
|
||||||
|
@Query('from') from: string,
|
||||||
|
@Query('till') till: string,
|
||||||
|
) {
|
||||||
|
return this.sharesService.getHistory(secid, from, till);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -102,4 +102,26 @@ export class SharesService {
|
|||||||
meta: { fromCache, cachedAt },
|
meta: { 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 {
|
||||||
|
data: 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,
|
||||||
|
})),
|
||||||
|
meta: { fromCache, cachedAt },
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user