MVP реализации MoexVibe — NestJS бэкенд + React фронтенд + CI/CD #1
@ -1,10 +1,4 @@
|
||||
import {
|
||||
ExceptionFilter,
|
||||
Catch,
|
||||
ArgumentsHost,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
|
||||
@Catch()
|
||||
|
||||
@ -1,21 +1,11 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
} from '@nestjs/common';
|
||||
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { ApiResponse } from '../dto/api-response.dto';
|
||||
|
||||
@Injectable()
|
||||
export class TransformInterceptor<T>
|
||||
implements NestInterceptor<T, ApiResponse<T>>
|
||||
{
|
||||
intercept(
|
||||
context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
): Observable<ApiResponse<T>> {
|
||||
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
|
||||
return next.handle().pipe(
|
||||
map((data) => {
|
||||
if (data instanceof ApiResponse) return data;
|
||||
|
||||
@ -5,10 +5,7 @@ export default registerAs('app', () => ({
|
||||
moex: {
|
||||
baseUrl: process.env.MOEX_BASE_URL || 'https://iss.moex.com/iss',
|
||||
rateLimit: parseInt(process.env.MOEX_RATE_LIMIT || '10', 10),
|
||||
circuitBreakerThreshold: parseInt(
|
||||
process.env.MOEX_CIRCUIT_BREAKER_THRESHOLD || '5',
|
||||
10,
|
||||
),
|
||||
circuitBreakerThreshold: parseInt(process.env.MOEX_CIRCUIT_BREAKER_THRESHOLD || '5', 10),
|
||||
circuitBreakerResetSeconds: parseInt(
|
||||
process.env.MOEX_CIRCUIT_BREAKER_RESET_SECONDS || '30',
|
||||
10,
|
||||
|
||||
@ -20,10 +20,7 @@ async function bootstrap() {
|
||||
|
||||
app.enableCors();
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('MoexVibe API')
|
||||
.setVersion('1.0.0')
|
||||
.build();
|
||||
const config = new DocumentBuilder().setTitle('MoexVibe API').setVersion('1.0.0').build();
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('api/docs', app, document);
|
||||
|
||||
|
||||
@ -16,7 +16,11 @@ describe('BondsService', () => {
|
||||
MoexClientService,
|
||||
{
|
||||
provide: 'CACHE_MANAGER',
|
||||
useValue: { get: () => undefined, set: () => Promise.resolve(), del: () => Promise.resolve() },
|
||||
useValue: {
|
||||
get: () => undefined,
|
||||
set: () => Promise.resolve(),
|
||||
del: () => Promise.resolve(),
|
||||
},
|
||||
},
|
||||
CacheService,
|
||||
],
|
||||
|
||||
@ -10,7 +10,11 @@ export class BondsService {
|
||||
) {}
|
||||
|
||||
async getBond(secid: string) {
|
||||
const { data: bond, fromCache, cachedAt } = await this.cache.getOrFetch(
|
||||
const {
|
||||
data: bond,
|
||||
fromCache,
|
||||
cachedAt,
|
||||
} = await this.cache.getOrFetch(
|
||||
'bond',
|
||||
[secid],
|
||||
() => this.moexClient.getBondData(secid),
|
||||
@ -71,7 +75,11 @@ export class BondsService {
|
||||
}
|
||||
|
||||
async getMarketData(secid: string) {
|
||||
const { data: mkt, fromCache, cachedAt } = await this.cache.getOrFetch(
|
||||
const {
|
||||
data: mkt,
|
||||
fromCache,
|
||||
cachedAt,
|
||||
} = await this.cache.getOrFetch(
|
||||
'marketdata',
|
||||
['bonds', secid],
|
||||
() => this.moexClient.getBondMarketData(secid),
|
||||
|
||||
@ -17,7 +17,11 @@ describe('CandlesService', () => {
|
||||
MoexClientService,
|
||||
{
|
||||
provide: 'CACHE_MANAGER',
|
||||
useValue: { get: () => undefined, set: () => Promise.resolve(), del: () => Promise.resolve() },
|
||||
useValue: {
|
||||
get: () => undefined,
|
||||
set: () => Promise.resolve(),
|
||||
del: () => Promise.resolve(),
|
||||
},
|
||||
},
|
||||
CacheService,
|
||||
],
|
||||
|
||||
@ -25,8 +25,7 @@ export class CandlesService {
|
||||
const { data, fromCache, cachedAt } = await this.cache.getOrFetch(
|
||||
'candles',
|
||||
[market, secid, String(moexInterval), from, till],
|
||||
() =>
|
||||
this.moexClient.getCandles('stock', market, secid, moexInterval, from, till),
|
||||
() => this.moexClient.getCandles('stock', market, secid, moexInterval, from, till),
|
||||
'candlesTtl',
|
||||
);
|
||||
|
||||
|
||||
@ -9,9 +9,7 @@ describe('MoexClientService', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ load: [configuration] }),
|
||||
],
|
||||
imports: [ConfigModule.forRoot({ load: [configuration] })],
|
||||
providers: [MoexClientService],
|
||||
}).compile();
|
||||
|
||||
|
||||
@ -25,19 +25,9 @@ export class MoexClientService {
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
const baseUrl = this.configService.get<string>('app.moex.baseUrl')!;
|
||||
this.threshold = this.configService.get<number>(
|
||||
'app.moex.circuitBreakerThreshold',
|
||||
5,
|
||||
);
|
||||
this.resetMs =
|
||||
this.configService.get<number>(
|
||||
'app.moex.circuitBreakerResetSeconds',
|
||||
30,
|
||||
) * 1000;
|
||||
const rateLimit = this.configService.get<number>(
|
||||
'app.moex.rateLimit',
|
||||
10,
|
||||
);
|
||||
this.threshold = this.configService.get<number>('app.moex.circuitBreakerThreshold', 5);
|
||||
this.resetMs = this.configService.get<number>('app.moex.circuitBreakerResetSeconds', 30) * 1000;
|
||||
const rateLimit = this.configService.get<number>('app.moex.rateLimit', 10);
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL: baseUrl,
|
||||
@ -163,7 +153,9 @@ export class MoexClientService {
|
||||
open: mkt ? parseFloat((mkt.OPEN as string) || '') : null,
|
||||
low: mkt ? parseFloat((mkt.LOW as string) || '') : null,
|
||||
high: mkt ? parseFloat((mkt.HIGH as string) || '') : null,
|
||||
last: mkt ? parseFloat((mkt.LAST as string) || '') : parseFloat((share.PREVPRICE as string) || ''),
|
||||
last: mkt
|
||||
? parseFloat((mkt.LAST as string) || '')
|
||||
: parseFloat((share.PREVPRICE as string) || ''),
|
||||
lastChange: mkt ? parseFloat((mkt.LASTCHANGE as string) || '') : null,
|
||||
lastChangePrcnt: mkt ? parseFloat((mkt.LASTCHANGEPRCNT as string) || '') : null,
|
||||
volume: mkt ? parseInt((mkt.VOLTODAY as string) || '0', 10) : 0,
|
||||
@ -278,11 +270,7 @@ export class MoexClientService {
|
||||
}));
|
||||
}
|
||||
|
||||
async getHistory(
|
||||
secid: string,
|
||||
from: string,
|
||||
till: string,
|
||||
): Promise<MoexHistoryEntry[]> {
|
||||
async getHistory(secid: string, from: string, till: string): Promise<MoexHistoryEntry[]> {
|
||||
const data = await this.request<Record<string, unknown>>(
|
||||
`/engines/stock/markets/shares/securities/${secid}`,
|
||||
{ from, till },
|
||||
@ -304,11 +292,7 @@ export class MoexClientService {
|
||||
}));
|
||||
}
|
||||
|
||||
async getBondHistory(
|
||||
secid: string,
|
||||
from: string,
|
||||
till: string,
|
||||
): Promise<MoexBondHistoryEntry[]> {
|
||||
async getBondHistory(secid: string, from: string, till: string): Promise<MoexBondHistoryEntry[]> {
|
||||
const data = await this.request<Record<string, unknown>>(
|
||||
`/engines/stock/markets/bonds/securities/${secid}`,
|
||||
{ from, till },
|
||||
@ -320,8 +304,7 @@ export class MoexClientService {
|
||||
return this.extractTable(data, tableName).map((h) => ({
|
||||
tradeDate: h.TRADEDATE as string,
|
||||
close: h.CLOSE != null ? parseFloat(h.CLOSE as string) : null,
|
||||
legalClosePrice:
|
||||
h.LEGALCLOSEPRICE != null ? parseFloat(h.LEGALCLOSEPRICE as string) : null,
|
||||
legalClosePrice: h.LEGALCLOSEPRICE != null ? parseFloat(h.LEGALCLOSEPRICE as string) : null,
|
||||
waprice: h.WAPRICE != null ? parseFloat(h.WAPRICE as string) : null,
|
||||
yieldClose: h.YIELDCLOSE != null ? parseFloat(h.YIELDCLOSE as string) : null,
|
||||
duration: h.DURATION != null ? parseFloat(h.DURATION as string) : null,
|
||||
|
||||
@ -8,7 +8,15 @@ describe('SecuritiesController', () => {
|
||||
let service: SecuritiesService;
|
||||
|
||||
const mockResults = [
|
||||
{ secid: 'SBER', isin: 'RU0009029540', shortName: 'Сбербанк', type: 'share', listLevel: 1, currency: 'RUB', price: null },
|
||||
{
|
||||
secid: 'SBER',
|
||||
isin: 'RU0009029540',
|
||||
shortName: 'Сбербанк',
|
||||
type: 'share',
|
||||
listLevel: 1,
|
||||
currency: 'RUB',
|
||||
price: null,
|
||||
},
|
||||
];
|
||||
|
||||
const mockService = {
|
||||
@ -18,9 +26,7 @@ describe('SecuritiesController', () => {
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [SecuritiesController],
|
||||
providers: [
|
||||
{ provide: SecuritiesService, useValue: mockService },
|
||||
],
|
||||
providers: [{ provide: SecuritiesService, useValue: mockService }],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<SecuritiesController>(SecuritiesController);
|
||||
|
||||
@ -17,7 +17,11 @@ describe('SecuritiesService', () => {
|
||||
MoexClientService,
|
||||
{
|
||||
provide: 'CACHE_MANAGER',
|
||||
useValue: { get: () => undefined, set: () => Promise.resolve(), del: () => Promise.resolve() },
|
||||
useValue: {
|
||||
get: () => undefined,
|
||||
set: () => Promise.resolve(),
|
||||
del: () => Promise.resolve(),
|
||||
},
|
||||
},
|
||||
CacheService,
|
||||
],
|
||||
|
||||
@ -28,9 +28,14 @@ export class SecuritiesService {
|
||||
const results = await this.moexClient.searchSecurities(query);
|
||||
return results
|
||||
.map((s): SearchResultItem | null => {
|
||||
const type = (s.group === 'stock_shares' || s.type === 'common_share' || s.type === 'preferred_share')
|
||||
? 'share' as const
|
||||
: (s.group === 'stock_bonds' ? 'bond' as const : null);
|
||||
const type =
|
||||
s.group === 'stock_shares' ||
|
||||
s.type === 'common_share' ||
|
||||
s.type === 'preferred_share'
|
||||
? ('share' as const)
|
||||
: s.group === 'stock_bonds'
|
||||
? ('bond' as const)
|
||||
: null;
|
||||
if (!type) return null;
|
||||
return {
|
||||
secid: s.secid,
|
||||
|
||||
@ -16,7 +16,11 @@ describe('SharesService', () => {
|
||||
MoexClientService,
|
||||
{
|
||||
provide: 'CACHE_MANAGER',
|
||||
useValue: { get: () => undefined, set: () => Promise.resolve(), del: () => Promise.resolve() },
|
||||
useValue: {
|
||||
get: () => undefined,
|
||||
set: () => Promise.resolve(),
|
||||
del: () => Promise.resolve(),
|
||||
},
|
||||
},
|
||||
CacheService,
|
||||
],
|
||||
|
||||
@ -11,7 +11,14 @@ export class SharesService {
|
||||
|
||||
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')) {
|
||||
if (
|
||||
!desc ||
|
||||
!(
|
||||
desc.group === 'stock_shares' ||
|
||||
desc.type === 'common_share' ||
|
||||
desc.type === 'preferred_share'
|
||||
)
|
||||
) {
|
||||
throw new NotFoundException(`Share ${secid} not found`);
|
||||
}
|
||||
|
||||
@ -55,7 +62,11 @@ export class SharesService {
|
||||
}
|
||||
|
||||
async getMarketData(secid: string) {
|
||||
const { data: marketData, fromCache, cachedAt } = await this.cache.getOrFetch(
|
||||
const {
|
||||
data: marketData,
|
||||
fromCache,
|
||||
cachedAt,
|
||||
} = await this.cache.getOrFetch(
|
||||
'marketdata',
|
||||
['shares', secid],
|
||||
() => this.moexClient.getShareMarketData(secid),
|
||||
|
||||
@ -15,7 +15,10 @@ import type {
|
||||
|
||||
const BASE = '';
|
||||
|
||||
async function request<T>(path: string, params?: Record<string, string>): Promise<{ data: T; meta: ApiResponseMeta }> {
|
||||
async function request<T>(
|
||||
path: string,
|
||||
params?: Record<string, string>,
|
||||
): Promise<{ data: T; meta: ApiResponseMeta }> {
|
||||
const url = new URL(`${BASE}${path}`, window.location.origin);
|
||||
if (params) {
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
@ -37,19 +40,31 @@ export function searchSecurities(
|
||||
type: 'all' | 'share' | 'bond' = 'all',
|
||||
limit = 20,
|
||||
): Promise<{ data: SearchResultItem[]; meta: ApiResponseMeta }> {
|
||||
return request<SearchResultItem[]>('/api/v1/securities/search', { q, type, limit: String(limit) });
|
||||
return request<SearchResultItem[]>('/api/v1/securities/search', {
|
||||
q,
|
||||
type,
|
||||
limit: String(limit),
|
||||
});
|
||||
}
|
||||
|
||||
export function getShare(secid: string): Promise<{ data: ShareResponse; meta: ApiResponseMeta }> {
|
||||
return request<ShareResponse>(`/api/v1/securities/shares/${encodeURIComponent(secid)}`);
|
||||
}
|
||||
|
||||
export function getShareMarketData(secid: string): Promise<{ data: StockMarketData; meta: ApiResponseMeta }> {
|
||||
return request<StockMarketData>(`/api/v1/securities/shares/${encodeURIComponent(secid)}/marketdata`);
|
||||
export function getShareMarketData(
|
||||
secid: string,
|
||||
): Promise<{ data: StockMarketData; meta: ApiResponseMeta }> {
|
||||
return request<StockMarketData>(
|
||||
`/api/v1/securities/shares/${encodeURIComponent(secid)}/marketdata`,
|
||||
);
|
||||
}
|
||||
|
||||
export function getShareDividends(secid: string): Promise<{ data: DividendItem[]; meta: ApiResponseMeta }> {
|
||||
return request<DividendItem[]>(`/api/v1/securities/shares/${encodeURIComponent(secid)}/dividends`);
|
||||
export function getShareDividends(
|
||||
secid: string,
|
||||
): Promise<{ data: DividendItem[]; meta: ApiResponseMeta }> {
|
||||
return request<DividendItem[]>(
|
||||
`/api/v1/securities/shares/${encodeURIComponent(secid)}/dividends`,
|
||||
);
|
||||
}
|
||||
|
||||
export function getShareHistory(
|
||||
@ -57,15 +72,22 @@ export function getShareHistory(
|
||||
from: string,
|
||||
till: string,
|
||||
): Promise<{ data: ShareHistoryItem[]; meta: ApiResponseMeta }> {
|
||||
return request<ShareHistoryItem[]>(`/api/v1/securities/shares/${encodeURIComponent(secid)}/history`, { from, till });
|
||||
return request<ShareHistoryItem[]>(
|
||||
`/api/v1/securities/shares/${encodeURIComponent(secid)}/history`,
|
||||
{ from, till },
|
||||
);
|
||||
}
|
||||
|
||||
export function getBond(secid: string): Promise<{ data: BondResponse; meta: ApiResponseMeta }> {
|
||||
return request<BondResponse>(`/api/v1/securities/bonds/${encodeURIComponent(secid)}`);
|
||||
}
|
||||
|
||||
export function getBondMarketData(secid: string): Promise<{ data: BondMarketData; meta: ApiResponseMeta }> {
|
||||
return request<BondMarketData>(`/api/v1/securities/bonds/${encodeURIComponent(secid)}/marketdata`);
|
||||
export function getBondMarketData(
|
||||
secid: string,
|
||||
): Promise<{ data: BondMarketData; meta: ApiResponseMeta }> {
|
||||
return request<BondMarketData>(
|
||||
`/api/v1/securities/bonds/${encodeURIComponent(secid)}/marketdata`,
|
||||
);
|
||||
}
|
||||
|
||||
export function getBondHistory(
|
||||
@ -73,7 +95,10 @@ export function getBondHistory(
|
||||
from: string,
|
||||
till: string,
|
||||
): Promise<{ data: BondHistoryItem[]; meta: ApiResponseMeta }> {
|
||||
return request<BondHistoryItem[]>(`/api/v1/securities/bonds/${encodeURIComponent(secid)}/history`, { from, till });
|
||||
return request<BondHistoryItem[]>(
|
||||
`/api/v1/securities/bonds/${encodeURIComponent(secid)}/history`,
|
||||
{ from, till },
|
||||
);
|
||||
}
|
||||
|
||||
export function getShareCandles(
|
||||
|
||||
@ -4,427 +4,427 @@
|
||||
*/
|
||||
|
||||
export interface paths {
|
||||
"/api/v1/health": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Проверка состояния сервиса */
|
||||
get: operations["HealthController_check"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
'/api/v1/health': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
"/api/v1/securities/search": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Поиск по инструментам */
|
||||
get: operations["SecuritiesController_search"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
/** Проверка состояния сервиса */
|
||||
get: operations['HealthController_check'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/securities/search': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
"/api/v1/securities/shares/{secid}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Получить спецификацию акции */
|
||||
get: operations["SharesController_getShare"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
/** Поиск по инструментам */
|
||||
get: operations['SecuritiesController_search'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/securities/shares/{secid}': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
"/api/v1/securities/shares/{secid}/marketdata": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Получить рыночные данные акции */
|
||||
get: operations["SharesController_getMarketData"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
/** Получить спецификацию акции */
|
||||
get: operations['SharesController_getShare'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/securities/shares/{secid}/marketdata': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
"/api/v1/securities/shares/{secid}/dividends": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Получить дивиденды */
|
||||
get: operations["SharesController_getDividends"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
/** Получить рыночные данные акции */
|
||||
get: operations['SharesController_getMarketData'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/securities/shares/{secid}/dividends': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
"/api/v1/securities/shares/{secid}/history": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Получить дневную историю торгов акции */
|
||||
get: operations["SharesController_getHistory"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
/** Получить дивиденды */
|
||||
get: operations['SharesController_getDividends'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/securities/shares/{secid}/history': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
"/api/v1/securities/bonds/{secid}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Получить спецификацию облигации */
|
||||
get: operations["BondsController_getBond"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
/** Получить дневную историю торгов акции */
|
||||
get: operations['SharesController_getHistory'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/securities/bonds/{secid}': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
"/api/v1/securities/bonds/{secid}/marketdata": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Получить рыночные данные облигации */
|
||||
get: operations["BondsController_getMarketData"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
/** Получить спецификацию облигации */
|
||||
get: operations['BondsController_getBond'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/securities/bonds/{secid}/marketdata': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
"/api/v1/securities/bonds/{secid}/history": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Получить дневную историю торгов облигации */
|
||||
get: operations["BondsController_getHistory"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
/** Получить рыночные данные облигации */
|
||||
get: operations['BondsController_getMarketData'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/securities/bonds/{secid}/history': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
"/api/v1/securities/shares/{secid}/candles": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Получить свечи акции */
|
||||
get: operations["CandlesController_getShareCandles"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
/** Получить дневную историю торгов облигации */
|
||||
get: operations['BondsController_getHistory'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/securities/shares/{secid}/candles': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
"/api/v1/securities/bonds/{secid}/candles": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Получить свечи облигации */
|
||||
get: operations["CandlesController_getBondCandles"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
/** Получить свечи акции */
|
||||
get: operations['CandlesController_getShareCandles'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/securities/bonds/{secid}/candles': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Получить свечи облигации */
|
||||
get: operations['CandlesController_getBondCandles'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
}
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: never;
|
||||
responses: never;
|
||||
parameters: never;
|
||||
requestBodies: never;
|
||||
headers: never;
|
||||
pathItems: never;
|
||||
schemas: never;
|
||||
responses: never;
|
||||
parameters: never;
|
||||
requestBodies: never;
|
||||
headers: never;
|
||||
pathItems: never;
|
||||
}
|
||||
export type $defs = Record<string, never>;
|
||||
export interface operations {
|
||||
HealthController_check: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
HealthController_check: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
SecuritiesController_search: {
|
||||
parameters: {
|
||||
query: {
|
||||
/** @description Поисковый запрос (тикер, название, ISIN) */
|
||||
q: string;
|
||||
type?: "all" | "share" | "bond";
|
||||
limit?: number;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
SharesController_getShare: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
secid: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SecuritiesController_search: {
|
||||
parameters: {
|
||||
query: {
|
||||
/** @description Поисковый запрос (тикер, название, ISIN) */
|
||||
q: string;
|
||||
type?: 'all' | 'share' | 'bond';
|
||||
limit?: number;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
SharesController_getMarketData: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
secid: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
SharesController_getDividends: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
secid: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SharesController_getShare: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
secid: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
SharesController_getHistory: {
|
||||
parameters: {
|
||||
query: {
|
||||
from: string;
|
||||
till: string;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
secid: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
BondsController_getBond: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
secid: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SharesController_getMarketData: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
secid: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
BondsController_getMarketData: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
secid: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
BondsController_getHistory: {
|
||||
parameters: {
|
||||
query: {
|
||||
from: string;
|
||||
till: string;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
secid: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SharesController_getDividends: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
secid: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
CandlesController_getShareCandles: {
|
||||
parameters: {
|
||||
query: {
|
||||
interval: "1h" | "24h";
|
||||
from: string;
|
||||
till: string;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
secid: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
CandlesController_getBondCandles: {
|
||||
parameters: {
|
||||
query: {
|
||||
interval: "1h" | "24h";
|
||||
from: string;
|
||||
till: string;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
secid: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SharesController_getHistory: {
|
||||
parameters: {
|
||||
query: {
|
||||
from: string;
|
||||
till: string;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
secid: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
BondsController_getBond: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
secid: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
BondsController_getMarketData: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
secid: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
BondsController_getHistory: {
|
||||
parameters: {
|
||||
query: {
|
||||
from: string;
|
||||
till: string;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
secid: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
CandlesController_getShareCandles: {
|
||||
parameters: {
|
||||
query: {
|
||||
interval: '1h' | '24h';
|
||||
from: string;
|
||||
till: string;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
secid: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
CandlesController_getBondCandles: {
|
||||
parameters: {
|
||||
query: {
|
||||
interval: '1h' | '24h';
|
||||
from: string;
|
||||
till: string;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
secid: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@ -15,22 +15,27 @@ export function BondDetails({ bond }: BondDetailsProps) {
|
||||
const md = bond.marketData;
|
||||
|
||||
return (
|
||||
<div style={{ background: 'var(--color-surface)', borderRadius: 'var(--border-radius)', boxShadow: 'var(--shadow)', padding: 24 }}>
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--color-surface)',
|
||||
borderRadius: 'var(--border-radius)',
|
||||
boxShadow: 'var(--shadow)',
|
||||
padding: 24,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<h2 style={{ fontSize: 28, fontWeight: 700 }}>{bond.shortName}</h2>
|
||||
<div style={{ fontSize: 14, color: 'var(--color-text-secondary)' }}>
|
||||
{bond.isin}
|
||||
</div>
|
||||
<div style={{ fontSize: 14, color: 'var(--color-text-secondary)' }}>{bond.isin}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 36, fontWeight: 700, marginBottom: 4 }}>
|
||||
{md.price.toFixed(2)}%
|
||||
</div>
|
||||
<div style={{ fontSize: 36, fontWeight: 700, marginBottom: 4 }}>{md.price.toFixed(2)}%</div>
|
||||
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<div style={rowStyle}>
|
||||
<span>Номинал</span>
|
||||
<span>{bond.faceValue.toLocaleString('ru-RU')} {bond.faceUnit}</span>
|
||||
<span>
|
||||
{bond.faceValue.toLocaleString('ru-RU')} {bond.faceUnit}
|
||||
</span>
|
||||
</div>
|
||||
<div style={rowStyle}>
|
||||
<span>Дата погашения</span>
|
||||
@ -38,7 +43,9 @@ export function BondDetails({ bond }: BondDetailsProps) {
|
||||
</div>
|
||||
<div style={rowStyle}>
|
||||
<span>Купон</span>
|
||||
<span>{md.couponValue} ₽ {md.couponPercent != null ? `(${md.couponPercent}%)` : ''}</span>
|
||||
<span>
|
||||
{md.couponValue} ₽ {md.couponPercent != null ? `(${md.couponPercent}%)` : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div style={rowStyle}>
|
||||
<span>Период купона</span>
|
||||
|
||||
@ -14,7 +14,15 @@ export function Layout() {
|
||||
gap: 24,
|
||||
}}
|
||||
>
|
||||
<Link to="/" style={{ fontSize: 20, fontWeight: 700, color: 'var(--color-text)', textDecoration: 'none' }}>
|
||||
<Link
|
||||
to="/"
|
||||
style={{
|
||||
fontSize: 20,
|
||||
fontWeight: 700,
|
||||
color: 'var(--color-text)',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
MoexVibe
|
||||
</Link>
|
||||
<SearchBar />
|
||||
|
||||
@ -32,7 +32,10 @@ export function SearchBar() {
|
||||
type="text"
|
||||
placeholder="Поиск акций и облигаций..."
|
||||
value={query}
|
||||
onChange={e => { setQuery(e.target.value); setOpen(true); }}
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value);
|
||||
setOpen(true);
|
||||
}}
|
||||
onFocus={() => setOpen(true)}
|
||||
style={{
|
||||
width: '100%',
|
||||
@ -66,13 +69,15 @@ export function SearchBar() {
|
||||
<li style={{ padding: 12, color: '#888' }}>Ничего не найдено</li>
|
||||
)}
|
||||
{!isLoading &&
|
||||
results?.map(item => (
|
||||
results?.map((item) => (
|
||||
<li
|
||||
key={item.secid}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
setQuery('');
|
||||
navigate(item.type === 'share' ? `/stocks/${item.secid}` : `/bonds/${item.secid}`);
|
||||
navigate(
|
||||
item.type === 'share' ? `/stocks/${item.secid}` : `/bonds/${item.secid}`,
|
||||
);
|
||||
}}
|
||||
style={{
|
||||
padding: '10px 12px',
|
||||
@ -82,14 +87,16 @@ export function SearchBar() {
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.background = '#f5f5f5')}
|
||||
onMouseLeave={e => (e.currentTarget.style.background = '')}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background = '#f5f5f5')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background = '')}
|
||||
>
|
||||
<span>
|
||||
<strong>{item.shortName}</strong>
|
||||
<span style={{ marginLeft: 8, color: '#888', fontSize: 12 }}>{item.secid}</span>
|
||||
</span>
|
||||
<span style={{ fontSize: 12, color: item.type === 'share' ? '#1976d2' : '#2e7d32' }}>
|
||||
<span
|
||||
style={{ fontSize: 12, color: item.type === 'share' ? '#1976d2' : '#2e7d32' }}
|
||||
>
|
||||
{item.type === 'share' ? 'Акция' : 'Облигация'}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
@ -16,7 +16,14 @@ export function StockDetails({ stock }: StockDetailsProps) {
|
||||
const isPositive = md.change >= 0;
|
||||
|
||||
return (
|
||||
<div style={{ background: 'var(--color-surface)', borderRadius: 'var(--border-radius)', boxShadow: 'var(--shadow)', padding: 24 }}>
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--color-surface)',
|
||||
borderRadius: 'var(--border-radius)',
|
||||
boxShadow: 'var(--shadow)',
|
||||
padding: 24,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<h2 style={{ fontSize: 28, fontWeight: 700 }}>
|
||||
{stock.shortName} ({stock.secid})
|
||||
@ -28,8 +35,14 @@ export function StockDetails({ stock }: StockDetailsProps) {
|
||||
|
||||
<div style={{ fontSize: 36, fontWeight: 700, marginBottom: 4 }}>
|
||||
{md.price.toLocaleString('ru-RU', { minimumFractionDigits: 2 })}{' '}
|
||||
<span style={{ fontSize: 18, color: isPositive ? 'var(--color-positive)' : 'var(--color-negative)' }}>
|
||||
{isPositive ? '+' : ''}{md.change.toFixed(2)} ({md.changePercent.toFixed(2)}%)
|
||||
<span
|
||||
style={{
|
||||
fontSize: 18,
|
||||
color: isPositive ? 'var(--color-positive)' : 'var(--color-negative)',
|
||||
}}
|
||||
>
|
||||
{isPositive ? '+' : ''}
|
||||
{md.change.toFixed(2)} ({md.changePercent.toFixed(2)}%)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -53,9 +66,7 @@ export function StockDetails({ stock }: StockDetailsProps) {
|
||||
<div style={rowStyle}>
|
||||
<span>Капитализация</span>
|
||||
<span>
|
||||
{md.issueCapitalization
|
||||
? (md.issueCapitalization / 1e9).toFixed(2) + ' млрд ₽'
|
||||
: '—'}
|
||||
{md.issueCapitalization ? (md.issueCapitalization / 1e9).toFixed(2) + ' млрд ₽' : '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div style={rowStyle}>
|
||||
|
||||
@ -18,7 +18,14 @@ export function BondPage() {
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<BondDetails bond={bond} />
|
||||
|
||||
<div style={{ background: 'var(--color-surface)', borderRadius: 'var(--border-radius)', boxShadow: 'var(--shadow)', padding: 24 }}>
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--color-surface)',
|
||||
borderRadius: 'var(--border-radius)',
|
||||
boxShadow: 'var(--shadow)',
|
||||
padding: 24,
|
||||
}}
|
||||
>
|
||||
<h3 style={{ marginBottom: 16 }}>График цены</h3>
|
||||
<PriceChart data={candles ?? []} />
|
||||
</div>
|
||||
|
||||
@ -1,15 +1,11 @@
|
||||
export function HomePage() {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', paddingTop: 120 }}>
|
||||
<h1 style={{ fontSize: 32, fontWeight: 700, marginBottom: 12 }}>
|
||||
MoexVibe
|
||||
</h1>
|
||||
<h1 style={{ fontSize: 32, fontWeight: 700, marginBottom: 12 }}>MoexVibe</h1>
|
||||
<p style={{ color: 'var(--color-text-secondary)', fontSize: 16, marginBottom: 32 }}>
|
||||
Анализ акций и облигаций Московской биржи
|
||||
</p>
|
||||
<p style={{ color: '#888', fontSize: 13 }}>
|
||||
Введите название или тикер в строку поиска выше
|
||||
</p>
|
||||
<p style={{ color: '#888', fontSize: 13 }}>Введите название или тикер в строку поиска выше</p>
|
||||
<p style={{ color: '#aaa', fontSize: 12, marginTop: 8 }}>
|
||||
Данные задерживаются на 15 минут · Бесплатный API MOEX ISS
|
||||
</p>
|
||||
|
||||
@ -20,13 +20,27 @@ export function StockPage() {
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<StockDetails stock={stock} />
|
||||
|
||||
<div style={{ background: 'var(--color-surface)', borderRadius: 'var(--border-radius)', boxShadow: 'var(--shadow)', padding: 24 }}>
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--color-surface)',
|
||||
borderRadius: 'var(--border-radius)',
|
||||
boxShadow: 'var(--shadow)',
|
||||
padding: 24,
|
||||
}}
|
||||
>
|
||||
<h3 style={{ marginBottom: 16 }}>График цены</h3>
|
||||
<PriceChart data={candles ?? []} />
|
||||
</div>
|
||||
|
||||
{dividends && dividends.length > 0 && (
|
||||
<div style={{ background: 'var(--color-surface)', borderRadius: 'var(--border-radius)', boxShadow: 'var(--shadow)', padding: 24 }}>
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--color-surface)',
|
||||
borderRadius: 'var(--border-radius)',
|
||||
boxShadow: 'var(--shadow)',
|
||||
padding: 24,
|
||||
}}
|
||||
>
|
||||
<h3 style={{ marginBottom: 16 }}>Дивиденды</h3>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user