Path: blob/master/ invest-robot-contest_tinkoff-trading-bot-develop/tests/strategies/interval/backtest/conftest.py
5931 views
from datetime import timedelta1from pathlib import Path2from typing import List3from unittest.mock import AsyncMock45import pytest6from pytest_mock import MockerFixture7from tinkoff.invest import (8GetAccountsResponse,9Account,10Client,11GetOrdersResponse,12GetLastPricesResponse,13LastPrice,14PortfolioResponse,15PortfolioPosition,16Quotation,17OrderDirection,18MoneyValue,19)20from tinkoff.invest.caching.cache_settings import MarketDataCacheSettings21from tinkoff.invest.services import MarketDataCache, Services22from tinkoff.invest.utils import now2324from app.client import TinkoffClient25from app.settings import settings26from app.strategies.interval.models import IntervalStrategyConfig27from app.utils.quotation import quotation_to_float282930class NoMoreDataError(Exception):31pass323334@pytest.fixture35def account_id():36return "test_id"373839@pytest.fixture(scope="session")40def figi() -> str:41return "BBG000QDVR53"424344@pytest.fixture(scope="session")45def comission() -> float:46return 0.003474849@pytest.fixture50def accounts_response(account_id: str) -> GetAccountsResponse:51return GetAccountsResponse(accounts=[Account(id=account_id)])525354@pytest.fixture55def orders_response(account_id: str) -> GetOrdersResponse:56return GetOrdersResponse(orders=[])575859@pytest.fixture60def get_portfolio_response(account_id: str) -> GetOrdersResponse:61return GetOrdersResponse(orders=[])626364@pytest.fixture(scope="session")65def test_config() -> IntervalStrategyConfig:66return IntervalStrategyConfig(67interval_size=0.8,68days_back_to_consider=7,69check_interval=600,70stop_loss_percentage=0.1,71quantity_limit=10,72)737475@pytest.fixture76def client() -> Services:77with Client(settings.token) as client:78yield client798081class CandleHandler:82def __init__(self, config: IntervalStrategyConfig):83self.now = now()84self.from_date = self.now - timedelta(days=90)85self.candles = []86self.config = config8788async def get_all_candles(self, **kwargs):89if not self.candles:90with Client(settings.token) as client:91market_data_cache = MarketDataCache(92settings=MarketDataCacheSettings(base_cache_dir=Path("market_data_cache")),93services=client,94)95self.candles = list(96market_data_cache.get_all_candles(97figi=kwargs["figi"],98to=self.now,99from_=self.from_date,100interval=kwargs["interval"],101)102)103104any_returned = False105for candle in self.candles:106if self.from_date < candle.time:107if candle.time < self.from_date + timedelta(days=self.config.days_back_to_consider):108any_returned = True109yield candle110else:111break112113if not any_returned:114raise NoMoreDataError()115self.from_date += timedelta(seconds=self.config.check_interval)116117async def get_last_prices(self, figi: List[str]) -> GetLastPricesResponse:118for candle in self.candles:119if candle.time >= self.from_date + timedelta(days=self.config.days_back_to_consider):120return GetLastPricesResponse(121last_prices=[LastPrice(figi=figi[0], price=candle.close, time=candle.time)]122)123raise NoMoreDataError()124125126class PortfolioHandler:127def __init__(self, figi: str, comission: float, candle_handler: CandleHandler):128self.positions = 0129self.resources = 0130self.figi = figi131self.comission = comission132self.candle_handler = candle_handler133self.average_price = MoneyValue(units=0, nano=0)134135async def get_portfolio(self, **kwargs) -> PortfolioResponse:136return PortfolioResponse(137positions=[138PortfolioPosition(139figi=self.figi,140quantity=Quotation(units=self.positions, nano=0),141average_position_price=self.average_price,142)143]144)145146async def post_order(147self, quantity: int = 0, direction: OrderDirection = OrderDirection(0), **kwargs148):149last_price_quotation = (150(await self.candle_handler.get_last_prices(figi=[self.figi])).last_prices[0].price151)152last_price = quotation_to_float(last_price_quotation)153# TODO: Make it count average price respecting amount154if direction == OrderDirection.ORDER_DIRECTION_BUY:155self.positions += quantity156self.resources -= quantity * last_price + (self.comission * quantity * last_price)157self.average_price = MoneyValue(158units=last_price_quotation.units, nano=last_price_quotation.nano159)160elif direction == OrderDirection.ORDER_DIRECTION_SELL:161self.positions -= quantity162self.resources += quantity * last_price - (self.comission * quantity * last_price)163self.average_price = MoneyValue(units=0, nano=0)164165166@pytest.fixture(scope="session")167def candle_handler(test_config: IntervalStrategyConfig) -> CandleHandler:168return CandleHandler(test_config)169170171@pytest.fixture(scope="session")172def portfolio_handler(173figi: str, comission: float, candle_handler: CandleHandler174) -> PortfolioHandler:175return PortfolioHandler(figi, comission, candle_handler)176177178@pytest.fixture179def mock_client(180mocker: MockerFixture,181accounts_response: GetAccountsResponse,182orders_response: GetOrdersResponse,183candle_handler: CandleHandler,184portfolio_handler: PortfolioHandler,185figi: str,186client: Services,187test_config: IntervalStrategyConfig,188) -> TinkoffClient:189client_mock = mocker.patch("app.strategies.interval.IntervalStrategy.client")190client_mock.get_accounts = AsyncMock(return_value=accounts_response)191client_mock.get_orders = AsyncMock(return_value=orders_response)192193client_mock.get_all_candles = candle_handler.get_all_candles194client_mock.get_last_prices = AsyncMock(side_effect=candle_handler.get_last_prices)195196client_mock.get_portfolio = AsyncMock(side_effect=portfolio_handler.get_portfolio)197client_mock.post_order = AsyncMock(side_effect=portfolio_handler.post_order)198199return client_mock200201202