Compare commits
No commits in common. "platform" and "main" have entirely different histories.
98
README.md
98
README.md
@ -1,98 +0,0 @@
|
|||||||
# Финансово-аналитическая платформа
|
|
||||||
|
|
||||||
Платформа для отображения финансовых данных, новостей и аналитики.
|
|
||||||
|
|
||||||
## Структура проекта
|
|
||||||
|
|
||||||
Проект состоит из следующих компонентов:
|
|
||||||
|
|
||||||
- **Backend**: FastAPI сервер для обработки запросов
|
|
||||||
- **Frontend**: Next.js приложение для отображения данных
|
|
||||||
- **Сборщик данных**: Скрипт для сбора и обновления данных
|
|
||||||
|
|
||||||
## Требования
|
|
||||||
|
|
||||||
- Python 3.8+
|
|
||||||
- Node.js 16+
|
|
||||||
- npm или yarn
|
|
||||||
|
|
||||||
## Установка и запуск
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
|
|
||||||
1. Установите зависимости Python:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Запустите сборщик данных для первоначального сбора:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python data_collector.py --init
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Запустите FastAPI сервер:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uvicorn api:app --reload
|
|
||||||
```
|
|
||||||
|
|
||||||
Сервер будет доступен по адресу: http://localhost:8000
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
|
|
||||||
1. Перейдите в директорию frontend:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Установите зависимости:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
# или
|
|
||||||
yarn install
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Запустите Next.js приложение:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
# или
|
|
||||||
yarn dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Приложение будет доступно по адресу: http://localhost:3000
|
|
||||||
|
|
||||||
## Запуск планировщика задач
|
|
||||||
|
|
||||||
Для регулярного обновления данных запустите планировщик задач:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python data_collector.py --schedule
|
|
||||||
```
|
|
||||||
|
|
||||||
Планировщик будет выполнять следующие задачи:
|
|
||||||
- Сбор финансовых данных каждые 12 часов
|
|
||||||
- Сбор новостей каждые 2 часа
|
|
||||||
- Анализ новостей каждые 24 часа в полночь
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
- `/sectors` - Получение списка всех доступных секторов
|
|
||||||
- `/sector/{sector_name}` - Получение данных по сектору
|
|
||||||
- `/tickers` - Получение списка всех доступных тикеров
|
|
||||||
- `/ticker/{ticker_name}` - Получение данных по тикеру
|
|
||||||
- `/news/topics` - Получение списка всех доступных тем новостей
|
|
||||||
- `/news` - Получение новостей
|
|
||||||
- `/analytics/latest` - Получение последнего доступного анализа новостей
|
|
||||||
- `/analytics/{date}` - Получение анализа новостей по дате
|
|
||||||
- `/dashboard` - Получение сводных данных для главного экрана
|
|
||||||
|
|
||||||
## Структура базы данных
|
|
||||||
|
|
||||||
- **moex_data.db** - База данных с финансовыми данными
|
|
||||||
- **news_data.db** - База данных с новостями
|
|
||||||
- **analytics.db** - База данных с результатами анализа новостей
|
|
||||||
236
api.py
236
api.py
@ -1,236 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, Query
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from typing import List, Dict, Any, Optional
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import logging
|
|
||||||
from models import FinancialDataManager, NewsManager, AnalyticsManager
|
|
||||||
from config import API_CONFIG
|
|
||||||
|
|
||||||
# Настройка логирования
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
||||||
handlers=[
|
|
||||||
logging.FileHandler("api.log"),
|
|
||||||
logging.StreamHandler()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
logger = logging.getLogger("api")
|
|
||||||
|
|
||||||
# Создаем экземпляр FastAPI
|
|
||||||
app = FastAPI(
|
|
||||||
title="Финансово-аналитическая платформа API",
|
|
||||||
description="API для доступа к финансовым данным, новостям и аналитике",
|
|
||||||
version="1.0.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Настройка CORS
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=["*"], # В продакшене следует указать конкретные домены
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Инициализация менеджеров данных
|
|
||||||
financial_manager = FinancialDataManager()
|
|
||||||
news_manager = NewsManager()
|
|
||||||
analytics_manager = AnalyticsManager()
|
|
||||||
|
|
||||||
# Модели данных для API
|
|
||||||
class SectorData(BaseModel):
|
|
||||||
sector: str
|
|
||||||
date: str
|
|
||||||
open: float
|
|
||||||
high: float
|
|
||||||
low: float
|
|
||||||
close: float
|
|
||||||
volume: float
|
|
||||||
|
|
||||||
class TickerData(BaseModel):
|
|
||||||
ticker: str
|
|
||||||
sector: str
|
|
||||||
date: str
|
|
||||||
open: float
|
|
||||||
high: float
|
|
||||||
low: float
|
|
||||||
close: float
|
|
||||||
volume: float
|
|
||||||
|
|
||||||
class NewsItem(BaseModel):
|
|
||||||
id: int
|
|
||||||
date: str
|
|
||||||
topic: Optional[str] = None
|
|
||||||
title: str
|
|
||||||
content: str
|
|
||||||
url: Optional[str] = None
|
|
||||||
|
|
||||||
class AnalyticsItem(BaseModel):
|
|
||||||
id: int
|
|
||||||
date: str
|
|
||||||
summary: str
|
|
||||||
business_problems: Optional[str] = None
|
|
||||||
solutions: Optional[str] = None
|
|
||||||
sentiment: Optional[float] = None
|
|
||||||
|
|
||||||
# Маршруты API
|
|
||||||
@app.get("/")
|
|
||||||
async def root():
|
|
||||||
"""Корневой маршрут API"""
|
|
||||||
return {
|
|
||||||
"message": "Добро пожаловать в API финансово-аналитической платформы",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"endpoints": [
|
|
||||||
"/sectors",
|
|
||||||
"/sectors/{sector}",
|
|
||||||
"/tickers",
|
|
||||||
"/tickers/{ticker}",
|
|
||||||
"/news",
|
|
||||||
"/news/topics",
|
|
||||||
"/analytics"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Маршруты для финансовых данных
|
|
||||||
@app.get("/sectors", response_model=List[str])
|
|
||||||
async def get_sectors():
|
|
||||||
"""Получить список всех доступных секторов"""
|
|
||||||
try:
|
|
||||||
sectors = financial_manager.get_all_sectors()
|
|
||||||
return sectors
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при получении списка секторов: {str(e)}")
|
|
||||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
|
||||||
|
|
||||||
@app.get("/sectors/{sector}", response_model=List[Dict[str, Any]])
|
|
||||||
async def get_sector_data(
|
|
||||||
sector: str,
|
|
||||||
start_date: Optional[str] = Query(None, description="Начальная дата в формате YYYY-MM-DD"),
|
|
||||||
end_date: Optional[str] = Query(None, description="Конечная дата в формате YYYY-MM-DD")
|
|
||||||
):
|
|
||||||
"""Получить данные по указанному сектору за период"""
|
|
||||||
try:
|
|
||||||
# Если даты не указаны, используем последние 30 дней
|
|
||||||
if not end_date:
|
|
||||||
end_date = datetime.now().strftime("%Y-%m-%d")
|
|
||||||
if not start_date:
|
|
||||||
start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
data = financial_manager.get_sector_data(sector, start_date, end_date)
|
|
||||||
|
|
||||||
if not data:
|
|
||||||
raise HTTPException(status_code=404, detail=f"Данные для сектора {sector} не найдены")
|
|
||||||
|
|
||||||
return data
|
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при получении данных сектора {sector}: {str(e)}")
|
|
||||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
|
||||||
|
|
||||||
@app.get("/tickers", response_model=Dict[str, List[str]])
|
|
||||||
async def get_tickers():
|
|
||||||
"""Получить список всех доступных тикеров, сгруппированных по секторам"""
|
|
||||||
try:
|
|
||||||
tickers = financial_manager.get_all_tickers()
|
|
||||||
return tickers
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при получении списка тикеров: {str(e)}")
|
|
||||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
|
||||||
|
|
||||||
@app.get("/tickers/{ticker}", response_model=List[Dict[str, Any]])
|
|
||||||
async def get_ticker_data(
|
|
||||||
ticker: str,
|
|
||||||
start_date: Optional[str] = Query(None, description="Начальная дата в формате YYYY-MM-DD"),
|
|
||||||
end_date: Optional[str] = Query(None, description="Конечная дата в формате YYYY-MM-DD")
|
|
||||||
):
|
|
||||||
"""Получить данные по указанному тикеру за период"""
|
|
||||||
try:
|
|
||||||
# Если даты не указаны, используем последние 30 дней
|
|
||||||
if not end_date:
|
|
||||||
end_date = datetime.now().strftime("%Y-%m-%d")
|
|
||||||
if not start_date:
|
|
||||||
start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
data = financial_manager.get_ticker_data(ticker, start_date, end_date)
|
|
||||||
|
|
||||||
if not data:
|
|
||||||
raise HTTPException(status_code=404, detail=f"Данные для тикера {ticker} не найдены")
|
|
||||||
|
|
||||||
return data
|
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при получении данных тикера {ticker}: {str(e)}")
|
|
||||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
|
||||||
|
|
||||||
# Маршруты для новостей
|
|
||||||
@app.get("/news", response_model=List[Dict[str, Any]])
|
|
||||||
async def get_news(
|
|
||||||
start_date: Optional[str] = Query(None, description="Начальная дата в формате YYYY-MM-DD"),
|
|
||||||
end_date: Optional[str] = Query(None, description="Конечная дата в формате YYYY-MM-DD"),
|
|
||||||
topic: Optional[str] = Query(None, description="Фильтр по теме новости")
|
|
||||||
):
|
|
||||||
"""Получить новости за указанный период с возможностью фильтрации по теме"""
|
|
||||||
try:
|
|
||||||
# Если даты не указаны, используем последние 7 дней
|
|
||||||
if not end_date:
|
|
||||||
end_date = datetime.now().strftime("%Y-%m-%d")
|
|
||||||
if not start_date:
|
|
||||||
start_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
if topic:
|
|
||||||
news = news_manager.get_news_by_topic(topic, start_date, end_date)
|
|
||||||
else:
|
|
||||||
news = news_manager.get_news_by_date(start_date, end_date)
|
|
||||||
|
|
||||||
return news
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при получении новостей: {str(e)}")
|
|
||||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
|
||||||
|
|
||||||
@app.get("/news/topics", response_model=List[str])
|
|
||||||
async def get_news_topics():
|
|
||||||
"""Получить список всех доступных тем новостей"""
|
|
||||||
try:
|
|
||||||
topics = news_manager.get_topics()
|
|
||||||
return topics
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при получении списка тем новостей: {str(e)}")
|
|
||||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
|
||||||
|
|
||||||
# Маршруты для аналитики
|
|
||||||
@app.get("/analytics", response_model=List[Dict[str, Any]])
|
|
||||||
async def get_analytics(
|
|
||||||
start_date: Optional[str] = Query(None, description="Начальная дата в формате YYYY-MM-DD"),
|
|
||||||
end_date: Optional[str] = Query(None, description="Конечная дата в формате YYYY-MM-DD")
|
|
||||||
):
|
|
||||||
"""Получить аналитику за указанный период"""
|
|
||||||
try:
|
|
||||||
# Если даты не указаны, используем последние 7 дней
|
|
||||||
if not end_date:
|
|
||||||
end_date = datetime.now().strftime("%Y-%m-%d")
|
|
||||||
if not start_date:
|
|
||||||
start_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
analytics = analytics_manager.get_analytics_by_date(start_date, end_date)
|
|
||||||
|
|
||||||
return analytics
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при получении аналитики: {str(e)}")
|
|
||||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
|
||||||
|
|
||||||
# Запуск сервера
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import uvicorn
|
|
||||||
uvicorn.run(
|
|
||||||
"api:app",
|
|
||||||
host=API_CONFIG['host'],
|
|
||||||
port=API_CONFIG['port'],
|
|
||||||
reload=True
|
|
||||||
)
|
|
||||||
138
classes/exchange.py
Normal file
138
classes/exchange.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import yfinance as yf
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
class ForexDataHandler:
|
||||||
|
def __init__(self):
|
||||||
|
"""
|
||||||
|
Инициализация обработчика валютных данных
|
||||||
|
"""
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
# logging.basicConfig(level=logging.INFO)
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
# logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
# Словари с тикерами
|
||||||
|
self.russian_indices = {
|
||||||
|
'IMOEX': 'IMOEX.ME', # Индекс МосБиржи
|
||||||
|
'RTSI': 'RTSI.ME', # Индекс РТС
|
||||||
|
'MOEXBC': 'MOEXBC.ME' # Индекс голубых фишек
|
||||||
|
}
|
||||||
|
self.russian_sectors = {
|
||||||
|
'oil_gas': 'MOEXOG.ME', # Нефть и газ
|
||||||
|
'finance': 'MOEXFN.ME', # Финансы
|
||||||
|
'telecom': 'MOEXTL.ME', # Телекоммуникации
|
||||||
|
'metals': 'MOEXMM.ME', # Металлы и добыча
|
||||||
|
'consumer': 'MOEXCN.ME', # Потребительский сектор
|
||||||
|
'transport': 'MOEXTN.ME' # Транспорт
|
||||||
|
}
|
||||||
|
self.russian_stocks = {
|
||||||
|
'Gazprom': 'GAZP.ME',
|
||||||
|
'Sberbank': 'SBER.ME',
|
||||||
|
'Lukoil': 'LKOH.ME',
|
||||||
|
'Rosneft': 'ROSN.ME',
|
||||||
|
'VTB': 'VTBR.ME'
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_forex_rate(self, from_currency: str, to_currency: str) -> dict:
|
||||||
|
"""
|
||||||
|
Получает текущий курс валют с Yahoo Finance
|
||||||
|
|
||||||
|
Args:
|
||||||
|
from_currency: исходная валюта (например, 'USD')
|
||||||
|
to_currency: целевая валюта (например, 'RUB')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Словарь с данными о курсе валют
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Формируем тикер для валютной пары
|
||||||
|
ticker = f"{from_currency}{to_currency}=X"
|
||||||
|
self.logger.info(f"Получение данных для пары {ticker}")
|
||||||
|
|
||||||
|
currency_pair = yf.Ticker(ticker)
|
||||||
|
|
||||||
|
# Получаем данные за последний день
|
||||||
|
end_date = datetime.now()
|
||||||
|
start_date = end_date - timedelta(days=1)
|
||||||
|
hist = currency_pair.history(start=start_date, end=end_date)
|
||||||
|
|
||||||
|
if hist.empty:
|
||||||
|
raise ValueError(f"Данные не получены для пары {ticker}")
|
||||||
|
|
||||||
|
latest_data = hist.iloc[-1]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'close': float(latest_data['Close']),
|
||||||
|
'timestamp': latest_data.name.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'open': float(latest_data['Open']),
|
||||||
|
'high': float(latest_data['High']),
|
||||||
|
'low': float(latest_data['Low']),
|
||||||
|
'volume': float(latest_data['Volume']) if 'Volume' in latest_data else None
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Ошибка при получении данных для пары {from_currency}/{to_currency}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_market_data(self, ticker: str, period: str = "1d") -> dict:
|
||||||
|
"""
|
||||||
|
Получает данные по указанному тикеру
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ticker: тикер инструмента
|
||||||
|
period: период данных ("1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "10y", "ytd", "max")
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
stock = yf.Ticker(ticker)
|
||||||
|
info = stock.info
|
||||||
|
hist = stock.history(period=period)
|
||||||
|
|
||||||
|
if hist.empty:
|
||||||
|
raise ValueError(f"Данные не получены для {ticker}")
|
||||||
|
|
||||||
|
latest_data = hist.iloc[-1]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'close': float(latest_data['Close']),
|
||||||
|
'open': float(latest_data['Open']),
|
||||||
|
'high': float(latest_data['High']),
|
||||||
|
'low': float(latest_data['Low']),
|
||||||
|
'volume': float(latest_data['Volume']) if 'Volume' in latest_data else None,
|
||||||
|
'timestamp': latest_data.name.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'name': info.get('longName', None),
|
||||||
|
'sector': info.get('sector', None),
|
||||||
|
'market_cap': info.get('marketCap', None),
|
||||||
|
'pe_ratio': info.get('trailingPE', None),
|
||||||
|
'dividend_yield': info.get('dividendYield', None),
|
||||||
|
'currency': info.get('currency', None)
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Ошибка при получении данных для {ticker}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_sector_performance(self, sector: str) -> dict:
|
||||||
|
"""
|
||||||
|
Получает данные по определенному сектору
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sector: ключ сектора из словаря russian_sectors
|
||||||
|
"""
|
||||||
|
if sector not in self.russian_sectors:
|
||||||
|
raise ValueError(f"Неизвестный сектор: {sector}")
|
||||||
|
|
||||||
|
return self.get_market_data(self.russian_sectors[sector])
|
||||||
|
|
||||||
|
def get_index_data(self, index: str) -> dict:
|
||||||
|
"""
|
||||||
|
Получает данные по индексу
|
||||||
|
|
||||||
|
Args:
|
||||||
|
index: ключ индекса из словаря russian_indices
|
||||||
|
"""
|
||||||
|
if index not in self.russian_indices:
|
||||||
|
raise ValueError(f"Неизвестный индекс: {index}")
|
||||||
|
|
||||||
|
return self.get_market_data(self.russian_indices[index])
|
||||||
|
|
||||||
|
|
||||||
70
classes/moex_class.py
Normal file
70
classes/moex_class.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import requests
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
class RussianMarketData:
|
||||||
|
def __init__(self):
|
||||||
|
self.moex_base_url = "https://iss.moex.com/iss"
|
||||||
|
|
||||||
|
# Словарь с индексами
|
||||||
|
self.indices = {
|
||||||
|
'oil_gas': 'MOEXOG', # Нефть и газ
|
||||||
|
'finance': 'MOEXFN', # Финансы
|
||||||
|
'telecom': 'MOEXTL', # Телекоммуникации
|
||||||
|
'metals': 'MOEXMM', # Металлы и добыча
|
||||||
|
'consumer': 'MOEXCN', # Потребительский сектор
|
||||||
|
'transport': 'MOEXTN' # Транспорт
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_moex_index_history(self, index_code='MOEXOG', start_date=None, end_date=None):
|
||||||
|
"""
|
||||||
|
Получение исторических данных индекса MOEX.
|
||||||
|
"""
|
||||||
|
if start_date is None:
|
||||||
|
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y-%m-%d')
|
||||||
|
if end_date is None:
|
||||||
|
end_date = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
all_data = []
|
||||||
|
start = 0
|
||||||
|
while True:
|
||||||
|
url = f"{self.moex_base_url}/history/engines/stock/markets/index/securities/{index_code}.json"
|
||||||
|
params = {
|
||||||
|
'from': start_date,
|
||||||
|
'till': end_date,
|
||||||
|
'start': start
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(url, params=params)
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
# Проверка наличия данных
|
||||||
|
if 'history' not in data or len(data['history']['data']) == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Добавление данных в общий список
|
||||||
|
all_data.extend(data['history']['data'])
|
||||||
|
start += 100 # Увеличиваем смещение на 100 для следующего запроса
|
||||||
|
|
||||||
|
# Преобразование данных в DataFrame
|
||||||
|
columns = data['history']['columns']
|
||||||
|
df = pd.DataFrame(all_data, columns=columns)
|
||||||
|
|
||||||
|
# Обработка данных
|
||||||
|
df['TRADEDATE'] = pd.to_datetime(df['TRADEDATE'])
|
||||||
|
df = df.set_index('TRADEDATE')
|
||||||
|
|
||||||
|
return df[['CLOSE', 'VOLUME']]
|
||||||
|
|
||||||
|
def get_historical_data(self, sector: str, start_date=None, end_date=None):
|
||||||
|
"""
|
||||||
|
Получает исторические данные для указанного сектора.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sector: ключ сектора из словаря indices
|
||||||
|
"""
|
||||||
|
if sector not in self.indices:
|
||||||
|
raise ValueError(f"Неизвестный сектор: {sector}")
|
||||||
|
|
||||||
|
index_code = self.indices[sector]
|
||||||
|
return self.get_moex_index_history(index_code, start_date, end_date)
|
||||||
@ -1,141 +1,228 @@
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import aiohttp
|
import requests
|
||||||
import asyncio
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import sys
|
from typing import List, Dict, Optional
|
||||||
import os
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
# Добавляем родительскую директорию в путь для импорта
|
import numpy as np
|
||||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
|
||||||
from config import sector_indices
|
|
||||||
|
|
||||||
|
|
||||||
class MOEXHistoricalData:
|
class MOEXHistoricalData:
|
||||||
"""Класс для получения исторических данных с Московской биржи"""
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Инициализация класса для работы с историческими данными MOEX"""
|
|
||||||
self.base_url = "https://iss.moex.com/iss"
|
self.base_url = "https://iss.moex.com/iss"
|
||||||
|
|
||||||
async def _make_request(self, url: str) -> dict:
|
# Словарь соответствия секторов и их индексов на MOEX
|
||||||
|
self.sector_indices = {
|
||||||
|
'metals_mining': 'MOEXMM', # Индекс Металлов и добычи
|
||||||
|
'oil_gas': 'MOEXOG', # Индекс Нефти и газа
|
||||||
|
'chemicals': 'MOEXCH', # Индекс Химии и нефтехимии
|
||||||
|
'electric_utilities': 'MOEXEU', # Индекс Электроэнергетики
|
||||||
|
'telecom': 'MOEXTL', # Индекс Телекоммуникаций
|
||||||
|
'finance': 'MOEXFN', # Индекс Финансов
|
||||||
|
'consumer': 'MOEXCN', # Индекс Потребительского сектора
|
||||||
|
'transport': 'MOEXTN' # Индекс Транспорта
|
||||||
|
}
|
||||||
|
|
||||||
|
async def get_security_history(
|
||||||
|
self,
|
||||||
|
ticker: str,
|
||||||
|
start_date: str,
|
||||||
|
end_date: str,
|
||||||
|
engine: str = "stock",
|
||||||
|
market: str = "shares",
|
||||||
|
board: str = "TQBR"
|
||||||
|
) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Выполняет асинхронный запрос к API MOEX
|
Получение исторических данных по отдельному тикеру
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url: URL для запроса
|
ticker: Тикер акции
|
||||||
|
start_date: Начальная дата в формате YYYY-MM-DD
|
||||||
|
end_date: Конечная дата в формате YYYY-MM-DD
|
||||||
|
engine: Торговый движок (по умолчанию stock)
|
||||||
|
market: Рынок (по умолчанию shares)
|
||||||
|
board: Режим торгов (по умолчанию TQBR)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Ответ от API в формате JSON
|
DataFrame с историческими данными
|
||||||
"""
|
"""
|
||||||
|
url = f"{self.base_url}/history/engines/{engine}/markets/{market}/boards/{board}/securities/{ticker}.json"
|
||||||
|
|
||||||
|
all_data = []
|
||||||
|
start = 0
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url) as response:
|
while True:
|
||||||
if response.status == 200:
|
params = {
|
||||||
return await response.json()
|
"from": start_date,
|
||||||
else:
|
"till": end_date,
|
||||||
raise Exception(f"Ошибка при запросе к API MOEX: {response.status}")
|
"start": start,
|
||||||
|
"limit": 100
|
||||||
async def get_official_sector_index(self, sector: str, start_date: str, end_date: str) -> pd.DataFrame:
|
}
|
||||||
|
|
||||||
|
async with session.get(url, params=params) as response:
|
||||||
|
data = await response.json()
|
||||||
|
|
||||||
|
# Получаем данные истории
|
||||||
|
history_data = data['history']
|
||||||
|
|
||||||
|
if not history_data['data']:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Добавляем данные в общий список
|
||||||
|
all_data.extend(history_data['data'])
|
||||||
|
start += 100
|
||||||
|
|
||||||
|
if len(history_data['data']) < 100:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Создаем DataFrame
|
||||||
|
df = pd.DataFrame(all_data, columns=history_data['columns'])
|
||||||
|
|
||||||
|
# Конвертируем даты и числовые значения
|
||||||
|
df['TRADEDATE'] = pd.to_datetime(df['TRADEDATE'])
|
||||||
|
numeric_columns = ['OPEN', 'HIGH', 'LOW', 'CLOSE', 'VALUE', 'VOLUME']
|
||||||
|
df[numeric_columns] = df[numeric_columns].apply(pd.to_numeric)
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
async def get_sector_history(
|
||||||
|
self,
|
||||||
|
sector_tickers: List[str],
|
||||||
|
start_date: str,
|
||||||
|
end_date: str,
|
||||||
|
engine: str = "stock",
|
||||||
|
market: str = "shares",
|
||||||
|
board: str = "TQBR"
|
||||||
|
) -> Dict[str, pd.DataFrame]:
|
||||||
"""
|
"""
|
||||||
Получает исторические данные по официальному индексу сектора
|
Получение исторических данных по всем тикерам сектора
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
sector: Название сектора
|
sector_tickers: Список тикеров сектора
|
||||||
start_date: Начальная дата в формате 'YYYY-MM-DD'
|
start_date: Начальная дата в формате YYYY-MM-DD
|
||||||
end_date: Конечная дата в формате 'YYYY-MM-DD'
|
end_date: Конечная дата в формате YYYY-MM-DD
|
||||||
|
engine: Торговый движок (по умолчанию stock)
|
||||||
|
market: Рынок (по умолчанию shares)
|
||||||
|
board: Режим торгов (по умолчанию TQBR)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
pd.DataFrame: Исторические данные по индексу
|
Словарь {тикер: DataFrame с историческими данными}
|
||||||
"""
|
"""
|
||||||
# Получаем код индекса для сектора
|
tasks = []
|
||||||
index_code = sector_indices.get(sector)
|
for ticker in sector_tickers:
|
||||||
|
task = self.get_security_history(
|
||||||
if not index_code:
|
ticker=ticker,
|
||||||
raise ValueError(f"Неизвестный сектор: {sector}")
|
start_date=start_date,
|
||||||
|
end_date=end_date,
|
||||||
# Формируем URL для запроса
|
engine=engine,
|
||||||
url = f"{self.base_url}/history/engines/stock/markets/index/securities/{index_code}/candles.json"
|
market=market,
|
||||||
url += f"?from={start_date}&till={end_date}&interval=24"
|
board=board
|
||||||
|
)
|
||||||
# Выполняем запрос
|
tasks.append(task)
|
||||||
response = await self._make_request(url)
|
|
||||||
|
|
||||||
# Преобразуем ответ в DataFrame
|
|
||||||
if 'candles' in response and 'data' in response['candles']:
|
|
||||||
df = pd.DataFrame(response['candles']['data'], columns=response['candles']['columns'])
|
|
||||||
|
|
||||||
# Переименовываем колонки для соответствия формату
|
results = await asyncio.gather(*tasks)
|
||||||
df = df.rename(columns={
|
return dict(zip(sector_tickers, results))
|
||||||
'begin': 'TRADEDATE',
|
|
||||||
'open': 'OPEN',
|
async def get_official_sector_index(
|
||||||
'high': 'HIGH',
|
self,
|
||||||
'low': 'LOW',
|
sector: str,
|
||||||
'close': 'CLOSE',
|
start_date: str,
|
||||||
'volume': 'VOLUME'
|
end_date: str
|
||||||
})
|
) -> pd.DataFrame:
|
||||||
|
|
||||||
# Преобразуем дату в формат datetime
|
|
||||||
df['TRADEDATE'] = pd.to_datetime(df['TRADEDATE']).dt.date
|
|
||||||
|
|
||||||
return df
|
|
||||||
else:
|
|
||||||
return pd.DataFrame()
|
|
||||||
|
|
||||||
async def get_security_history(self, ticker: str, start_date: str, end_date: str) -> pd.DataFrame:
|
|
||||||
"""
|
"""
|
||||||
Получает исторические данные по ценной бумаге
|
Получение официального отраслевого индекса с MOEX
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ticker: Тикер ценной бумаги
|
sector: Название сектора (ключ из словаря sector_indices)
|
||||||
start_date: Начальная дата в формате 'YYYY-MM-DD'
|
start_date: Начальная дата в формате YYYY-MM-DD
|
||||||
end_date: Конечная дата в формате 'YYYY-MM-DD'
|
end_date: Конечная дата в формате YYYY-MM-DD
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
pd.DataFrame: Исторические данные по ценной бумаге
|
DataFrame с данными индекса
|
||||||
"""
|
"""
|
||||||
# Формируем URL для запроса
|
if sector not in self.sector_indices:
|
||||||
url = f"{self.base_url}/history/engines/stock/markets/shares/securities/{ticker}/candles.json"
|
raise ValueError(f"Неизвестный сектор: {sector}. Доступные секторы: {list(self.sector_indices.keys())}")
|
||||||
url += f"?from={start_date}&till={end_date}&interval=24"
|
|
||||||
|
index_ticker = self.sector_indices[sector]
|
||||||
|
url = f"{self.base_url}/history/engines/stock/markets/index/securities/{index_ticker}.json"
|
||||||
|
|
||||||
# Выполняем запрос
|
all_data = []
|
||||||
response = await self._make_request(url)
|
start = 0
|
||||||
|
|
||||||
# Преобразуем ответ в DataFrame
|
async with aiohttp.ClientSession() as session:
|
||||||
if 'candles' in response and 'data' in response['candles']:
|
while True:
|
||||||
df = pd.DataFrame(response['candles']['data'], columns=response['candles']['columns'])
|
params = {
|
||||||
|
"from": start_date,
|
||||||
# Переименовываем колонки для соответствия формату
|
"till": end_date,
|
||||||
df = df.rename(columns={
|
"start": start,
|
||||||
'begin': 'TRADEDATE',
|
"limit": 100
|
||||||
'open': 'OPEN',
|
}
|
||||||
'high': 'HIGH',
|
|
||||||
'low': 'LOW',
|
async with session.get(url, params=params) as response:
|
||||||
'close': 'CLOSE',
|
data = await response.json()
|
||||||
'volume': 'VOLUME'
|
|
||||||
})
|
history_data = data['history']
|
||||||
|
|
||||||
# Преобразуем дату в формат datetime
|
if not history_data['data']:
|
||||||
df['TRADEDATE'] = pd.to_datetime(df['TRADEDATE']).dt.date
|
break
|
||||||
|
|
||||||
return df
|
all_data.extend(history_data['data'])
|
||||||
else:
|
start += 100
|
||||||
return pd.DataFrame()
|
|
||||||
|
if len(history_data['data']) < 100:
|
||||||
|
break
|
||||||
|
|
||||||
|
df = pd.DataFrame(all_data, columns=history_data['columns'])
|
||||||
|
|
||||||
|
# Конвертируем даты и числовые значения
|
||||||
|
df['TRADEDATE'] = pd.to_datetime(df['TRADEDATE'])
|
||||||
|
numeric_columns = ['OPEN', 'HIGH', 'LOW', 'CLOSE', 'VALUE', 'VOLUME']
|
||||||
|
df[numeric_columns] = df[numeric_columns].apply(pd.to_numeric)
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def calculate_sector_index(
|
||||||
# Пример использования
|
self,
|
||||||
if __name__ == "__main__":
|
sector_data: Dict[str, pd.DataFrame],
|
||||||
async def test():
|
weights: Optional[Dict[str, float]] = None
|
||||||
moex = MOEXHistoricalData()
|
) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Расчет индекса сектора на основе исторических данных входящих в него компаний
|
||||||
|
|
||||||
# Получаем данные по индексу нефти и газа за последний месяц
|
Args:
|
||||||
end_date = datetime.now()
|
sector_data: Словарь с историческими данными по тикерам {тикер: DataFrame}
|
||||||
start_date = end_date - timedelta(days=30)
|
weights: Словарь с весами компаний {тикер: вес}. Если None, веса будут равными
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame с рассчитанным индексом сектора
|
||||||
|
"""
|
||||||
|
# Если веса не указаны, используем равные веса
|
||||||
|
if weights is None:
|
||||||
|
weights = {ticker: 1/len(sector_data) for ticker in sector_data.keys()}
|
||||||
|
|
||||||
|
# Создаем DataFrame с датами и ценами закрытия для каждого тикера
|
||||||
|
prices_df = pd.DataFrame()
|
||||||
|
|
||||||
df = await moex.get_official_sector_index(
|
for ticker, df in sector_data.items():
|
||||||
'oil_gas',
|
prices_df[ticker] = df.set_index('TRADEDATE')['CLOSE']
|
||||||
start_date.strftime('%Y-%m-%d'),
|
|
||||||
end_date.strftime('%Y-%m-%d')
|
# Рассчитываем относительное изменение цен
|
||||||
)
|
returns_df = prices_df.pct_change()
|
||||||
|
|
||||||
print(df.head())
|
# Рассчитываем взвешенную доходность индекса
|
||||||
|
weighted_returns = pd.DataFrame()
|
||||||
asyncio.run(test())
|
for ticker in returns_df.columns:
|
||||||
|
weighted_returns[ticker] = returns_df[ticker] * weights[ticker]
|
||||||
|
|
||||||
|
index_returns = weighted_returns.sum(axis=1)
|
||||||
|
|
||||||
|
# Рассчитываем значения индекса
|
||||||
|
index_values = (1 + index_returns).cumprod() * 1000 # Начальное значение 1000
|
||||||
|
|
||||||
|
# Создаем итоговый DataFrame
|
||||||
|
result_df = pd.DataFrame({
|
||||||
|
'INDEX_VALUE': index_values,
|
||||||
|
'INDEX_RETURN': index_returns
|
||||||
|
})
|
||||||
|
|
||||||
|
return result_df
|
||||||
305
config.py
305
config.py
@ -1,54 +1,255 @@
|
|||||||
# Конфигурационный файл для финансово-аналитической платформы
|
|
||||||
|
|
||||||
# Словарь соответствия секторов и их официальных индексов на MOEX
|
|
||||||
sector_indices = {
|
sector_indices = {
|
||||||
'oil_gas': 'MOEXOG', # Нефть и газ
|
'metals_mining': 'MOEXMM', # Индекс Металлов и добычи
|
||||||
'electric': 'MOEXEU', # Электроэнергетика
|
'oil_gas': 'MOEXOG', # Индекс Нефти и газа
|
||||||
'telecom': 'MOEXTL', # Телекоммуникации
|
'chemicals': 'MOEXCH', # Индекс Химии и нефтехимии
|
||||||
'metals': 'MOEXMM', # Металлы и добыча
|
'electric_utilities': 'MOEXEU', # Индекс Электроэнергетики
|
||||||
'finance': 'MOEXFN', # Финансы
|
'telecom': 'MOEXTL', # Индекс Телекоммуникаций
|
||||||
'consumer': 'MOEXCN', # Потребительский сектор
|
'finance': 'MOEXFN', # Индекс Финансов
|
||||||
'chemicals': 'MOEXCH', # Химия и нефтехимия
|
'consumer': 'MOEXCN', # Индекс Потребительского сектора
|
||||||
'transport': 'MOEXTN', # Транспорт
|
'transport': 'MOEXTN' # Индекс Транспорта
|
||||||
'it': 'MOEXIT' # Информационные технологии
|
}
|
||||||
}
|
|
||||||
|
|
||||||
# Словарь соответствия секторов и тикеров компаний, входящих в них
|
sector_tickers = {'metals_mining': ['ALRS',
|
||||||
sector_tickers = {
|
'AMEZ',
|
||||||
'oil_gas': ['GAZP', 'LKOH', 'ROSN', 'SNGS', 'TATN', 'NVTK'],
|
'BELO',
|
||||||
'electric': ['IRAO', 'MSNG', 'RSTI', 'HYDR', 'FEES', 'UPRO'],
|
'BLNG',
|
||||||
'telecom': ['MTSS', 'RTKM', 'MGTS', 'MFON'],
|
'CHEP',
|
||||||
'metals': ['GMKN', 'NLMK', 'MAGN', 'CHMF', 'PLZL', 'POLY', 'ALRS'],
|
'CHMF',
|
||||||
'finance': ['SBER', 'VTBR', 'CBOM', 'MOEX', 'TCSG'],
|
'CHMK',
|
||||||
'consumer': ['MGNT', 'FIVE', 'DSKY', 'FIXP', 'OZON'],
|
'CHZN',
|
||||||
'chemicals': ['PHOR', 'AKRN', 'NKNC', 'KAZT'],
|
'ENPG',
|
||||||
'transport': ['AFLT', 'RTKM', 'NMTP'],
|
'GMKN',
|
||||||
'it': ['YNDX', 'QIWI', 'CIAN', 'VKCO']
|
'KBTK',
|
||||||
}
|
'KOGK',
|
||||||
|
'LNZL',
|
||||||
# Настройки базы данных
|
'LNZLP',
|
||||||
DB_CONFIG = {
|
'MAGN',
|
||||||
'financial_data': 'moex_data.db',
|
'MGOK',
|
||||||
'news_data': 'news_data.db',
|
'MTLR',
|
||||||
'analytics': 'analytics.db'
|
'MTLRP',
|
||||||
}
|
'NLMK',
|
||||||
|
'PGIL',
|
||||||
# Настройки сбора данных
|
'PLZL',
|
||||||
DATA_COLLECTION = {
|
'PMTL',
|
||||||
'financial_interval_hours': 12,
|
'POGR',
|
||||||
'news_interval_hours': 2,
|
'POLY',
|
||||||
'analytics_interval_hours': 24
|
'RASP',
|
||||||
}
|
'RUAL',
|
||||||
|
'RUALR',
|
||||||
# Настройки API
|
'SELG',
|
||||||
API_CONFIG = {
|
'SELGP',
|
||||||
'host': '0.0.0.0',
|
'TRMK',
|
||||||
'port': 8000
|
'UGLD',
|
||||||
}
|
'UNKL',
|
||||||
|
'VSMO',
|
||||||
# Настройки для OpenAI API (для анализа новостей)
|
'VSMZ'],
|
||||||
OPENAI_CONFIG = {
|
'oil_gas': ['BANE',
|
||||||
'model': 'gpt-3.5-turbo',
|
'BANEP',
|
||||||
'temperature': 0.3,
|
'GAZP',
|
||||||
'max_tokens': 500
|
'JNOSP',
|
||||||
}
|
'KRKNP',
|
||||||
|
'LKOH',
|
||||||
|
'MFGS',
|
||||||
|
'MFGSP',
|
||||||
|
'NOTK',
|
||||||
|
'NVTK',
|
||||||
|
'RITK',
|
||||||
|
'RNFT',
|
||||||
|
'RNHSP',
|
||||||
|
'ROSN',
|
||||||
|
'SIBN',
|
||||||
|
'SNGS',
|
||||||
|
'SNGSP',
|
||||||
|
'TATN',
|
||||||
|
'TATNP',
|
||||||
|
'TNBP',
|
||||||
|
'TNBPP',
|
||||||
|
'TRMK',
|
||||||
|
'TRNFP'],
|
||||||
|
'chemicals': ['AKRN',
|
||||||
|
'AZKM',
|
||||||
|
'DGBZ',
|
||||||
|
'DGBZP',
|
||||||
|
'KAZT',
|
||||||
|
'KZOS',
|
||||||
|
'KZOSP',
|
||||||
|
'MGNZ',
|
||||||
|
'NKNC',
|
||||||
|
'NKNCP',
|
||||||
|
'OMSH',
|
||||||
|
'PHOR',
|
||||||
|
'SILV',
|
||||||
|
'URKA',
|
||||||
|
'YASH'],
|
||||||
|
'electric_utilities': ['ARSB',
|
||||||
|
'BEGY',
|
||||||
|
'DVEC',
|
||||||
|
'EESR',
|
||||||
|
'EESRP',
|
||||||
|
'ELFV',
|
||||||
|
'ENRU',
|
||||||
|
'EONR',
|
||||||
|
'FEES',
|
||||||
|
'HYDR',
|
||||||
|
'IRAO',
|
||||||
|
'IRGZ',
|
||||||
|
'KISB',
|
||||||
|
'KRNG',
|
||||||
|
'KRSG',
|
||||||
|
'LSNG',
|
||||||
|
'LSNGP',
|
||||||
|
'MGSV',
|
||||||
|
'MRKC',
|
||||||
|
'MRKH',
|
||||||
|
'MRKK',
|
||||||
|
'MRKP',
|
||||||
|
'MRKS',
|
||||||
|
'MRKU',
|
||||||
|
'MRKV',
|
||||||
|
'MRKY',
|
||||||
|
'MRKZ',
|
||||||
|
'MSNG',
|
||||||
|
'MSRS',
|
||||||
|
'MSSB',
|
||||||
|
'MSSV',
|
||||||
|
'OGK1',
|
||||||
|
'OGK2',
|
||||||
|
'OGK4',
|
||||||
|
'OGK6',
|
||||||
|
'OGKA',
|
||||||
|
'OGKB',
|
||||||
|
'OGKC',
|
||||||
|
'OGKD',
|
||||||
|
'OGKE',
|
||||||
|
'OGKF',
|
||||||
|
'RSTI',
|
||||||
|
'RSTIP',
|
||||||
|
'SAGO',
|
||||||
|
'SARE',
|
||||||
|
'SVER',
|
||||||
|
'TGKA',
|
||||||
|
'TGKB',
|
||||||
|
'TGKD',
|
||||||
|
'TGKE',
|
||||||
|
'TGKF',
|
||||||
|
'TGKH',
|
||||||
|
'TGKI',
|
||||||
|
'TGKJ',
|
||||||
|
'TGKN',
|
||||||
|
'TNSE',
|
||||||
|
'UPRO',
|
||||||
|
'VRAO',
|
||||||
|
'VTGK',
|
||||||
|
'YKEN'],
|
||||||
|
'telecom': ['AFKC',
|
||||||
|
'AFKS',
|
||||||
|
'BISV',
|
||||||
|
'BISVP',
|
||||||
|
'CMST',
|
||||||
|
'CNTL',
|
||||||
|
'CNTLP',
|
||||||
|
'CTLK',
|
||||||
|
'DLSV',
|
||||||
|
'DLSVP',
|
||||||
|
'MFON',
|
||||||
|
'MGTS',
|
||||||
|
'MGTSP',
|
||||||
|
'MTSI',
|
||||||
|
'MTSS',
|
||||||
|
'RTKM',
|
||||||
|
'RTKMP',
|
||||||
|
'SPTL',
|
||||||
|
'SPTLP',
|
||||||
|
'STKM',
|
||||||
|
'STKMP',
|
||||||
|
'TTLK',
|
||||||
|
'URSI',
|
||||||
|
'URSIP',
|
||||||
|
'UTEL',
|
||||||
|
'VTEL',
|
||||||
|
'VTELP'],
|
||||||
|
'finance': ['AFKS',
|
||||||
|
'BSPB',
|
||||||
|
'BSPBP',
|
||||||
|
'CBOM',
|
||||||
|
'EPLN',
|
||||||
|
'FTRE',
|
||||||
|
'LEAS',
|
||||||
|
'MBNK',
|
||||||
|
'MMBM',
|
||||||
|
'MOEX',
|
||||||
|
'PSBR',
|
||||||
|
'QIWI',
|
||||||
|
'RENI',
|
||||||
|
'ROSB',
|
||||||
|
'SBER',
|
||||||
|
'SBERP',
|
||||||
|
'SFIN',
|
||||||
|
'SPBE',
|
||||||
|
'T',
|
||||||
|
'TAVR',
|
||||||
|
'TCSG',
|
||||||
|
'TRHN',
|
||||||
|
'URSAP',
|
||||||
|
'VTBR',
|
||||||
|
'VTBS',
|
||||||
|
'VZRZ',
|
||||||
|
'VZRZP',
|
||||||
|
'YRSL',
|
||||||
|
'ZAYM'],
|
||||||
|
'consumer': ['ABIO',
|
||||||
|
'AGRO',
|
||||||
|
'APTK',
|
||||||
|
'AQUA',
|
||||||
|
'AVAZ',
|
||||||
|
'AVAZP',
|
||||||
|
'BELU',
|
||||||
|
'DELI',
|
||||||
|
'DIXY',
|
||||||
|
'DSKY',
|
||||||
|
'EUTR',
|
||||||
|
'FIVE',
|
||||||
|
'FIXP',
|
||||||
|
'GCHE',
|
||||||
|
'GEMC',
|
||||||
|
'GRAZ',
|
||||||
|
'HNFG',
|
||||||
|
'ISKJ',
|
||||||
|
'KLNA',
|
||||||
|
'LENT',
|
||||||
|
'LIFE',
|
||||||
|
'LNTA',
|
||||||
|
'MDMG',
|
||||||
|
'MGNT',
|
||||||
|
'MVID',
|
||||||
|
'OBUV',
|
||||||
|
'OKEY',
|
||||||
|
'ORUP',
|
||||||
|
'OTCP',
|
||||||
|
'PHST',
|
||||||
|
'PKBA',
|
||||||
|
'PKBAP',
|
||||||
|
'PRMD',
|
||||||
|
'PRTK',
|
||||||
|
'ROST',
|
||||||
|
'RSEA',
|
||||||
|
'SCOH',
|
||||||
|
'SCON',
|
||||||
|
'SVAV',
|
||||||
|
'SYNG',
|
||||||
|
'VFRM',
|
||||||
|
'VRPH',
|
||||||
|
'VSEH',
|
||||||
|
'WBDF',
|
||||||
|
'WUSH',
|
||||||
|
'YNDX'],
|
||||||
|
'transport': ['AFLT',
|
||||||
|
'FESH',
|
||||||
|
'FLOT',
|
||||||
|
'GLTR',
|
||||||
|
'GTRK',
|
||||||
|
'NKHP',
|
||||||
|
'NMTP',
|
||||||
|
'TAER',
|
||||||
|
'TRCN',
|
||||||
|
'UTAR']}
|
||||||
@ -1,356 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import asyncio
|
|
||||||
import sqlite3
|
|
||||||
import os
|
|
||||||
import schedule
|
|
||||||
import time
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import pandas as pd
|
|
||||||
import logging
|
|
||||||
from models import FinancialDataManager, NewsManager, AnalyticsManager
|
|
||||||
from classes.moex_history import MOEXHistoricalData
|
|
||||||
from news_parser import NewsParser
|
|
||||||
from config import sector_tickers, sector_indices, DB_CONFIG, DATA_COLLECTION
|
|
||||||
|
|
||||||
# Настройка логирования
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
||||||
handlers=[
|
|
||||||
logging.FileHandler("data_collector.log"),
|
|
||||||
logging.StreamHandler()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
logger = logging.getLogger("data_collector")
|
|
||||||
|
|
||||||
class DataCollector:
|
|
||||||
"""Класс для сбора и обновления данных"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Инициализация сборщика данных"""
|
|
||||||
self.moex_data = MOEXHistoricalData()
|
|
||||||
self.news_parser = NewsParser()
|
|
||||||
self.financial_manager = FinancialDataManager()
|
|
||||||
self.news_manager = NewsManager()
|
|
||||||
self.analytics_manager = AnalyticsManager()
|
|
||||||
|
|
||||||
# Создаем директории для баз данных, если они не существуют
|
|
||||||
os.makedirs('data', exist_ok=True)
|
|
||||||
|
|
||||||
def _init_databases(self):
|
|
||||||
"""Инициализация баз данных"""
|
|
||||||
# Инициализация базы данных для финансовых данных
|
|
||||||
conn = sqlite3.connect(f"data/{DB_CONFIG['financial_data']}")
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
# Создаем таблицу для секторных индексов
|
|
||||||
cursor.execute('''
|
|
||||||
CREATE TABLE IF NOT EXISTS sector_indices (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
sector TEXT NOT NULL,
|
|
||||||
date DATE NOT NULL,
|
|
||||||
open REAL,
|
|
||||||
high REAL,
|
|
||||||
low REAL,
|
|
||||||
close REAL,
|
|
||||||
volume REAL,
|
|
||||||
UNIQUE(sector, date)
|
|
||||||
)
|
|
||||||
''')
|
|
||||||
|
|
||||||
# Создаем таблицу для данных по тикерам
|
|
||||||
cursor.execute('''
|
|
||||||
CREATE TABLE IF NOT EXISTS ticker_data (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
ticker TEXT NOT NULL,
|
|
||||||
sector TEXT NOT NULL,
|
|
||||||
date DATE NOT NULL,
|
|
||||||
open REAL,
|
|
||||||
high REAL,
|
|
||||||
low REAL,
|
|
||||||
close REAL,
|
|
||||||
volume REAL,
|
|
||||||
UNIQUE(ticker, date)
|
|
||||||
)
|
|
||||||
''')
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
# Инициализация базы данных для новостей
|
|
||||||
conn = sqlite3.connect(f"data/{DB_CONFIG['news_data']}")
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
cursor.execute('''
|
|
||||||
CREATE TABLE IF NOT EXISTS news (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
date DATE NOT NULL,
|
|
||||||
topic TEXT,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
url TEXT,
|
|
||||||
UNIQUE(title, date)
|
|
||||||
)
|
|
||||||
''')
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
# Инициализация базы данных для аналитики
|
|
||||||
conn = sqlite3.connect(f"data/{DB_CONFIG['analytics']}")
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
cursor.execute('''
|
|
||||||
CREATE TABLE IF NOT EXISTS analytics (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
date DATE NOT NULL,
|
|
||||||
summary TEXT NOT NULL,
|
|
||||||
business_problems TEXT,
|
|
||||||
solutions TEXT,
|
|
||||||
sentiment REAL,
|
|
||||||
UNIQUE(date)
|
|
||||||
)
|
|
||||||
''')
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
logger.info("Базы данных успешно инициализированы")
|
|
||||||
|
|
||||||
async def _collect_sector_data(self, days=30):
|
|
||||||
"""Сбор данных по секторным индексам"""
|
|
||||||
logger.info("Начинаем сбор данных по секторным индексам")
|
|
||||||
|
|
||||||
end_date = datetime.now()
|
|
||||||
start_date = end_date - timedelta(days=days)
|
|
||||||
|
|
||||||
# Подключаемся к базе данных
|
|
||||||
conn = sqlite3.connect(f"data/{DB_CONFIG['financial_data']}")
|
|
||||||
|
|
||||||
# Собираем данные по каждому сектору
|
|
||||||
for sector, index_code in sector_indices.items():
|
|
||||||
try:
|
|
||||||
logger.info(f"Получаем данные для сектора: {sector}")
|
|
||||||
|
|
||||||
# Получаем данные по индексу
|
|
||||||
df = await self.moex_data.get_official_sector_index(
|
|
||||||
sector,
|
|
||||||
start_date.strftime('%Y-%m-%d'),
|
|
||||||
end_date.strftime('%Y-%m-%d')
|
|
||||||
)
|
|
||||||
|
|
||||||
if not df.empty:
|
|
||||||
# Добавляем колонку с названием сектора
|
|
||||||
df['sector'] = sector
|
|
||||||
|
|
||||||
# Сохраняем данные в базу
|
|
||||||
df.to_sql('sector_indices', conn, if_exists='append', index=False,
|
|
||||||
dtype={
|
|
||||||
'sector': 'TEXT',
|
|
||||||
'TRADEDATE': 'DATE',
|
|
||||||
'OPEN': 'REAL',
|
|
||||||
'HIGH': 'REAL',
|
|
||||||
'LOW': 'REAL',
|
|
||||||
'CLOSE': 'REAL',
|
|
||||||
'VOLUME': 'REAL'
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.info(f"Сохранено {len(df)} записей для сектора {sector}")
|
|
||||||
else:
|
|
||||||
logger.warning(f"Нет данных для сектора {sector}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при сборе данных для сектора {sector}: {str(e)}")
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
logger.info("Сбор данных по секторным индексам завершен")
|
|
||||||
|
|
||||||
async def _collect_ticker_data(self, days=30):
|
|
||||||
"""Сбор данных по отдельным тикерам"""
|
|
||||||
logger.info("Начинаем сбор данных по тикерам")
|
|
||||||
|
|
||||||
end_date = datetime.now()
|
|
||||||
start_date = end_date - timedelta(days=days)
|
|
||||||
|
|
||||||
# Подключаемся к базе данных
|
|
||||||
conn = sqlite3.connect(f"data/{DB_CONFIG['financial_data']}")
|
|
||||||
|
|
||||||
# Собираем данные по каждому тикеру в каждом секторе
|
|
||||||
for sector, tickers in sector_tickers.items():
|
|
||||||
for ticker in tickers:
|
|
||||||
try:
|
|
||||||
logger.info(f"Получаем данные для тикера: {ticker} (сектор: {sector})")
|
|
||||||
|
|
||||||
# Получаем данные по тикеру
|
|
||||||
df = await self.moex_data.get_security_history(
|
|
||||||
ticker,
|
|
||||||
start_date.strftime('%Y-%m-%d'),
|
|
||||||
end_date.strftime('%Y-%m-%d')
|
|
||||||
)
|
|
||||||
|
|
||||||
if not df.empty:
|
|
||||||
# Добавляем колонки с тикером и сектором
|
|
||||||
df['ticker'] = ticker
|
|
||||||
df['sector'] = sector
|
|
||||||
|
|
||||||
# Сохраняем данные в базу
|
|
||||||
df.to_sql('ticker_data', conn, if_exists='append', index=False,
|
|
||||||
dtype={
|
|
||||||
'ticker': 'TEXT',
|
|
||||||
'sector': 'TEXT',
|
|
||||||
'TRADEDATE': 'DATE',
|
|
||||||
'OPEN': 'REAL',
|
|
||||||
'HIGH': 'REAL',
|
|
||||||
'LOW': 'REAL',
|
|
||||||
'CLOSE': 'REAL',
|
|
||||||
'VOLUME': 'REAL'
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.info(f"Сохранено {len(df)} записей для тикера {ticker}")
|
|
||||||
else:
|
|
||||||
logger.warning(f"Нет данных для тикера {ticker}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при сборе данных для тикера {ticker}: {str(e)}")
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
logger.info("Сбор данных по тикерам завершен")
|
|
||||||
|
|
||||||
def _collect_news(self, days=7):
|
|
||||||
"""Сбор новостей"""
|
|
||||||
logger.info(f"Начинаем сбор новостей за последние {days} дней")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Получаем новости
|
|
||||||
news_df = self.news_parser.parse_news(days)
|
|
||||||
|
|
||||||
if not news_df.empty:
|
|
||||||
# Подключаемся к базе данных
|
|
||||||
conn = sqlite3.connect(f"data/{DB_CONFIG['news_data']}")
|
|
||||||
|
|
||||||
# Сохраняем новости в базу
|
|
||||||
news_df.to_sql('news', conn, if_exists='append', index=False,
|
|
||||||
dtype={
|
|
||||||
'date': 'DATE',
|
|
||||||
'topic': 'TEXT',
|
|
||||||
'title': 'TEXT',
|
|
||||||
'content': 'TEXT',
|
|
||||||
'url': 'TEXT'
|
|
||||||
})
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
logger.info(f"Сохранено {len(news_df)} новостей")
|
|
||||||
else:
|
|
||||||
logger.warning("Нет новых новостей для сохранения")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при сборе новостей: {str(e)}")
|
|
||||||
|
|
||||||
def _analyze_news(self):
|
|
||||||
"""Анализ новостей"""
|
|
||||||
logger.info("Начинаем анализ новостей")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Получаем новости за последние 24 часа
|
|
||||||
yesterday = datetime.now() - timedelta(days=1)
|
|
||||||
news = self.news_manager.get_news_by_date(yesterday.strftime('%Y-%m-%d'))
|
|
||||||
|
|
||||||
if news:
|
|
||||||
# Анализируем новости
|
|
||||||
analysis_result = self.analytics_manager.analyze_news(news)
|
|
||||||
|
|
||||||
if analysis_result:
|
|
||||||
logger.info("Анализ новостей успешно завершен")
|
|
||||||
else:
|
|
||||||
logger.warning("Не удалось выполнить анализ новостей")
|
|
||||||
else:
|
|
||||||
logger.warning("Нет новостей для анализа")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при анализе новостей: {str(e)}")
|
|
||||||
|
|
||||||
async def collect_initial_data(self):
|
|
||||||
"""Сбор начальных данных при первом запуске"""
|
|
||||||
logger.info("Начинаем сбор начальных данных")
|
|
||||||
|
|
||||||
# Инициализируем базы данных
|
|
||||||
self._init_databases()
|
|
||||||
|
|
||||||
# Собираем финансовые данные за последние 90 дней
|
|
||||||
await self._collect_sector_data(days=90)
|
|
||||||
await self._collect_ticker_data(days=90)
|
|
||||||
|
|
||||||
# Собираем новости за последние 14 дней
|
|
||||||
self._collect_news(days=14)
|
|
||||||
|
|
||||||
# Анализируем новости
|
|
||||||
self._analyze_news()
|
|
||||||
|
|
||||||
logger.info("Сбор начальных данных завершен")
|
|
||||||
|
|
||||||
async def update_data(self):
|
|
||||||
"""Обновление данных по расписанию"""
|
|
||||||
logger.info("Начинаем обновление данных")
|
|
||||||
|
|
||||||
# Обновляем финансовые данные за последние 2 дня
|
|
||||||
await self._collect_sector_data(days=2)
|
|
||||||
await self._collect_ticker_data(days=2)
|
|
||||||
|
|
||||||
# Обновляем новости за последний день
|
|
||||||
self._collect_news(days=1)
|
|
||||||
|
|
||||||
logger.info("Обновление данных завершено")
|
|
||||||
|
|
||||||
def schedule_tasks(self):
|
|
||||||
"""Настройка расписания для регулярного обновления данных"""
|
|
||||||
logger.info("Настраиваем расписание для обновления данных")
|
|
||||||
|
|
||||||
# Обновление финансовых данных
|
|
||||||
schedule.every(DATA_COLLECTION['financial_interval_hours']).hours.do(
|
|
||||||
lambda: asyncio.run(self.update_data())
|
|
||||||
)
|
|
||||||
|
|
||||||
# Обновление новостей
|
|
||||||
schedule.every(DATA_COLLECTION['news_interval_hours']).hours.do(
|
|
||||||
lambda: self._collect_news(days=1)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Анализ новостей
|
|
||||||
schedule.every(DATA_COLLECTION['analytics_interval_hours']).hours.do(
|
|
||||||
lambda: self._analyze_news()
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Расписание настроено")
|
|
||||||
|
|
||||||
# Запускаем бесконечный цикл для выполнения задач по расписанию
|
|
||||||
while True:
|
|
||||||
schedule.run_pending()
|
|
||||||
time.sleep(60)
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
"""Основная функция"""
|
|
||||||
parser = argparse.ArgumentParser(description='Сборщик данных для финансово-аналитической платформы')
|
|
||||||
parser.add_argument('--init', action='store_true', help='Инициализировать базы данных и собрать начальные данные')
|
|
||||||
parser.add_argument('--update', action='store_true', help='Обновить данные')
|
|
||||||
parser.add_argument('--schedule', action='store_true', help='Запустить сбор данных по расписанию')
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
collector = DataCollector()
|
|
||||||
|
|
||||||
if args.init:
|
|
||||||
await collector.collect_initial_data()
|
|
||||||
elif args.update:
|
|
||||||
await collector.update_data()
|
|
||||||
elif args.schedule:
|
|
||||||
collector.schedule_tasks()
|
|
||||||
else:
|
|
||||||
parser.print_help()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "finance-analytics-platform",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"dev": "next dev",
|
|
||||||
"build": "next build",
|
|
||||||
"start": "next start",
|
|
||||||
"lint": "next lint"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"next": "^14.0.0",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"axios": "^1.6.0",
|
|
||||||
"chart.js": "^4.4.0",
|
|
||||||
"react-chartjs-2": "^5.2.0",
|
|
||||||
"react-markdown": "^9.0.0",
|
|
||||||
"date-fns": "^2.30.0",
|
|
||||||
"@mui/material": "^5.14.18",
|
|
||||||
"@mui/icons-material": "^5.14.18",
|
|
||||||
"@emotion/react": "^11.11.1",
|
|
||||||
"@emotion/styled": "^11.11.0",
|
|
||||||
"swr": "^2.2.4"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^20.9.0",
|
|
||||||
"@types/react": "^18.2.37",
|
|
||||||
"@types/react-dom": "^18.2.15",
|
|
||||||
"typescript": "^5.2.2",
|
|
||||||
"eslint": "^8.53.0",
|
|
||||||
"eslint-config-next": "^14.0.0",
|
|
||||||
"autoprefixer": "^10.4.16",
|
|
||||||
"postcss": "^8.4.31",
|
|
||||||
"tailwindcss": "^3.3.5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@ -1,126 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
CircularProgress,
|
|
||||||
Alert,
|
|
||||||
TextField,
|
|
||||||
Button,
|
|
||||||
Grid,
|
|
||||||
Paper
|
|
||||||
} from '@mui/material';
|
|
||||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
|
||||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
|
||||||
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
|
||||||
import { ru } from 'date-fns/locale';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import AnalyticsView from '@/components/AnalyticsView';
|
|
||||||
import { getLatestAnalytics, getAnalyticsByDate, Analytics } from '@/lib/api';
|
|
||||||
|
|
||||||
export default function AnalyticsPage() {
|
|
||||||
const [analytics, setAnalytics] = useState<Analytics | null>(null);
|
|
||||||
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
|
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Загрузка последнего анализа при монтировании компонента
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchLatestAnalytics = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const data = await getLatestAnalytics();
|
|
||||||
setAnalytics(data);
|
|
||||||
|
|
||||||
// Устанавливаем дату последнего анализа
|
|
||||||
if (data.date) {
|
|
||||||
setSelectedDate(new Date(data.date));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Ошибка при загрузке аналитики:', err);
|
|
||||||
setError('Не удалось загрузить последний анализ.');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchLatestAnalytics();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDateChange = (date: Date | null) => {
|
|
||||||
setSelectedDate(date);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSearch = async () => {
|
|
||||||
if (!selectedDate) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
const formattedDate = format(selectedDate, 'yyyy-MM-dd');
|
|
||||||
const data = await getAnalyticsByDate(formattedDate);
|
|
||||||
setAnalytics(data);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Ошибка при загрузке аналитики по дате:', err);
|
|
||||||
setError('Не удалось загрузить анализ за выбранную дату.');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="h4" component="h1" gutterBottom>
|
|
||||||
Бизнес-аналитика
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Alert severity="error" sx={{ mb: 2 }}>
|
|
||||||
{error}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Paper elevation={3} sx={{ p: 3, mb: 4 }}>
|
|
||||||
<Grid container spacing={2} alignItems="center">
|
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={ru}>
|
|
||||||
<DatePicker
|
|
||||||
label="Выберите дату"
|
|
||||||
value={selectedDate}
|
|
||||||
onChange={handleDateChange}
|
|
||||||
format="dd.MM.yyyy"
|
|
||||||
slotProps={{ textField: { fullWidth: true } }}
|
|
||||||
/>
|
|
||||||
</LocalizationProvider>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={handleSearch}
|
|
||||||
fullWidth
|
|
||||||
disabled={!selectedDate}
|
|
||||||
>
|
|
||||||
Найти анализ
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<Box display="flex" justifyContent="center" alignItems="center" height={300}>
|
|
||||||
<CircularProgress />
|
|
||||||
</Box>
|
|
||||||
) : analytics ? (
|
|
||||||
<AnalyticsView analytics={analytics} />
|
|
||||||
) : (
|
|
||||||
<Alert severity="info">
|
|
||||||
Анализ не найден. Попробуйте выбрать другую дату.
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--foreground-rgb: 0, 0, 0;
|
|
||||||
--background-rgb: 255, 255, 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
color: rgb(var(--foreground-rgb));
|
|
||||||
background: rgb(var(--background-rgb));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Стили для скроллбара */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: #f1f1f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: #0072ff;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #005bcc;
|
|
||||||
}
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
import type { Metadata } from 'next';
|
|
||||||
import { Inter } from 'next/font/google';
|
|
||||||
import './globals.css';
|
|
||||||
import Header from '@/components/Header';
|
|
||||||
import { Box, Container, CssBaseline, ThemeProvider, createTheme } from '@mui/material';
|
|
||||||
|
|
||||||
const inter = Inter({ subsets: ['latin', 'cyrillic'] });
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Финансово-аналитическая платформа',
|
|
||||||
description: 'Платформа для анализа финансовых данных и новостей',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Создаем тему
|
|
||||||
const theme = createTheme({
|
|
||||||
palette: {
|
|
||||||
primary: {
|
|
||||||
main: '#0072ff',
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
main: '#7b7b7b',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
typography: {
|
|
||||||
fontFamily: inter.style.fontFamily,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function RootLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<html lang="ru">
|
|
||||||
<body className={inter.className}>
|
|
||||||
<ThemeProvider theme={theme}>
|
|
||||||
<CssBaseline />
|
|
||||||
<Box display="flex" flexDirection="column" minHeight="100vh">
|
|
||||||
<Header />
|
|
||||||
<Container component="main" maxWidth="xl" sx={{ flexGrow: 1, py: 4 }}>
|
|
||||||
{children}
|
|
||||||
</Container>
|
|
||||||
<Box component="footer" sx={{ p: 2, bgcolor: 'secondary.100', textAlign: 'center' }}>
|
|
||||||
<Container>
|
|
||||||
© {new Date().getFullYear()} Финансово-аналитическая платформа
|
|
||||||
</Container>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</ThemeProvider>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,259 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
Grid,
|
|
||||||
Paper,
|
|
||||||
CircularProgress,
|
|
||||||
Alert,
|
|
||||||
Tabs,
|
|
||||||
Tab,
|
|
||||||
FormControl,
|
|
||||||
InputLabel,
|
|
||||||
Select,
|
|
||||||
MenuItem,
|
|
||||||
SelectChangeEvent,
|
|
||||||
Slider,
|
|
||||||
Stack
|
|
||||||
} from '@mui/material';
|
|
||||||
import ChartComponent from '@/components/ChartComponent';
|
|
||||||
import { getSectors, getTickers, getSectorData, getTickerData, SectorData, TickerData } from '@/lib/api';
|
|
||||||
|
|
||||||
export default function Markets() {
|
|
||||||
const [sectors, setSectors] = useState<string[]>([]);
|
|
||||||
const [tickers, setTickers] = useState<string[]>([]);
|
|
||||||
const [selectedSector, setSelectedSector] = useState<string>('');
|
|
||||||
const [selectedTicker, setSelectedTicker] = useState<string>('');
|
|
||||||
const [sectorData, setSectorData] = useState<SectorData | null>(null);
|
|
||||||
const [tickerData, setTickerData] = useState<TickerData | null>(null);
|
|
||||||
const [period, setPeriod] = useState<number>(30);
|
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Преобразуем названия секторов для отображения
|
|
||||||
const sectorNames: { [key: string]: string } = {
|
|
||||||
'metals_mining': 'Металлы и добыча',
|
|
||||||
'oil_gas': 'Нефть и газ',
|
|
||||||
'chemicals': 'Химия и нефтехимия',
|
|
||||||
'electric_utilities': 'Электроэнергетика',
|
|
||||||
'telecom': 'Телекоммуникации',
|
|
||||||
'finance': 'Финансы',
|
|
||||||
'consumer': 'Потребительский сектор',
|
|
||||||
'transport': 'Транспорт'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Загрузка списка секторов при монтировании компонента
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchSectors = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const data = await getSectors();
|
|
||||||
setSectors(data);
|
|
||||||
|
|
||||||
if (data.length > 0) {
|
|
||||||
setSelectedSector(data[0]);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Ошибка при загрузке секторов:', err);
|
|
||||||
setError('Не удалось загрузить список секторов.');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchSectors();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Загрузка тикеров при изменении выбранного сектора
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedSector) return;
|
|
||||||
|
|
||||||
const fetchTickers = async () => {
|
|
||||||
try {
|
|
||||||
const data = await getTickers(selectedSector);
|
|
||||||
setTickers(data);
|
|
||||||
|
|
||||||
if (data.length > 0) {
|
|
||||||
setSelectedTicker(data[0]);
|
|
||||||
} else {
|
|
||||||
setSelectedTicker('');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Ошибка при загрузке тикеров:', err);
|
|
||||||
setError('Не удалось загрузить список тикеров.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchTickers();
|
|
||||||
}, [selectedSector]);
|
|
||||||
|
|
||||||
// Загрузка данных по сектору при изменении выбранного сектора или периода
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedSector) return;
|
|
||||||
|
|
||||||
const fetchSectorData = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const data = await getSectorData(selectedSector, period);
|
|
||||||
setSectorData(data);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Ошибка при загрузке данных сектора:', err);
|
|
||||||
setError('Не удалось загрузить данные сектора.');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchSectorData();
|
|
||||||
}, [selectedSector, period]);
|
|
||||||
|
|
||||||
// Загрузка данных по тикеру при изменении выбранного тикера или периода
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedTicker) return;
|
|
||||||
|
|
||||||
const fetchTickerData = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const data = await getTickerData(selectedTicker, period);
|
|
||||||
setTickerData(data);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Ошибка при загрузке данных тикера:', err);
|
|
||||||
setError('Не удалось загрузить данные тикера.');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchTickerData();
|
|
||||||
}, [selectedTicker, period]);
|
|
||||||
|
|
||||||
const handleSectorChange = (event: SelectChangeEvent) => {
|
|
||||||
setSelectedSector(event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTickerChange = (event: SelectChangeEvent) => {
|
|
||||||
setSelectedTicker(event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePeriodChange = (event: Event, newValue: number | number[]) => {
|
|
||||||
setPeriod(newValue as number);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="h4" component="h1" gutterBottom>
|
|
||||||
Рынки
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Alert severity="error" sx={{ mb: 2 }}>
|
|
||||||
{error}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Paper elevation={3} sx={{ p: 3, mb: 4 }}>
|
|
||||||
<Grid container spacing={3}>
|
|
||||||
<Grid item xs={12} md={4}>
|
|
||||||
<FormControl fullWidth>
|
|
||||||
<InputLabel id="sector-select-label">Сектор</InputLabel>
|
|
||||||
<Select
|
|
||||||
labelId="sector-select-label"
|
|
||||||
id="sector-select"
|
|
||||||
value={selectedSector}
|
|
||||||
label="Сектор"
|
|
||||||
onChange={handleSectorChange}
|
|
||||||
>
|
|
||||||
{sectors.map((sector) => (
|
|
||||||
<MenuItem key={sector} value={sector}>
|
|
||||||
{sectorNames[sector] || sector}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item xs={12} md={4}>
|
|
||||||
<FormControl fullWidth>
|
|
||||||
<InputLabel id="ticker-select-label">Тикер</InputLabel>
|
|
||||||
<Select
|
|
||||||
labelId="ticker-select-label"
|
|
||||||
id="ticker-select"
|
|
||||||
value={selectedTicker}
|
|
||||||
label="Тикер"
|
|
||||||
onChange={handleTickerChange}
|
|
||||||
disabled={tickers.length === 0}
|
|
||||||
>
|
|
||||||
{tickers.map((ticker) => (
|
|
||||||
<MenuItem key={ticker} value={ticker}>
|
|
||||||
{ticker}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item xs={12} md={4}>
|
|
||||||
<Typography gutterBottom>
|
|
||||||
Период: {period} дней
|
|
||||||
</Typography>
|
|
||||||
<Slider
|
|
||||||
value={period}
|
|
||||||
onChange={handlePeriodChange}
|
|
||||||
min={7}
|
|
||||||
max={365}
|
|
||||||
step={1}
|
|
||||||
marks={[
|
|
||||||
{ value: 7, label: '7д' },
|
|
||||||
{ value: 30, label: '30д' },
|
|
||||||
{ value: 90, label: '90д' },
|
|
||||||
{ value: 180, label: '180д' },
|
|
||||||
{ value: 365, label: '365д' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
<Grid container spacing={4}>
|
|
||||||
{/* Сектор */}
|
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
{loading && !sectorData ? (
|
|
||||||
<Box display="flex" justifyContent="center" alignItems="center" height={300}>
|
|
||||||
<CircularProgress />
|
|
||||||
</Box>
|
|
||||||
) : sectorData ? (
|
|
||||||
<ChartComponent
|
|
||||||
title={`Индекс: ${sectorNames[selectedSector] || selectedSector}`}
|
|
||||||
data={sectorData.data}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Alert severity="info">
|
|
||||||
Нет данных для отображения.
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Тикер */}
|
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
{loading && !tickerData ? (
|
|
||||||
<Box display="flex" justifyContent="center" alignItems="center" height={300}>
|
|
||||||
<CircularProgress />
|
|
||||||
</Box>
|
|
||||||
) : tickerData ? (
|
|
||||||
<ChartComponent
|
|
||||||
title={`Тикер: ${tickerData.ticker}`}
|
|
||||||
data={tickerData.data}
|
|
||||||
color="rgb(75, 192, 192)"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Alert severity="info">
|
|
||||||
Выберите тикер для отображения данных.
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,139 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
CircularProgress,
|
|
||||||
Alert,
|
|
||||||
FormControl,
|
|
||||||
InputLabel,
|
|
||||||
Select,
|
|
||||||
MenuItem,
|
|
||||||
SelectChangeEvent,
|
|
||||||
Slider,
|
|
||||||
Grid,
|
|
||||||
Paper
|
|
||||||
} from '@mui/material';
|
|
||||||
import NewsList from '@/components/NewsList';
|
|
||||||
import { getNews, getNewsTopics, NewsItem } from '@/lib/api';
|
|
||||||
|
|
||||||
export default function News() {
|
|
||||||
const [news, setNews] = useState<NewsItem[]>([]);
|
|
||||||
const [topics, setTopics] = useState<string[]>([]);
|
|
||||||
const [selectedTopic, setSelectedTopic] = useState<string>('all');
|
|
||||||
const [days, setDays] = useState<number>(7);
|
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Загрузка тем новостей при монтировании компонента
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchTopics = async () => {
|
|
||||||
try {
|
|
||||||
const data = await getNewsTopics();
|
|
||||||
setTopics(data);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Ошибка при загрузке тем новостей:', err);
|
|
||||||
setError('Не удалось загрузить темы новостей.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchTopics();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Загрузка новостей при изменении выбранной темы или периода
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchNews = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const topic = selectedTopic === 'all' ? undefined : selectedTopic;
|
|
||||||
const data = await getNews(days, topic);
|
|
||||||
setNews(data);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Ошибка при загрузке новостей:', err);
|
|
||||||
setError('Не удалось загрузить новости.');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchNews();
|
|
||||||
}, [selectedTopic, days]);
|
|
||||||
|
|
||||||
const handleTopicChange = (event: SelectChangeEvent) => {
|
|
||||||
setSelectedTopic(event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDaysChange = (event: Event, newValue: number | number[]) => {
|
|
||||||
setDays(newValue as number);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="h4" component="h1" gutterBottom>
|
|
||||||
Новости
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Alert severity="error" sx={{ mb: 2 }}>
|
|
||||||
{error}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Paper elevation={3} sx={{ p: 3, mb: 4 }}>
|
|
||||||
<Grid container spacing={3}>
|
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
<FormControl fullWidth>
|
|
||||||
<InputLabel id="topic-select-label">Тема</InputLabel>
|
|
||||||
<Select
|
|
||||||
labelId="topic-select-label"
|
|
||||||
id="topic-select"
|
|
||||||
value={selectedTopic}
|
|
||||||
label="Тема"
|
|
||||||
onChange={handleTopicChange}
|
|
||||||
>
|
|
||||||
<MenuItem value="all">Все темы</MenuItem>
|
|
||||||
{topics.map((topic) => (
|
|
||||||
<MenuItem key={topic} value={topic}>
|
|
||||||
{topic}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
<Typography gutterBottom>
|
|
||||||
Период: {days} дней
|
|
||||||
</Typography>
|
|
||||||
<Slider
|
|
||||||
value={days}
|
|
||||||
onChange={handleDaysChange}
|
|
||||||
min={1}
|
|
||||||
max={30}
|
|
||||||
step={1}
|
|
||||||
marks={[
|
|
||||||
{ value: 1, label: '1д' },
|
|
||||||
{ value: 7, label: '7д' },
|
|
||||||
{ value: 14, label: '14д' },
|
|
||||||
{ value: 30, label: '30д' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<Box display="flex" justifyContent="center" alignItems="center" height={300}>
|
|
||||||
<CircularProgress />
|
|
||||||
</Box>
|
|
||||||
) : news.length > 0 ? (
|
|
||||||
<NewsList news={news} topics={topics} />
|
|
||||||
) : (
|
|
||||||
<Alert severity="info">
|
|
||||||
Новости не найдены. Попробуйте изменить параметры поиска.
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,145 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
Grid,
|
|
||||||
Paper,
|
|
||||||
CircularProgress,
|
|
||||||
Alert,
|
|
||||||
Tabs,
|
|
||||||
Tab
|
|
||||||
} from '@mui/material';
|
|
||||||
import ChartComponent from '@/components/ChartComponent';
|
|
||||||
import NewsList from '@/components/NewsList';
|
|
||||||
import AnalyticsView from '@/components/AnalyticsView';
|
|
||||||
import { getDashboardData, DashboardData } from '@/lib/api';
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
|
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [selectedSector, setSelectedSector] = useState<string>('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const data = await getDashboardData();
|
|
||||||
setDashboardData(data);
|
|
||||||
|
|
||||||
// Устанавливаем первый сектор как выбранный по умолчанию
|
|
||||||
if (data.sectors_data && Object.keys(data.sectors_data).length > 0) {
|
|
||||||
setSelectedSector(Object.keys(data.sectors_data)[0]);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Ошибка при загрузке данных:', err);
|
|
||||||
setError('Не удалось загрузить данные. Пожалуйста, попробуйте позже.');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSectorChange = (event: React.SyntheticEvent, newValue: string) => {
|
|
||||||
setSelectedSector(newValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<Box display="flex" justifyContent="center" alignItems="center" minHeight="50vh">
|
|
||||||
<CircularProgress />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<Alert severity="error" sx={{ mt: 2 }}>
|
|
||||||
{error}
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dashboardData) {
|
|
||||||
return (
|
|
||||||
<Alert severity="info" sx={{ mt: 2 }}>
|
|
||||||
Нет данных для отображения.
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем данные выбранного сектора
|
|
||||||
const selectedSectorData = selectedSector ? dashboardData.sectors_data[selectedSector] : null;
|
|
||||||
|
|
||||||
// Преобразуем названия секторов для отображения
|
|
||||||
const sectorNames: { [key: string]: string } = {
|
|
||||||
'metals_mining': 'Металлы и добыча',
|
|
||||||
'oil_gas': 'Нефть и газ',
|
|
||||||
'chemicals': 'Химия и нефтехимия',
|
|
||||||
'electric_utilities': 'Электроэнергетика',
|
|
||||||
'telecom': 'Телекоммуникации',
|
|
||||||
'finance': 'Финансы',
|
|
||||||
'consumer': 'Потребительский сектор',
|
|
||||||
'transport': 'Транспорт'
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="h4" component="h1" gutterBottom>
|
|
||||||
Финансово-аналитическая платформа
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Grid container spacing={4}>
|
|
||||||
{/* Левая колонка - Финансовые данные */}
|
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
<Paper elevation={3} sx={{ p: 2, mb: 3 }}>
|
|
||||||
<Typography variant="h5" component="h2" gutterBottom>
|
|
||||||
Индексы секторов
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Tabs
|
|
||||||
value={selectedSector}
|
|
||||||
onChange={handleSectorChange}
|
|
||||||
variant="scrollable"
|
|
||||||
scrollButtons="auto"
|
|
||||||
sx={{ mb: 2 }}
|
|
||||||
>
|
|
||||||
{Object.keys(dashboardData.sectors_data).map((sector) => (
|
|
||||||
<Tab
|
|
||||||
key={sector}
|
|
||||||
label={sectorNames[sector] || sector}
|
|
||||||
value={sector}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
{selectedSectorData && (
|
|
||||||
<ChartComponent
|
|
||||||
title={sectorNames[selectedSector] || selectedSector}
|
|
||||||
data={selectedSectorData}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Paper>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Правая колонка - Новости и аналитика */}
|
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
{dashboardData.analytics && (
|
|
||||||
<AnalyticsView analytics={dashboardData.analytics} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{dashboardData.latest_news && dashboardData.latest_news.length > 0 && (
|
|
||||||
<NewsList
|
|
||||||
news={dashboardData.latest_news}
|
|
||||||
topics={Array.from(new Set(dashboardData.latest_news.map(item => item.topic)))}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,128 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
Paper,
|
|
||||||
Accordion,
|
|
||||||
AccordionSummary,
|
|
||||||
AccordionDetails,
|
|
||||||
Divider
|
|
||||||
} from '@mui/material';
|
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { ru } from 'date-fns/locale';
|
|
||||||
import { Analytics } from '@/lib/api';
|
|
||||||
import ReactMarkdown from 'react-markdown';
|
|
||||||
|
|
||||||
interface AnalyticsViewProps {
|
|
||||||
analytics: Analytics;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AnalyticsView: React.FC<AnalyticsViewProps> = ({ analytics }) => {
|
|
||||||
// Функция для форматирования текста аналитики в нормальный вид
|
|
||||||
const formatAnalyticsText = (text: string) => {
|
|
||||||
return text
|
|
||||||
.replace(/\*\*/g, '') // Удаляем маркеры жирного текста
|
|
||||||
.replace(/\*\s*\*/g, ''); // Удаляем пустые маркеры
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция для парсинга проблем и решений
|
|
||||||
const parseProblemsAndSolutions = (text: string) => {
|
|
||||||
const problems = [];
|
|
||||||
let currentProblem = null;
|
|
||||||
|
|
||||||
const lines = text.split('\n');
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = lines[i].trim();
|
|
||||||
|
|
||||||
if (line.startsWith('Проблема №') || line.startsWith('Проблема:')) {
|
|
||||||
// Если начинается новая проблема, сохраняем предыдущую
|
|
||||||
if (currentProblem) {
|
|
||||||
problems.push(currentProblem);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentProblem = {
|
|
||||||
problem: line.replace(/^Проблема №\d+:\s*|^Проблема:\s*/, '').trim(),
|
|
||||||
solution: ''
|
|
||||||
};
|
|
||||||
} else if (line.startsWith('Решение:') && currentProblem) {
|
|
||||||
// Если это решение для текущей проблемы
|
|
||||||
currentProblem.solution = line.replace(/^Решение:\s*/, '').trim();
|
|
||||||
|
|
||||||
// Если есть следующие строки, которые не начинаются с "Проблема", добавляем их к решению
|
|
||||||
let j = i + 1;
|
|
||||||
while (j < lines.length &&
|
|
||||||
!lines[j].trim().startsWith('Проблема №') &&
|
|
||||||
!lines[j].trim().startsWith('Проблема:')) {
|
|
||||||
if (lines[j].trim()) {
|
|
||||||
currentProblem.solution += '\n' + lines[j].trim();
|
|
||||||
}
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Переходим к следующей проблеме
|
|
||||||
i = j - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавляем последнюю проблему
|
|
||||||
if (currentProblem) {
|
|
||||||
problems.push(currentProblem);
|
|
||||||
}
|
|
||||||
|
|
||||||
return problems;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formattedSummary = formatAnalyticsText(analytics.summary);
|
|
||||||
const problems = parseProblemsAndSolutions(analytics.problems_solutions);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper elevation={3} sx={{ p: 3, mb: 4 }}>
|
|
||||||
<Typography variant="h5" component="h2" gutterBottom>
|
|
||||||
Бизнес-аналитика на {format(new Date(analytics.date), 'dd MMMM yyyy', { locale: ru })}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Box mb={4}>
|
|
||||||
<Typography variant="h6" component="h3" gutterBottom sx={{ color: 'primary.main' }}>
|
|
||||||
Краткая сводка новостей:
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body1" paragraph>
|
|
||||||
{formattedSummary}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Divider sx={{ mb: 3 }} />
|
|
||||||
|
|
||||||
<Typography variant="h6" component="h3" gutterBottom sx={{ color: 'primary.main', mb: 2 }}>
|
|
||||||
Выявленные бизнес-проблемы и решения:
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{problems.map((item, index) => (
|
|
||||||
<Accordion key={index} sx={{ mb: 2 }}>
|
|
||||||
<AccordionSummary
|
|
||||||
expandIcon={<ExpandMoreIcon />}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: 'secondary.50',
|
|
||||||
'&:hover': { backgroundColor: 'secondary.100' }
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
|
|
||||||
Проблема №{index + 1}: {item.problem}
|
|
||||||
</Typography>
|
|
||||||
</AccordionSummary>
|
|
||||||
<AccordionDetails>
|
|
||||||
<Typography variant="h6" color="primary" gutterBottom>
|
|
||||||
Решение:
|
|
||||||
</Typography>
|
|
||||||
<ReactMarkdown>
|
|
||||||
{item.solution}
|
|
||||||
</ReactMarkdown>
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
))}
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AnalyticsView;
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
Chart as ChartJS,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
PointElement,
|
|
||||||
LineElement,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
} from 'chart.js';
|
|
||||||
import { Line } from 'react-chartjs-2';
|
|
||||||
import { Box, Paper, Typography } from '@mui/material';
|
|
||||||
|
|
||||||
// Регистрируем необходимые компоненты для ChartJS
|
|
||||||
ChartJS.register(
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
PointElement,
|
|
||||||
LineElement,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Legend
|
|
||||||
);
|
|
||||||
|
|
||||||
interface ChartComponentProps {
|
|
||||||
title: string;
|
|
||||||
data: Array<{
|
|
||||||
TRADEDATE: string;
|
|
||||||
CLOSE: number;
|
|
||||||
[key: string]: any;
|
|
||||||
}>;
|
|
||||||
color?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartComponent: React.FC<ChartComponentProps> = ({ title, data, color = 'rgb(0, 114, 255)' }) => {
|
|
||||||
// Подготавливаем данные для графика
|
|
||||||
const chartData = {
|
|
||||||
labels: data.map(item => item.TRADEDATE),
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: title,
|
|
||||||
data: data.map(item => item.CLOSE),
|
|
||||||
borderColor: color,
|
|
||||||
backgroundColor: `${color}33`, // Добавляем прозрачность
|
|
||||||
tension: 0.1,
|
|
||||||
fill: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Опции для графика
|
|
||||||
const options = {
|
|
||||||
responsive: true,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
position: 'top' as const,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: title,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
ticks: {
|
|
||||||
maxTicksLimit: 10, // Ограничиваем количество меток по оси X
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Вычисляем изменение цены
|
|
||||||
const firstPrice = data.length > 0 ? data[0].CLOSE : 0;
|
|
||||||
const lastPrice = data.length > 0 ? data[data.length - 1].CLOSE : 0;
|
|
||||||
const priceChange = lastPrice - firstPrice;
|
|
||||||
const priceChangePercent = firstPrice !== 0 ? (priceChange / firstPrice) * 100 : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper elevation={3} sx={{ p: 2, mb: 3 }}>
|
|
||||||
<Typography variant="h6" component="h3" gutterBottom>
|
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Box display="flex" justifyContent="space-between" mb={2}>
|
|
||||||
<Typography variant="body1">
|
|
||||||
Цена: {lastPrice.toFixed(2)}
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
sx={{
|
|
||||||
color: priceChange >= 0 ? 'success.main' : 'error.main',
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{priceChange >= 0 ? '+' : ''}{priceChange.toFixed(2)} ({priceChangePercent.toFixed(2)}%)
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box height={300}>
|
|
||||||
<Line options={options} data={chartData} />
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChartComponent;
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { AppBar, Toolbar, Typography, Button, Container, Box } from '@mui/material';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
const Header: React.FC = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppBar position="static" sx={{ backgroundColor: '#0072ff' }}>
|
|
||||||
<Container maxWidth="xl">
|
|
||||||
<Toolbar disableGutters>
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
noWrap
|
|
||||||
component="div"
|
|
||||||
onClick={() => router.push('/')}
|
|
||||||
sx={{
|
|
||||||
mr: 2,
|
|
||||||
display: { xs: 'none', md: 'flex' },
|
|
||||||
fontWeight: 700,
|
|
||||||
cursor: 'pointer'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
ФИНАНСОВО-АНАЛИТИЧЕСКАЯ ПЛАТФОРМА
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Box sx={{ flexGrow: 1, display: { xs: 'none', md: 'flex' } }}>
|
|
||||||
<Button
|
|
||||||
onClick={() => router.push('/')}
|
|
||||||
sx={{ my: 2, color: 'white', display: 'block' }}
|
|
||||||
>
|
|
||||||
Главная
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => router.push('/markets')}
|
|
||||||
sx={{ my: 2, color: 'white', display: 'block' }}
|
|
||||||
>
|
|
||||||
Рынки
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => router.push('/news')}
|
|
||||||
sx={{ my: 2, color: 'white', display: 'block' }}
|
|
||||||
>
|
|
||||||
Новости
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => router.push('/analytics')}
|
|
||||||
sx={{ my: 2, color: 'white', display: 'block' }}
|
|
||||||
>
|
|
||||||
Аналитика
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Toolbar>
|
|
||||||
</Container>
|
|
||||||
</AppBar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Header;
|
|
||||||
@ -1,126 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemText,
|
|
||||||
Chip,
|
|
||||||
Divider,
|
|
||||||
Paper,
|
|
||||||
Accordion,
|
|
||||||
AccordionSummary,
|
|
||||||
AccordionDetails,
|
|
||||||
FormControl,
|
|
||||||
InputLabel,
|
|
||||||
Select,
|
|
||||||
MenuItem,
|
|
||||||
SelectChangeEvent
|
|
||||||
} from '@mui/material';
|
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { ru } from 'date-fns/locale';
|
|
||||||
import { NewsItem } from '@/lib/api';
|
|
||||||
|
|
||||||
interface NewsListProps {
|
|
||||||
news: NewsItem[];
|
|
||||||
topics?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const NewsList: React.FC<NewsListProps> = ({ news, topics = [] }) => {
|
|
||||||
const [selectedTopic, setSelectedTopic] = useState<string>('all');
|
|
||||||
|
|
||||||
const handleTopicChange = (event: SelectChangeEvent<string>) => {
|
|
||||||
setSelectedTopic(event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Фильтруем новости по выбранной теме
|
|
||||||
const filteredNews = selectedTopic === 'all'
|
|
||||||
? news
|
|
||||||
: news.filter(item => item.topic === selectedTopic);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper elevation={3} sx={{ p: 3, mb: 4 }}>
|
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
|
||||||
<Typography variant="h5" component="h2" gutterBottom>
|
|
||||||
Последние новости
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{topics.length > 0 && (
|
|
||||||
<FormControl variant="outlined" size="small" sx={{ minWidth: 200 }}>
|
|
||||||
<InputLabel id="topic-select-label">Тема</InputLabel>
|
|
||||||
<Select
|
|
||||||
labelId="topic-select-label"
|
|
||||||
id="topic-select"
|
|
||||||
value={selectedTopic}
|
|
||||||
onChange={handleTopicChange}
|
|
||||||
label="Тема"
|
|
||||||
>
|
|
||||||
<MenuItem value="all">Все темы</MenuItem>
|
|
||||||
{topics.map((topic) => (
|
|
||||||
<MenuItem key={topic} value={topic}>
|
|
||||||
{topic}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{filteredNews.length === 0 ? (
|
|
||||||
<Typography variant="body1" color="text.secondary" sx={{ textAlign: 'center', py: 4 }}>
|
|
||||||
Новости не найдены
|
|
||||||
</Typography>
|
|
||||||
) : (
|
|
||||||
<List>
|
|
||||||
{filteredNews.map((item, index) => (
|
|
||||||
<React.Fragment key={item.id}>
|
|
||||||
{index > 0 && <Divider component="li" />}
|
|
||||||
<ListItem alignItems="flex-start" sx={{ py: 2 }}>
|
|
||||||
<ListItemText
|
|
||||||
primary={
|
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
|
||||||
<Typography variant="h6" component="h3" sx={{ fontWeight: 'bold', mb: 1 }}>
|
|
||||||
{item.title}
|
|
||||||
</Typography>
|
|
||||||
<Chip
|
|
||||||
label={item.topic}
|
|
||||||
color="primary"
|
|
||||||
size="small"
|
|
||||||
sx={{ fontSize: '0.75rem' }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
secondary={
|
|
||||||
<>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{ mb: 1, display: 'block' }}
|
|
||||||
>
|
|
||||||
{format(new Date(item.date), 'dd MMMM yyyy', { locale: ru })}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Accordion>
|
|
||||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
|
||||||
<Typography>Подробнее...</Typography>
|
|
||||||
</AccordionSummary>
|
|
||||||
<AccordionDetails>
|
|
||||||
<Typography variant="body2">
|
|
||||||
{item.content}
|
|
||||||
</Typography>
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
)}
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NewsList;
|
|
||||||
@ -1,136 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
// Базовый URL API
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
|
||||||
|
|
||||||
// Создаем экземпляр axios с базовым URL
|
|
||||||
const api = axios.create({
|
|
||||||
baseURL: API_URL,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Интерфейсы для типизации данных
|
|
||||||
export interface SectorData {
|
|
||||||
sector: string;
|
|
||||||
data: Array<{
|
|
||||||
TRADEDATE: string;
|
|
||||||
OPEN: number;
|
|
||||||
HIGH: number;
|
|
||||||
LOW: number;
|
|
||||||
CLOSE: number;
|
|
||||||
VOLUME: number;
|
|
||||||
[key: string]: any;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TickerData {
|
|
||||||
ticker: string;
|
|
||||||
data: Array<{
|
|
||||||
TRADEDATE: string;
|
|
||||||
OPEN: number;
|
|
||||||
HIGH: number;
|
|
||||||
LOW: number;
|
|
||||||
CLOSE: number;
|
|
||||||
VOLUME: number;
|
|
||||||
[key: string]: any;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NewsItem {
|
|
||||||
id: number;
|
|
||||||
date: string;
|
|
||||||
topic: string;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
created_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Analytics {
|
|
||||||
date: string;
|
|
||||||
summary: string;
|
|
||||||
problems_solutions: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DashboardData {
|
|
||||||
analytics: Analytics;
|
|
||||||
sectors_data: {
|
|
||||||
[key: string]: Array<{
|
|
||||||
TRADEDATE: string;
|
|
||||||
OPEN: number;
|
|
||||||
HIGH: number;
|
|
||||||
LOW: number;
|
|
||||||
CLOSE: number;
|
|
||||||
VOLUME: number;
|
|
||||||
[key: string]: any;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
latest_news: NewsItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// API функции для получения данных
|
|
||||||
|
|
||||||
// Получение списка секторов
|
|
||||||
export const getSectors = async (): Promise<string[]> => {
|
|
||||||
const response = await api.get<string[]>('/sectors');
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Получение данных по сектору
|
|
||||||
export const getSectorData = async (sector: string, period: number = 30): Promise<SectorData> => {
|
|
||||||
const response = await api.get<SectorData>(`/sector/${sector}`, {
|
|
||||||
params: { period },
|
|
||||||
});
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Получение списка тикеров
|
|
||||||
export const getTickers = async (sector?: string): Promise<string[]> => {
|
|
||||||
const response = await api.get<string[]>('/tickers', {
|
|
||||||
params: { sector },
|
|
||||||
});
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Получение данных по тикеру
|
|
||||||
export const getTickerData = async (ticker: string, period: number = 30): Promise<TickerData> => {
|
|
||||||
const response = await api.get<TickerData>(`/ticker/${ticker}`, {
|
|
||||||
params: { period },
|
|
||||||
});
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Получение тем новостей
|
|
||||||
export const getNewsTopics = async (): Promise<string[]> => {
|
|
||||||
const response = await api.get<string[]>('/news/topics');
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Получение новостей
|
|
||||||
export const getNews = async (days: number = 7, topic?: string): Promise<NewsItem[]> => {
|
|
||||||
const response = await api.get<NewsItem[]>('/news', {
|
|
||||||
params: { days, topic },
|
|
||||||
});
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Получение последнего анализа
|
|
||||||
export const getLatestAnalytics = async (): Promise<Analytics> => {
|
|
||||||
const response = await api.get<Analytics>('/analytics/latest');
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Получение анализа по дате
|
|
||||||
export const getAnalyticsByDate = async (date: string): Promise<Analytics> => {
|
|
||||||
const response = await api.get<Analytics>(`/analytics/${date}`);
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Получение данных для дашборда
|
|
||||||
export const getDashboardData = async (): Promise<DashboardData> => {
|
|
||||||
const response = await api.get<DashboardData>('/dashboard');
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default api;
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
content: [
|
|
||||||
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
|
||||||
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
|
||||||
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
primary: {
|
|
||||||
50: '#e6f1ff',
|
|
||||||
100: '#cce3ff',
|
|
||||||
200: '#99c7ff',
|
|
||||||
300: '#66aaff',
|
|
||||||
400: '#338eff',
|
|
||||||
500: '#0072ff',
|
|
||||||
600: '#005bcc',
|
|
||||||
700: '#004499',
|
|
||||||
800: '#002e66',
|
|
||||||
900: '#001733',
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
50: '#f5f5f5',
|
|
||||||
100: '#e9e9e9',
|
|
||||||
200: '#d9d9d9',
|
|
||||||
300: '#c4c4c4',
|
|
||||||
400: '#9d9d9d',
|
|
||||||
500: '#7b7b7b',
|
|
||||||
600: '#555555',
|
|
||||||
700: '#434343',
|
|
||||||
800: '#262626',
|
|
||||||
900: '#171717',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
}
|
|
||||||
383
models.py
383
models.py
@ -1,383 +0,0 @@
|
|||||||
import sqlite3
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import pandas as pd
|
|
||||||
import logging
|
|
||||||
from typing import List, Dict, Any, Optional
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Для работы с OpenAI API
|
|
||||||
from openai import OpenAI
|
|
||||||
|
|
||||||
# Импортируем конфигурацию
|
|
||||||
from config import sector_indices, sector_tickers, DB_CONFIG, OPENAI_CONFIG
|
|
||||||
|
|
||||||
# Настройка логирования
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
||||||
)
|
|
||||||
logger = logging.getLogger("models")
|
|
||||||
|
|
||||||
|
|
||||||
class FinancialDataManager:
|
|
||||||
"""Класс для управления финансовыми данными"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Инициализация менеджера финансовых данных"""
|
|
||||||
self.db_path = f"data/{DB_CONFIG['financial_data']}"
|
|
||||||
|
|
||||||
def _get_connection(self):
|
|
||||||
"""
|
|
||||||
Создает и возвращает соединение с базой данных.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
sqlite3.Connection: Объект соединения с базой данных
|
|
||||||
"""
|
|
||||||
return sqlite3.connect(self.db_path)
|
|
||||||
|
|
||||||
def get_sector_data(self, sector: str, start_date: str, end_date: str) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Получение данных по сектору за указанный период
|
|
||||||
|
|
||||||
Args:
|
|
||||||
sector: Название сектора
|
|
||||||
start_date: Начальная дата в формате 'YYYY-MM-DD'
|
|
||||||
end_date: Конечная дата в формате 'YYYY-MM-DD'
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[Dict[str, Any]]: Данные по сектору
|
|
||||||
"""
|
|
||||||
query = f"""
|
|
||||||
SELECT * FROM sector_indices
|
|
||||||
WHERE sector = ? AND date BETWEEN ? AND ?
|
|
||||||
ORDER BY date
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
with self._get_connection() as conn:
|
|
||||||
df = pd.read_sql_query(query, conn, params=(sector, start_date, end_date))
|
|
||||||
|
|
||||||
if not df.empty:
|
|
||||||
# Преобразуем даты в строки для JSON
|
|
||||||
df['date'] = df['date'].astype(str)
|
|
||||||
return df.to_dict(orient='records')
|
|
||||||
return []
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при получении данных сектора {sector}: {str(e)}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_ticker_data(self, ticker: str, start_date: str, end_date: str) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Получение данных по тикеру за указанный период
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ticker: Тикер ценной бумаги
|
|
||||||
start_date: Начальная дата в формате 'YYYY-MM-DD'
|
|
||||||
end_date: Конечная дата в формате 'YYYY-MM-DD'
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[Dict[str, Any]]: Данные по тикеру
|
|
||||||
"""
|
|
||||||
query = f"""
|
|
||||||
SELECT * FROM ticker_data
|
|
||||||
WHERE ticker = ? AND date BETWEEN ? AND ?
|
|
||||||
ORDER BY date
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
with self._get_connection() as conn:
|
|
||||||
df = pd.read_sql_query(query, conn, params=(ticker, start_date, end_date))
|
|
||||||
|
|
||||||
if not df.empty:
|
|
||||||
# Преобразуем даты в строки для JSON
|
|
||||||
df['date'] = df['date'].astype(str)
|
|
||||||
return df.to_dict(orient='records')
|
|
||||||
return []
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при получении данных тикера {ticker}: {str(e)}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_all_sectors(self) -> List[str]:
|
|
||||||
"""
|
|
||||||
Получение списка всех доступных секторов
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[str]: Список секторов
|
|
||||||
"""
|
|
||||||
return list(sector_indices.keys())
|
|
||||||
|
|
||||||
def get_all_tickers(self) -> Dict[str, List[str]]:
|
|
||||||
"""
|
|
||||||
Получение списка всех доступных тикеров, сгруппированных по секторам
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, List[str]]: Словарь с тикерами по секторам
|
|
||||||
"""
|
|
||||||
return sector_tickers
|
|
||||||
|
|
||||||
|
|
||||||
class NewsManager:
|
|
||||||
"""Класс для управления новостями"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Инициализация менеджера новостей"""
|
|
||||||
self.db_path = f"data/{DB_CONFIG['news_data']}"
|
|
||||||
|
|
||||||
def _get_connection(self):
|
|
||||||
"""
|
|
||||||
Создает и возвращает соединение с базой данных.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
sqlite3.Connection: Объект соединения с базой данных
|
|
||||||
"""
|
|
||||||
return sqlite3.connect(self.db_path)
|
|
||||||
|
|
||||||
def get_news_by_date(self, start_date: str, end_date: str = None) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Получение новостей за указанный период
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start_date: Начальная дата в формате 'YYYY-MM-DD'
|
|
||||||
end_date: Конечная дата в формате 'YYYY-MM-DD' (опционально)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[Dict[str, Any]]: Данные новостей
|
|
||||||
"""
|
|
||||||
if not end_date:
|
|
||||||
end_date = datetime.now().strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
query = """
|
|
||||||
SELECT * FROM news
|
|
||||||
WHERE date BETWEEN ? AND ?
|
|
||||||
ORDER BY date DESC
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
with self._get_connection() as conn:
|
|
||||||
df = pd.read_sql_query(query, conn, params=(start_date, end_date))
|
|
||||||
|
|
||||||
if not df.empty:
|
|
||||||
# Преобразуем даты в строки для JSON
|
|
||||||
df['date'] = df['date'].astype(str)
|
|
||||||
return df.to_dict(orient='records')
|
|
||||||
return []
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при получении новостей: {str(e)}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_news_by_topic(self, topic: str, start_date: str, end_date: str = None) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Получение новостей по теме за указанный период
|
|
||||||
|
|
||||||
Args:
|
|
||||||
topic: Тема новостей
|
|
||||||
start_date: Начальная дата в формате 'YYYY-MM-DD'
|
|
||||||
end_date: Конечная дата в формате 'YYYY-MM-DD' (опционально)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[Dict[str, Any]]: Данные новостей
|
|
||||||
"""
|
|
||||||
if not end_date:
|
|
||||||
end_date = datetime.now().strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
query = """
|
|
||||||
SELECT * FROM news
|
|
||||||
WHERE topic = ? AND date BETWEEN ? AND ?
|
|
||||||
ORDER BY date DESC
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
with self._get_connection() as conn:
|
|
||||||
df = pd.read_sql_query(query, conn, params=(topic, start_date, end_date))
|
|
||||||
|
|
||||||
if not df.empty:
|
|
||||||
# Преобразуем даты в строки для JSON
|
|
||||||
df['date'] = df['date'].astype(str)
|
|
||||||
return df.to_dict(orient='records')
|
|
||||||
return []
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при получении новостей по теме {topic}: {str(e)}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_topics(self) -> List[str]:
|
|
||||||
"""
|
|
||||||
Получение списка всех доступных тем новостей
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[str]: Список тем
|
|
||||||
"""
|
|
||||||
query = "SELECT DISTINCT topic FROM news"
|
|
||||||
|
|
||||||
try:
|
|
||||||
with self._get_connection() as conn:
|
|
||||||
df = pd.read_sql_query(query, conn)
|
|
||||||
return df['topic'].tolist()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при получении списка тем новостей: {str(e)}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
class AnalyticsManager:
|
|
||||||
"""Класс для управления аналитикой"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Инициализация менеджера аналитики"""
|
|
||||||
self.db_path = f"data/{DB_CONFIG['analytics']}"
|
|
||||||
self.news_manager = NewsManager()
|
|
||||||
|
|
||||||
def _get_connection(self):
|
|
||||||
"""
|
|
||||||
Создает и возвращает соединение с базой данных.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
sqlite3.Connection: Объект соединения с базой данных
|
|
||||||
"""
|
|
||||||
return sqlite3.connect(self.db_path)
|
|
||||||
|
|
||||||
def analyze_news(self, news: List[Dict[str, Any]]) -> bool:
|
|
||||||
"""
|
|
||||||
Анализирует новости и сохраняет результаты в базу данных
|
|
||||||
|
|
||||||
Args:
|
|
||||||
news: Список новостей для анализа
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: Успешность операции
|
|
||||||
"""
|
|
||||||
if not news:
|
|
||||||
logger.warning("Нет новостей для анализа")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Собираем контент новостей для анализа
|
|
||||||
news_content = [item['content'] for item in news if 'content' in item]
|
|
||||||
|
|
||||||
if not news_content:
|
|
||||||
logger.warning("Нет содержимого новостей для анализа")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Подготавливаем промпт для анализа
|
|
||||||
prompt = """
|
|
||||||
Проанализируйте следующие новости и предоставьте:
|
|
||||||
1. Краткую сводку основных событий
|
|
||||||
2. Выявленные бизнес-проблемы
|
|
||||||
3. Возможные решения для этих проблем
|
|
||||||
4. Общую оценку настроения новостей (от -1 до 1, где -1 - крайне негативное, 1 - крайне позитивное)
|
|
||||||
|
|
||||||
Формат ответа:
|
|
||||||
{
|
|
||||||
"summary": "Краткая сводка основных событий...",
|
|
||||||
"business_problems": "Проблема 1... Проблема 2...",
|
|
||||||
"solutions": "Решение 1... Решение 2...",
|
|
||||||
"sentiment": 0.5
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Создаем клиент для OpenAI API
|
|
||||||
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", ""))
|
|
||||||
|
|
||||||
# Преобразуем массив новостей в строку
|
|
||||||
news_text = "\n\n".join(news_content)
|
|
||||||
|
|
||||||
# Отправляем запрос к API
|
|
||||||
completion = client.chat.completions.create(
|
|
||||||
model=OPENAI_CONFIG['model'],
|
|
||||||
messages=[
|
|
||||||
{"role": "system", "content": prompt},
|
|
||||||
{"role": "user", "content": news_text}
|
|
||||||
],
|
|
||||||
temperature=OPENAI_CONFIG['temperature'],
|
|
||||||
max_tokens=OPENAI_CONFIG['max_tokens'],
|
|
||||||
response_format={"type": "json_object"}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Получаем результат анализа
|
|
||||||
analysis_result = completion.choices[0].message.content
|
|
||||||
|
|
||||||
# Парсим JSON-ответ
|
|
||||||
import json
|
|
||||||
result = json.loads(analysis_result)
|
|
||||||
|
|
||||||
# Создаем запись в базе данных
|
|
||||||
today = datetime.now().date().strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
with self._get_connection() as conn:
|
|
||||||
# Проверяем, есть ли уже анализ за сегодня
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute("SELECT id FROM analytics WHERE date = ?", (today,))
|
|
||||||
existing = cursor.fetchone()
|
|
||||||
|
|
||||||
if existing:
|
|
||||||
# Обновляем существующий анализ
|
|
||||||
cursor.execute(
|
|
||||||
"""
|
|
||||||
UPDATE analytics
|
|
||||||
SET summary = ?, business_problems = ?, solutions = ?, sentiment = ?
|
|
||||||
WHERE date = ?
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
result.get("summary", ""),
|
|
||||||
result.get("business_problems", ""),
|
|
||||||
result.get("solutions", ""),
|
|
||||||
result.get("sentiment", 0),
|
|
||||||
today
|
|
||||||
)
|
|
||||||
)
|
|
||||||
logger.info(f"Анализ за {today} обновлен")
|
|
||||||
else:
|
|
||||||
# Создаем новый анализ
|
|
||||||
cursor.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO analytics (date, summary, business_problems, solutions, sentiment)
|
|
||||||
VALUES (?, ?, ?, ?, ?)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
today,
|
|
||||||
result.get("summary", ""),
|
|
||||||
result.get("business_problems", ""),
|
|
||||||
result.get("solutions", ""),
|
|
||||||
result.get("sentiment", 0)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
logger.info(f"Анализ за {today} сохранен")
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при анализе новостей: {str(e)}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_analytics_by_date(self, start_date: str, end_date: str = None) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Получение аналитики за указанный период
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start_date: Начальная дата в формате 'YYYY-MM-DD'
|
|
||||||
end_date: Конечная дата в формате 'YYYY-MM-DD' (опционально)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[Dict[str, Any]]: Данные аналитики
|
|
||||||
"""
|
|
||||||
if not end_date:
|
|
||||||
end_date = datetime.now().strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
query = """
|
|
||||||
SELECT * FROM analytics
|
|
||||||
WHERE date BETWEEN ? AND ?
|
|
||||||
ORDER BY date DESC
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
with self._get_connection() as conn:
|
|
||||||
df = pd.read_sql_query(query, conn, params=(start_date, end_date))
|
|
||||||
|
|
||||||
if not df.empty:
|
|
||||||
# Преобразуем даты в строки для JSON
|
|
||||||
df['date'] = df['date'].astype(str)
|
|
||||||
return df.to_dict(orient='records')
|
|
||||||
return []
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при получении аналитики: {str(e)}")
|
|
||||||
return []
|
|
||||||
@ -1,90 +0,0 @@
|
|||||||
import datetime
|
|
||||||
import requests
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
class NewsParser:
|
|
||||||
def __init__(self):
|
|
||||||
# Можно использовать один session для всех запросов
|
|
||||||
self.requests_session = requests.Session()
|
|
||||||
|
|
||||||
def parse_news(self, last_days=1):
|
|
||||||
"""
|
|
||||||
Парсит новости с Lenta.ru за указанное число последних дней.
|
|
||||||
|
|
||||||
:param last_days: количество последних дней, за которые нужно собрать новости.
|
|
||||||
:return: pd.DataFrame с колонками [dates, topics, titles, content].
|
|
||||||
"""
|
|
||||||
|
|
||||||
dates = []
|
|
||||||
topics = []
|
|
||||||
titles = []
|
|
||||||
contents = []
|
|
||||||
|
|
||||||
# Перебираем дни от 0 до last_days-1
|
|
||||||
for day in range(last_days):
|
|
||||||
num_pages = 1
|
|
||||||
# Формируем дату в нужном формате: 'YYYY/MM/DD'
|
|
||||||
date_str = (datetime.datetime.today() - datetime.timedelta(days=day)).strftime('%Y/%m/%d')
|
|
||||||
|
|
||||||
while True:
|
|
||||||
url = f'https://lenta.ru/{date_str}/page/{num_pages}/'
|
|
||||||
response = self.requests_session.get(url)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
break
|
|
||||||
|
|
||||||
soup = BeautifulSoup(response.text, 'lxml')
|
|
||||||
news_list = soup.find_all('a', {"class": "card-full-news _archive"}, href=True)
|
|
||||||
|
|
||||||
# Если на странице нет новостей, выходим из цикла пагинации
|
|
||||||
if len(news_list) == 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
for news in news_list:
|
|
||||||
dates.append(date_str)
|
|
||||||
|
|
||||||
# Заголовок
|
|
||||||
title_el = news.find('h3', {"class": "card-full-news__title"})
|
|
||||||
if title_el:
|
|
||||||
titles.append(title_el.get_text(strip=True))
|
|
||||||
else:
|
|
||||||
titles.append('None')
|
|
||||||
|
|
||||||
# Рубрика/тема
|
|
||||||
topic_el = news.find('span', {"class": "card-full-news__info-item card-full-news__rubric"})
|
|
||||||
if topic_el:
|
|
||||||
topics.append(topic_el.get_text(strip=True))
|
|
||||||
else:
|
|
||||||
topics.append('None')
|
|
||||||
|
|
||||||
# Текст статьи
|
|
||||||
news_url = 'https://lenta.ru' + news['href']
|
|
||||||
article_req = self.requests_session.get(news_url)
|
|
||||||
article_soup = BeautifulSoup(article_req.text, 'lxml')
|
|
||||||
|
|
||||||
paragraphs = article_soup.find_all('p', class_='topic-body__content-text')
|
|
||||||
# Склеиваем все абзацы в одну строку
|
|
||||||
news_content = ' '.join([p.get_text(strip=True) for p in paragraphs])
|
|
||||||
contents.append(news_content)
|
|
||||||
|
|
||||||
num_pages += 1
|
|
||||||
|
|
||||||
# Формируем DataFrame
|
|
||||||
df = pd.DataFrame({
|
|
||||||
'dates': dates,
|
|
||||||
'topics': topics,
|
|
||||||
'titles': titles,
|
|
||||||
'content': contents
|
|
||||||
})
|
|
||||||
|
|
||||||
# Преобразуем колонки с датами к формату datetime
|
|
||||||
df['dates'] = pd.to_datetime(df['dates'], errors='coerce')
|
|
||||||
return df
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Пример использования
|
|
||||||
parser = NewsParser()
|
|
||||||
news_df = parser.parse_news() # Сбор новостей за последние 2 дня
|
|
||||||
print(news_df.head())
|
|
||||||
@ -400,7 +400,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"df_economy = df[(df['topics'] == 'Экономика') | (df['topics'] == 'Политика')]\n",
|
"df_economy = df[(df['topics'] == 'Экономика') | (df['topics'] == 'Политика') | (df['topics'] == 'Интернет и СМИ')]\n",
|
||||||
"df_economy\n"
|
"df_economy\n"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,15 +1,19 @@
|
|||||||
fastapi==0.104.1
|
pandas
|
||||||
uvicorn==0.23.2
|
plotly
|
||||||
pandas==2.1.1
|
requests
|
||||||
numpy==1.26.0
|
apimoex
|
||||||
requests==2.31.0
|
aiohttp
|
||||||
beautifulsoup4==4.12.2
|
requests
|
||||||
lxml==4.9.3
|
httpx
|
||||||
openai==1.3.0
|
pybit
|
||||||
schedule==1.2.0
|
aiohttp
|
||||||
python-dateutil==2.8.2
|
altair
|
||||||
pydantic==2.4.2
|
pymc
|
||||||
sqlalchemy==2.0.22
|
scikit-learn
|
||||||
aiohttp==3.8.6
|
numpy
|
||||||
asyncio==3.4.3
|
seaborn
|
||||||
python-multipart==0.0.6
|
statsmodels
|
||||||
|
xgboost
|
||||||
|
lightgbm
|
||||||
|
groq
|
||||||
|
yfinance
|
||||||
Loading…
Reference in New Issue
Block a user