Path: blob/master/ invest-robot-contest_tinkoff-invest-volume-analysis-robot-master/strategies/profile_touch_strategy.py
5932 views
import datetime1import logging2import threading3from typing import List, Optional45import pandas as pd6from tinkoff.invest import TradeDirection, OrderDirection78from constants import FIVE_MINUTES_TO_SECONDS9from domains.order import Order10from utils.exchange_util import is_open_orders, is_premarket_time11from visualizers.finplot_graph import FinplotGraph12from settings import PROFILE_PERIOD, FIRST_TOUCH_VOLUME_LEVEL, SECOND_TOUCH_VOLUME_LEVEL, FIRST_GOAL, \13PERCENTAGE_STOP_LOSS, SIGNAL_CLUSTER_PERIOD, IS_SHOW_CHART, GOAL_STEP, COUNT_LOTS, COUNT_GOALS14from utils.order_util import prepare_orders15from utils.strategy_util import is_price_in_range_cluster, ticks_to_cluster, calculate_ratio, \16processed_volume_levels_to_times, apply_frame_type1718pd.options.display.max_columns = None19pd.options.display.max_rows = None20pd.options.display.width = None2122logger = logging.getLogger(__name__)232425# стратегия касание объемного уровня26class ProfileTouchStrategy(threading.Thread):27def __init__(self, instrument_name):28super().__init__()2930self.instrument_name = instrument_name3132self.df = pd.DataFrame(columns=["figi", "direction", "price", "quantity", "time"])33self.df = apply_frame_type(self.df)3435self.first_tick_time = None36self.fix_date = {}37self.clusters = None38self.processed_volume_levels = {}3940if IS_SHOW_CHART:41self.visualizer = FinplotGraph(SIGNAL_CLUSTER_PERIOD)42self.visualizer.start()4344def set_df(self, df: pd.DataFrame):45self.df = df46logger.info("загружен новый DataFrame")4748def analyze(49self,50trade_df: pd.DataFrame51) -> Optional[List[Order]]:52trade_data = trade_df.iloc[0]53current_price = trade_data["price"]54time = trade_data["time"]5556if is_premarket_time(time):57return5859if PROFILE_PERIOD not in self.fix_date:60self.fix_date[PROFILE_PERIOD] = time.hour6162if self.first_tick_time is None:63# сбрасываю секунды, чтобы сравнивать "целые" минутные свечи64self.first_tick_time = time.replace(second=0, microsecond=0)6566if self.fix_date[PROFILE_PERIOD] < time.hour:67# построение кластерных свечей и графика раз в 1 час68self.fix_date[PROFILE_PERIOD] = time.hour69self.calculate_clusters()7071self.df = pd.concat([self.df, trade_df])7273if self.clusters is not None:74for index, cluster in self.clusters.iterrows():75cluster_time = cluster["time"]76cluster_price = cluster["max_volume_price"]7778# цена может коснуться объемного уровня в заданном процентном диапазоне79is_price_in_range = is_price_in_range_cluster(current_price, cluster_price)80if is_price_in_range:81timedelta = time - cluster_time82if timedelta < datetime.timedelta(minutes=FIRST_TOUCH_VOLUME_LEVEL):83continue8485if cluster_price not in self.processed_volume_levels:86# инициализация первого касания уровня87self.processed_volume_levels[cluster_price] = {}88self.processed_volume_levels[cluster_price]["count_touches"] = 089self.processed_volume_levels[cluster_price]["times"] = {}90else:91# обработка второго и последующего касания уровня на основе времени последнего касания92if self.processed_volume_levels[cluster_price]["last_touch_time"] is not None:93timedelta = time - self.processed_volume_levels[cluster_price]["last_touch_time"]94if timedelta < datetime.timedelta(minutes=SECOND_TOUCH_VOLUME_LEVEL):95continue9697# установка параметров при касании уровня98self.processed_volume_levels[cluster_price]["count_touches"] += 199self.processed_volume_levels[cluster_price]["last_touch_time"] = time100self.processed_volume_levels[cluster_price]["times"][time] = None101102count_touches = self.processed_volume_levels[cluster_price]['count_touches']103logger.info("объемный уровень %s сформирован %s", cluster_price, cluster_time)104logger.info("время %s: цена %s подошла к объемному уровню %s раз\n", time, current_price, count_touches)105break106107if (time - self.first_tick_time).total_seconds() >= FIVE_MINUTES_TO_SECONDS:108# сбрасываю секунды, чтобы сравнивать завершенные свечи109self.first_tick_time = time.replace(second=0, microsecond=0)110# если торги доступны, то каждую завершенную минуту проверяю кластера на возможную ТВ111if is_open_orders(time) and len(self.processed_volume_levels) > 0:112return self.check_entry_points(current_price, time)113114def calculate_clusters(self):115if self.df.empty:116return117self.clusters = ticks_to_cluster(self.df, period=PROFILE_PERIOD)118valid_entry_points, invalid_entry_points = processed_volume_levels_to_times(119self.processed_volume_levels)120if IS_SHOW_CHART:121self.visualizer.render(self.df,122valid_entry_points=valid_entry_points,123invalid_entry_points=invalid_entry_points,124clusters=self.clusters)125126def check_entry_points(127self,128current_price: float,129time: datetime130) -> Optional[List[Order]]:131for volume_price, volume_level in self.processed_volume_levels.items():132for touch_time, value in volume_level["times"].items():133if value is not None:134continue135candles = ticks_to_cluster(self.df, period=SIGNAL_CLUSTER_PERIOD)136candles = calculate_ratio(candles)137prev_candle = candles.iloc[-3]138current_candle = candles.iloc[-2]139if current_candle.empty or prev_candle.empty:140logger.error("свеча не найдена")141continue142143if current_candle["win"] is True:144# если свеча является сигнальной, то осуществляю сделку145max_volume_price = current_candle["max_volume_price"]146percent = (max_volume_price * PERCENTAGE_STOP_LOSS / 100)147self.processed_volume_levels[volume_price]["times"][touch_time] = True148149if current_candle["direction"] == TradeDirection.TRADE_DIRECTION_BUY:150if prev_candle["open"] < current_candle["open"]:151logger.info("пропуск входа - предыдущая свеча открылась ниже текущей")152return153154# todo условие дает плохое соотношение155# если подошли к объемному уровню снизу вверх на лонговой свече, то пропускаю вход156# if current_candle['close'] < volume_price:157# logger.info(f"пропуск входа - цена закрытия ниже объемного уровня, time={time}, price={current_price}")158# return159160if current_price < max_volume_price:161logger.info(162"пропуск входа - цена открытия ниже макс объема в сигнальной свече, время %s, цена %s",163time,164current_price165)166return167168stop = max_volume_price - percent169orders = self.prepare_orders(170current_price=current_price,171time=time,172stop=stop,173direction=OrderDirection.ORDER_DIRECTION_BUY174)175logger.info("подтверждена точка входа в лонг, ордера: %s", orders)176return orders177178else:179if prev_candle["open"] > current_candle["open"]:180logger.info("пропуск входа - предыдущая свеча открылась выше текущей")181return182183# todo условие дает плохое соотношение184# если подошли к объемному уровню сверху вниз на шортовой свече, то пропускаю вход185# if current_candle['close'] > volume_price:186# logger.info(f"пропуск входа - цена закрытия выше объемного уровня, time={time}, price={current_price}")187# return188189if current_price > max_volume_price:190logger.info(191"пропуск входа - цена открытия выше макс объема в сигнальной свече, время %s, цена %s",192time,193current_price194)195return196197stop = max_volume_price + percent198orders = self.prepare_orders(199current_price=current_price,200time=time,201stop=stop,202direction=OrderDirection.ORDER_DIRECTION_SELL203)204logger.info("подтверждена точка входа в шорт, ордера: %s", orders)205return orders206207else:208# если текущая свеча не сигнальная, то ожидаю следующую для возможного входа209self.processed_volume_levels[volume_price]["times"][touch_time] = False210self.processed_volume_levels[volume_price]["last_touch_time"] = None211212def prepare_orders(213self,214current_price: float,215time: datetime,216stop: float,217direction: OrderDirection218) -> List[Order]:219return prepare_orders(220instrument=self.instrument_name,221current_price=current_price,222time=time,223stop_loss=stop,224direction=direction,225count_lots=COUNT_LOTS,226count_goals=COUNT_GOALS,227goal_step=GOAL_STEP,228first_goal=FIRST_GOAL229)230231232