class InvestMintGateway(gateways.DivGateway): """Обновление данных с https://investmint.ru/.""" _logger = adapters.AsyncLogger() async def __call__(self, ticker: str) -> Optional[pd.DataFrame]: """Получение дивидендов для заданного тикера.""" self._logger(ticker) cols_desc = get_col_desc(ticker) url = _prepare_url(ticker) try: html = await parser.get_html(url) except description.ParserError: return None try: table_index = _find_table_n(html) except description.ParserError: return None try: df = parser.get_df_from_html(html, table_index, cols_desc) except description.ParserError: return None return description.reformat_df_with_cur(df, ticker)
class CloseGateway(gateways.DivGateway): """Обновление данных с https://закрытияреестров.рф/.""" _logger = adapters.AsyncLogger() async def __call__(self, ticker: str) -> Optional[pd.DataFrame]: """Получение дивидендов для заданного тикера.""" self._logger(ticker) cols_desc = _get_col_desc(ticker) base_ticker = ticker[:BASE_TICKER_LENGTH] if base_ticker == "RUAL": base_ticker = "RUALR" url = f"{URL}{base_ticker}" try: df = await parser.get_df_from_url(url, TABLE_INDEX, cols_desc) except description.ParserError: return None raw_data = df[ticker] df[col.CURRENCY] = raw_data.str.slice(-3) df[ticker] = raw_data.str.slice(stop=-3).astype(float) df = df.dropna() return df.sort_index()
class BCSGateway(gateways.DivGateway): """Обновление данных с https://bcs-express.ru.""" _logger = adapters.AsyncLogger() async def __call__(self, ticker: str) -> Optional[pd.DataFrame]: """Получение дивидендов для заданного тикера.""" self._logger(ticker) try: rows = await _get_rows(ticker) except description.ParserError: return None div_data = [(_parse_date(row), *_parse_div(row)) for row in rows] df = pd.DataFrame(data=div_data, columns=[col.DATE, ticker, col.CURRENCY]) df = df.set_index(col.DATE) df = df.groupby(lambda date: date).agg( { ticker: "sum", col.CURRENCY: "last", }, ) return df.sort_index()
class IndexesGateway(gateways.BaseGateway): """Котировки различных индексов на MOEX.""" _logger = adapters.AsyncLogger() async def __call__( self, ticker: str, start_date: Optional[str], last_date: str, ) -> pd.DataFrame: """Получение значений индекса на закрытие для диапазона дат.""" self._logger(f"{ticker}({start_date}, {last_date})") json = await aiomoex.get_market_history( session=self._session, start=start_date, end=last_date, security=ticker, columns=("TRADEDATE", "CLOSE"), market="index", ) df = pd.DataFrame(json) df.columns = [col.DATE, col.CLOSE] df[col.DATE] = pd.to_datetime(df[col.DATE]) return df.set_index(col.DATE)
class SecuritiesGateway(gateways.BaseGateway): """Информация о всех торгующихся акциях.""" _logger = adapters.AsyncLogger() async def __call__(self, market: str, board: str) -> pd.DataFrame: """Получение списка торгуемых акций с ISIN и размером лота.""" self._logger("Загрузка данных по торгуемым бумагам") columns = ( "SECID", "ISIN", "LOTSIZE", "SECTYPE", ) json = await aiomoex.get_board_securities( self._session, market=market, board=board, columns=columns, ) df = pd.DataFrame(json) df.columns = [col.TICKER, col.ISIN, col.LOT_SIZE, col.TICKER_TYPE] return df.set_index(col.TICKER)
class AliasesGateway(gateways.BaseGateway): """Ищет все тикеры с эквивалентным регистрационным номером.""" _logger = adapters.AsyncLogger() async def __call__(self, isin: str) -> List[str]: """Ищет все тикеры с эквивалентным ISIN.""" self._logger(isin) json = await aiomoex.find_securities(self._session, isin, columns=("secid", "isin")) return [row["secid"] for row in json if row["isin"] == isin]
class SmartLabGateway(gateways.DivStatusGateway): """Обновление данных с https://www.smart-lab.ru.""" _logger = adapters.AsyncLogger() async def __call__(self) -> pd.DataFrame: """Получение ожидаемых дивидендов.""" self._logger("Загрузка данных") cols_desc = get_col_desc() df = await parser.get_df_from_url(URL, TABLE_INDEX, cols_desc) return df.dropna()
class CPIGateway(gateways.BaseGateway): """Обновление данных инфляции с https://rosstat.gov.ru.""" _logger = adapters.AsyncLogger() async def __call__(self) -> pd.DataFrame: """Получение данных по инфляции.""" self._logger("Загрузка инфляции") df = await _load_xlsx(self._session) _validate(df) return _clean_up(df)
class EventBus(Generic[EntityType]): """Шина для обработки событий.""" _logger = adapters.AsyncLogger() def __init__( self, uow_factory: Callable[[], UoW[EntityType]], event_handler: domain.AbstractHandler[EntityType], ): """Для работы нужна фабрика транзакций и обработчик событий.""" self._uow_factory = uow_factory self._event_handler = event_handler def handle_event( self, event: domain.AbstractEvent, ) -> None: """Обработка события.""" loop = asyncio.get_event_loop() loop.run_until_complete(self._handle_event(event)) async def _handle_event( self, event: domain.AbstractEvent, ) -> None: """Асинхронная обработка события и следующих за ним.""" pending: PendingTasks = self._create_tasks([event]) while pending: done, pending = await asyncio.wait( pending, return_when=asyncio.FIRST_COMPLETED) for task in done: pending |= self._create_tasks(task.result()) def _create_tasks(self, events: list[domain.AbstractEvent]) -> set[FutureEvent]: """Создает задания для событий.""" return { asyncio.create_task(self._handle_one_command(event)) for event in events } async def _handle_one_command( self, event: domain.AbstractEvent) -> list[domain.AbstractEvent]: """Обрабатывает одно событие и помечает его сделанным.""" self._logger(str(event)) async with self._uow_factory() as repo: return await self._event_handler.handle_event(event, repo)
class MOEXStatusGateway(gateways.DivStatusGateway): """Данные о закрытии реестров https://www.moex.com. Загружаются только данные по иностранным бумагам. """ _logger = adapters.AsyncLogger() async def __call__(self) -> pd.DataFrame: """Получение ожидаемых дивидендов.""" self._logger("Загрузка данных") cols_desc = get_col_desc() df = await parser.get_df_from_url(URL, TABLE_INDEX, cols_desc) return df.iloc[df.index.notnull()]
class TradingDatesGateway(gateways.BaseGateway): """Обновление для таблиц с диапазоном доступных торговых дат.""" _logger = adapters.AsyncLogger() async def __call__(self) -> pd.DataFrame: """Получение обновленных данных о доступном диапазоне торговых дат.""" self._logger("Загрузка данных по торговым дням") json = await aiomoex.get_board_dates( self._session, board="TQBR", market="shares", engine="stock", ) return pd.DataFrame(json, dtype="datetime64[ns]")
class CloseGateway(gateways.DivGateway): """Обновление данных с https://закрытияреестров.рф/.""" _logger = adapters.AsyncLogger() async def __call__(self, ticker: str) -> Optional[pd.DataFrame]: """Получение дивидендов для заданного тикера.""" self._logger(ticker) cols_desc = _get_col_desc(ticker) base_ticker = ticker[:BASE_TICKER_LENGTH] url = f"{URL}{base_ticker}" try: df = await parser.get_df_from_url(url, TABLE_INDEX, cols_desc) except description.ParserError: return None df[col.CURRENCY] = col.RUR return df
class FinRangeGateway(gateways.DivGateway): """Обновление данных с https://finrange.com/.""" _logger = adapters.AsyncLogger() async def __call__(self, ticker: str) -> Optional[pd.DataFrame]: """Получение дивидендов для заданного тикера.""" self._logger(ticker) url = _prepare_url(ticker) html = await _get_page_html(url) cols_desc = _get_col_desc(ticker) try: df = parser.get_df_from_html(html, TABLE_NUM, cols_desc) except description.ParserError: return None return description.reformat_df_with_cur(df, ticker)
class DohodGateway(gateways.DivGateway): """Обновление данных с https://dohod.ru.""" _logger = adapters.AsyncLogger() async def __call__(self, ticker: str) -> Optional[pd.DataFrame]: """Получение дивидендов для заданного тикера.""" self._logger(ticker) cols_desc = get_col_desc(ticker) url = f"{URL}{ticker.lower()}" try: df = await parser.get_df_from_url(url, TABLE_INDEX, cols_desc) except description.ParserError: return None df = self._sort_and_agg(df) df[col.CURRENCY] = col.RUR return df
class StreetInsider(gateways.DivGateway): """Обновление данных с https://www.streetinsider.com/.""" _logger = adapters.AsyncLogger() async def __call__(self, ticker: str) -> Optional[pd.DataFrame]: """Получение дивидендов для заданного тикера.""" self._logger(ticker) cols_desc = get_col_desc(ticker) international_ticker = ticker.lower()[:-3] url = f"{URL}{international_ticker}" try: df = await parser.get_df_from_url(url, TABLE_INDEX, cols_desc) except description.ParserError: return None df = self._sort_and_agg(df) df[col.CURRENCY] = col.USD return df
class NASDAQGateway(gateways.DivGateway): """Обновление данных с https://www.nasdaq.com/.""" _logger = adapters.AsyncLogger() async def __call__(self, ticker: str) -> Optional[pd.DataFrame]: """Получение дивидендов для заданного тикера.""" self._logger(ticker) cols_desc = get_col_desc(ticker) url = "".join([URL_START, ticker[:-3], URL_END]) html = await _load_ticker_page(url) try: df = parser.get_df_from_html(html, 0, cols_desc) except description.ParserError: return None df = df.groupby(lambda date: date).sum() df[col.CURRENCY] = col.USD return df
class USDGateway(gateways.BaseGateway): """Курс доллара.""" _logger = adapters.AsyncLogger() async def __call__( self, start_date: Optional[str], last_date: str, ) -> pd.DataFrame: """Получение значений курса для диапазона дат.""" self._logger(f"({start_date}, {last_date})") json = await aiomoex.get_market_candles( self._session, "USD000UTSTOM", market="selt", engine="currency", start=start_date, end=last_date, ) return _format_candles_df(json)
class DividendsGateway(gateways.DivGateway): """Обновление данных из базы данных, заполняемой в ручную.""" _logger = adapters.AsyncLogger() def __init__( self, div_col: collection.Collection = DIV_COL, ): """Сохраняет коллекцию для доступа к первоисточнику дивидендов.""" super().__init__() self._collection = div_col async def __call__(self, ticker: str) -> pd.DataFrame: """Получение дивидендов для заданного тикера.""" self._logger(ticker) docs_cursor = self._collection.find( {"ticker": ticker}, projection={ "_id": False, "date": True, "dividends": True, "currency": True }, ) json = await docs_cursor.to_list(length=None) if not json: return pd.DataFrame(columns=[ticker, col.CURRENCY]) df = pd.DataFrame.from_records(json, index="date") df.columns = [ticker, col.CURRENCY] cur_err = set(df[col.CURRENCY]) - {col.RUR, col.USD} if cur_err: raise WrongCurrencyError(cur_err) return df.sort_index(axis=0)
class QuotesGateway(gateways.BaseGateway): """Загружает котировки акций.""" _logger = adapters.AsyncLogger() async def __call__( self, ticker: str, market: str, start_date: Optional[str], last_date: str, ) -> pd.DataFrame: """Получение котировок акций в формате OCHLV.""" self._logger(f"{ticker}({start_date}, {last_date})") json = await aiomoex.get_market_candles( self._session, ticker, market=market, start=start_date, end=last_date, ) return _format_candles_df(json)
class ConomyGateway(gateways.DivGateway): """Обновление для таблиц с дивидендами на https://www.conomy.ru/.""" _logger = adapters.AsyncLogger() async def __call__(self, ticker: str) -> Optional[pd.DataFrame]: """Получение дивидендов для заданного тикера.""" self._logger(ticker) try: # На некоторых компьютерах/операционных системах Chromium перестает реагировать на команды # Поэтому загрузка принудительно приостанавливается html = await asyncio.wait_for(_get_html(ticker), timeout=CHROMIUM_TIMEOUT) except (errors.TimeoutError, asyncio.exceptions.TimeoutError): return None cols_desc = _get_col_desc(ticker) df = parser.get_df_from_html(html, TABLE_INDEX, cols_desc) df = df.dropna() df = self._sort_and_agg(df) df[col.CURRENCY] = col.RUR return df
class TestLog: """Тестовый класс для проверки работы логгера.""" logger = adapters.AsyncLogger()