Path: blob/master/ invest-robot-contest_tinkoffSDK-master/my_strategy.py
5925 views
import logging1from datetime import datetime, timedelta2from decimal import Decimal3from typing import Callable, Iterable, List4from typing_extensions import Self5import uuid67import numpy as np89from tinkoff.invest.strategies.base.account_manager import AccountManager10from tinkoff.invest.strategies.base.errors import (11CandleEventForDateNotFound,12NotEnoughData,13OldCandleObservingError,14)15from tinkoff.invest.strategies.base.models import CandleEvent16from tinkoff.invest.strategies.base.signal import (17CloseLongMarketOrder,18CloseShortMarketOrder,19OpenLongMarketOrder,20OpenShortMarketOrder,21Signal,22)23from tinkoff.invest.strategies.base.strategy_interface import InvestStrategy24from tinkoff.invest.strategies.moving_average.strategy_settings import (25MovingAverageStrategySettings,26)27from tinkoff.invest.strategies.moving_average.strategy_state import (28MovingAverageStrategyState,29)30from tinkoff.invest.utils import (31candle_interval_to_timedelta,32ceil_datetime,33floor_datetime,34now,35)3637logger = logging.getLogger(__name__)383940class MovingAverageStrategy(InvestStrategy):41def __init__(42self,43settings: MovingAverageStrategySettings,44account_manager: AccountManager,45state: MovingAverageStrategyState,46):47self._data: List[CandleEvent] = []48self._settings = settings49self._account_manager = account_manager5051self._state = state52self._MA_LONG_START: Decimal53self._candle_interval_timedelta = candle_interval_to_timedelta(54self._settings.candle_interval55)5657def _ensure_enough_candles(self) -> None:58candles_needed = (59self._settings.short_period + self._settings.long_period60) / self._settings.candle_interval_timedelta61if candles_needed > len(self._data):62#raise NotEnoughData()63logger.info("NotEnoughData for strategy")64else:65logger.info("Got enough data for strategy")6667def fit(self, candles: Iterable[CandleEvent]) -> None:68logger.debug("Strategy fitting with candles %s", candles)69for candle in candles:70self.observe(candle)71self._ensure_enough_candles()7273def _append_candle_event(self, candle_event: CandleEvent) -> None:74last_candle_event = self._data[-1]75last_interval_floor = floor_datetime(76last_candle_event.time, self._candle_interval_timedelta77)78last_interval_ceil = ceil_datetime(79last_candle_event.time, self._candle_interval_timedelta80)8182if candle_event.time < last_interval_floor:83raise OldCandleObservingError()84if (85candle_event.time < last_interval_ceil86or candle_event.time == last_interval_floor87):88self._data[-1] = candle_event89else:90self._data.append(candle_event)9192def observe(self, candle: CandleEvent) -> None:93logger.debug("Observing candle event: %s", candle)9495if len(self._data) > 0:96self._append_candle_event(candle)97else:98self._data.append(candle)99100@staticmethod101def _get_newer_than_datetime_predicate(102anchor: datetime,103) -> Callable[[CandleEvent], bool]:104def _(event: CandleEvent) -> bool:105return event.time > anchor106107return _108109def _filter_from_the_end_with_early_stop(110self, predicate: Callable[[CandleEvent], bool]111) -> Iterable[CandleEvent]:112for event in reversed(self._data):113if not predicate(event):114break115yield event116117def _select_for_period(self, period: timedelta):118predicate = self._get_newer_than_datetime_predicate(now() - period)119return self._filter_from_the_end_with_early_stop(predicate)120121@staticmethod122def _get_prices(events: Iterable[CandleEvent]) -> Iterable[Decimal]:123for event in events:124yield event.candle.close125126def _calculate_moving_average(self, period: timedelta) -> Decimal:127prices = list(self._get_prices(self._select_for_period(period)))128logger.info("On %s second of work. Selected prices: %s", period.seconds,prices)129return np.mean(prices, axis=0) # type: ignore130131def _calculate_std(self, period: timedelta) -> Decimal:132prices = list(self._get_prices(self._select_for_period(period)))133return np.std(prices, axis=0) # type: ignore134135def _get_first_candle_before(self, date: datetime) -> CandleEvent:136predicate = self._get_newer_than_datetime_predicate(date)137for event in reversed(self._data):138if not predicate(event):139return event140raise CandleEventForDateNotFound()141142def _init_MA_LONG_START(self):143date = now() - self._settings.short_period144event = self._get_first_candle_before(date)145self._MA_LONG_START = event.candle.close146147@staticmethod148def _is_long_open_signal(149MA_SHORT: Decimal,150MA_LONG: Decimal,151PRICE: Decimal,152STD: Decimal,153MA_LONG_START: Decimal,154) -> bool:155logger.debug("Try long opening")156logger.debug("\tMA_SHORT > MA_LONG, %s", MA_SHORT > MA_LONG)157return (MA_SHORT > MA_LONG) #Если идет возрастающий тренд покупаем158159@staticmethod160def _is_short_open_signal(161MA_SHORT: Decimal,162MA_LONG: Decimal,163PRICE: Decimal,164STD: Decimal,165MA_LONG_START: Decimal,166) -> bool:167logger.debug("Try short opening")168logger.debug("\tMA_SHORT < MA_LONG, %s", MA_SHORT < MA_LONG)169logger.debug(170"\tand abs((PRICE - MA_LONG) / MA_LONG) < STD, %s",171abs((PRICE - MA_LONG) / MA_LONG) < STD,172)173logger.debug("\tand MA_LONG > MA_LONG_START, %s", MA_LONG < MA_LONG_START)174logger.debug(175"== %s",176MA_SHORT < MA_LONG < MA_LONG_START177and abs((PRICE - MA_LONG) / MA_LONG) < STD,178)179return (180MA_SHORT < MA_LONG < MA_LONG_START181and abs((PRICE - MA_LONG) / MA_LONG) < STD182)183184@staticmethod185def _is_long_close_signal(186MA_LONG: Decimal,187MA_SHORT: Decimal,188PRICE: Decimal,189STD: Decimal,190has_short_open_signal: bool191) -> bool:192logger.debug("Try long closing")193#logger.debug("\tPRICE > MA_LONG + 10 * STD, %s", PRICE > MA_LONG + 10 * STD)194#logger.debug("\tor has_short_open_signal, %s", has_short_open_signal)195#logger.debug("\tor PRICE < MA_LONG - 3 * STD, %s", PRICE < MA_LONG - 3 * STD)196logger.debug("MA_SHORT < MA_LONG %s",MA_SHORT < MA_LONG)197198return (MA_SHORT < MA_LONG) #Если тренд на падение закрываем позицию199200@staticmethod201def _is_short_close_signal(202MA_LONG: Decimal,203PRICE: Decimal,204STD: Decimal,205has_long_open_signal: bool,206) -> bool:207logger.debug("Try short closing")208logger.debug("\tPRICE < MA_LONG - 10 * STD, %s", PRICE < MA_LONG - 10 * STD)209logger.debug("\tor has_long_open_signal, %s", has_long_open_signal)210logger.debug("\tor PRICE > MA_LONG + 3 * STD, %s", PRICE > MA_LONG + 3 * STD)211logger.debug(212"== %s",213PRICE < MA_LONG - 10 * STD # кажется, что не работает закрытие214or has_long_open_signal215or PRICE > MA_LONG + 3 * STD,216)217return (218PRICE < MA_LONG - 10 * STD219or has_long_open_signal220or PRICE > MA_LONG + 3 * STD221)222223def predict(self) -> Iterable[Signal]: # noqa: C901224logger.info("Strategy predict")225self._init_MA_LONG_START()226MA_LONG_START = self._MA_LONG_START227logger.debug("MA_LONG_START: %s", MA_LONG_START)228PRICE = self._data[-1].candle.close229logger.debug("PRICE: %s", PRICE)230MA_LONG = self._calculate_moving_average(self._settings.long_period)231logger.debug("MA_LONG: %s", MA_LONG)232MA_SHORT = self._calculate_moving_average(self._settings.short_period)233logger.debug("MA_SHORT: %s", MA_SHORT)234STD = self._calculate_std(self._settings.std_period)235logger.debug("STD: %s", STD)236MONEY,SHARES = self._account_manager.get_current_balance() #Получаем деньги на счете, количество акций не важно237logger.debug("MONEY: %s", MONEY)238239has_long_open_signal = False240has_short_open_signal = False241242lot_size = 10000 #Актуально, для акций ВТБ243#possible_lots = int(MONEY // (PRICE * lot_size))244possible_lots = 1 #Количество лотов можно рассчитывать, но для отработки робота, пока не будем.245246247if (248# not self._state.long_open249True #Упростим стратегию, не будем проверять срабатывание предыдущих сигналов250and self._is_long_open_signal(251MA_SHORT=MA_SHORT,252MA_LONG=MA_LONG,253PRICE=PRICE,254STD=STD,255MA_LONG_START=MA_LONG_START,256)257and possible_lots > 0258):259has_long_open_signal = True260yield OpenLongMarketOrder(lots=possible_lots)261262if (263#not self._state.short_open264False # Коротки позиции запрещены265and self._is_short_open_signal(266MA_SHORT=MA_SHORT,267MA_LONG=MA_LONG,268PRICE=PRICE,269STD=STD,270MA_LONG_START=MA_LONG_START,271)272and possible_lots > 0273):274has_short_open_signal = True275yield OpenShortMarketOrder(lots=possible_lots)276277#В конце дня длинные позиции должны быть закрыты278279#my_stop_signal = self._data.pop().time > self._account_manager._services._real_market_data_test_start + timedelta(hours=6)280#logger.info("my_stop_signal: %s", my_stop_signal)281if self._state.long_open and self._is_long_close_signal(282#if self._is_long_close_signal(283MA_LONG=MA_LONG,284MA_SHORT = MA_SHORT,285PRICE=PRICE,286STD=STD,287has_short_open_signal=has_short_open_signal288#my_stop_signal = my_stop_signal289):290yield CloseLongMarketOrder(lots=self._state.position)291292#Если короткие позиции не открывать, то и закрывать их не надо.293if self._state.short_open and self._is_short_close_signal(294MA_LONG=MA_LONG,295PRICE=PRICE,296STD=STD,297has_long_open_signal=has_long_open_signal,298):299yield CloseShortMarketOrder(lots=self._state.position)300301302