Compare commits
10 Commits
f2be35d544
...
9d191763e1
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d191763e1 | |||
| d4a7ccf1c9 | |||
| 9ae3906971 | |||
| 4b753266b3 | |||
| 49437817a5 | |||
| 5d80c510eb | |||
| d850f3f4fd | |||
| 45042d9cc7 | |||
| fdb8b52e38 | |||
| f0a9a13d57 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -3,3 +3,6 @@ dist/
|
|||||||
.env
|
.env
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
*.tsbuildinfo
|
||||||
|
vite.config.d.ts
|
||||||
|
vite.config.js
|
||||||
|
|||||||
67
AGENTS.md
Normal file
67
AGENTS.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# MoexVibe — Инструкция для агента
|
||||||
|
|
||||||
|
## Репозиторий
|
||||||
|
|
||||||
|
npm workspaces монорепозиторий: `apps/backend` (NestJS), `apps/frontend` (React + Vite).
|
||||||
|
|
||||||
|
## Обязательный подход к разработке
|
||||||
|
|
||||||
|
- **SDD (Specification-Driven Development)**: перед написанием кода сначала сформировать спецификацию — PRD, доменную модель, ADR, OpenAPI-контракт, архитектуру фронтенда и бэкенда, план реализации по этапам.
|
||||||
|
- **Superpowers**: обязательно использовать скиллы (Skills) при старте любой задачи — brainstorming, frontend-design, test-driven-development, writing-plans, executing-plans, requesting-code-review.
|
||||||
|
- **MCP-инструменты**: использовать MCP для анализа и генерации дизайна, работы с API, генерации кода.
|
||||||
|
|
||||||
|
## Команды
|
||||||
|
|
||||||
|
| Команда | Что делает |
|
||||||
|
|---|---|
|
||||||
|
| `npm run dev:backend` | Запуск NestJS в режиме watch на :3000 |
|
||||||
|
| `npm run dev:frontend` | Vite dev-сервер на :5173, проксирует `/api` → :3000 |
|
||||||
|
| `npm run build:backend` | `nest build` |
|
||||||
|
| `npm run build:frontend` | `tsc -b && vite build` (в две фазы) |
|
||||||
|
| `npm run test:backend` | `vitest run` (SWC, не ts-jest) |
|
||||||
|
| `npm run lint` | ESLint только для бэкенда |
|
||||||
|
| `npm run format` | Prettier для всех `*.{ts,tsx}` |
|
||||||
|
| `npm run codegen -w apps/frontend` | `openapi-typescript` из локального Swagger → `src/api/types.ts` |
|
||||||
|
|
||||||
|
Один тест: `npx vitest run path/to/test.spec.ts -w apps/backend`
|
||||||
|
|
||||||
|
## Переменные окружения
|
||||||
|
|
||||||
|
| Переменная | По умолчанию | Описание |
|
||||||
|
|---|---|---|
|
||||||
|
| `PORT` | 3000 | Порт бэкенда |
|
||||||
|
| `MOEX_BASE_URL` | `https://iss.moex.com/iss` | Endpoint MOEX ISS |
|
||||||
|
| `MOEX_RATE_LIMIT` | 10 | Запросов/с к MOEX |
|
||||||
|
| `CACHE_MARKET_DATA_TTL` | 900 | TTL рыночных данных (с) |
|
||||||
|
| `CACHE_HISTORY_TTL` | 3600 | TTL истории (с) |
|
||||||
|
| `CACHE_CANDLES_TTL` | 3600 | TTL свечей (с) |
|
||||||
|
| `CACHE_SECURITY_TTL` | 86400 | TTL спецификации (с) |
|
||||||
|
| `CACHE_SEARCH_TTL` | 3600 | TTL результатов поиска (с) |
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
- **Бэкенд** — единственный клиент MOEX. Фронтенд никогда не обращается к MOEX напрямую.
|
||||||
|
- Feature-модули: `MoexClientModule` (глобальный), `CacheModule` (глобальный), `SharesModule`, `BondsModule`, `SecuritiesModule`, `CandlesModule`, `HealthModule`.
|
||||||
|
- `MoexClientService` использует p-queue (rate limiter) + circuit breaker (5 ошибок → 30s открыт).
|
||||||
|
- In-memory кеш через `@nestjs/cache-manager`. Путь миграции на Redis описан (см. ADR-002).
|
||||||
|
- Глобальный префикс NestJS: `/api/v1`. Swagger: `/api/docs`.
|
||||||
|
- Глобальный ValidationPipe (`transform: true, whitelist: true`), `HttpExceptionFilter`, `TransformInterceptor`, middleware логирования запросов.
|
||||||
|
- Ответы API обёрнуты в `{ data: T, meta: { fromCache, cachedAt } }`.
|
||||||
|
- Алиасы: `@/*` → `src/*` в обоих пакетах.
|
||||||
|
|
||||||
|
## Фронтенд
|
||||||
|
|
||||||
|
- React 18 + react-router-dom v6 + TanStack Query v5.
|
||||||
|
- `lightweight-charts` v4 для графиков цен.
|
||||||
|
- `openapi-fetch` + рукописные типы `responses.ts` (не полностью codegen'овые).
|
||||||
|
- TanStack Query по умолчанию: `staleTime: 900s`, `retry: 2`, `refetchOnWindowFocus: false`.
|
||||||
|
- Конвенция ключей запросов: `['stock', secid]`, `['securities', 'search', query]`, и т.д.
|
||||||
|
- CSS через `styles.css` (CSS custom properties, без CSS-in-JS или Tailwind).
|
||||||
|
|
||||||
|
## Стиль кода
|
||||||
|
|
||||||
|
- Prettier: одинарные кавычки, trailing commas, printWidth 100, точки с запятой.
|
||||||
|
- Бэкенд: `const`, PascalCase для модулей/контроллеров/сервисов, DTO в `dto/` внутри каждого модуля.
|
||||||
|
- Бэкенд использует SWC через `unplugin-swc` (vitest config).
|
||||||
|
- Тесты фронтенда отсутствуют.
|
||||||
|
- CI/CD в репозитории нет.
|
||||||
51
README.md
Normal file
51
README.md
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# MoexVibe
|
||||||
|
|
||||||
|
Веб-приложение для анализа ценных бумаг Московской биржи (MOEX).
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Backend:** NestJS, TypeScript, OpenAPI (Swagger)
|
||||||
|
- **Frontend:** React, TypeScript, Vite, TanStack Query, lightweight-charts
|
||||||
|
- **Infrastructure:** Docker, docker-compose
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Start backend (http://localhost:3000)
|
||||||
|
npm run dev:backend
|
||||||
|
|
||||||
|
# Start frontend (http://localhost:5173)
|
||||||
|
npm run dev:frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
Swagger UI: http://localhost:3000/api/docs
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
- Frontend: http://localhost:80
|
||||||
|
- Backend: http://localhost:3000
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:backend
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/
|
||||||
|
backend/ — NestJS API (single point of access to MOEX ISS)
|
||||||
|
frontend/ — React SPA with Vite
|
||||||
|
docs/
|
||||||
|
architecture/ — ADR documents and diagrams
|
||||||
|
openapi/ — OpenAPI specification
|
||||||
|
superpowers/ — Design specs and implementation plans
|
||||||
|
```
|
||||||
@ -15,7 +15,8 @@ async function bootstrap() {
|
|||||||
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
|
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
|
||||||
app.useGlobalFilters(new HttpExceptionFilter());
|
app.useGlobalFilters(new HttpExceptionFilter());
|
||||||
app.useGlobalInterceptors(new TransformInterceptor());
|
app.useGlobalInterceptors(new TransformInterceptor());
|
||||||
app.use(new RequestLoggingMiddleware().use);
|
const reqLogMiddleware = new RequestLoggingMiddleware();
|
||||||
|
app.use(reqLogMiddleware.use.bind(reqLogMiddleware));
|
||||||
|
|
||||||
app.enableCors();
|
app.enableCors();
|
||||||
|
|
||||||
|
|||||||
31
apps/backend/src/modules/bonds/bonds.controller.ts
Normal file
31
apps/backend/src/modules/bonds/bonds.controller.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||||
|
import { BondsService } from './bonds.service';
|
||||||
|
|
||||||
|
@ApiTags('Bonds')
|
||||||
|
@Controller('securities/bonds')
|
||||||
|
export class BondsController {
|
||||||
|
constructor(private readonly bondsService: BondsService) {}
|
||||||
|
|
||||||
|
@Get(':secid')
|
||||||
|
@ApiOperation({ summary: 'Получить спецификацию облигации' })
|
||||||
|
async getBond(@Param('secid') secid: string) {
|
||||||
|
return this.bondsService.getBond(secid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':secid/marketdata')
|
||||||
|
@ApiOperation({ summary: 'Получить рыночные данные облигации' })
|
||||||
|
async getMarketData(@Param('secid') secid: string) {
|
||||||
|
return this.bondsService.getMarketData(secid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':secid/history')
|
||||||
|
@ApiOperation({ summary: 'Получить дневную историю торгов облигации' })
|
||||||
|
async getHistory(
|
||||||
|
@Param('secid') secid: string,
|
||||||
|
@Query('from') from: string,
|
||||||
|
@Query('till') till: string,
|
||||||
|
) {
|
||||||
|
return this.bondsService.getHistory(secid, from, till);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/backend/src/modules/bonds/bonds.module.ts
Normal file
10
apps/backend/src/modules/bonds/bonds.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { BondsController } from './bonds.controller';
|
||||||
|
import { BondsService } from './bonds.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [BondsController],
|
||||||
|
providers: [BondsService],
|
||||||
|
exports: [BondsService],
|
||||||
|
})
|
||||||
|
export class BondsModule {}
|
||||||
37
apps/backend/src/modules/bonds/bonds.service.spec.ts
Normal file
37
apps/backend/src/modules/bonds/bonds.service.spec.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { BondsService } from './bonds.service';
|
||||||
|
import { MoexClientService } from '../moex-client/moex-client.service';
|
||||||
|
import { CacheService } from '../cache/cache.service';
|
||||||
|
import configuration from '../../config/configuration';
|
||||||
|
|
||||||
|
describe('BondsService', () => {
|
||||||
|
let service: BondsService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [ConfigModule.forRoot({ load: [configuration] })],
|
||||||
|
providers: [
|
||||||
|
BondsService,
|
||||||
|
MoexClientService,
|
||||||
|
{
|
||||||
|
provide: 'CACHE_MANAGER',
|
||||||
|
useValue: { get: () => undefined, set: () => Promise.resolve(), del: () => Promise.resolve() },
|
||||||
|
},
|
||||||
|
CacheService,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<BondsService>(BondsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OFZ bond data for SU26207RMFS9', async () => {
|
||||||
|
const result = await service.getBond('SU26207RMFS9');
|
||||||
|
expect(result.data.secid).toBe('SU26207RMFS9');
|
||||||
|
expect(result.data.marketData).toBeDefined();
|
||||||
|
}, 15000);
|
||||||
|
});
|
||||||
124
apps/backend/src/modules/bonds/bonds.service.ts
Normal file
124
apps/backend/src/modules/bonds/bonds.service.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
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 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
101
apps/backend/src/modules/bonds/dto/bond-response.dto.ts
Normal file
101
apps/backend/src/modules/bonds/dto/bond-response.dto.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class BondMarketDataDto {
|
||||||
|
@ApiProperty({ description: 'Цена в % от номинала', example: 100.45 })
|
||||||
|
price!: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 12.71 })
|
||||||
|
yieldToMaturity!: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
duration!: number | null;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 29.48 })
|
||||||
|
accruedInt!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 40.64 })
|
||||||
|
couponValue!: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 8.15 })
|
||||||
|
couponPercent!: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: '2026-08-05' })
|
||||||
|
nextCouponDate!: string | null;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
open!: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
high!: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
low!: number | null;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
volume!: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
updatedAt!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BondResponseDto {
|
||||||
|
@ApiProperty({ example: 'SU26207RMFS9' })
|
||||||
|
secid!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'RU000A0JS3W6' })
|
||||||
|
isin!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'ОФЗ-ПД 26207 03/02/27' })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'ОФЗ 26207' })
|
||||||
|
shortName!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
latName!: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 1 })
|
||||||
|
listLevel!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 370200604 })
|
||||||
|
issueSize!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 1000 })
|
||||||
|
faceValue!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'RUB' })
|
||||||
|
faceUnit!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2027-02-03' })
|
||||||
|
matDate!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 40.64 })
|
||||||
|
couponValue!: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 8.15 })
|
||||||
|
couponPercent!: number | null;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 182 })
|
||||||
|
couponPeriod!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2026-08-05' })
|
||||||
|
nextCoupon!: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 29.48 })
|
||||||
|
accruedInt!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Фикс с известным купоном' })
|
||||||
|
bondType!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'До погашения' })
|
||||||
|
bondSubType!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
offerDate!: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
buybackDate!: string | null;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
marketData!: BondMarketDataDto;
|
||||||
|
}
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Global, Module } from '@nestjs/common';
|
||||||
import { CacheModule as NestCacheModule } from '@nestjs/cache-manager';
|
import { CacheModule as NestCacheModule } from '@nestjs/cache-manager';
|
||||||
import { CacheService } from './cache.service';
|
import { CacheService } from './cache.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
NestCacheModule.register({
|
NestCacheModule.register({
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
@ -182,7 +182,7 @@ export class MoexClientService {
|
|||||||
{ boards: boardId },
|
{ boards: boardId },
|
||||||
);
|
);
|
||||||
const rows = this.extractTable(data, 'securities');
|
const rows = this.extractTable(data, 'securities');
|
||||||
const bond = rows.find((r) => r.BOARDID === boardId);
|
const bond = rows.find((r) => r.BOARDID === boardId) || rows[0];
|
||||||
if (!bond) return null;
|
if (!bond) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
import { StockMarketDataDto } from './share-response.dto';
|
||||||
|
|
||||||
|
export class ShareMarketDataResponseDto extends StockMarketDataDto {}
|
||||||
68
apps/backend/src/modules/shares/dto/share-response.dto.ts
Normal file
68
apps/backend/src/modules/shares/dto/share-response.dto.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class StockMarketDataDto {
|
||||||
|
@ApiProperty({ example: 322.35 })
|
||||||
|
price!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 1.15 })
|
||||||
|
change!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 0.36 })
|
||||||
|
changePercent!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 321.3 })
|
||||||
|
open!: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 322.66 })
|
||||||
|
high!: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 321.2 })
|
||||||
|
low!: number | null;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 1925163 })
|
||||||
|
volume!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 620184479 })
|
||||||
|
value!: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 6958336818320 })
|
||||||
|
issueCapitalization!: number | null;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
updatedAt!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ShareResponseDto {
|
||||||
|
@ApiProperty({ example: 'SBER' })
|
||||||
|
secid!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'RU0009029540' })
|
||||||
|
isin!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Сбербанк России ПАО ао' })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Сбербанк' })
|
||||||
|
shortName!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
latName!: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 1 })
|
||||||
|
listLevel!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 21586948000 })
|
||||||
|
issueSize!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 3 })
|
||||||
|
faceValue!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'RUB' })
|
||||||
|
faceUnit!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'common_share' })
|
||||||
|
type!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
marketData!: StockMarketDataDto;
|
||||||
|
}
|
||||||
38
apps/backend/src/modules/shares/shares.controller.ts
Normal file
38
apps/backend/src/modules/shares/shares.controller.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||||
|
import { SharesService } from './shares.service';
|
||||||
|
|
||||||
|
@ApiTags('Shares')
|
||||||
|
@Controller('securities/shares')
|
||||||
|
export class SharesController {
|
||||||
|
constructor(private readonly sharesService: SharesService) {}
|
||||||
|
|
||||||
|
@Get(':secid')
|
||||||
|
@ApiOperation({ summary: 'Получить спецификацию акции' })
|
||||||
|
async getShare(@Param('secid') secid: string) {
|
||||||
|
const share = await this.sharesService.getShare(secid);
|
||||||
|
return { data: share, meta: { cachedAt: null, fromCache: false } };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':secid/marketdata')
|
||||||
|
@ApiOperation({ summary: 'Получить рыночные данные акции' })
|
||||||
|
async getMarketData(@Param('secid') secid: string) {
|
||||||
|
return this.sharesService.getMarketData(secid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':secid/dividends')
|
||||||
|
@ApiOperation({ summary: 'Получить дивиденды' })
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/backend/src/modules/shares/shares.module.ts
Normal file
10
apps/backend/src/modules/shares/shares.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { SharesController } from './shares.controller';
|
||||||
|
import { SharesService } from './shares.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [SharesController],
|
||||||
|
providers: [SharesService],
|
||||||
|
exports: [SharesService],
|
||||||
|
})
|
||||||
|
export class SharesModule {}
|
||||||
37
apps/backend/src/modules/shares/shares.service.spec.ts
Normal file
37
apps/backend/src/modules/shares/shares.service.spec.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { SharesService } from './shares.service';
|
||||||
|
import { MoexClientService } from '../moex-client/moex-client.service';
|
||||||
|
import { CacheService } from '../cache/cache.service';
|
||||||
|
import configuration from '../../config/configuration';
|
||||||
|
|
||||||
|
describe('SharesService', () => {
|
||||||
|
let service: SharesService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [ConfigModule.forRoot({ load: [configuration] })],
|
||||||
|
providers: [
|
||||||
|
SharesService,
|
||||||
|
MoexClientService,
|
||||||
|
{
|
||||||
|
provide: 'CACHE_MANAGER',
|
||||||
|
useValue: { get: () => undefined, set: () => Promise.resolve(), del: () => Promise.resolve() },
|
||||||
|
},
|
||||||
|
CacheService,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<SharesService>(SharesService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return SBER share data', async () => {
|
||||||
|
const share = await service.getShare('SBER');
|
||||||
|
expect(share.secid).toBe('SBER');
|
||||||
|
expect(share.marketData).toBeDefined();
|
||||||
|
}, 15000);
|
||||||
|
});
|
||||||
127
apps/backend/src/modules/shares/shares.service.ts
Normal file
127
apps/backend/src/modules/shares/shares.service.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { MoexClientService } from '../moex-client/moex-client.service';
|
||||||
|
import { CacheService } from '../cache/cache.service';
|
||||||
|
|
||||||
|
@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 {
|
||||||
|
data: {
|
||||||
|
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(),
|
||||||
|
},
|
||||||
|
meta: { fromCache, cachedAt },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDividends(secid: string) {
|
||||||
|
const { data, fromCache, cachedAt } = await this.cache.getOrFetch(
|
||||||
|
'dividends',
|
||||||
|
[secid],
|
||||||
|
() => this.moexClient.getDividends(secid),
|
||||||
|
'dividendsTtl',
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data.map((d) => ({
|
||||||
|
registryCloseDate: d.registryCloseDate,
|
||||||
|
value: d.value,
|
||||||
|
currency: d.currencyId,
|
||||||
|
})),
|
||||||
|
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 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
12
apps/frontend/index.html
Normal file
12
apps/frontend/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>MoexVibe</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
28
apps/frontend/package.json
Normal file
28
apps/frontend/package.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "@moex-vibe/frontend",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"codegen": "openapi-typescript http://localhost:3000/api/docs-json -o src/api/types.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.3.0",
|
||||||
|
"react-dom": "^18.3.0",
|
||||||
|
"react-router-dom": "^6.20.0",
|
||||||
|
"@tanstack/react-query": "^5.20.0",
|
||||||
|
"openapi-fetch": "^0.9.0",
|
||||||
|
"lightweight-charts": "^4.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.0",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.2.0",
|
||||||
|
"typescript": "^5.3.0",
|
||||||
|
"vite": "^5.4.0",
|
||||||
|
"openapi-typescript": "^7.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/frontend/src/App.tsx
Normal file
10
apps/frontend/src/App.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import { AppRoutes } from './routes';
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<AppRoutes />
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
103
apps/frontend/src/api/client.ts
Normal file
103
apps/frontend/src/api/client.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import type {
|
||||||
|
ApiEnvelope,
|
||||||
|
ApiResponseMeta,
|
||||||
|
ShareResponse,
|
||||||
|
StockMarketData,
|
||||||
|
DividendItem,
|
||||||
|
ShareHistoryItem,
|
||||||
|
BondResponse,
|
||||||
|
BondMarketData,
|
||||||
|
BondHistoryItem,
|
||||||
|
CandleItem,
|
||||||
|
SearchResultItem,
|
||||||
|
HealthResponse,
|
||||||
|
} from './responses';
|
||||||
|
|
||||||
|
const BASE = '';
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
if (v !== undefined) url.searchParams.set(k, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const res = await fetch(url.toString());
|
||||||
|
if (!res.ok) throw new Error(`API error: ${res.status} ${res.statusText}`);
|
||||||
|
const json: ApiEnvelope<{ data: T; meta: ApiResponseMeta }> = await res.json();
|
||||||
|
return json.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHealth(): Promise<{ data: HealthResponse; meta: ApiResponseMeta }> {
|
||||||
|
return request<HealthResponse>('/api/v1/health');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function searchSecurities(
|
||||||
|
q: string,
|
||||||
|
type: 'all' | 'share' | 'bond' = 'all',
|
||||||
|
limit = 20,
|
||||||
|
): Promise<{ data: SearchResultItem[]; meta: ApiResponseMeta }> {
|
||||||
|
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 getShareDividends(secid: string): Promise<{ data: DividendItem[]; meta: ApiResponseMeta }> {
|
||||||
|
return request<DividendItem[]>(`/api/v1/securities/shares/${encodeURIComponent(secid)}/dividends`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getShareHistory(
|
||||||
|
secid: string,
|
||||||
|
from: string,
|
||||||
|
till: string,
|
||||||
|
): Promise<{ data: ShareHistoryItem[]; meta: ApiResponseMeta }> {
|
||||||
|
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 getBondHistory(
|
||||||
|
secid: string,
|
||||||
|
from: string,
|
||||||
|
till: string,
|
||||||
|
): Promise<{ data: BondHistoryItem[]; meta: ApiResponseMeta }> {
|
||||||
|
return request<BondHistoryItem[]>(`/api/v1/securities/bonds/${encodeURIComponent(secid)}/history`, { from, till });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getShareCandles(
|
||||||
|
secid: string,
|
||||||
|
interval: '1h' | '24h',
|
||||||
|
from: string,
|
||||||
|
till: string,
|
||||||
|
): Promise<{ data: CandleItem[]; meta: ApiResponseMeta }> {
|
||||||
|
return request<CandleItem[]>(`/api/v1/securities/shares/${encodeURIComponent(secid)}/candles`, {
|
||||||
|
interval,
|
||||||
|
from,
|
||||||
|
till,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBondCandles(
|
||||||
|
secid: string,
|
||||||
|
interval: '1h' | '24h',
|
||||||
|
from: string,
|
||||||
|
till: string,
|
||||||
|
): Promise<{ data: CandleItem[]; meta: ApiResponseMeta }> {
|
||||||
|
return request<CandleItem[]>(`/api/v1/securities/bonds/${encodeURIComponent(secid)}/candles`, {
|
||||||
|
interval,
|
||||||
|
from,
|
||||||
|
till,
|
||||||
|
});
|
||||||
|
}
|
||||||
124
apps/frontend/src/api/responses.ts
Normal file
124
apps/frontend/src/api/responses.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
export interface ApiResponseMeta {
|
||||||
|
cachedAt: string | null;
|
||||||
|
fromCache: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiEnvelope<T> {
|
||||||
|
data: T;
|
||||||
|
meta: ApiResponseMeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StockMarketData {
|
||||||
|
price: number;
|
||||||
|
change: number;
|
||||||
|
changePercent: number;
|
||||||
|
open: number;
|
||||||
|
high: number | null;
|
||||||
|
low: number | null;
|
||||||
|
volume: number;
|
||||||
|
value: number;
|
||||||
|
issueCapitalization: number | null;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShareResponse {
|
||||||
|
secid: string;
|
||||||
|
isin: string;
|
||||||
|
name: string;
|
||||||
|
shortName: string;
|
||||||
|
latName: string | null;
|
||||||
|
listLevel: number;
|
||||||
|
issueSize: number;
|
||||||
|
faceValue: number;
|
||||||
|
faceUnit: string;
|
||||||
|
type: string;
|
||||||
|
marketData: StockMarketData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DividendItem {
|
||||||
|
registryCloseDate: string;
|
||||||
|
value: number;
|
||||||
|
currency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShareHistoryItem {
|
||||||
|
date: string;
|
||||||
|
open: number;
|
||||||
|
high: number;
|
||||||
|
low: number;
|
||||||
|
close: number;
|
||||||
|
volume: number;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BondMarketData {
|
||||||
|
price: number;
|
||||||
|
yieldToMaturity: number | null;
|
||||||
|
duration: number | null;
|
||||||
|
accruedInt: number;
|
||||||
|
couponValue: number;
|
||||||
|
couponPercent: number | null;
|
||||||
|
nextCouponDate: string | null;
|
||||||
|
open: number;
|
||||||
|
high: number | null;
|
||||||
|
low: number | null;
|
||||||
|
volume: number;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BondResponse {
|
||||||
|
secid: string;
|
||||||
|
isin: string;
|
||||||
|
name: string;
|
||||||
|
shortName: string;
|
||||||
|
latName: string | null;
|
||||||
|
listLevel: number;
|
||||||
|
issueSize: number;
|
||||||
|
faceValue: number;
|
||||||
|
faceUnit: string;
|
||||||
|
matDate: string;
|
||||||
|
couponValue: number;
|
||||||
|
couponPercent: number | null;
|
||||||
|
couponPeriod: number;
|
||||||
|
nextCoupon: string | null;
|
||||||
|
accruedInt: number;
|
||||||
|
bondType: string;
|
||||||
|
bondSubType: string;
|
||||||
|
offerDate: string | null;
|
||||||
|
buybackDate: string | null;
|
||||||
|
marketData: BondMarketData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BondHistoryItem {
|
||||||
|
date: string;
|
||||||
|
closePrice: number;
|
||||||
|
yieldClose: number | null;
|
||||||
|
duration: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CandleItem {
|
||||||
|
open: number;
|
||||||
|
high: number;
|
||||||
|
low: number;
|
||||||
|
close: number;
|
||||||
|
volume: number;
|
||||||
|
value: number;
|
||||||
|
begin: string;
|
||||||
|
end: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchResultItem {
|
||||||
|
secid: string;
|
||||||
|
isin: string;
|
||||||
|
shortName: string;
|
||||||
|
type: 'share' | 'bond';
|
||||||
|
listLevel: number;
|
||||||
|
currency: string | null;
|
||||||
|
price: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthResponse {
|
||||||
|
status: string;
|
||||||
|
timestamp: string;
|
||||||
|
uptime: number;
|
||||||
|
}
|
||||||
430
apps/frontend/src/api/types.ts
Normal file
430
apps/frontend/src/api/types.ts
Normal file
@ -0,0 +1,430 @@
|
|||||||
|
/**
|
||||||
|
* This file was auto-generated by openapi-typescript.
|
||||||
|
* Do not make direct changes to the file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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/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;
|
||||||
|
};
|
||||||
|
"/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;
|
||||||
|
};
|
||||||
|
"/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;
|
||||||
|
};
|
||||||
|
"/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;
|
||||||
|
};
|
||||||
|
"/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;
|
||||||
|
};
|
||||||
|
"/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;
|
||||||
|
};
|
||||||
|
"/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;
|
||||||
|
};
|
||||||
|
"/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;
|
||||||
|
};
|
||||||
|
"/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;
|
||||||
|
};
|
||||||
|
"/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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
SharesController_getShare: {
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
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_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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
74
apps/frontend/src/components/BondDetails.tsx
Normal file
74
apps/frontend/src/components/BondDetails.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import type { BondResponse } from '../api/responses';
|
||||||
|
|
||||||
|
interface BondDetailsProps {
|
||||||
|
bond: BondResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowStyle: React.CSSProperties = {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
padding: '8px 0',
|
||||||
|
borderBottom: '1px solid #eee',
|
||||||
|
};
|
||||||
|
|
||||||
|
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={{ marginBottom: 16 }}>
|
||||||
|
<h2 style={{ fontSize: 28, fontWeight: 700 }}>{bond.shortName}</h2>
|
||||||
|
<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={{ marginTop: 16 }}>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span>Номинал</span>
|
||||||
|
<span>{bond.faceValue.toLocaleString('ru-RU')} {bond.faceUnit}</span>
|
||||||
|
</div>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span>Дата погашения</span>
|
||||||
|
<span>{bond.matDate}</span>
|
||||||
|
</div>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span>Купон</span>
|
||||||
|
<span>{md.couponValue} ₽ {md.couponPercent != null ? `(${md.couponPercent}%)` : ''}</span>
|
||||||
|
</div>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span>Период купона</span>
|
||||||
|
<span>{bond.couponPeriod} дней</span>
|
||||||
|
</div>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span>Следующий купон</span>
|
||||||
|
<span>{md.nextCouponDate ?? '—'}</span>
|
||||||
|
</div>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span>НКД</span>
|
||||||
|
<span>{md.accruedInt.toFixed(2)} ₽</span>
|
||||||
|
</div>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span>Доходность к погашению</span>
|
||||||
|
<span>{md.yieldToMaturity != null ? md.yieldToMaturity.toFixed(2) + '%' : '—'}</span>
|
||||||
|
</div>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span>Дюрация</span>
|
||||||
|
<span>{md.duration != null ? md.duration.toFixed(2) : '—'}</span>
|
||||||
|
</div>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span>Тип</span>
|
||||||
|
<span>{bond.bondType}</span>
|
||||||
|
</div>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span>ISIN</span>
|
||||||
|
<span>{bond.isin}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
apps/frontend/src/components/Layout.tsx
Normal file
27
apps/frontend/src/components/Layout.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Outlet, Link } from 'react-router-dom';
|
||||||
|
import { SearchBar } from './SearchBar';
|
||||||
|
|
||||||
|
export function Layout() {
|
||||||
|
return (
|
||||||
|
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<header
|
||||||
|
style={{
|
||||||
|
background: 'var(--color-surface)',
|
||||||
|
borderBottom: '1px solid #e0e0e0',
|
||||||
|
padding: '12px 24px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 24,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link to="/" style={{ fontSize: 20, fontWeight: 700, color: 'var(--color-text)', textDecoration: 'none' }}>
|
||||||
|
MoexVibe
|
||||||
|
</Link>
|
||||||
|
<SearchBar />
|
||||||
|
</header>
|
||||||
|
<main style={{ flex: 1, padding: 24, maxWidth: 1200, width: '100%', margin: '0 auto' }}>
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
apps/frontend/src/components/PriceChart.tsx
Normal file
71
apps/frontend/src/components/PriceChart.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { createChart, ColorType, CandlestickData, Time } from 'lightweight-charts';
|
||||||
|
|
||||||
|
interface PriceChartProps {
|
||||||
|
data: Array<{
|
||||||
|
open: number;
|
||||||
|
high: number;
|
||||||
|
low: number;
|
||||||
|
close: number;
|
||||||
|
begin: string;
|
||||||
|
}>;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PriceChart({ data, height = 400 }: PriceChartProps) {
|
||||||
|
const chartContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!chartContainerRef.current) return;
|
||||||
|
|
||||||
|
const chart = createChart(chartContainerRef.current, {
|
||||||
|
layout: {
|
||||||
|
background: { type: ColorType.Solid, color: '#ffffff' },
|
||||||
|
textColor: '#333',
|
||||||
|
},
|
||||||
|
width: chartContainerRef.current.clientWidth,
|
||||||
|
height,
|
||||||
|
grid: {
|
||||||
|
vertLines: { color: '#f0f0f0' },
|
||||||
|
horzLines: { color: '#f0f0f0' },
|
||||||
|
},
|
||||||
|
timeScale: {
|
||||||
|
timeVisible: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const candleSeries = chart.addCandlestickSeries({
|
||||||
|
upColor: '#2e7d32',
|
||||||
|
downColor: '#c62828',
|
||||||
|
borderDownColor: '#c62828',
|
||||||
|
borderUpColor: '#2e7d32',
|
||||||
|
wickDownColor: '#c62828',
|
||||||
|
wickUpColor: '#2e7d32',
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartData: CandlestickData[] = data.map((candle) => ({
|
||||||
|
time: (new Date(candle.begin).getTime() / 1000) as Time,
|
||||||
|
open: candle.open,
|
||||||
|
high: candle.high,
|
||||||
|
low: candle.low,
|
||||||
|
close: candle.close,
|
||||||
|
}));
|
||||||
|
|
||||||
|
candleSeries.setData(chartData);
|
||||||
|
chart.timeScale().fitContent();
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
if (chartContainerRef.current) {
|
||||||
|
chart.applyOptions({ width: chartContainerRef.current.clientWidth });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
chart.remove();
|
||||||
|
};
|
||||||
|
}, [data, height]);
|
||||||
|
|
||||||
|
return <div ref={chartContainerRef} />;
|
||||||
|
}
|
||||||
101
apps/frontend/src/components/SearchBar.tsx
Normal file
101
apps/frontend/src/components/SearchBar.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useSearch } from '../hooks/useSearch';
|
||||||
|
|
||||||
|
export function SearchBar() {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const [debounced, setDebounced] = useState('');
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setTimeout(() => setDebounced(query), 300);
|
||||||
|
return () => clearTimeout(id);
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
const { data: results, isLoading } = useSearch(debounced);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClick(e: MouseEvent) {
|
||||||
|
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', handleClick);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClick);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const showResults = open && debounced.length >= 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} style={{ position: 'relative', width: 400, maxWidth: '100%' }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Поиск акций и облигаций..."
|
||||||
|
value={query}
|
||||||
|
onChange={e => { setQuery(e.target.value); setOpen(true); }}
|
||||||
|
onFocus={() => setOpen(true)}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '8px 12px',
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: 6,
|
||||||
|
fontSize: 14,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{showResults && (
|
||||||
|
<ul
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '100%',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
background: '#fff',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
borderRadius: 6,
|
||||||
|
marginTop: 4,
|
||||||
|
padding: 0,
|
||||||
|
listStyle: 'none',
|
||||||
|
zIndex: 100,
|
||||||
|
maxHeight: 360,
|
||||||
|
overflowY: 'auto',
|
||||||
|
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isLoading && <li style={{ padding: 12, color: '#888' }}>Загрузка...</li>}
|
||||||
|
{!isLoading && results && results.length === 0 && (
|
||||||
|
<li style={{ padding: 12, color: '#888' }}>Ничего не найдено</li>
|
||||||
|
)}
|
||||||
|
{!isLoading &&
|
||||||
|
results?.map(item => (
|
||||||
|
<li
|
||||||
|
key={item.secid}
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
setQuery('');
|
||||||
|
navigate(item.type === 'share' ? `/stocks/${item.secid}` : `/bonds/${item.secid}`);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
padding: '10px 12px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderBottom: '1px solid #f0f0f0',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
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' }}>
|
||||||
|
{item.type === 'share' ? 'Акция' : 'Облигация'}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
apps/frontend/src/components/StockDetails.tsx
Normal file
72
apps/frontend/src/components/StockDetails.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import type { ShareResponse } from '../api/responses';
|
||||||
|
|
||||||
|
interface StockDetailsProps {
|
||||||
|
stock: ShareResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowStyle: React.CSSProperties = {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
padding: '8px 0',
|
||||||
|
borderBottom: '1px solid #eee',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function StockDetails({ stock }: StockDetailsProps) {
|
||||||
|
const md = stock.marketData;
|
||||||
|
const isPositive = md.change >= 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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})
|
||||||
|
</h2>
|
||||||
|
<div style={{ fontSize: 14, color: 'var(--color-text-secondary)' }}>
|
||||||
|
{stock.name} · {stock.isin}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span>Открытие</span>
|
||||||
|
<span>{md.open.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span>Максимум</span>
|
||||||
|
<span>{md.high?.toFixed(2) ?? '—'}</span>
|
||||||
|
</div>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span>Минимум</span>
|
||||||
|
<span>{md.low?.toFixed(2) ?? '—'}</span>
|
||||||
|
</div>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span>Объём</span>
|
||||||
|
<span>{md.volume.toLocaleString('ru-RU')}</span>
|
||||||
|
</div>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span>Капитализация</span>
|
||||||
|
<span>
|
||||||
|
{md.issueCapitalization
|
||||||
|
? (md.issueCapitalization / 1e9).toFixed(2) + ' млрд ₽'
|
||||||
|
: '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span>ISIN</span>
|
||||||
|
<span>{stock.isin}</span>
|
||||||
|
</div>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span>Уровень листинга</span>
|
||||||
|
<span>{stock.listLevel}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
apps/frontend/src/hooks/useBond.ts
Normal file
14
apps/frontend/src/hooks/useBond.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { getBond } from '../api/client';
|
||||||
|
import type { BondResponse } from '../api/responses';
|
||||||
|
|
||||||
|
export function useBond(secid: string) {
|
||||||
|
return useQuery<BondResponse>({
|
||||||
|
queryKey: ['bond', secid],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await getBond(secid);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
staleTime: 900_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
14
apps/frontend/src/hooks/useBondCandles.ts
Normal file
14
apps/frontend/src/hooks/useBondCandles.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { getBondCandles } from '../api/client';
|
||||||
|
import type { CandleItem } from '../api/responses';
|
||||||
|
|
||||||
|
export function useBondCandles(secid: string, interval: '1h' | '24h', from: string, till: string) {
|
||||||
|
return useQuery<CandleItem[]>({
|
||||||
|
queryKey: ['bondCandles', secid, interval, from, till],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await getBondCandles(secid, interval, from, till);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
staleTime: 3600_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
15
apps/frontend/src/hooks/useSearch.ts
Normal file
15
apps/frontend/src/hooks/useSearch.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { searchSecurities } from '../api/client';
|
||||||
|
import type { SearchResultItem } from '../api/responses';
|
||||||
|
|
||||||
|
export function useSearch(query: string) {
|
||||||
|
return useQuery<SearchResultItem[]>({
|
||||||
|
queryKey: ['securities', 'search', query],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await searchSecurities(query);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
enabled: query.length >= 2,
|
||||||
|
staleTime: 60_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
14
apps/frontend/src/hooks/useStock.ts
Normal file
14
apps/frontend/src/hooks/useStock.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { getShare } from '../api/client';
|
||||||
|
import type { ShareResponse } from '../api/responses';
|
||||||
|
|
||||||
|
export function useStock(secid: string) {
|
||||||
|
return useQuery<ShareResponse>({
|
||||||
|
queryKey: ['stock', secid],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await getShare(secid);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
staleTime: 900_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
14
apps/frontend/src/hooks/useStockCandles.ts
Normal file
14
apps/frontend/src/hooks/useStockCandles.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { getShareCandles } from '../api/client';
|
||||||
|
import type { CandleItem } from '../api/responses';
|
||||||
|
|
||||||
|
export function useStockCandles(secid: string, interval: '1h' | '24h', from: string, till: string) {
|
||||||
|
return useQuery<CandleItem[]>({
|
||||||
|
queryKey: ['stockCandles', secid, interval, from, till],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await getShareCandles(secid, interval, from, till);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
staleTime: 3600_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
14
apps/frontend/src/hooks/useStockDividends.ts
Normal file
14
apps/frontend/src/hooks/useStockDividends.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { getShareDividends } from '../api/client';
|
||||||
|
import type { DividendItem } from '../api/responses';
|
||||||
|
|
||||||
|
export function useStockDividends(secid: string) {
|
||||||
|
return useQuery<DividendItem[]>({
|
||||||
|
queryKey: ['stockDividends', secid],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await getShareDividends(secid);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
staleTime: 86400_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
23
apps/frontend/src/main.tsx
Normal file
23
apps/frontend/src/main.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import App from './App';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
retry: 2,
|
||||||
|
staleTime: 900_000,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<App />
|
||||||
|
</QueryClientProvider>
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
27
apps/frontend/src/pages/BondPage.tsx
Normal file
27
apps/frontend/src/pages/BondPage.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { useBond } from '../hooks/useBond';
|
||||||
|
import { useBondCandles } from '../hooks/useBondCandles';
|
||||||
|
import { BondDetails } from '../components/BondDetails';
|
||||||
|
import { PriceChart } from '../components/PriceChart';
|
||||||
|
|
||||||
|
export function BondPage() {
|
||||||
|
const { secid } = useParams<{ secid: string }>();
|
||||||
|
const { data: bond, isLoading, error } = useBond(secid!);
|
||||||
|
const till = new Date().toISOString().split('T')[0];
|
||||||
|
const from = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||||
|
const { data: candles } = useBondCandles(secid!, '24h', from, till);
|
||||||
|
|
||||||
|
if (isLoading) return <div>Загрузка...</div>;
|
||||||
|
if (error || !bond) return <div>Инструмент не найден</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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 }}>
|
||||||
|
<h3 style={{ marginBottom: 16 }}>График цены</h3>
|
||||||
|
<PriceChart data={candles ?? []} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
apps/frontend/src/pages/HomePage.tsx
Normal file
18
apps/frontend/src/pages/HomePage.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export function HomePage() {
|
||||||
|
return (
|
||||||
|
<div style={{ textAlign: 'center', paddingTop: 120 }}>
|
||||||
|
<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: '#aaa', fontSize: 12, marginTop: 8 }}>
|
||||||
|
Данные задерживаются на 15 минут · Бесплатный API MOEX ISS
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
apps/frontend/src/pages/StockPage.tsx
Normal file
53
apps/frontend/src/pages/StockPage.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { useStock } from '../hooks/useStock';
|
||||||
|
import { useStockCandles } from '../hooks/useStockCandles';
|
||||||
|
import { useStockDividends } from '../hooks/useStockDividends';
|
||||||
|
import { StockDetails } from '../components/StockDetails';
|
||||||
|
import { PriceChart } from '../components/PriceChart';
|
||||||
|
|
||||||
|
export function StockPage() {
|
||||||
|
const { secid } = useParams<{ secid: string }>();
|
||||||
|
const { data: stock, isLoading, error } = useStock(secid!);
|
||||||
|
const till = new Date().toISOString().split('T')[0];
|
||||||
|
const from = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||||
|
const { data: candles } = useStockCandles(secid!, '24h', from, till);
|
||||||
|
const { data: dividends } = useStockDividends(secid!);
|
||||||
|
|
||||||
|
if (isLoading) return <div>Загрузка...</div>;
|
||||||
|
if (error || !stock) return <div>Инструмент не найден</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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 }}>
|
||||||
|
<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 }}>
|
||||||
|
<h3 style={{ marginBottom: 16 }}>Дивиденды</h3>
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ borderBottom: '2px solid #eee' }}>
|
||||||
|
<th style={{ textAlign: 'left', padding: 8 }}>Дата закрытия реестра</th>
|
||||||
|
<th style={{ textAlign: 'right', padding: 8 }}>Сумма</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{dividends.map((d, i) => (
|
||||||
|
<tr key={i} style={{ borderBottom: '1px solid #eee' }}>
|
||||||
|
<td style={{ padding: 8 }}>{d.registryCloseDate}</td>
|
||||||
|
<td style={{ textAlign: 'right', padding: 8 }}>
|
||||||
|
{d.value.toFixed(2)} {d.currency}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
apps/frontend/src/routes.tsx
Normal file
17
apps/frontend/src/routes.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Routes, Route } from 'react-router-dom';
|
||||||
|
import { Layout } from './components/Layout';
|
||||||
|
import { HomePage } from './pages/HomePage';
|
||||||
|
import { StockPage } from './pages/StockPage';
|
||||||
|
import { BondPage } from './pages/BondPage';
|
||||||
|
|
||||||
|
export function AppRoutes() {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route element={<Layout />}>
|
||||||
|
<Route path="/" element={<HomePage />} />
|
||||||
|
<Route path="/stocks/:secid" element={<StockPage />} />
|
||||||
|
<Route path="/bonds/:secid" element={<BondPage />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
apps/frontend/src/styles.css
Normal file
31
apps/frontend/src/styles.css
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--color-bg: #f5f5f5;
|
||||||
|
--color-surface: #ffffff;
|
||||||
|
--color-text: #1a1a1a;
|
||||||
|
--color-text-secondary: #666;
|
||||||
|
--color-primary: #1976d2;
|
||||||
|
--color-positive: #2e7d32;
|
||||||
|
--color-negative: #c62828;
|
||||||
|
--border-radius: 8px;
|
||||||
|
--shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
1
apps/frontend/src/vite-env.d.ts
vendored
Normal file
1
apps/frontend/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
24
apps/frontend/tsconfig.json
Normal file
24
apps/frontend/tsconfig.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
15
apps/frontend/tsconfig.node.json
Normal file
15
apps/frontend/tsconfig.node.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"strict": true,
|
||||||
|
"composite": true,
|
||||||
|
"outDir": "./dist/tsconfig-node"
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
21
apps/frontend/vite.config.ts
Normal file
21
apps/frontend/vite.config.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:3000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
20
docker-compose.yml
Normal file
20
docker-compose.yml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/Dockerfile.backend
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- PORT=3000
|
||||||
|
- MOEX_BASE_URL=https://iss.moex.com/iss
|
||||||
|
- MOEX_RATE_LIMIT=10
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/Dockerfile.frontend
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
14
docker/Dockerfile.backend
Normal file
14
docker/Dockerfile.backend
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
FROM node:20-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json tsconfig.base.json ./
|
||||||
|
COPY apps/backend/ apps/backend/
|
||||||
|
RUN npm install
|
||||||
|
RUN npm run build -w apps/backend
|
||||||
|
|
||||||
|
FROM node:20-alpine AS production
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/apps/backend/dist ./dist
|
||||||
|
COPY --from=build /app/apps/backend/node_modules ./node_modules
|
||||||
|
COPY --from=build /app/apps/backend/package.json ./
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "dist/main.js"]
|
||||||
12
docker/Dockerfile.frontend
Normal file
12
docker/Dockerfile.frontend
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
FROM node:20-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json tsconfig.base.json ./
|
||||||
|
COPY apps/frontend/ apps/frontend/
|
||||||
|
RUN npm install
|
||||||
|
RUN npm run build -w apps/frontend
|
||||||
|
|
||||||
|
FROM nginx:alpine
|
||||||
|
COPY --from=build /app/apps/frontend/dist /usr/share/nginx/html
|
||||||
|
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
15
docker/nginx.conf
Normal file
15
docker/nginx.conf
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://backend:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
docs/architecture/overview.md
Normal file
24
docs/architecture/overview.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Architecture Overview
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User
|
||||||
|
participant Frontend as React SPA
|
||||||
|
participant Backend as NestJS API
|
||||||
|
participant Cache as In-Memory Cache
|
||||||
|
participant MOEX as MOEX ISS
|
||||||
|
|
||||||
|
User->>Frontend: Search / View instrument
|
||||||
|
Frontend->>Backend: GET /api/v1/securities/search?q=SBER
|
||||||
|
Backend->>Cache: getOrFetch('search:sber')
|
||||||
|
alt Cache miss
|
||||||
|
Cache->>Backend: null
|
||||||
|
Backend->>MOEX: GET /iss/securities?q=SBER
|
||||||
|
MOEX-->>Backend: raw data
|
||||||
|
Backend->>Cache: set('search:sber', normalized, TTL=3600)
|
||||||
|
else Cache hit
|
||||||
|
Cache-->>Backend: cached data
|
||||||
|
end
|
||||||
|
Backend-->>Frontend: normalized response
|
||||||
|
Frontend-->>User: rendered UI
|
||||||
|
```
|
||||||
822
package-lock.json
generated
822
package-lock.json
generated
@ -48,6 +48,26 @@
|
|||||||
"vitest": "^1.0.0"
|
"vitest": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"apps/frontend": {
|
||||||
|
"name": "@moex-vibe/frontend",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/react-query": "^5.20.0",
|
||||||
|
"lightweight-charts": "^4.1.0",
|
||||||
|
"openapi-fetch": "^0.9.0",
|
||||||
|
"react": "^18.3.0",
|
||||||
|
"react-dom": "^18.3.0",
|
||||||
|
"react-router-dom": "^6.20.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.0",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.2.0",
|
||||||
|
"openapi-typescript": "^7.0.0",
|
||||||
|
"typescript": "^5.3.0",
|
||||||
|
"vite": "^5.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@angular-devkit/core": {
|
"node_modules/@angular-devkit/core": {
|
||||||
"version": "17.3.11",
|
"version": "17.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz",
|
||||||
@ -223,6 +243,173 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/compat-data": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/core": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.29.7",
|
||||||
|
"@babel/generator": "^7.29.7",
|
||||||
|
"@babel/helper-compilation-targets": "^7.29.7",
|
||||||
|
"@babel/helper-module-transforms": "^7.29.7",
|
||||||
|
"@babel/helpers": "^7.29.7",
|
||||||
|
"@babel/parser": "^7.29.7",
|
||||||
|
"@babel/template": "^7.29.7",
|
||||||
|
"@babel/traverse": "^7.29.7",
|
||||||
|
"@babel/types": "^7.29.7",
|
||||||
|
"@jridgewell/remapping": "^2.3.5",
|
||||||
|
"convert-source-map": "^2.0.0",
|
||||||
|
"debug": "^4.1.0",
|
||||||
|
"gensync": "^1.0.0-beta.2",
|
||||||
|
"json5": "^2.2.3",
|
||||||
|
"semver": "^6.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/babel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/core/node_modules/semver": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/generator": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.29.7",
|
||||||
|
"@babel/types": "^7.29.7",
|
||||||
|
"@jridgewell/gen-mapping": "^0.3.12",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.28",
|
||||||
|
"jsesc": "^3.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-compilation-targets": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/compat-data": "^7.29.7",
|
||||||
|
"@babel/helper-validator-option": "^7.29.7",
|
||||||
|
"browserslist": "^4.24.0",
|
||||||
|
"lru-cache": "^5.1.1",
|
||||||
|
"semver": "^6.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"yallist": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-compilation-targets/node_modules/semver": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-globals": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-module-imports": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/traverse": "^7.29.7",
|
||||||
|
"@babel/types": "^7.29.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-module-transforms": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-module-imports": "^7.29.7",
|
||||||
|
"@babel/helper-validator-identifier": "^7.29.7",
|
||||||
|
"@babel/traverse": "^7.29.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/core": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-plugin-utils": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-string-parser": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
"version": "7.29.7",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||||
@ -233,6 +420,126 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/helper-validator-option": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helpers": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/template": "^7.29.7",
|
||||||
|
"@babel/types": "^7.29.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/parser": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.29.7"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"parser": "bin/babel-parser.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/plugin-transform-react-jsx-self": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-plugin-utils": "^7.29.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/core": "^7.0.0-0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/plugin-transform-react-jsx-source": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-plugin-utils": "^7.29.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/core": "^7.0.0-0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/template": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.29.7",
|
||||||
|
"@babel/parser": "^7.29.7",
|
||||||
|
"@babel/types": "^7.29.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/traverse": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.29.7",
|
||||||
|
"@babel/generator": "^7.29.7",
|
||||||
|
"@babel/helper-globals": "^7.29.7",
|
||||||
|
"@babel/parser": "^7.29.7",
|
||||||
|
"@babel/template": "^7.29.7",
|
||||||
|
"@babel/types": "^7.29.7",
|
||||||
|
"debug": "^4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/types": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-string-parser": "^7.29.7",
|
||||||
|
"@babel/helper-validator-identifier": "^7.29.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@borewit/text-codec": {
|
"node_modules/@borewit/text-codec": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz",
|
||||||
@ -1027,6 +1334,10 @@
|
|||||||
"resolved": "apps/backend",
|
"resolved": "apps/backend",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@moex-vibe/frontend": {
|
||||||
|
"resolved": "apps/frontend",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@nestjs/axios": {
|
"node_modules/@nestjs/axios": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.1.3.tgz",
|
||||||
@ -1589,6 +1900,118 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@redocly/ajv": {
|
||||||
|
"version": "8.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz",
|
||||||
|
"integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.1",
|
||||||
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
"require-from-string": "^2.0.2",
|
||||||
|
"uri-js-replace": "^1.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redocly/config": {
|
||||||
|
"version": "0.22.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz",
|
||||||
|
"integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@redocly/openapi-core": {
|
||||||
|
"version": "1.34.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.15.tgz",
|
||||||
|
"integrity": "sha512-HAwCnNyKcs5XGQqms+9t7OdAPM/5TDstmhF+0i7tdCFato2QKuYIlyWETwkXd8c5zbltr1oB+6y9NTeQLr2d6Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@redocly/ajv": "8.11.2",
|
||||||
|
"@redocly/config": "0.22.0",
|
||||||
|
"colorette": "1.4.0",
|
||||||
|
"https-proxy-agent": "7.0.6",
|
||||||
|
"js-levenshtein": "1.1.6",
|
||||||
|
"js-yaml": "4.1.1",
|
||||||
|
"minimatch": "5.1.9",
|
||||||
|
"pluralize": "8.0.0",
|
||||||
|
"yaml-ast-parser": "0.0.43"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.17.0",
|
||||||
|
"npm": ">=9.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redocly/openapi-core/node_modules/agent-base": {
|
||||||
|
"version": "7.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redocly/openapi-core/node_modules/https-proxy-agent": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redocly/openapi-core/node_modules/js-yaml": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"js-yaml": "bin/js-yaml.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redocly/openapi-core/node_modules/minimatch": {
|
||||||
|
"version": "5.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
|
||||||
|
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@remix-run/router": {
|
||||||
|
"version": "1.23.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz",
|
||||||
|
"integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/pluginutils": {
|
||||||
|
"version": "1.0.0-beta.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||||
|
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@rollup/pluginutils": {
|
"node_modules/@rollup/pluginutils": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz",
|
||||||
@ -2251,6 +2674,32 @@
|
|||||||
"@swc/counter": "^0.1.3"
|
"@swc/counter": "^0.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/query-core": {
|
||||||
|
"version": "5.101.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz",
|
||||||
|
"integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "5.101.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz",
|
||||||
|
"integrity": "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "5.101.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tokenizer/inflate": {
|
"node_modules/@tokenizer/inflate": {
|
||||||
"version": "0.2.7",
|
"version": "0.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz",
|
||||||
@ -2275,6 +2724,51 @@
|
|||||||
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
|
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/babel__core": {
|
||||||
|
"version": "7.20.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
|
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.20.7",
|
||||||
|
"@babel/types": "^7.20.7",
|
||||||
|
"@types/babel__generator": "*",
|
||||||
|
"@types/babel__template": "*",
|
||||||
|
"@types/babel__traverse": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/babel__generator": {
|
||||||
|
"version": "7.27.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
|
||||||
|
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/babel__template": {
|
||||||
|
"version": "7.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
|
||||||
|
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.1.0",
|
||||||
|
"@babel/types": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/babel__traverse": {
|
||||||
|
"version": "7.28.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
|
||||||
|
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.28.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/body-parser": {
|
"node_modules/@types/body-parser": {
|
||||||
"version": "1.19.6",
|
"version": "1.19.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||||
@ -2382,6 +2876,13 @@
|
|||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/prop-types": {
|
||||||
|
"version": "15.7.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||||
|
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.15.1",
|
"version": "6.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
|
||||||
@ -2396,6 +2897,27 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react": {
|
||||||
|
"version": "18.3.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
|
||||||
|
"integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/prop-types": "*",
|
||||||
|
"csstype": "^3.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/react-dom": {
|
||||||
|
"version": "18.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||||
|
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/send": {
|
"node_modules/@types/send": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
||||||
@ -2635,6 +3157,27 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitejs/plugin-react": {
|
||||||
|
"version": "4.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||||
|
"integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.28.0",
|
||||||
|
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
||||||
|
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
||||||
|
"@rolldown/pluginutils": "1.0.0-beta.27",
|
||||||
|
"@types/babel__core": "^7.20.5",
|
||||||
|
"react-refresh": "^0.17.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.18.0 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "1.6.1",
|
"version": "1.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz",
|
||||||
@ -3529,6 +4072,13 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/change-case": {
|
||||||
|
"version": "5.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
|
||||||
|
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/chardet": {
|
"node_modules/chardet": {
|
||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
|
||||||
@ -3681,6 +4231,13 @@
|
|||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/colorette": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/combined-stream": {
|
"node_modules/combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
@ -3778,6 +4335,13 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/convert-source-map": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "0.7.2",
|
"version": "0.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
@ -3859,6 +4423,13 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/csstype": {
|
||||||
|
"version": "3.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@ -4690,6 +5261,12 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fancy-canvas": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@ -5080,6 +5657,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gensync": {
|
||||||
|
"version": "1.0.0-beta.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
|
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-func-name": {
|
"node_modules/get-func-name": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
|
||||||
@ -5428,6 +6015,19 @@
|
|||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/index-to-position": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/inflight": {
|
"node_modules/inflight": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
@ -5661,11 +6261,20 @@
|
|||||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-levenshtein": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
@ -5680,6 +6289,19 @@
|
|||||||
"js-yaml": "bin/js-yaml.js"
|
"js-yaml": "bin/js-yaml.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsesc": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"jsesc": "bin/jsesc"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/json-buffer": {
|
"node_modules/json-buffer": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||||
@ -5771,6 +6393,15 @@
|
|||||||
"integrity": "sha512-NdB6O6QvlGMCoG003m0YIKG2+Xw7DjmCZhmc1RH+K6HncADUbRf8TZeLegxBBN1VFyPHcNpPTKpIhYLXzJVy1Q==",
|
"integrity": "sha512-NdB6O6QvlGMCoG003m0YIKG2+Xw7DjmCZhmc1RH+K6HncADUbRf8TZeLegxBBN1VFyPHcNpPTKpIhYLXzJVy1Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lightweight-charts": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-5kS/2hY3wNYNzhnS8Gb+GAS07DX8GPF2YVDnd2NMC85gJVQ6RLU6YrXNgNJ6eg0AnWPwCnvaGtYmGky3HiLQEw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"fancy-canvas": "2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lines-and-columns": {
|
"node_modules/lines-and-columns": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||||
@ -5871,6 +6502,18 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/loose-envify": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"loose-envify": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/loupe": {
|
"node_modules/loupe": {
|
||||||
"version": "2.3.7",
|
"version": "2.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
|
||||||
@ -6331,6 +6974,86 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openapi-fetch": {
|
||||||
|
"version": "0.9.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.9.8.tgz",
|
||||||
|
"integrity": "sha512-zM6elH0EZStD/gSiNlcPrzXcVQ/pZo3BDvC6CDwRDUt1dDzxlshpmQnpD6cZaJ39THaSmwVCxxRrPKNM1hHrDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"openapi-typescript-helpers": "^0.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openapi-typescript": {
|
||||||
|
"version": "7.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.13.0.tgz",
|
||||||
|
"integrity": "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@redocly/openapi-core": "^1.34.6",
|
||||||
|
"ansi-colors": "^4.1.3",
|
||||||
|
"change-case": "^5.4.4",
|
||||||
|
"parse-json": "^8.3.0",
|
||||||
|
"supports-color": "^10.2.2",
|
||||||
|
"yargs-parser": "^21.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"openapi-typescript": "bin/cli.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openapi-typescript-helpers": {
|
||||||
|
"version": "0.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.8.tgz",
|
||||||
|
"integrity": "sha512-1eNjQtbfNi5Z/kFhagDIaIRj6qqDzhjNJKz8cmMW0CVdGwT6e1GLbAfgI0d28VTJa1A8jz82jm/4dG8qNoNS8g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/openapi-typescript/node_modules/parse-json": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.26.2",
|
||||||
|
"index-to-position": "^1.1.0",
|
||||||
|
"type-fest": "^4.39.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openapi-typescript/node_modules/supports-color": {
|
||||||
|
"version": "10.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
|
||||||
|
"integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openapi-typescript/node_modules/type-fest": {
|
||||||
|
"version": "4.41.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
|
||||||
|
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@ -6804,6 +7527,31 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-dom": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0",
|
||||||
|
"scheduler": "^0.23.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||||
@ -6811,6 +7559,48 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-refresh": {
|
||||||
|
"version": "0.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
|
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router": {
|
||||||
|
"version": "6.30.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz",
|
||||||
|
"integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@remix-run/router": "1.23.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router-dom": {
|
||||||
|
"version": "6.30.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz",
|
||||||
|
"integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@remix-run/router": "1.23.3",
|
||||||
|
"react-router": "6.30.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8",
|
||||||
|
"react-dom": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readable-stream": {
|
"node_modules/readable-stream": {
|
||||||
"version": "3.6.2",
|
"version": "3.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
@ -7124,6 +7914,15 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/scheduler": {
|
||||||
|
"version": "0.23.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||||
|
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/schema-utils": {
|
"node_modules/schema-utils": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
||||||
@ -8153,6 +8952,13 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uri-js-replace": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@ -8587,6 +9393,20 @@
|
|||||||
"node": ">=0.4"
|
"node": ">=0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yallist": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/yaml-ast-parser": {
|
||||||
|
"version": "0.0.43",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz",
|
||||||
|
"integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/yargs-parser": {
|
"node_modules/yargs-parser": {
|
||||||
"version": "21.1.1",
|
"version": "21.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user