MVP реализации MoexVibe — NestJS бэкенд + React фронтенд + CI/CD #1

Merged
ksv741 merged 21 commits from feat/mvp-implementation into main 2026-06-13 21:07:34 +03:00
7 changed files with 183 additions and 1 deletions
Showing only changes of commit d850f3f4fd - Show all commits

View 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);
}
}

View 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 {}

View 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);
});

View 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 },
};
}
}

View 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;
}

View File

@ -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 { SharesService } from './shares.service';
@ -25,4 +25,14 @@ export class SharesController {
async getDividends(@Param('secid') secid: string) {
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);
}
}

View File

@ -102,4 +102,26 @@ export class SharesService {
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 },
};
}
}