Path: blob/master/ invest-robot-contest_invest-bot-main/trading/trader.py
5931 views
import datetime1import logging2from decimal import Decimal34from tinkoff.invest import Candle5from tinkoff.invest.utils import quotation_to_decimal67from blog.blogger import Blogger8from invest_api.services.client_service import ClientService9from invest_api.services.instruments_service import InstrumentService10from invest_api.services.market_data_service import MarketDataService11from invest_api.services.operations_service import OperationService12from invest_api.services.orders_service import OrderService13from invest_api.services.market_data_stream_service import MarketDataStreamService14from invest_api.utils import candle_to_historiccandle15from trade_system.signal import SignalType16from trade_system.strategies.base_strategy import IStrategy17from trading.trade_results import TradeResults18from configuration.settings import TradingSettings1920__all__ = ("Trader")2122logger = logging.getLogger(__name__)232425class Trader:26"""27The class encapsulate main trade logic.28"""2930def __init__(31self,32client_service: ClientService,33instrument_service: InstrumentService,34operation_service: OperationService,35order_service: OrderService,36stream_service: MarketDataStreamService,37market_data_service: MarketDataService,38blogger: Blogger39) -> None:40self.__today_trade_results: TradeResults = None41self.__client_service = client_service42self.__instrument_service = instrument_service43self.__operation_service = operation_service44self.__order_service = order_service45self.__stream_service = stream_service46self.__market_data_service = market_data_service47self.__blogger = blogger4849async def trade_day(50self,51account_id: str,52trading_settings: TradingSettings,53strategies: list[IStrategy],54trade_day_end_time: datetime,55min_rub: int56) -> None:57logger.info("Start preparations for trading today")58today_trade_strategies = self.__get_today_strategies(strategies)59if not today_trade_strategies:60logger.info("No shares to trade today.")61return None6263self.__clear_all_positions(account_id, today_trade_strategies)6465rub_before_trade_day = self.__operation_service.available_rub_on_account(account_id)66logger.info(f"Amount of RUB on account {rub_before_trade_day} and minimum for trading: {min_rub}")67if rub_before_trade_day < min_rub:68return None6970logger.info("Start trading today")71self.__blogger.start_trading_message(today_trade_strategies, rub_before_trade_day)7273try:74await self.__trading(75account_id,76trading_settings,77today_trade_strategies,78trade_day_end_time79)80logger.debug("Test Results:")81logger.debug(f"Current: {self.__today_trade_results.get_current_open_orders()}")82logger.debug(f"Old: {self.__today_trade_results.get_closed_orders()}")83except Exception as ex:84logger.error(f"Trading error: {repr(ex)}")8586logger.info("Finishing trading today")87self.__blogger.finish_trading_message()8889try:90if self.__today_trade_results:91for key_figi, value_order_id in self.__clear_all_positions(account_id, today_trade_strategies).items():92trade_order = self.__today_trade_results.close_position(key_figi, value_order_id)93self.__blogger.close_position_message(trade_order)94else:95self.__clear_all_positions(account_id, today_trade_strategies)96except Exception as ex:97logger.error(f"Finishing trading error: {repr(ex)}")9899logger.info("Show trade results today")100try:101self.__summary_today_trade_results(account_id, rub_before_trade_day)102except Exception as ex:103logger.error(f"Summary trading day error: {repr(ex)}")104105async def __trading(106self,107account_id: str,108trading_settings: TradingSettings,109strategies: dict[str, IStrategy],110trade_day_end_time: datetime111) -> None:112logger.info(f"Subscribe and read Candles for {strategies.keys()}")113114# End trading before close trade session115trade_before_time: datetime = \116trade_day_end_time - datetime.timedelta(seconds=trading_settings.stop_trade_before_close)117118signals_before_time: datetime = \119trade_day_end_time - datetime.timedelta(minutes=trading_settings.stop_signals_before_close)120logger.debug(f"Stop time: signals - {signals_before_time}, trading - {trade_before_time}")121122current_candles: dict[str, Candle] = dict()123self.__today_trade_results = TradeResults()124125async for candle in self.__stream_service.start_async_candles_stream(126list(strategies.keys()),127trade_before_time128):129current_figi_candle = current_candles.setdefault(candle.figi, candle)130if candle.time < current_figi_candle.time:131# it can be based on API documentation132logger.debug("Skip candle from past.")133continue134135# check price from candle for take or stop price levels136current_trade_order = self.__today_trade_results.get_current_trade_order(candle.figi)137if current_trade_order:138high, low = quotation_to_decimal(candle.high), quotation_to_decimal(candle.low)139140# Logic is:141# if stop or take price level is between high and low, then stop or take will be executed142if low <= current_trade_order.signal.stop_loss_level <= high:143logger.info(f"STOP LOSS: {current_trade_order}")144close_order_id = \145self.__close_position_by_figi(account_id, [candle.figi], strategies).get(candle.figi, None)146if close_order_id:147trade_order = self.__today_trade_results.close_position(candle.figi, close_order_id)148self.__blogger.close_position_message(trade_order)149150elif low <= current_trade_order.signal.take_profit_level <= high:151logger.info(f"TAKE PROFIT: {current_trade_order}")152close_order_id = \153self.__close_position_by_figi(account_id, [candle.figi], strategies).get(candle.figi, None)154if close_order_id:155trade_order = self.__today_trade_results.close_position(candle.figi, close_order_id)156self.__blogger.close_position_message(trade_order)157158if candle.time > current_figi_candle.time and \159datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc) <= signals_before_time:160signal_new = strategies[candle.figi].analyze_candles(161[candle_to_historiccandle(current_figi_candle)]162)163164if signal_new:165logger.info(f"New signal: {signal_new}")166167if self.__today_trade_results.get_current_trade_order(candle.figi):168logger.info(f"New signal has been skipped. Previous signal is still alive.")169elif not self.__market_data_service.is_stock_ready_for_trading(candle.figi):170logger.info(f"New signal has been skipped. Stock isn't ready for trading")171else:172available_lots = self.__open_position_lots_count(173account_id,174strategies[candle.figi].settings.max_lots_per_order,175quotation_to_decimal(candle.close),176strategies[candle.figi].settings.lot_size177)178179logger.debug(f"Available lots: {available_lots}")180if available_lots:181open_order_id = self.__order_service.post_market_order(182account_id=account_id,183figi=candle.figi,184count_lots=available_lots,185is_buy=(signal_new.signal_type == SignalType.LONG)186)187open_position = self.__today_trade_results.open_position(188candle.figi,189open_order_id,190signal_new191)192self.__blogger.open_position_message(open_position)193logger.info(f"Open position: {open_position}")194else:195logger.info(f"New signal has been skipped. No available money")196197current_candles[candle.figi] = candle198199logger.info("Today trading has been completed")200201def __summary_today_trade_results(202self,203account_id: str,204rub_before_trade_day: Decimal205) -> None:206logger.info("Today trading summary:")207self.__blogger.summary_message()208209current_rub_on_depo = self.__operation_service.available_rub_on_account(account_id)210logger.info(f"RUBs on account before:{rub_before_trade_day}, after:{current_rub_on_depo}")211212today_profit = current_rub_on_depo - rub_before_trade_day213today_percent_profit = (today_profit / rub_before_trade_day) * 100214logger.info(f"Today Profit:{today_profit} rub ({today_percent_profit} %)")215self.__blogger.trading_depo_summary_message(rub_before_trade_day, current_rub_on_depo)216217if self.__today_trade_results:218logger.info(f"Today Open Signals:")219for figi_key, trade_order_value in self.__today_trade_results.get_current_open_orders().items():220logger.info(f"Stock: {figi_key}")221222open_order_state = self.__order_service.get_order_state(account_id, trade_order_value.open_order_id)223logger.info(f"Signal {trade_order_value.signal}")224logger.info(f"Open: {open_order_state}")225self.__blogger.summary_open_signal_message(trade_order_value, open_order_state)226227logger.info(f"All open positions should be closed manually.")228229logger.info(f"Today Closed Signals:")230for figi_key, trade_orders_value in self.__today_trade_results.get_closed_orders().items():231logger.info(f"Stock: {figi_key}")232for trade_order in trade_orders_value:233open_order_state = self.__order_service.get_order_state(account_id, trade_order.open_order_id)234close_order_state = self.__order_service.get_order_state(account_id, trade_order.close_order_id)235logger.info(f"Signal {trade_order.signal}")236logger.info(f"Open: {open_order_state}")237logger.info(f"Close: {close_order_state}")238self.__blogger.summary_closed_signal_message(trade_order, open_order_state, close_order_state)239else:240logger.info(f"Something went wrong: today trade results is empty")241logger.info(f"All open positions should be closed manually.")242self.__blogger.fail_message()243244self.__blogger.final_message()245246def __open_position_lots_count(247self,248account_id: str,249max_lots_per_order: int,250price: Decimal,251share_lot_size: int252) -> int:253"""254Calculate counts of lots for order255"""256current_rub_on_depo = self.__operation_service.available_rub_on_account(account_id)257258available_lots = int(current_rub_on_depo / (share_lot_size * price))259260return available_lots if max_lots_per_order > available_lots else max_lots_per_order261262def __clear_all_positions(263self,264account_id: str,265strategies: dict[str, IStrategy]266) -> dict[str, str]:267logger.info("Clear all orders and close all open positions")268269logger.debug("Cancel all order.")270self.__client_service.cancel_all_orders(account_id)271272logger.debug("Close all positions.")273return self.__close_position_by_figi(account_id, strategies.keys(), strategies)274275def __close_position_by_figi(276self,277account_id: str,278figies: list[str],279strategies: dict[str, IStrategy]280) -> dict[str, str]:281result: dict[str, str] = dict()282current_positions = self.__operation_service.positions_securities(account_id)283284if current_positions:285logger.info(f"Current positions: {current_positions}")286for position in current_positions:287if position.figi in figies:288# Check a stock289if self.__market_data_service.is_stock_ready_for_trading(position.figi):290result[position.figi] = self.__order_service.post_market_order(291account_id=account_id,292figi=position.figi,293count_lots=abs(int(position.balance / strategies[position.figi].settings.lot_size)),294is_buy=(position.balance < 0)295)296297return result298299def __get_today_strategies(self, strategies: list[IStrategy]) -> dict[str, IStrategy]:300"""301Check and Select stocks for trading today.302"""303logger.info("Check shares and strategy settings")304today_trade_strategy: dict[str, IStrategy] = dict()305306for strategy in strategies:307share_settings = self.__instrument_service.share_by_figi(strategy.settings.figi)308logger.debug(f"Check share settings for figi {strategy.settings.figi}: {share_settings}")309310if (not share_settings.otc_flag) \311and share_settings.buy_available_flag \312and share_settings.sell_available_flag \313and share_settings.api_trade_available_flag:314logger.debug(f"Share is ready for trading")315316# refresh information by latest info317strategy.update_lot_count(share_settings.lot)318strategy.update_short_status(share_settings.short_enabled_flag)319320today_trade_strategy[strategy.settings.figi] = strategy321322return today_trade_strategy323324325