def __init__(self, portfolio: Portfolio, p_value: float = P_VALUE): """Учитывается градиент, его ошибку и ликвидность бумаг. :param portfolio: Оптимизируемый портфель. :param p_value: Требуемая значимость отклонения градиента от нуля. """ self._portfolio = portfolio self._p_value = p_value self._metrics = metrics.MetricsResample(portfolio)
def __init__(self, portfolio: Portfolio, *, p_value: float = config.P_VALUE, for_sell: int = 1): """Учитывается градиент, его ошибку и ликвидность бумаг. :param portfolio: Оптимизируемый портфель. :param p_value: Требуемая значимость отклонения градиента от нуля. :param for_sell: Количество претендентов на продажу. """ self._portfolio = portfolio self._p_value = p_value self._metrics = metrics.MetricsResample(portfolio) self._for_sell = for_sell - 1
def __init__(self, portfolio: Portfolio, wl_portfolio: Portfolio = None, step_sz=0.01, turnover_thresh=0.1): """Учитывается градиент, его ошибку и ликвидность бумаг. :param portfolio: Оптимизируемый портфель. :param wl_portfolio: Портфель, содержащий список всех допустимых тикеров (white list), используется для фильтрации рекомендаций. :param step_sz: Размер оптимизационного шага в доле от портфеля :param turnover_thresh: Максимальны суммарный оборот в доле от портфеля в процессе оптимизации """ self._portfolio = portfolio self._wl_portfolio = wl_portfolio self._metrics = metrics.MetricsResample(portfolio) self._logger = logging.getLogger() self.rec = None self._turnover_thresh = turnover_thresh * self.portfolio.value["PORTFOLIO"] self._step_sz = step_sz * self.portfolio.value["PORTFOLIO"]
def make_resample(): """Данне для тестов.""" positions = {"BSPB": 4890, "FESH": 1300} port = portfolio.Portfolio("test", "2020-05-14", 84449, positions) mean1 = pd.Series([0.09, 0.06], index=list(positions)) cov1 = np.array([[0.04, 0.005], [0.005, 0.0625]]) mean2 = pd.Series([0.05, 0.09], index=list(positions)) cov12 = np.array([[0.0225, 0.0042], [0.0042, 0.0196]]) def fake_get_forecasts(*_): yield from ( SimpleNamespace( mean=mean1, cov=cov1, history_days=1, cor=0.4, shrinkage=0.3, risk_aversion=1, error_tolerance=0, ), SimpleNamespace( mean=mean2, cov=cov12, history_days=2, cor=0.5, shrinkage=0.2, risk_aversion=1, error_tolerance=0, ), ) saved_get_forecast = metrics.evolve.get_forecasts metrics.evolve.get_forecasts = fake_get_forecasts yield metrics.MetricsResample(port) metrics.evolve.get_forecasts = saved_get_forecast
def _for_trade(self) -> dict[str, pd.DataFrame]: """Осуществляет расчет рекомендуемых операций.""" self._logger.info("\nОПТИМИЗАЦИЯ ПОРТФЕЛЯ\n") cur_prot = self.portfolio rec, op = None, None # используется для определения цикла в операциях (получение портфеля который был ранее) ports_set = set() turnover = None while True: cur_metrics = metrics.MetricsResample(cur_prot) grads = cur_metrics.all_gradients.iloc[:-2] # гармоническое среднее квантилей градиентов вместо бутстрапа # вычислительно существенно быстрее q_trans_grads = quantile_transform(grads, n_quantiles=grads.shape[0]) # обработка (маскировка) возможных NA от плохих моделей q_trans_grads = np.ma.array(q_trans_grads, mask=~(q_trans_grads > 0)) # гармоническое среднее сильнее штрафует за низкие значения (близкие к 0), # но его использование не принципиально - можно заменить на просто среднее или медиану hmean_q_trans_grads = stats.hmean(q_trans_grads, axis=1) # учёт оборота при ранжировании if turnover is None: turnover = quantile_transform(cur_prot.turnover_factor.loc[grads.index].values.reshape(-1, 1), n_quantiles=grads.shape[0]) priority = stats.hmean(np.hstack([hmean_q_trans_grads.reshape(-1, 1), turnover]), axis=1) rec = pd.Series(data=priority, index=grads.index).to_frame(name="PRIORITY") rec.sort_values(["PRIORITY"], ascending=[False], inplace=True) # так как все операции производятся в лотах, нужно знать стоимость лота и текущее количество лотов rec["LOT_size"] = cur_prot.lot_size.loc[rec.index] rec["lots"] = (cur_prot.shares.loc[rec.index] / rec["LOT_size"]).fillna(0).astype(int) rec["LOT_price"] = (cur_prot.lot_size.loc[rec.index] * cur_prot.price.loc[rec.index]).round( 2 ) rec['is_acceptable'] = rec['LOT_price'] < self._step_sz if self._wl_portfolio is not None: # помечаем все тикеры, которых нет в white list portfolio # также продаём все недопустимые позиции banned_tickers = rec.index.difference(self._wl_portfolio.index) rec.loc[banned_tickers, 'is_acceptable'] = False rec.loc[banned_tickers, 'lots'] = 0 cash = cur_prot.value[CASH] + self.portfolio.value["PORTFOLIO"] - cur_prot.value["PORTFOLIO"] # определяем операцию: # покупка, если на покупку лучшего тикера хватает CASH # иначе - продажа худшго тикера из тех, что в наличии top_share = rec.loc[rec['is_acceptable']].index[0] top_share_lots = self._step_sz // rec.loc[top_share, "LOT_price"] top_share_sum = top_share_lots * rec.loc[top_share, "LOT_price"] if cash > top_share_sum: rec.loc[top_share, "lots"] += top_share_lots cash -= top_share_sum op = ("BUY", top_share) self._turnover_thresh -= top_share_sum else: bot_share = rec.loc[rec["lots"] > 0].index[-1] bot_share_lots = int(np.ceil((top_share_sum - cash) / rec.loc[bot_share, 'LOT_price'])) bot_share_lots = min(rec.loc[bot_share, 'lots'], bot_share_lots) bot_share_sum = rec.loc[bot_share, 'LOT_price'] * bot_share_lots self._turnover_thresh -= bot_share_sum rec.loc[bot_share, "lots"] -= bot_share_lots cash += bot_share_sum op = ("SELL", bot_share) cur_prot = self._update_portfolio(rec, cash) log_str = '\t'.join([f'{str(len(ports_set) + 1): <7}', f'{op[0]: <4}', f'{op[1]: <7}', f"PRIORITY: {rec.loc[op[1], 'PRIORITY']:.3f}", f"CASH: {cash:.0f}"]) self._logger.info(log_str) # проверка цикла port_tuple = tuple(cur_prot.shares.drop(CASH).tolist()) if port_tuple in ports_set or self._turnover_thresh < 0: break ports_set.add(port_tuple) # оптимизированный портфель получен # сортируем по новому весу от портфеля для наглядности и приоритезации сделок на покупку rec["SUM"] = (rec["lots"] * rec["LOT_price"]).round(2) rec.sort_values("SUM", ascending=False, inplace=True) # найдем разницу с текущим портфелем rec["LOTS_exists"] = ( (self.portfolio.shares.loc[rec.index] / rec["LOT_size"]).fillna(0).astype(int) ) rec["SHARES_after"] = rec["lots"] * rec["LOT_size"] report = dict() report["current_port_summary"] = self.portfolio._main_info_df() report["SELL"] = rec.loc[rec["lots"] < rec["LOTS_exists"]].copy() report["SELL"]["lots"] = report["SELL"]["LOTS_exists"] - report["SELL"]["lots"] report["BUY"] = rec.loc[rec["lots"] > rec["LOTS_exists"]].copy() report["BUY"]["lots"] = report["BUY"]["lots"] - report["BUY"]["LOTS_exists"] report["new_port_summary"] = cur_prot._main_info_df() for op in ["SELL", "BUY"]: report[op]["SHARES"] = report[op]["lots"] * report[op]["LOT_size"] report[op]["SHARES_exists"] = report[op]["LOTS_exists"] * report[op]["LOT_size"] # корректируем сумму учитывая целое количество лотов report[op]["SUM"] = (report[op]["lots"] * report[op]["LOT_price"]).round(2) # изменение порядка столбцов report[op] = report[op][ [ "LOT_size", "LOT_price", "SHARES_exists", "LOTS_exists", "lots", "SHARES", "SUM", "SHARES_after", ] ] report[op].rename( {"lots": f"LOTS_to_{op}", "SHARES": f"SHARES_to_{op}", "SUM": f"SUM_to_{op}"}, inplace=True, axis="columns", ) return report