Path: blob/master/ invest-robot-contest_investRobot-master/robotlib/strategy.py
5927 views
import datetime1import math2import random34from abc import ABC, abstractmethod5from dataclasses import dataclass, field67from tinkoff.invest import (8Candle,9HistoricCandle,10Instrument,11MarketDataResponse,12OrderType,13OrderDirection,14OrderState,15Quotation,16SubscriptionInterval,17)18from robotlib.money import Money19from robotlib.vizualization import Visualizer202122@dataclass23class TradeStrategyParams:24instrument_balance: int25currency_balance: float26pending_orders: list[OrderState]272829@dataclass30class RobotTradeOrder:31quantity: int32direction: OrderDirection33price: Money | None = None34order_type: OrderType = OrderType.ORDER_TYPE_MARKET353637@dataclass38class StrategyDecision:39robot_trade_order: RobotTradeOrder | None = None40cancel_orders: list[OrderState] = field(default_factory=list)414243class TradeStrategyBase(ABC):44instrument_info: Instrument4546@property47@abstractmethod48def candle_subscription_interval(self) -> SubscriptionInterval:49return SubscriptionInterval.SUBSCRIPTION_INTERVAL_ONE_MINUTE5051@property52@abstractmethod53def order_book_subscription_depth(self) -> int | None: # set not None to subscribe robot to order book54return None5556@property57@abstractmethod58def trades_subscription(self) -> bool: # set True to subscribe robot to trades stream59return False6061@property62@abstractmethod63def strategy_id(self) -> str:64"""65string representing short strategy name for logger66"""67raise NotImplementedError()6869def load_instrument_info(self, instrument_info: Instrument):70self.instrument_info = instrument_info7172def load_candles(self, candles: list[HistoricCandle]) -> None:73"""74Method used by robot to load historic data75"""76pass7778@abstractmethod79def decide(self, market_data: MarketDataResponse, params: TradeStrategyParams) -> StrategyDecision:80if market_data.candle:81return self.decide_by_candle(market_data.candle, params)82return StrategyDecision()8384@abstractmethod85def decide_by_candle(self, candle: Candle | HistoricCandle, params: TradeStrategyParams) -> StrategyDecision:86pass878889class RandomStrategy(TradeStrategyBase):90request_candles: bool = True91strategy_id: str = 'random'9293low: int94high: int9596def __init__(self, low: int, high: int):97self.low = low98self.high = high99100def decide(self, market_data: MarketDataResponse, params: TradeStrategyParams) -> StrategyDecision:101return self.decide_by_candle(market_data.candle, params)102103def decide_by_candle(self, candle: Candle | HistoricCandle, params: TradeStrategyParams) -> StrategyDecision:104low = max(self.low, -params.instrument_balance)105high = min(self.high, math.floor(params.currency_balance / self.convert_quotation(candle.close)))106107quantity = random.randint(low, high)108direction = OrderDirection.ORDER_DIRECTION_BUY if quantity > 0 else OrderDirection.ORDER_DIRECTION_SELL109110return StrategyDecision(RobotTradeOrder(quantity=quantity, direction=direction))111112@staticmethod113def convert_quotation(amount: Quotation) -> float | None:114if amount is None:115return None116return amount.units + amount.nano / (10 ** 9)117118119class MAEStrategy(TradeStrategyBase):120request_candles: bool = True121strategy_id: str = 'mae'122123candle_subscription_interval: SubscriptionInterval = SubscriptionInterval.SUBSCRIPTION_INTERVAL_ONE_MINUTE124order_book_subscription_depth = None125trades_subscription = None126127short_len: int128long_len: int129trade_count: int130prices = dict[datetime.datetime, Money]131prev_sign: bool132133def __init__(self, short_len: int = 5, long_len: int = 20, trade_count: int = 1, visualizer: Visualizer = None):134assert long_len > short_len135self.short_len = short_len136self.long_len = long_len137self.trade_count = trade_count138self.prices = {}139self.visualizer = visualizer140141def load_candles(self, candles: list[HistoricCandle]) -> None:142self.prices = {candle.time.replace(second=0, microsecond=0): Money(candle.close)143for candle in candles[-self.long_len:]}144self.prev_sign = self._long_avg() > self._short_avg()145146def decide(self, market_data: MarketDataResponse, params: TradeStrategyParams) -> StrategyDecision:147return self.decide_by_candle(market_data.candle, params)148149def decide_by_candle(self, candle: Candle | HistoricCandle, params: TradeStrategyParams) -> StrategyDecision:150time: datetime = candle.time.replace(second=0, microsecond=0)151order: RobotTradeOrder | None = None152if time not in self.prices: # make order only once a minute (when minutely candle is ready)153sign = self._long_avg() > self._short_avg()154if sign != self.prev_sign:155if sign:156if params.instrument_balance > 0:157order = RobotTradeOrder(quantity=min(self.trade_count, params.instrument_balance),158direction=OrderDirection.ORDER_DIRECTION_SELL)159if self.visualizer:160self.visualizer.add_sell(time)161else:162lot_price = Money(candle.close).to_float() * self.instrument_info.lot163lots_available = int(params.currency_balance / lot_price)164if params.currency_balance >= lot_price:165order = RobotTradeOrder(quantity=min(self.trade_count, lots_available),166direction=OrderDirection.ORDER_DIRECTION_BUY)167if self.visualizer:168self.visualizer.add_buy(time)169170self.prev_sign = sign171self.prices[time] = Money(candle.close)172if self.visualizer:173self.visualizer.add_price(time, Money(candle.close).to_float())174self.visualizer.update_plot()175176return StrategyDecision(robot_trade_order=order)177178def get_prices_list(self) -> list[Money]:179# sort by keys and then convert to a list of values180return list(map(lambda x: x[1], sorted(self.prices.items(), key=lambda x: x[0])))181182def _long_avg(self):183return sum(float(price) for price in self.get_prices_list()[-self.long_len:]) / self.long_len184185def _short_avg(self):186return sum(float(price) for price in self.get_prices_list()[-self.short_len:]) / self.short_len187188189