def _validate_index(self, item: str, df: pd.DataFrame): """Проверяет индекс данных с учетом настроек.""" if self._unique_index and not df.index.is_unique: raise POptimizerError( f"Индекс {self._mongo.collection.full_name}.{item} не уникальный" ) if self._ascending_index and not df.index.is_monotonic_increasing: raise POptimizerError( f"Индекс {self._mongo.collection.full_name}.{item} не возрастает" )
def _validate(df: pd.DataFrame): """Проверка заголовков таблицы""" months, _ = df.shape first_year = df.columns[0] first_month = df.index[0] if months != NUM_OF_MONTH: raise POptimizerError( "Таблица должна содержать 12 строк с месяцами") if first_year != FIRST_YEAR: raise POptimizerError("Первый год должен быть 1991") if first_month != FIRST_MONTH: raise POptimizerError("Первый месяц должен быть январь")
def __init__( self, date: Union[str, pd.Timestamp], cash: int, positions: Dict[str, int], value: Optional[float] = None, ): """При создании может быть осуществлена проверка совпадения расчетной стоимости и введенной. :param date: Дата на которую рассчитываются параметры портфеля. :param cash: Количество наличных в портфеле. :param positions: Словарь с тикерами и количеством акций. :param value: Стоимость портфеля на отчетную дату. """ self._date = pd.Timestamp(date) self._shares = pd.Series(positions) self._shares.sort_index(inplace=True) self._shares[CASH] = cash self._shares[PORTFOLIO] = 1 if value is not None and not np.isclose(self.value[PORTFOLIO], value): raise POptimizerError( f"Введенная стоимость портфеля {value} " f"не равна расчетной {self.value[PORTFOLIO]}")
def __init__(self, html: str, table_index: int): soup = bs4.BeautifulSoup(html, "lxml") try: self._table = soup.find_all("table")[table_index] except IndexError: raise POptimizerError(f"На странице нет таблицы {table_index}") self._parsed_table = []
def _download(self, item: str, last_index: Optional[Any]) -> List[Dict[str, Any]]: if item != SMART_LAB: raise POptimizerError( f"Отсутствуют данные {self._mongo.collection.full_name}.{item}" ) with self._session.get(URL) as respond: try: respond.raise_for_status() except requests.HTTPError: raise POptimizerError(f"Данные {URL} не загружены") else: html = respond.text table = parser.HTMLTableParser(html, TABLE_INDEX) columns = [TICKER_COLUMN, DATE_COLUMN, DIVIDENDS_COLUMN] return table.get_formatted_data(columns, HEADER_SIZE, FOOTER_SIZE)
def update_data( report_name: str, date: pd.Timestamp, value: float, inflows: dict, dividends: float ): """Обновляет файл с данными статистики изменения стоимости портфеля. Проверяет, что в этом месяце статистика еще не вносилась. :param report_name: Название файла с отчетом. :param date: Дата отчета. :param value: Стоимость активов. :param inflows: Словарь с именами инвесторов и внесенных ими средств за период. :param dividends: Дивиденды за период. """ df = read_data(report_name) last_date = df.index[-1] if last_date + pd.DateOffset(months=1, day=1) > date: raise POptimizerError("В этом месяце данные уже вносились в отчет") total_inflow = 0 for investor, inflow in inflows.items(): if investor not in df.columns: raise POptimizerError(f"Неверное имя инвестора - {investor}") df.loc[date, investor] = inflow total_inflow += inflow df.loc[date, "Value"] = value portfolio_return = (value - total_inflow) / df.loc[last_date, "Value"] investors = pdf_upper.get_investors_names(df) value_labels = "Value_" + investors pre_inflow_value = df.loc[last_date, value_labels] * portfolio_return df.loc[date, value_labels] = pre_inflow_value.add( df.loc[date, investors].values, fill_value=0 ) if dividends == 0: df.loc[date, "Dividends"] = np.nan else: df.loc[date, "Dividends"] = dividends df.to_excel(REPORTS_PATH / f"{report_name}.xlsx", SHEET_NAME)
def is_common(ticker: str): """Определяет является ли акция обыкновенной.""" if len(ticker) == COMMON_TICKER_LENGTH: return True elif (len(ticker) == COMMON_TICKER_LENGTH + 1 and ticker[COMMON_TICKER_LENGTH] == PREFERRED_TICKER_ENDING): return False raise POptimizerError(f"Некорректный тикер {ticker}")
def _validate_columns(self, columns): """Проверка значений в колонках.""" table = self.parsed_table for column in columns: for row, value in column.validation_dict.items(): if value not in table[row][column.index]: raise POptimizerError( f"Значение в таблице {table[row][column.index]!r} - должно быть {value!r}" )
def _validate_new(self, item: str, data: List[Dict[str, Any]], data_new: List[Dict[str, Any]]): """Проверяет соответствие старых и новых данных.""" if self._validate_last: data = data[-1:] data_new = data_new[:1] elif len(data) > len(data_new): raise POptimizerError( f"{self._mongo.collection.full_name}.{item}: " f"Новые {len(data_new)} короче старых {len(data)} данных") for old, new in zip(data, data_new): for col in old: not_float_not_eq = (not isinstance(old[col], float) and old[col] != new[col]) float_not_eq = isinstance( old[col], float) and not np.allclose(old[col], new[col]) if not_float_not_eq or float_not_eq: raise POptimizerError( f"{self._mongo.collection.full_name}.{item}: " f"Новые {new} не соответствуют старым {old} данным")
def _download(self, item: str, last_index: Optional[Any]) -> List[Dict[str, Any]]: """Загружает полностью данные о всех торгующихся акциях.""" if item != SECURITIES: raise POptimizerError( f"Отсутствуют данные {self._mongo.collection.full_name}.{item}" ) columns = ("SECID", "REGNUMBER", "LOTSIZE") data = apimoex.get_board_securities(self._session, columns=columns) formatters = dict( SECID=lambda x: (TICKER, x), REGNUMBER=lambda x: (REG_NUMBER, x), LOTSIZE=lambda x: (LOT_SIZE, x), ) return manager.data_formatter(data, formatters)
def price(self): """Цены позиций. CASH - 1 и PORTFOLIO - расчетная стоимость. """ price = data.prices(tuple(self.index[:-2]), self.date) try: price = price.loc[self.date] except KeyError: raise POptimizerError( f"Для даты {self._date.date()} отсутствуют исторические котировки" ) price[CASH] = 1 price[PORTFOLIO] = (self.shares[:-1] * price).sum(axis=0) return price
def _validate_new( self, name: str, df_old: Union[pd.DataFrame, pd.Series], df_new: Union[pd.DataFrame, pd.Series], ): """Проверяет соответствие старых и новых данных""" common_index = df_old.index.intersection(df_new.index) if not np.allclose(df_old.loc[common_index], df_new.loc[common_index]): raise POptimizerError( f"Существующие данные не соответствуют новым:\n" f"Категория - {self.category}\n" f"Название - {name}\n" f"Дата последнего обновления - {self._data[name].timestamp}\n" f"Старые значения:\n{df_old.loc[common_index]}\n" f"Новые значения:\n{df_new.loc[common_index]}\n")
def _download(self, item: str, last_index: Optional[Any]) -> List[Dict[str, Any]]: url = f"https://www.dohod.ru/ik/analytics/dividend/{item.lower()}" with self._session.get(url) as respond: try: respond.raise_for_status() except requests.HTTPError: raise POptimizerError(f"Данные {url} не загружены") else: html = respond.text table = parser.HTMLTableParser(html, TABLE_INDEX) date_col = parser.DataColumn( DATE, 0, {0: "Дата закрытия реестра"}, parser.date_parser ) div_col = parser.DataColumn(item, 2, {0: "Дивиденд (руб.)"}, parser.div_parser) columns = [date_col, div_col] data = table.get_formatted_data(columns, HEADER_SIZE) return sort_and_group(item, data)
def _find_aliases(self, ticker: str) -> Tuple[List[str], pd.Timestamp]: """Ищет все тикеры с эквивалентным регистрационным номером.""" securities = SecuritiesListing(self._mongo.db.name)[LISTING] reg_date = securities.at[ticker, DATE] reg_date = pd.to_datetime(reg_date, format="%d.%m.%Y %H:%M:%S") reg_number = securities.at[ticker, REG_NUMBER] if reg_number is None: raise POptimizerError( f"{ticker} - акция без регистрационного номера") results = apimoex.find_securities(self._session, reg_number) tickers = [ row["secid"] for row in results if row["regnumber"] == reg_number ] # noinspection PyTypeChecker return tickers, reg_date
async def _download(self, name: str): url = f"https://www.dohod.ru/ik/analytics/dividend/{name.lower()}" async with aiohttp.ClientSession() as session: async with session.get(url) as resp: try: resp.raise_for_status() except aiohttp.ClientResponseError: raise POptimizerError(f"Данные {url} не загружены") else: html = await resp.text() table = parser.HTMLTableParser(html, TABLE_INDEX) columns = [DATE_COLUMN, DIVIDENDS_COLUMN] df = table.make_df(columns, HEADER_SIZE) df.columns = [DATE, name] df = df.groupby(DATE, as_index=False).sum() df.set_index(DATE, inplace=True) df.sort_index(inplace=True) return df[name]
def valid_model(params: dict, examples: Examples) -> dict: """Осуществляет валидацию модели по R2. Осуществляется проверка, что не достигнут максимум итераций, возвращается RMSE, R2 и параметры модели с оптимальным количеством итераций в формате целевой функции hyperopt. :param params: Словарь с параметрами модели и данных. :param examples: Класс создания обучающих примеров. :return: Словарь с результатом в формате hyperopt: * ключ 'loss' - нормированная RMSE на кросс-валидации (для hyperopt), * ключ 'status' - успешного прохождения (для hyperopt), * ключ 'std' - RMSE на кросс-валидации, * ключ 'r2' - 1- нормированная RMSE на кросс-валидации в квадрате, * ключ 'params' - параметры модели и данных, в которые добавлено оптимальное количество итераций градиентного бустинга на кросс-валидации и общие настройки. """ data_params, model_params = params["data"], params["model"] train_pool_params, val_pool_params = examples.train_val_pool_params( data_params) train_pool = catboost.Pool(**train_pool_params) val_pool = catboost.Pool(**val_pool_params) model_params = make_model_params(data_params, model_params) clf = catboost.CatBoostRegressor(**model_params) clf.fit(train_pool, eval_set=val_pool) if clf.tree_count_ == MAX_ITERATIONS: raise POptimizerError( f"Необходимо увеличить MAX_ITERATIONS = {MAX_ITERATIONS}") model_params["iterations"] = clf.tree_count_ scores = clf.get_best_score()["validation_0"] std = scores["RMSE"] r2 = scores["R2"] return dict( loss=-r2, status=hyperopt.STATUS_OK, std=std, r2=r2, data=data_params, model=model_params, )
def _download(self, item: str, last_index: Optional[Any]) -> List[Dict[str, Any]]: """Загружает полностью данные о всех торгующихся акциях.""" if item != LISTING: raise POptimizerError( f"Отсутствуют данные {self._mongo.collection.full_name}.{item}" ) converters = dict( TRADE_CODE=lambda x: x if len(x) else None, REGISTRY_NUMBER=lambda x: x if len(x) else None, REGISTRY_DATE=lambda x: x if len(x) else None, ) df = pd.read_csv( self.URL, encoding="CP1251", usecols=["TRADE_CODE", "REGISTRY_NUMBER", "REGISTRY_DATE"], converters=converters, ) df.columns = [TICKER, REG_NUMBER, DATE] return df.to_dict("records")
def _download(self, item: str, last_index: Optional[Any]) -> List[Dict[str, Any]]: """Загружает полностью данные по инфляции с сайта ФСГС.""" if item != CPI: raise POptimizerError( f"Отсутствуют данные {self._mongo.collection.full_name}.{item}" ) df = pd.read_excel(URL_CPI, **PARSING_PARAMETERS) self._validate(df) df = df.transpose().stack() first_year = df.index[0][0] df.index = pd.date_range( name=utils.DATE, freq="M", start=pd.Timestamp(year=first_year, month=1, day=31), periods=len(df), ) df.name = CPI # Данные должны быть не в процентах, а в долях df = df.div(100) return df.reset_index().to_dict("records")
def _download(self, item: str, last_index: Optional[Any]) -> List[Dict[str, Any]]: """Поддерживается частичная загрузка данных для обновления.""" if item != INDEX: raise POptimizerError( f"Отсутствуют данные {self._mongo.collection.full_name}.{item}" ) if last_index is not None: last_index = last_index.date() data = apimoex.get_board_history( self._session, start=last_index, security=INDEX, columns=("TRADEDATE", "CLOSE"), board="RTSI", market="index", ) formatters = dict( TRADEDATE=lambda x: (DATE, datetime.strptime(x, "%Y-%m-%d")), CLOSE=lambda x: (CLOSE, x), ) return manager.data_formatter(data, formatters)
def _validate_index(self, name: str, df): """Проверяет индекс данных с учетом настроек.""" if self.IS_UNIQUE and not df.index.is_unique: raise POptimizerError(f"Индекс {name} не уникальный") if self.IS_MONOTONIC and not df.index.is_monotonic_increasing: raise POptimizerError(f"Индекс {name} не возрастает монотонно")