Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
wiseplat
GitHub Repository: wiseplat/python-code
Path: blob/master/ invest-robot-contest_tinkoff-trading-bot-develop/tests/strategies/interval/backtest/conftest.py
5931 views
1
from datetime import timedelta
2
from pathlib import Path
3
from typing import List
4
from unittest.mock import AsyncMock
5
6
import pytest
7
from pytest_mock import MockerFixture
8
from tinkoff.invest import (
9
GetAccountsResponse,
10
Account,
11
Client,
12
GetOrdersResponse,
13
GetLastPricesResponse,
14
LastPrice,
15
PortfolioResponse,
16
PortfolioPosition,
17
Quotation,
18
OrderDirection,
19
MoneyValue,
20
)
21
from tinkoff.invest.caching.cache_settings import MarketDataCacheSettings
22
from tinkoff.invest.services import MarketDataCache, Services
23
from tinkoff.invest.utils import now
24
25
from app.client import TinkoffClient
26
from app.settings import settings
27
from app.strategies.interval.models import IntervalStrategyConfig
28
from app.utils.quotation import quotation_to_float
29
30
31
class NoMoreDataError(Exception):
32
pass
33
34
35
@pytest.fixture
36
def account_id():
37
return "test_id"
38
39
40
@pytest.fixture(scope="session")
41
def figi() -> str:
42
return "BBG000QDVR53"
43
44
45
@pytest.fixture(scope="session")
46
def comission() -> float:
47
return 0.003
48
49
50
@pytest.fixture
51
def accounts_response(account_id: str) -> GetAccountsResponse:
52
return GetAccountsResponse(accounts=[Account(id=account_id)])
53
54
55
@pytest.fixture
56
def orders_response(account_id: str) -> GetOrdersResponse:
57
return GetOrdersResponse(orders=[])
58
59
60
@pytest.fixture
61
def get_portfolio_response(account_id: str) -> GetOrdersResponse:
62
return GetOrdersResponse(orders=[])
63
64
65
@pytest.fixture(scope="session")
66
def test_config() -> IntervalStrategyConfig:
67
return IntervalStrategyConfig(
68
interval_size=0.8,
69
days_back_to_consider=7,
70
check_interval=600,
71
stop_loss_percentage=0.1,
72
quantity_limit=10,
73
)
74
75
76
@pytest.fixture
77
def client() -> Services:
78
with Client(settings.token) as client:
79
yield client
80
81
82
class CandleHandler:
83
def __init__(self, config: IntervalStrategyConfig):
84
self.now = now()
85
self.from_date = self.now - timedelta(days=90)
86
self.candles = []
87
self.config = config
88
89
async def get_all_candles(self, **kwargs):
90
if not self.candles:
91
with Client(settings.token) as client:
92
market_data_cache = MarketDataCache(
93
settings=MarketDataCacheSettings(base_cache_dir=Path("market_data_cache")),
94
services=client,
95
)
96
self.candles = list(
97
market_data_cache.get_all_candles(
98
figi=kwargs["figi"],
99
to=self.now,
100
from_=self.from_date,
101
interval=kwargs["interval"],
102
)
103
)
104
105
any_returned = False
106
for candle in self.candles:
107
if self.from_date < candle.time:
108
if candle.time < self.from_date + timedelta(days=self.config.days_back_to_consider):
109
any_returned = True
110
yield candle
111
else:
112
break
113
114
if not any_returned:
115
raise NoMoreDataError()
116
self.from_date += timedelta(seconds=self.config.check_interval)
117
118
async def get_last_prices(self, figi: List[str]) -> GetLastPricesResponse:
119
for candle in self.candles:
120
if candle.time >= self.from_date + timedelta(days=self.config.days_back_to_consider):
121
return GetLastPricesResponse(
122
last_prices=[LastPrice(figi=figi[0], price=candle.close, time=candle.time)]
123
)
124
raise NoMoreDataError()
125
126
127
class PortfolioHandler:
128
def __init__(self, figi: str, comission: float, candle_handler: CandleHandler):
129
self.positions = 0
130
self.resources = 0
131
self.figi = figi
132
self.comission = comission
133
self.candle_handler = candle_handler
134
self.average_price = MoneyValue(units=0, nano=0)
135
136
async def get_portfolio(self, **kwargs) -> PortfolioResponse:
137
return PortfolioResponse(
138
positions=[
139
PortfolioPosition(
140
figi=self.figi,
141
quantity=Quotation(units=self.positions, nano=0),
142
average_position_price=self.average_price,
143
)
144
]
145
)
146
147
async def post_order(
148
self, quantity: int = 0, direction: OrderDirection = OrderDirection(0), **kwargs
149
):
150
last_price_quotation = (
151
(await self.candle_handler.get_last_prices(figi=[self.figi])).last_prices[0].price
152
)
153
last_price = quotation_to_float(last_price_quotation)
154
# TODO: Make it count average price respecting amount
155
if direction == OrderDirection.ORDER_DIRECTION_BUY:
156
self.positions += quantity
157
self.resources -= quantity * last_price + (self.comission * quantity * last_price)
158
self.average_price = MoneyValue(
159
units=last_price_quotation.units, nano=last_price_quotation.nano
160
)
161
elif direction == OrderDirection.ORDER_DIRECTION_SELL:
162
self.positions -= quantity
163
self.resources += quantity * last_price - (self.comission * quantity * last_price)
164
self.average_price = MoneyValue(units=0, nano=0)
165
166
167
@pytest.fixture(scope="session")
168
def candle_handler(test_config: IntervalStrategyConfig) -> CandleHandler:
169
return CandleHandler(test_config)
170
171
172
@pytest.fixture(scope="session")
173
def portfolio_handler(
174
figi: str, comission: float, candle_handler: CandleHandler
175
) -> PortfolioHandler:
176
return PortfolioHandler(figi, comission, candle_handler)
177
178
179
@pytest.fixture
180
def mock_client(
181
mocker: MockerFixture,
182
accounts_response: GetAccountsResponse,
183
orders_response: GetOrdersResponse,
184
candle_handler: CandleHandler,
185
portfolio_handler: PortfolioHandler,
186
figi: str,
187
client: Services,
188
test_config: IntervalStrategyConfig,
189
) -> TinkoffClient:
190
client_mock = mocker.patch("app.strategies.interval.IntervalStrategy.client")
191
client_mock.get_accounts = AsyncMock(return_value=accounts_response)
192
client_mock.get_orders = AsyncMock(return_value=orders_response)
193
194
client_mock.get_all_candles = candle_handler.get_all_candles
195
client_mock.get_last_prices = AsyncMock(side_effect=candle_handler.get_last_prices)
196
197
client_mock.get_portfolio = AsyncMock(side_effect=portfolio_handler.get_portfolio)
198
client_mock.post_order = AsyncMock(side_effect=portfolio_handler.post_order)
199
200
return client_mock
201
202