RAPTOR v18.4: Исправлена отчетность, активированы выходные

This commit is contained in:
root
2026-04-18 23:26:45 +03:00
commit ef0958239e
312 changed files with 54247 additions and 0 deletions

View File

View File

@@ -0,0 +1,275 @@
import random
import uuid
from datetime import timedelta
from typing import Any, Callable, Dict, Iterator, Type
from unittest.mock import Mock
import pytest
from pytest_freezegun import freeze_time
from t_tech.invest import (
Bond,
BondResponse,
BondsResponse,
Client,
CurrenciesResponse,
Currency,
CurrencyResponse,
Etf,
EtfResponse,
EtfsResponse,
Future,
FutureResponse,
FuturesResponse,
InstrumentIdType,
Share,
ShareResponse,
SharesResponse,
)
from t_tech.invest.caching.instruments_cache.instruments_cache import InstrumentsCache
from t_tech.invest.caching.instruments_cache.models import InstrumentsResponse
from t_tech.invest.caching.instruments_cache.settings import InstrumentsCacheSettings
from t_tech.invest.services import Services
def uid() -> str:
return uuid.uuid4().hex
@pytest.fixture()
def token() -> str:
return uid()
@pytest.fixture()
def real_services(token: str) -> Iterator[Services]:
with Client(token) as services:
yield services
def gen_meta_ids() -> Dict[str, str]:
return {"class_code": uid(), "figi": uid(), "ticker": uid(), "uid": uid()}
def gen_instruments(type_: Type, instrument_count: int = 10):
return [
type_(name=f"{type_.__name__}_{i}", **gen_meta_ids())
for i in range(instrument_count)
]
def gen_instruments_response(
response_type: Type, type_: Type, instrument_count: int = 10
):
return response_type(instruments=gen_instruments(type_, instrument_count))
@pytest.fixture()
def instrument_map():
return {
Etf: gen_instruments_response(EtfsResponse, Etf),
Share: gen_instruments_response(SharesResponse, Share),
Bond: gen_instruments_response(BondsResponse, Bond),
Currency: gen_instruments_response(CurrenciesResponse, Currency),
Future: gen_instruments_response(FuturesResponse, Future),
}
def mock_get_by(instrument_response: InstrumentsResponse, response_type):
type_to_field_extractor = {
InstrumentIdType.INSTRUMENT_ID_UNSPECIFIED: lambda i: i.figi,
InstrumentIdType.INSTRUMENT_ID_TYPE_FIGI: lambda i: i.figi,
InstrumentIdType.INSTRUMENT_ID_TYPE_TICKER: lambda i: i.ticker,
InstrumentIdType.INSTRUMENT_ID_TYPE_UID: lambda i: i.uid,
}
def _mock_get_by(
*,
id_type: InstrumentIdType = InstrumentIdType(0),
class_code: str = "",
id: str = "",
):
get_id = type_to_field_extractor[id_type]
def filter_(instrument):
return get_id(instrument) == id and instrument.class_code == class_code
(found_instrument,) = filter(filter_, instrument_response.instruments)
return response_type(instrument=found_instrument)
return Mock(wraps=_mock_get_by)
@pytest.fixture()
def mock_instruments_service(
real_services: Services,
mocker,
instrument_map,
) -> Services:
real_services.instruments = mocker.Mock(
wraps=real_services.instruments,
)
real_services.instruments.etfs.__name__ = "etfs"
real_services.instruments.etfs.return_value = instrument_map[Etf]
real_services.instruments.etf_by = mock_get_by(instrument_map[Etf], EtfResponse)
real_services.instruments.shares.__name__ = "shares"
real_services.instruments.shares.return_value = instrument_map[Share]
real_services.instruments.share_by = mock_get_by(
instrument_map[Share], ShareResponse
)
real_services.instruments.bonds.__name__ = "bonds"
real_services.instruments.bonds.return_value = instrument_map[Bond]
real_services.instruments.bond_by = mock_get_by(instrument_map[Bond], BondResponse)
real_services.instruments.currencies.__name__ = "currencies"
real_services.instruments.currencies.return_value = instrument_map[Currency]
real_services.instruments.currency_by = mock_get_by(
instrument_map[Currency], CurrencyResponse
)
real_services.instruments.futures.__name__ = "futures"
real_services.instruments.futures.return_value = instrument_map[Future]
real_services.instruments.future_by = mock_get_by(
instrument_map[Future], FutureResponse
)
return real_services
@pytest.fixture()
def mocked_services(
real_services: Services,
mock_instruments_service,
) -> Services:
return real_services
@pytest.fixture()
def settings() -> InstrumentsCacheSettings:
return InstrumentsCacheSettings()
@pytest.fixture()
def frozen_datetime():
with freeze_time() as frozen_datetime:
yield frozen_datetime
@pytest.fixture()
def instruments_cache(
settings: InstrumentsCacheSettings, mocked_services, frozen_datetime
) -> InstrumentsCache:
return InstrumentsCache(
settings=settings, instruments_service=mocked_services.instruments
)
@pytest.mark.parametrize(
("get_instruments_of_type", "get_instrument_of_type_by"),
[
(
lambda instruments: instruments.etfs,
lambda instruments: instruments.etf_by,
),
(
lambda instruments: instruments.shares,
lambda instruments: instruments.share_by,
),
(
lambda instruments: instruments.bonds,
lambda instruments: instruments.bond_by,
),
(
lambda instruments: instruments.currencies,
lambda instruments: instruments.currency_by,
),
(
lambda instruments: instruments.futures,
lambda instruments: instruments.future_by,
),
],
)
@pytest.mark.parametrize(
("id_type", "get_id"),
[
(
InstrumentIdType.INSTRUMENT_ID_UNSPECIFIED,
lambda instrument: instrument.figi,
),
(
InstrumentIdType.INSTRUMENT_ID_TYPE_FIGI,
lambda instrument: instrument.figi,
),
(
InstrumentIdType.INSTRUMENT_ID_TYPE_TICKER,
lambda instrument: instrument.ticker,
),
(
InstrumentIdType.INSTRUMENT_ID_TYPE_UID,
lambda instrument: instrument.uid,
),
],
)
class TestInstrumentCache:
def test_gets_from_net_then_cache(
self,
mocked_services: Services,
settings: InstrumentsCacheSettings,
instruments_cache: InstrumentsCache,
get_instruments_of_type,
get_instrument_of_type_by,
id_type: InstrumentIdType,
get_id: Callable[[Any], str],
):
get_instruments = get_instruments_of_type(mocked_services.instruments)
get_instrument_by = get_instrument_of_type_by(mocked_services.instruments)
get_instrument_by_cached = get_instrument_of_type_by(instruments_cache)
(inst,) = random.sample(get_instruments().instruments, k=1)
from_server = get_instrument_by(
id_type=id_type,
class_code=inst.class_code,
id=get_id(inst),
)
get_instrument_by.assert_called_once()
get_instrument_by.reset_mock()
from_cache = get_instrument_by_cached(
id_type=id_type,
class_code=inst.class_code,
id=get_id(inst),
)
get_instrument_by.assert_not_called()
assert str(from_server) == str(from_cache)
@pytest.mark.parametrize(
"settings", [InstrumentsCacheSettings(ttl=timedelta(milliseconds=10))]
)
def test_refreshes_on_ttl(
self,
mocked_services: Services,
settings: InstrumentsCacheSettings,
instruments_cache: InstrumentsCache,
get_instruments_of_type,
get_instrument_of_type_by,
id_type: InstrumentIdType,
get_id: Callable[[Any], str],
frozen_datetime,
):
get_instruments = get_instruments_of_type(mocked_services.instruments)
get_instrument_by_cached = get_instrument_of_type_by(instruments_cache)
get_instruments.assert_called_once()
(inst,) = random.sample(get_instruments().instruments, k=1)
get_instruments.reset_mock()
frozen_datetime.tick(timedelta(seconds=10))
_ = get_instrument_by_cached(
id_type=id_type,
class_code=inst.class_code,
id=get_id(inst),
)
get_instruments.assert_called_once()

View File

@@ -0,0 +1,26 @@
from datetime import timedelta
from cachetools import TTLCache as StandardTTLCache
from pytest_freezegun import freeze_time
from t_tech.invest.caching.overrides import TTLCache as OverridenTTLCache
class TestTTLCache:
def _assert_ttl_cache(self, ttl_cache_class, expires):
with freeze_time() as frozen_datetime:
ttl = ttl_cache_class(
maxsize=10,
ttl=1,
)
ttl.update({"1": 1})
assert ttl.keys()
frozen_datetime.tick(timedelta(seconds=10000))
assert not ttl.keys() == expires
def test_overriden_cache(self):
self._assert_ttl_cache(OverridenTTLCache, expires=True)
def test_standard_cache(self):
self._assert_ttl_cache(StandardTTLCache, expires=False)

View File

@@ -0,0 +1,557 @@
import logging
import tempfile
import uuid
from datetime import datetime, timedelta
from pathlib import Path
from typing import List, Optional, Tuple
import pytest
from t_tech.invest import (
CandleInterval,
Client,
GetCandlesResponse,
HistoricCandle,
Quotation,
)
from t_tech.invest.caching.market_data_cache.cache import MarketDataCache
from t_tech.invest.caching.market_data_cache.cache_settings import (
FileMetaData,
MarketDataCacheSettings,
meta_file_context,
)
from t_tech.invest.caching.market_data_cache.instrument_market_data_storage import (
InstrumentMarketDataStorage,
)
from t_tech.invest.schemas import CandleSource
from t_tech.invest.services import MarketDataService
from t_tech.invest.utils import (
candle_interval_to_timedelta,
ceil_datetime,
floor_datetime,
now,
)
logging.basicConfig(format="%(asctime)s %(levelname)s:%(message)s", level=logging.DEBUG)
logger = logging.getLogger(__name__)
def get_historical_candle(
time: datetime,
is_complete: bool = True,
candle_source: Optional[CandleSource] = None,
):
quotation = Quotation(units=100, nano=0)
if candle_source is None:
candle_source = CandleSource.CANDLE_SOURCE_EXCHANGE
return HistoricCandle(
open=quotation,
high=quotation,
low=quotation,
close=quotation,
volume=100,
time=time,
is_complete=is_complete,
candle_source=candle_source,
volume_buy=45,
volume_sell=55,
)
def get_candles_response(
start: datetime,
end: datetime,
interval: CandleInterval,
candle_source_type: Optional[CandleSource] = None,
):
delta = candle_interval_to_timedelta(interval)
start_ceil = ceil_datetime(start.replace(second=0, microsecond=0), delta)
current_time = start_ceil
candles = []
while current_time <= end:
candles.append(
get_historical_candle(current_time, candle_source=candle_source_type)
)
current_time += delta
current_time.replace(second=0, microsecond=0)
if floor_datetime(end, delta) < end < ceil_datetime(end, delta):
candles.append(get_historical_candle(end, is_complete=False))
return GetCandlesResponse(candles=candles)
@pytest.fixture()
def market_data_service(mocker) -> MarketDataService:
service = mocker.Mock(spec=MarketDataService)
def _get_candles(
figi: str,
from_: datetime,
to: datetime,
interval: CandleInterval = CandleInterval(0),
instrument_id: str = "",
candle_source_type: Optional[CandleSource] = None,
) -> GetCandlesResponse:
return get_candles_response(
start=from_,
end=to,
interval=interval,
candle_source_type=candle_source_type,
)
service.get_candles = _get_candles
service.get_candles = mocker.Mock(wraps=service.get_candles)
return service
@pytest.fixture()
def client(mocker, market_data_service):
with Client(mocker.Mock()) as client:
client.market_data = market_data_service
yield client
@pytest.fixture()
def settings() -> MarketDataCacheSettings:
return MarketDataCacheSettings(base_cache_dir=Path(tempfile.gettempdir()))
@pytest.fixture()
def market_data_cache(settings: MarketDataCacheSettings, client) -> MarketDataCache:
return MarketDataCache(settings=settings, services=client)
@pytest.fixture()
def log(caplog): # noqa: PT004
caplog.set_level(logging.DEBUG)
@pytest.fixture()
def figi():
return uuid.uuid4().hex
class TestCachedLoad:
def test_loads_from_net(self, market_data_cache: MarketDataCache, figi: str):
result = list(
market_data_cache.get_all_candles(
figi=figi,
from_=now() - timedelta(days=30),
interval=CandleInterval.CANDLE_INTERVAL_HOUR,
)
)
assert result
def test_loads_from_net_then_from_cache(
self,
market_data_service: MarketDataService,
market_data_cache: MarketDataCache,
log,
figi: str,
):
interval = CandleInterval.CANDLE_INTERVAL_HOUR
from_, to = self._get_date_point_by_index(0, 3, interval=interval)
from_net = list(
market_data_cache.get_all_candles(
figi=figi,
from_=from_,
to=to,
interval=interval,
)
)
self.assert_in_range(from_net, start=from_, end=to, interval=interval)
market_data_service.get_candles.reset_mock()
from_cache = list(
market_data_cache.get_all_candles(
figi=figi,
from_=from_,
to=to,
interval=interval,
)
)
market_data_service.get_candles.assert_not_called()
self.assert_in_range(from_cache, start=from_, end=to, interval=interval)
assert len(from_net) == len(from_cache)
for cached_candle, net_candle in zip(from_cache, from_net):
assert cached_candle.__repr__() == net_candle.__repr__()
def test_loads_from_cache_and_left_from_net(
self,
market_data_service: MarketDataService,
market_data_cache: MarketDataCache,
figi: str,
):
interval = CandleInterval.CANDLE_INTERVAL_DAY
from_, to = self._get_date_point_by_index(0, 30, interval=interval)
from_net = list(
market_data_cache.get_all_candles(
figi=figi,
from_=from_,
to=to,
interval=interval,
)
)
self.assert_in_range(from_net, start=from_, end=to, interval=interval)
from_cache = list(
market_data_cache.get_all_candles(
figi=figi,
from_=from_,
to=to,
interval=interval,
)
)
self.assert_in_range(from_cache, start=from_, end=to, interval=interval)
market_data_service.get_candles.reset_mock()
from_early_uncached = from_ - timedelta(days=7)
cache_and_net = list(
market_data_cache.get_all_candles(
figi=figi,
from_=from_early_uncached,
to=to,
interval=interval,
)
)
assert len(market_data_service.get_candles.mock_calls) > 0
self.assert_in_range(
cache_and_net, start=from_early_uncached, end=to, interval=interval
)
def assert_distinct_candles(
self, candles: List[HistoricCandle], interval_delta: timedelta
):
for candle1, candle2 in zip(candles[:-1], candles[1:-1]):
diff_delta = candle2.time - candle1.time
assert timedelta() < diff_delta <= interval_delta
def assert_in_range(self, result_candles, start, end, interval):
delta = candle_interval_to_timedelta(interval)
assert result_candles[0].time == ceil_datetime(
start, delta
), "start time assertion error"
assert result_candles[-1].time == end, "end time assertion error"
for candle in result_candles:
assert start <= candle.time <= end
self.assert_distinct_candles(result_candles, delta)
def test_loads_from_cache_and_right_from_net(
self,
market_data_service: MarketDataService,
market_data_cache: MarketDataCache,
figi: str,
):
to = now().replace(second=0, microsecond=0)
from_ = to - timedelta(days=30)
interval = CandleInterval.CANDLE_INTERVAL_DAY
from_net = list(
market_data_cache.get_all_candles(
figi=figi,
from_=from_,
to=to,
interval=interval,
)
)
self.assert_in_range(from_net, start=from_, end=to, interval=interval)
market_data_service.get_candles.reset_mock()
to_later_uncached = to + timedelta(days=7)
cache_and_net = list(
market_data_cache.get_all_candles(
figi=figi,
from_=from_,
to=to_later_uncached,
interval=interval,
)
)
assert len(market_data_service.get_candles.mock_calls) > 0
self.assert_in_range(
cache_and_net, start=from_, end=to_later_uncached, interval=interval
)
def assert_has_cached_ranges(self, cache_storage, ranges):
meta_file = cache_storage._get_metafile(cache_storage._meta_path)
with meta_file_context(meta_file) as meta:
meta: FileMetaData = meta
assert len(meta.cached_range_in_file) == len(ranges)
assert set(meta.cached_range_in_file.keys()) == set(ranges)
def assert_file_count(self, cache_storage, count):
cached_ls = list(cache_storage._meta_path.parent.glob("*"))
assert len(cached_ls) == count
def test_loads_cache_miss(
self,
market_data_service: MarketDataService,
market_data_cache: MarketDataCache,
settings: MarketDataCacheSettings,
figi: str,
):
interval = CandleInterval.CANDLE_INTERVAL_DAY
# [A request B]
# [A cached B] [C request D]
A, B, C, D = self._get_date_point_by_index(0, 3, 6, 9)
self.get_by_range_and_assert_has_cache(
range=(A, B),
has_from_net=True,
figi=figi,
interval=interval,
market_data_cache=market_data_cache,
market_data_service=market_data_service,
)
self.get_by_range_and_assert_has_cache(
range=(C, D),
has_from_net=True,
figi=figi,
interval=interval,
market_data_cache=market_data_cache,
market_data_service=market_data_service,
)
cache_storage = InstrumentMarketDataStorage(
figi=figi, interval=interval, settings=settings
)
self.assert_has_cached_ranges(cache_storage, [(A, B), (C, D)])
self.assert_file_count(cache_storage, 3)
def _get_date_point_by_index(
self, *idx, interval=CandleInterval.CANDLE_INTERVAL_DAY
):
delta = candle_interval_to_timedelta(interval)
x0 = ceil_datetime(now(), delta).replace(second=0, microsecond=0)
result = []
for id_ in idx:
result.append(x0 + id_ * delta)
return result
def test_loads_cache_merge_out(
self,
market_data_service: MarketDataService,
market_data_cache: MarketDataCache,
settings: MarketDataCacheSettings,
log,
figi: str,
):
interval = CandleInterval.CANDLE_INTERVAL_DAY
# [A request B]
# [A cached B] [C request D]
# [A cached B] [C cached D]
# [E request F]
# [E cached F]
E, A, B, C, D, F = self._get_date_point_by_index(0, 1, 3, 4, 6, 7)
self.get_by_range_and_assert_has_cache(
range=(A, B),
has_from_net=True,
figi=figi,
interval=interval,
market_data_cache=market_data_cache,
market_data_service=market_data_service,
)
self.get_by_range_and_assert_has_cache(
range=(C, D),
has_from_net=True,
figi=figi,
interval=interval,
market_data_cache=market_data_cache,
market_data_service=market_data_service,
)
self.get_by_range_and_assert_has_cache(
range=(E, F),
has_from_net=True,
figi=figi,
interval=interval,
market_data_cache=market_data_cache,
market_data_service=market_data_service,
)
cache_storage = InstrumentMarketDataStorage(
figi=figi, interval=interval, settings=settings
)
self.assert_has_cached_ranges(cache_storage, [(E, F)])
self.assert_file_count(cache_storage, 2)
def get_by_range_and_assert_has_cache(
self,
range: Tuple[datetime, datetime],
has_from_net: bool,
figi: str,
interval: CandleInterval,
market_data_cache: MarketDataCache,
market_data_service: MarketDataService,
):
start, end = range
result = list(
market_data_cache.get_all_candles(
figi=figi,
from_=start,
to=end,
interval=interval,
)
)
self.assert_in_range(result, start=start, end=end, interval=interval)
if has_from_net:
assert (
len(market_data_service.get_candles.mock_calls) > 0
), "Net was not used"
else:
assert len(market_data_service.get_candles.mock_calls) == 0, "Net was used"
market_data_service.get_candles.reset_mock()
def get_by_range_and_assert_ranges(
self,
request_range: Tuple[datetime, datetime],
from_cache_ranges: List[Tuple[datetime, datetime]],
from_net_ranges: List[Tuple[datetime, datetime]],
figi: str,
interval: CandleInterval,
market_data_cache: MarketDataCache,
market_data_service: MarketDataService,
):
start, end = request_range
result = list(
market_data_cache.get_all_candles(
figi=figi,
from_=start,
to=end,
interval=interval,
)
)
net_calls = market_data_service.get_candles.mock_calls
assert len(net_calls) == len(from_net_ranges)
for actual_net_call, expected_net_range in zip(net_calls, from_net_ranges):
kwargs = actual_net_call.kwargs
actual_net_range = kwargs["from_"], kwargs["to"]
assert actual_net_range == expected_net_range
self.assert_in_range(result, start, end, interval)
market_data_service.get_candles.reset_mock()
def test_loads_cache_merge_out_right(
self,
market_data_service: MarketDataService,
market_data_cache: MarketDataCache,
settings: MarketDataCacheSettings,
log,
figi: str,
):
interval = CandleInterval.CANDLE_INTERVAL_DAY
# [A request B]
# [A cached B] [C request D]
# [A cached B] [C cached D]
# [E request F]
# [A cached F]
A, E, B, C, D, F = self._get_date_point_by_index(0, 1, 3, 4, 6, 7)
self.get_by_range_and_assert_has_cache(
range=(A, B),
has_from_net=True,
figi=figi,
interval=interval,
market_data_cache=market_data_cache,
market_data_service=market_data_service,
)
self.get_by_range_and_assert_has_cache(
range=(C, D),
has_from_net=True,
figi=figi,
interval=interval,
market_data_cache=market_data_cache,
market_data_service=market_data_service,
)
self.get_by_range_and_assert_has_cache(
range=(E, F),
has_from_net=True,
figi=figi,
interval=interval,
market_data_cache=market_data_cache,
market_data_service=market_data_service,
)
cache_storage = InstrumentMarketDataStorage(
figi=figi, interval=interval, settings=settings
)
self.assert_has_cached_ranges(cache_storage, [(A, F)])
self.assert_file_count(cache_storage, 2)
def test_loads_cache_merge_in_right(
self,
market_data_service: MarketDataService,
market_data_cache: MarketDataCache,
settings: MarketDataCacheSettings,
log,
figi: str,
):
interval = CandleInterval.CANDLE_INTERVAL_DAY
# [A request B]
# [A cached B] [C request D]
# [A cached B] [C cached D]
# [E request F]
# [A cached B] [C cached F]
A, B, C, E, D, F = self._get_date_point_by_index(0, 1, 3, 4, 6, 7)
self.get_by_range_and_assert_has_cache(
range=(A, B),
has_from_net=True,
figi=figi,
interval=interval,
market_data_cache=market_data_cache,
market_data_service=market_data_service,
)
self.get_by_range_and_assert_has_cache(
range=(C, D),
has_from_net=True,
figi=figi,
interval=interval,
market_data_cache=market_data_cache,
market_data_service=market_data_service,
)
self.get_by_range_and_assert_has_cache(
range=(E, F),
has_from_net=True,
figi=figi,
interval=interval,
market_data_cache=market_data_cache,
market_data_service=market_data_service,
)
cache_storage = InstrumentMarketDataStorage(
figi=figi, interval=interval, settings=settings
)
self.assert_has_cached_ranges(cache_storage, [(A, B), (C, F)])
self.assert_file_count(cache_storage, 3)
def test_creates_files_with_correct_extensions(
self,
market_data_service: MarketDataService,
market_data_cache: MarketDataCache,
settings: MarketDataCacheSettings,
log,
figi: str,
):
interval = CandleInterval.CANDLE_INTERVAL_HOUR
list(
market_data_cache.get_all_candles(
figi=figi,
from_=now() - timedelta(days=30),
interval=interval,
)
)
cache_storage = InstrumentMarketDataStorage(
figi=figi, interval=interval, settings=settings
)
cached_ls = list(cache_storage._meta_path.parent.glob("*"))
assert len(cached_ls) == 2
assert any(
str(file).endswith(f".{settings.format_extension.value}")
for file in cached_ls
)
assert any(
str(file).endswith(f".{settings.meta_extension}") for file in cached_ls
)

View File

@@ -0,0 +1,153 @@
# pylint:disable=redefined-outer-name
# pylint:disable=too-many-arguments
from copy import copy
from datetime import timedelta
import pytest
from t_tech.invest.schemas import (
CandleInterval,
GetCandlesResponse,
HistoricCandle,
Quotation,
)
from t_tech.invest.services import MarketDataService, Services
from t_tech.invest.utils import now
@pytest.fixture()
def figi():
return "figi"
@pytest.fixture()
def from_():
return now() - timedelta(days=31)
@pytest.fixture()
def to():
return now()
@pytest.fixture()
def historical_candle():
quotation = Quotation(units=100, nano=0)
return HistoricCandle(
open=quotation,
high=quotation,
low=quotation,
close=quotation,
volume=100,
time=now(),
is_complete=False,
)
@pytest.fixture()
def candles_response(historical_candle):
return GetCandlesResponse(candles=[historical_candle])
@pytest.fixture()
def market_data_service(mocker):
return mocker.Mock(spec=MarketDataService)
class TestGetAllCandles:
@pytest.mark.parametrize(
("interval", "call_count"),
[
(CandleInterval.CANDLE_INTERVAL_5_SEC, 224),
(CandleInterval.CANDLE_INTERVAL_10_SEC, 224),
(CandleInterval.CANDLE_INTERVAL_30_SEC, 38),
(CandleInterval.CANDLE_INTERVAL_1_MIN, 31),
(CandleInterval.CANDLE_INTERVAL_2_MIN, 31),
(CandleInterval.CANDLE_INTERVAL_3_MIN, 31),
(CandleInterval.CANDLE_INTERVAL_5_MIN, 31),
(CandleInterval.CANDLE_INTERVAL_10_MIN, 31),
(CandleInterval.CANDLE_INTERVAL_15_MIN, 31),
(CandleInterval.CANDLE_INTERVAL_HOUR, 5),
(CandleInterval.CANDLE_INTERVAL_2_HOUR, 5),
(CandleInterval.CANDLE_INTERVAL_4_HOUR, 5),
(CandleInterval.CANDLE_INTERVAL_DAY, 1),
(CandleInterval.CANDLE_INTERVAL_WEEK, 1),
(CandleInterval.CANDLE_INTERVAL_MONTH, 1),
],
)
@pytest.mark.parametrize("use_to", [True, False])
@pytest.mark.freeze_time("2023-10-21")
def test_get_all_candles(
self,
figi,
mocker,
market_data_service,
from_,
to,
candles_response,
interval,
call_count,
use_to,
):
services = mocker.Mock()
services.market_data = market_data_service
market_data_service.get_candles.return_value = candles_response
to_kwarg = {}
if use_to:
to_kwarg = {"to": to}
result = list(
Services.get_all_candles(
services,
figi=figi,
interval=interval,
from_=from_,
**to_kwarg,
)
)
assert result == candles_response.candles
assert market_data_service.get_candles.call_count == call_count
@pytest.mark.parametrize(
"interval",
[
*[
interval
for interval in CandleInterval
if interval != CandleInterval.CANDLE_INTERVAL_UNSPECIFIED
],
],
)
def test_deduplicates(
self,
figi,
mocker,
market_data_service,
from_,
to,
candles_response,
interval,
historical_candle,
):
services = mocker.Mock()
services.market_data = market_data_service
def _get_duplicated_candle_response(*args, **kwargs):
return GetCandlesResponse(
candles=[copy(historical_candle) for _ in range(3)]
)
market_data_service.get_candles = _get_duplicated_candle_response
candles = list(
Services.get_all_candles(
services,
figi=figi,
interval=interval,
from_=from_,
to=to,
)
)
assert len(candles) == 1

View File

@@ -0,0 +1,120 @@
from datetime import datetime
from typing import Tuple
import pytest
from t_tech.invest import CandleInterval
from t_tech.invest.utils import round_datetime_range
@pytest.mark.parametrize(
("interval", "date_range", "expected_range"),
[
(
CandleInterval.CANDLE_INTERVAL_1_MIN,
(
datetime(
year=2023, month=1, day=1, hour=1, minute=1, second=1, microsecond=1
),
datetime(
year=2023, month=1, day=2, hour=1, minute=1, second=1, microsecond=1
),
),
(
datetime(
year=2023, month=1, day=1, hour=1, minute=1, second=0, microsecond=0
),
datetime(
year=2023, month=1, day=2, hour=1, minute=2, second=0, microsecond=0
),
),
),
(
CandleInterval.CANDLE_INTERVAL_HOUR,
(
datetime(
year=2023, month=1, day=1, hour=1, minute=1, second=1, microsecond=1
),
datetime(
year=2023, month=1, day=2, hour=1, minute=1, second=1, microsecond=1
),
),
(
datetime(
year=2023, month=1, day=1, hour=1, minute=0, second=0, microsecond=0
),
datetime(
year=2023, month=1, day=2, hour=2, minute=0, second=0, microsecond=0
),
),
),
(
CandleInterval.CANDLE_INTERVAL_DAY,
(
datetime(
year=2023, month=1, day=1, hour=1, minute=1, second=1, microsecond=1
),
datetime(
year=2023, month=1, day=2, hour=1, minute=1, second=1, microsecond=1
),
),
(
datetime(
year=2023, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
),
datetime(
year=2023, month=1, day=3, hour=0, minute=0, second=0, microsecond=0
),
),
),
(
CandleInterval.CANDLE_INTERVAL_WEEK,
(
datetime(
year=2023, month=1, day=1, hour=1, minute=1, second=1, microsecond=1
),
datetime(
year=2023, month=1, day=2, hour=1, minute=1, second=1, microsecond=1
),
),
(
datetime(
year=2023, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
),
datetime(
year=2023, month=1, day=9, hour=0, minute=0, second=0, microsecond=0
),
),
),
(
CandleInterval.CANDLE_INTERVAL_MONTH,
(
datetime(
year=2023, month=1, day=1, hour=1, minute=1, second=1, microsecond=1
),
datetime(
year=2023, month=1, day=2, hour=1, minute=1, second=1, microsecond=1
),
),
(
datetime(
year=2023, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
),
datetime(
year=2023, month=2, day=1, hour=0, minute=0, second=0, microsecond=0
),
),
),
],
)
def test_round_datetime_range(
interval: CandleInterval,
date_range: Tuple[datetime, datetime],
expected_range: Tuple[datetime, datetime],
):
actual_range = round_datetime_range(
date_range=date_range,
interval=interval,
)
assert actual_range == expected_range

View File

@@ -0,0 +1,132 @@
# pylint: disable=redefined-outer-name,unused-variable
import datetime
import os
import tempfile
from datetime import timedelta
from pathlib import Path
from typing import Dict, Iterable
import pytest
from t_tech.invest import CandleInterval
from t_tech.invest.caching.market_data_cache.cache import MarketDataCache
from t_tech.invest.caching.market_data_cache.cache_settings import (
MarketDataCacheSettings,
)
from t_tech.invest.sandbox.client import SandboxClient
from t_tech.invest.utils import now
@pytest.fixture()
def sandbox_service():
with SandboxClient(token=os.environ["INVEST_SANDBOX_TOKEN"]) as client:
yield client
PROGRAMMERS_DAY = datetime.datetime(
2023, 1, 1, tzinfo=datetime.timezone.utc
) + timedelta(days=255)
@pytest.mark.skipif(
os.environ.get("INVEST_SANDBOX_TOKEN") is None,
reason="INVEST_SANDBOX_TOKEN should be specified",
)
@pytest.mark.skip("todo fix")
class TestSandboxCachedLoad:
@pytest.mark.parametrize(
"calls_kwargs",
[
({"from_": now() - timedelta(days=8)},),
(
{
"from_": datetime.datetime(
2023, 9, 1, 0, 0, tzinfo=datetime.timezone.utc
),
"to": datetime.datetime(
2023, 9, 5, 0, 0, tzinfo=datetime.timezone.utc
),
},
),
(
{
"from_": now() - timedelta(days=6),
},
{
"from_": now() - timedelta(days=10),
"to": now() - timedelta(days=7),
},
{
"from_": now() - timedelta(days=11),
"to": now() - timedelta(days=5),
},
),
(
{
"from_": PROGRAMMERS_DAY - timedelta(days=6),
},
{
"from_": PROGRAMMERS_DAY - timedelta(days=6),
},
{
"from_": PROGRAMMERS_DAY - timedelta(days=6),
},
{
"from_": PROGRAMMERS_DAY - timedelta(days=6),
},
),
(
{
"from_": PROGRAMMERS_DAY - timedelta(days=6),
},
{
"from_": PROGRAMMERS_DAY - timedelta(days=5),
},
{
"from_": PROGRAMMERS_DAY - timedelta(days=4),
},
{
"from_": PROGRAMMERS_DAY - timedelta(days=3),
},
),
(
{
"from_": PROGRAMMERS_DAY - timedelta(days=6),
"to": PROGRAMMERS_DAY,
},
{
"from_": PROGRAMMERS_DAY - timedelta(days=5),
"to": PROGRAMMERS_DAY,
},
{
"from_": PROGRAMMERS_DAY - timedelta(days=4),
"to": PROGRAMMERS_DAY,
},
{
"from_": PROGRAMMERS_DAY - timedelta(days=3),
"to": PROGRAMMERS_DAY,
},
),
],
)
def test_same_from_net_and_cache(
self, sandbox_service, calls_kwargs: Iterable[Dict[str, datetime.datetime]]
):
settings = MarketDataCacheSettings(base_cache_dir=Path(tempfile.gettempdir()))
market_data_cache = MarketDataCache(settings=settings, services=sandbox_service)
figi = "BBG004730N88"
for date_range_kwargs in calls_kwargs:
call_kwargs = dict(
figi=figi,
interval=CandleInterval.CANDLE_INTERVAL_DAY,
**date_range_kwargs,
)
candles_from_cache = list(market_data_cache.get_all_candles(**call_kwargs))
candles_from_net = list(sandbox_service.get_all_candles(**call_kwargs))
assert candles_from_cache
assert candles_from_net
assert candles_from_cache == candles_from_net, (
candles_from_cache,
candles_from_net,
)

View File

@@ -0,0 +1,72 @@
from datetime import datetime
from unittest.mock import ANY, call
import pytest
import pytest_asyncio
from t_tech.invest import CandleInterval, GetCandlesResponse
from t_tech.invest.async_services import AsyncServices, MarketDataService
@pytest_asyncio.fixture
async def marketdata_service(mocker) -> MarketDataService:
return mocker.create_autospec(MarketDataService)
@pytest_asyncio.fixture
async def async_services(
mocker, marketdata_service: MarketDataService
) -> AsyncServices:
async_services = mocker.create_autospec(AsyncServices)
async_services.market_data = marketdata_service
return async_services
class TestAsyncMarketData:
@pytest.mark.asyncio
@pytest.mark.parametrize(
"candle_interval,from_,to,expected",
[
(
CandleInterval.CANDLE_INTERVAL_DAY,
datetime(2020, 1, 1),
datetime(2020, 1, 2),
1,
),
(
CandleInterval.CANDLE_INTERVAL_DAY,
datetime(2020, 1, 1),
datetime(2021, 3, 3),
2,
),
],
)
async def test_get_candles(
self,
async_services: AsyncServices,
marketdata_service: MarketDataService,
candle_interval: CandleInterval,
from_: datetime,
to: datetime,
expected: int,
):
marketdata_service.get_candles.return_value = GetCandlesResponse(candles=[])
[
candle
async for candle in AsyncServices.get_all_candles(
async_services, interval=candle_interval, from_=from_, to=to
)
]
marketdata_service.get_candles.assert_has_calls(
[
call(
from_=ANY,
to=ANY,
interval=candle_interval,
candle_source_type=None,
figi="",
instrument_id="",
)
for _ in range(expected)
]
)

View File

@@ -0,0 +1,25 @@
from datetime import timedelta
import pytest
from t_tech.invest import CandleInterval
from t_tech.invest.utils import (
candle_interval_to_timedelta,
ceil_datetime,
floor_datetime,
now,
)
@pytest.fixture(params=[i.value for i in CandleInterval])
def interval(request) -> timedelta:
return candle_interval_to_timedelta(request.param)
def test_floor_ceil(interval: timedelta):
now_ = now()
a, b = floor_datetime(now_, interval), ceil_datetime(now_, interval)
assert a < b
assert b - a == interval

View File

@@ -0,0 +1,221 @@
# pylint: disable=redefined-outer-name,unused-variable
import os
from unittest import mock
import pytest
from t_tech.invest import (
Client,
InstrumentIdType,
InstrumentRequest,
InstrumentsRequest,
InstrumentStatus,
)
from t_tech.invest.schemas import StructuredNote
from t_tech.invest.services import InstrumentsService
@pytest.fixture()
def instruments_service():
return mock.MagicMock(spec=InstrumentsService)
@pytest.fixture()
def instruments_client_service():
with Client(token=os.environ["INVEST_SANDBOX_TOKEN"]) as client:
yield client.instruments
def test_trading_schedules(instruments_service):
responce = instruments_service.trading_schedules( # noqa: F841
exchange=mock.Mock(),
from_=mock.Mock(),
to=mock.Mock(),
)
instruments_service.trading_schedules.assert_called_once()
def test_bond_by(instruments_service):
responce = instruments_service.bond_by( # noqa: F841
id_type=mock.Mock(),
class_code=mock.Mock(),
id=mock.Mock(),
)
instruments_service.bond_by.assert_called_once()
def test_bonds(instruments_service):
responce = instruments_service.bonds( # noqa: F841
instrument_status=mock.Mock(),
)
instruments_service.bonds.assert_called_once()
def test_currency_by(instruments_service):
responce = instruments_service.currency_by( # noqa: F841
id_type=mock.Mock(),
class_code=mock.Mock(),
id=mock.Mock(),
)
instruments_service.currency_by.assert_called_once()
def test_currencies(instruments_service):
responce = instruments_service.currencies( # noqa: F841
instrument_status=mock.Mock(),
)
instruments_service.currencies.assert_called_once()
def test_etf_by(instruments_service):
responce = instruments_service.etf_by( # noqa: F841
id_type=mock.Mock(),
class_code=mock.Mock(),
id=mock.Mock(),
)
instruments_service.etf_by.assert_called_once()
def test_etfs(instruments_service):
responce = instruments_service.etfs( # noqa: F841
instrument_status=mock.Mock(),
)
instruments_service.etfs.assert_called_once()
def test_future_by(instruments_service):
responce = instruments_service.future_by( # noqa: F841
id_type=mock.Mock(),
class_code=mock.Mock(),
id=mock.Mock(),
)
instruments_service.future_by.assert_called_once()
def test_futures(instruments_service):
responce = instruments_service.futures( # noqa: F841
instrument_status=mock.Mock(),
)
instruments_service.futures.assert_called_once()
def test_share_by(instruments_service):
responce = instruments_service.share_by( # noqa: F841
id_type=mock.Mock(),
class_code=mock.Mock(),
id=mock.Mock(),
)
instruments_service.share_by.assert_called_once()
def test_shares(instruments_service):
responce = instruments_service.shares( # noqa: F841
instrument_status=mock.Mock(),
)
instruments_service.shares.assert_called_once()
def test_get_accrued_interests(instruments_service):
responce = instruments_service.get_accrued_interests( # noqa: F841
figi=mock.Mock(),
from_=mock.Mock(),
to=mock.Mock(),
)
instruments_service.get_accrued_interests.assert_called_once()
def test_get_futures_margin(instruments_service):
responce = instruments_service.get_futures_margin( # noqa: F841
figi=mock.Mock(),
)
instruments_service.get_futures_margin.assert_called_once()
def test_get_instrument_by(instruments_service):
responce = instruments_service.get_instrument_by( # noqa: F841
id_type=mock.Mock(),
class_code=mock.Mock(),
id=mock.Mock(),
)
instruments_service.get_instrument_by.assert_called_once()
def test_get_dividends(instruments_service):
responce = instruments_service.get_dividends( # noqa: F841
figi=mock.Mock(),
from_=mock.Mock(),
to=mock.Mock(),
)
instruments_service.get_dividends.assert_called_once()
def test_get_favorites(instruments_service):
response = instruments_service.get_favorites() # noqa: F841
instruments_service.get_favorites.assert_called_once()
def test_get_favorites_with_group(instruments_service):
response = instruments_service.get_favorites(group_id=mock.Mock()) # noqa: F841
instruments_service.get_favorites.assert_called_once()
def test_edit_favorites(instruments_service):
response = instruments_service.edit_favorites( # noqa: F841
instruments=mock.Mock(),
action_type=mock.Mock(),
)
instruments_service.edit_favorites.assert_called_once()
def test_create_favorite_group(instruments_service):
request = mock.Mock()
response = instruments_service.create_favorite_group( # noqa: F841
request=request,
)
instruments_service.create_favorite_group.assert_called_once_with(request=request)
def test_delete_favorite_group(instruments_service):
request = mock.Mock()
response = instruments_service.delete_favorite_group( # noqa: F841
request=request,
)
instruments_service.delete_favorite_group.assert_called_once_with(request=request)
def test_get_favorite_groups(instruments_service):
request = mock.Mock()
response = instruments_service.get_favorite_groups( # noqa: F841
request=request,
)
instruments_service.get_favorite_groups.assert_called_once_with(request=request)
def test_get_risk_rates(instruments_service):
request = mock.Mock()
response = instruments_service.get_risk_rates( # noqa: F841
request=request,
)
instruments_service.get_risk_rates.assert_called_once_with(request=request)
def test_get_insider_deals(instruments_service):
request = mock.Mock()
response = instruments_service.get_insider_deals(request=request) # noqa: F841
instruments_service.get_insider_deals.assert_called_once_with(request=request)
def test_structured_notes(instruments_client_service):
request = InstrumentsRequest(
instrument_status=InstrumentStatus.INSTRUMENT_STATUS_ALL
)
response = instruments_client_service.structured_notes(request=request)
assert len(response.instruments) > 0
def test_structured_notes_by(instruments_client_service):
request = InstrumentRequest(
id_type=InstrumentIdType.INSTRUMENT_ID_TYPE_FIGI, id="BBG012S2DCJ8"
)
response = instruments_client_service.structured_note_by(request=request)
assert isinstance(response.instrument, StructuredNote)

View File

@@ -0,0 +1,85 @@
# pylint: disable=redefined-outer-name,unused-variable
# pylint: disable=protected-access
from unittest import mock
import pytest
from google.protobuf.json_format import MessageToDict
from t_tech.invest._grpc_helpers import dataclass_to_protobuff
from t_tech.invest.grpc import marketdata_pb2
from t_tech.invest.schemas import (
GetMySubscriptions,
MarketDataRequest,
SubscribeTradesRequest,
SubscriptionAction,
TradeInstrument,
)
from t_tech.invest.services import MarketDataService
@pytest.fixture()
def market_data_service():
return mock.create_autospec(spec=MarketDataService)
def test_get_candles(market_data_service):
response = market_data_service.get_candles( # noqa: F841
figi=mock.Mock(),
from_=mock.Mock(),
to=mock.Mock(),
interval=mock.Mock(),
)
market_data_service.get_candles.assert_called_once()
def test_get_last_prices(market_data_service):
response = market_data_service.get_last_prices(figi=mock.Mock()) # noqa: F841
market_data_service.get_last_prices.assert_called_once()
def test_get_order_book(market_data_service):
response = market_data_service.get_order_book( # noqa: F841
figi=mock.Mock(), depth=mock.Mock()
)
market_data_service.get_order_book.assert_called_once()
def test_get_trading_status(market_data_service):
response = market_data_service.get_trading_status(figi=mock.Mock()) # noqa: F841
market_data_service.get_trading_status.assert_called_once()
def test_subscribe_trades_request():
expected = marketdata_pb2.MarketDataRequest(
subscribe_trades_request=marketdata_pb2.SubscribeTradesRequest(
instruments=[marketdata_pb2.TradeInstrument(figi="figi")],
subscription_action=SubscriptionAction.SUBSCRIPTION_ACTION_SUBSCRIBE,
with_open_interest=True,
)
)
result = dataclass_to_protobuff(
MarketDataRequest(
subscribe_trades_request=SubscribeTradesRequest(
instruments=[TradeInstrument(figi="figi")],
subscription_action=SubscriptionAction.SUBSCRIPTION_ACTION_SUBSCRIBE,
with_open_interest=True,
)
),
marketdata_pb2.MarketDataRequest(),
)
assert MessageToDict(result) == MessageToDict(expected)
def test_market_data_request_get_my_subscriptions():
expected = marketdata_pb2.MarketDataRequest(
get_my_subscriptions=marketdata_pb2.GetMySubscriptions()
)
result = dataclass_to_protobuff(
MarketDataRequest(get_my_subscriptions=GetMySubscriptions()),
marketdata_pb2.MarketDataRequest(),
)
assert MessageToDict(result) == MessageToDict(expected)

View File

@@ -0,0 +1,44 @@
# pylint: disable=redefined-outer-name,unused-variable
from unittest import mock
import pytest
from t_tech.invest.services import OperationsService
@pytest.fixture()
def operations_service():
return mock.create_autospec(spec=OperationsService)
def test_get_operations(operations_service):
response = operations_service.get_operations( # noqa: F841
account_id=mock.Mock(),
from_=mock.Mock(),
to=mock.Mock(),
state=mock.Mock(),
figi=mock.Mock(),
)
operations_service.get_operations.assert_called_once()
def test_get_portfolio(operations_service):
response = operations_service.get_portfolio( # noqa: F841
account_id=mock.Mock(),
)
operations_service.get_portfolio.assert_called_once()
def test_get_positions(operations_service):
response = operations_service.get_positions( # noqa: F841
account_id=mock.Mock(),
)
operations_service.get_positions.assert_called_once()
def test_get_withdraw_limits(operations_service):
response = operations_service.get_withdraw_limits( # noqa: F841
account_id=mock.Mock(),
)
operations_service.get_withdraw_limits.assert_called_once()

View File

@@ -0,0 +1,48 @@
# pylint: disable=redefined-outer-name,unused-variable
from unittest import mock
import pytest
from t_tech.invest.services import OrdersService
@pytest.fixture()
def orders_service():
return mock.create_autospec(spec=OrdersService)
def test_post_order(orders_service):
response = orders_service.post_order( # noqa: F841
figi=mock.Mock(),
quantity=mock.Mock(),
price=mock.Mock(),
direction=mock.Mock(),
account_id=mock.Mock(),
order_type=mock.Mock(),
order_id=mock.Mock(),
)
orders_service.post_order.assert_called_once()
def test_cancel_order(orders_service):
response = orders_service.cancel_order( # noqa: F841
account_id=mock.Mock(),
order_id=mock.Mock(),
)
orders_service.cancel_order.assert_called_once()
def test_get_order_state(orders_service):
response = orders_service.get_order_state( # noqa: F841
account_id=mock.Mock(),
order_id=mock.Mock(),
)
orders_service.get_order_state.assert_called_once()
def test_get_orders(orders_service):
response = orders_service.get_orders( # noqa: F841
account_id=mock.Mock(),
)
orders_service.get_orders.assert_called_once()

View File

@@ -0,0 +1,95 @@
import uuid
from typing import List
from unittest.mock import call
import pytest
import pytest_asyncio
from t_tech.invest import (
GetOrdersResponse,
GetStopOrdersResponse,
OrderState,
StopOrder,
)
from t_tech.invest.async_services import AsyncServices, OrdersService, StopOrdersService
from t_tech.invest.typedefs import AccountId
@pytest_asyncio.fixture()
async def orders_service(mocker) -> OrdersService:
return mocker.create_autospec(OrdersService)
@pytest_asyncio.fixture()
async def stop_orders_service(mocker) -> StopOrdersService:
return mocker.create_autospec(StopOrdersService)
@pytest_asyncio.fixture()
async def async_services(
mocker, orders_service: OrdersService, stop_orders_service: StopOrdersService
) -> AsyncServices:
async_services = mocker.create_autospec(AsyncServices)
async_services.orders = orders_service
async_services.stop_orders = stop_orders_service
return async_services
@pytest.fixture()
def account_id() -> AccountId:
return AccountId(uuid.uuid4().hex)
class TestAsyncOrdersCanceling:
@pytest.mark.asyncio
@pytest.mark.parametrize(
"orders",
[
[
OrderState(order_id=str(uuid.uuid4())),
OrderState(order_id=str(uuid.uuid4())),
OrderState(order_id=str(uuid.uuid4())),
],
[OrderState(order_id=str(uuid.uuid4()))],
[],
],
)
@pytest.mark.parametrize(
"stop_orders",
[
[
StopOrder(stop_order_id=str(uuid.uuid4())),
StopOrder(stop_order_id=str(uuid.uuid4())),
StopOrder(stop_order_id=str(uuid.uuid4())),
],
[
StopOrder(stop_order_id=str(uuid.uuid4())),
],
[],
],
)
async def test_cancels_all_orders(
self,
async_services: AsyncServices,
orders_service: OrdersService,
stop_orders_service: StopOrdersService,
account_id: AccountId,
orders: List[OrderState],
stop_orders: List[StopOrder],
):
orders_service.get_orders.return_value = GetOrdersResponse(orders=orders)
stop_orders_service.get_stop_orders.return_value = GetStopOrdersResponse(
stop_orders=stop_orders
)
await AsyncServices.cancel_all_orders(async_services, account_id=account_id)
orders_service.get_orders.assert_called_once()
orders_service.cancel_order.assert_has_calls(
call(account_id=account_id, order_id=order.order_id) for order in orders
)
stop_orders_service.get_stop_orders.assert_called_once()
stop_orders_service.cancel_stop_order.assert_has_calls(
call(account_id=account_id, stop_order_id=stop_order.stop_order_id)
for stop_order in stop_orders
)

View File

@@ -0,0 +1,93 @@
import uuid
from typing import List
from unittest.mock import call
import pytest
from t_tech.invest import (
GetOrdersResponse,
GetStopOrdersResponse,
OrderState,
StopOrder,
)
from t_tech.invest.services import OrdersService, Services, StopOrdersService
from t_tech.invest.typedefs import AccountId
@pytest.fixture()
def orders_service(mocker) -> OrdersService:
return mocker.create_autospec(OrdersService)
@pytest.fixture()
def stop_orders_service(mocker) -> StopOrdersService:
return mocker.create_autospec(StopOrdersService)
@pytest.fixture()
def services(
mocker, orders_service: OrdersService, stop_orders_service: StopOrdersService
) -> Services:
services = mocker.create_autospec(Services)
services.orders = orders_service
services.stop_orders = stop_orders_service
return services
@pytest.fixture()
def account_id() -> AccountId:
return AccountId(uuid.uuid4().hex)
class TestOrdersCanceler:
@pytest.mark.parametrize(
"orders",
[
[
OrderState(order_id=str(uuid.uuid4())),
OrderState(order_id=str(uuid.uuid4())),
OrderState(order_id=str(uuid.uuid4())),
],
[OrderState(order_id=str(uuid.uuid4()))],
[],
],
)
@pytest.mark.parametrize(
"stop_orders",
[
[
StopOrder(stop_order_id=str(uuid.uuid4())),
StopOrder(stop_order_id=str(uuid.uuid4())),
StopOrder(stop_order_id=str(uuid.uuid4())),
],
[
StopOrder(stop_order_id=str(uuid.uuid4())),
],
[],
],
)
def test_cancels_all_orders(
self,
services: Services,
orders_service: OrdersService,
stop_orders_service: StopOrdersService,
account_id: AccountId,
orders: List[OrderState],
stop_orders: List[StopOrder],
):
orders_service.get_orders.return_value = GetOrdersResponse(orders=orders)
stop_orders_service.get_stop_orders.return_value = GetStopOrdersResponse(
stop_orders=stop_orders
)
Services.cancel_all_orders(services, account_id=account_id)
orders_service.get_orders.assert_called_once()
orders_service.cancel_order.assert_has_calls(
call(account_id=account_id, order_id=order.order_id) for order in orders
)
stop_orders_service.get_stop_orders.assert_called_once()
stop_orders_service.cancel_stop_order.assert_has_calls(
call(account_id=account_id, stop_order_id=stop_order.stop_order_id)
for stop_order in stop_orders
)

View File

@@ -0,0 +1,56 @@
import logging
import os
import pytest
from t_tech.invest import (
EditFavoritesActionType,
EditFavoritesRequest as DataclassModel,
)
from t_tech.invest._grpc_helpers import protobuf_to_dataclass
from t_tech.invest.grpc.instruments_pb2 import EditFavoritesRequest as ProtoModel
logging.basicConfig(level=logging.DEBUG)
@pytest.fixture()
def unsupported_model() -> ProtoModel:
pb_obj = ProtoModel()
pb_obj.action_type = 137
return pb_obj
class TestProtobufToDataclass:
def test_protobuf_to_dataclass_does_not_raise_by_default(
self, unsupported_model: ProtoModel, caplog
):
expected = EditFavoritesActionType.EDIT_FAVORITES_ACTION_TYPE_UNSPECIFIED
actual = protobuf_to_dataclass(
pb_obj=unsupported_model, dataclass_type=DataclassModel
).action_type
assert expected == actual
@pytest.mark.parametrize("use_default_enum_if_error", ["True", "true", "1"])
def test_protobuf_to_dataclass_does_not_raise_when_set_true(
self, unsupported_model: ProtoModel, use_default_enum_if_error: str
):
expected = EditFavoritesActionType.EDIT_FAVORITES_ACTION_TYPE_UNSPECIFIED
os.environ["USE_DEFAULT_ENUM_IF_ERROR"] = use_default_enum_if_error
actual = protobuf_to_dataclass(
pb_obj=unsupported_model, dataclass_type=DataclassModel
).action_type
assert expected == actual
@pytest.mark.parametrize("use_default_enum_if_error", ["False", "false", "0"])
def test_protobuf_to_dataclass_does_raise_when_set_false(
self, unsupported_model: ProtoModel, use_default_enum_if_error: str
):
os.environ["USE_DEFAULT_ENUM_IF_ERROR"] = use_default_enum_if_error
with pytest.raises(ValueError):
_ = protobuf_to_dataclass(
pb_obj=unsupported_model, dataclass_type=DataclassModel
).action_type

View File

@@ -0,0 +1,239 @@
from decimal import Decimal
from random import randrange
import pytest
from t_tech.invest import Quotation
from t_tech.invest.utils import decimal_to_quotation, quotation_to_decimal
MAX_UNITS = 999_999_999_999
MAX_NANO = 999_999_999
@pytest.fixture()
def quotation(request) -> Quotation:
raw = request.param
return Quotation(units=raw["units"], nano=raw["nano"])
class TestQuotationArithmetic:
@pytest.mark.parametrize(
("quotation", "decimal"),
[
({"units": 114, "nano": 250000000}, Decimal("114.25")),
({"units": -200, "nano": -200000000}, Decimal("-200.20")),
({"units": -0, "nano": -10000000}, Decimal("-0.01")),
],
indirect=["quotation"],
)
def test_quotation_to_decimal(self, quotation: Quotation, decimal: Decimal):
actual = quotation_to_decimal(quotation)
assert actual == decimal
@pytest.mark.parametrize(
("quotation", "decimal"),
[
({"units": 114, "nano": 250000000}, Decimal("114.25")),
({"units": -200, "nano": -200000000}, Decimal("-200.20")),
({"units": -0, "nano": -10000000}, Decimal("-0.01")),
],
indirect=["quotation"],
)
def test_decimal_to_quotation(self, decimal: Decimal, quotation: Quotation):
actual = decimal_to_quotation(decimal)
assert actual.units == quotation.units
assert actual.nano == quotation.nano
@pytest.mark.parametrize(
("quotation_left", "quotation_right"),
[
(
Quotation(
units=randrange(-MAX_UNITS, MAX_UNITS),
nano=randrange(-MAX_NANO, MAX_NANO),
),
Quotation(
units=randrange(-MAX_UNITS, MAX_UNITS),
nano=randrange(-MAX_NANO, MAX_NANO),
),
),
(
Quotation(
units=randrange(-MAX_UNITS, MAX_UNITS),
nano=randrange(-MAX_NANO, MAX_NANO),
),
Quotation(units=0, nano=0),
),
(
Quotation(units=0, nano=0),
Quotation(
units=randrange(-MAX_UNITS, MAX_UNITS),
nano=randrange(-MAX_NANO, MAX_NANO),
),
),
(
Quotation(units=-0, nano=-200000000),
Quotation(
units=randrange(-MAX_UNITS, MAX_UNITS),
nano=randrange(-MAX_NANO, MAX_NANO),
),
),
(
Quotation(
units=randrange(-MAX_UNITS, MAX_UNITS),
nano=randrange(-MAX_NANO, MAX_NANO),
),
Quotation(units=-0, nano=-200000000),
),
(
Quotation(
units=MAX_UNITS,
nano=MAX_NANO,
),
Quotation(
units=MAX_UNITS,
nano=MAX_NANO,
),
),
],
)
@pytest.mark.parametrize(
"operation",
[
lambda x, y: x - y,
lambda x, y: x + y,
],
)
def test_operations(
self, quotation_left: Quotation, quotation_right: Quotation, operation
):
decimal_left = quotation_to_decimal(quotation_left)
decimal_right = quotation_to_decimal(quotation_right)
quotation = operation(quotation_left, quotation_right)
expected_decimal = operation(decimal_left, decimal_right)
actual_decimal = quotation_to_decimal(quotation)
assert actual_decimal == expected_decimal
@pytest.mark.parametrize(
("quotation_left", "quotation_right"),
[
(
Quotation(
units=randrange(-MAX_UNITS, MAX_UNITS),
nano=randrange(-MAX_NANO, MAX_NANO),
),
Quotation(
units=randrange(-MAX_UNITS, MAX_UNITS),
nano=randrange(-MAX_NANO, MAX_NANO),
),
),
(
Quotation(
units=randrange(-MAX_UNITS, MAX_UNITS),
nano=randrange(-MAX_NANO, MAX_NANO),
),
Quotation(units=0, nano=0),
),
(
Quotation(units=0, nano=0),
Quotation(
units=randrange(-MAX_UNITS, MAX_UNITS),
nano=randrange(-MAX_NANO, MAX_NANO),
),
),
(
Quotation(units=0, nano=0),
Quotation(units=0, nano=0),
),
(
Quotation(units=-10, nano=0),
Quotation(units=10, nano=0),
),
(
Quotation(units=0, nano=-200000000),
Quotation(units=0, nano=-200000000),
),
(
Quotation(
units=MAX_UNITS,
nano=MAX_NANO,
),
Quotation(
units=MAX_UNITS,
nano=MAX_NANO,
),
),
],
)
@pytest.mark.parametrize(
"comparator",
[
lambda x, y: x > y,
lambda x, y: x >= y,
lambda x, y: x < y,
lambda x, y: x <= y,
lambda x, y: x == y,
lambda x, y: x != y,
lambda y, x: x > y,
lambda y, x: x >= y,
lambda y, x: x < y,
lambda y, x: x <= y,
lambda y, x: x == y,
lambda y, x: x != y,
],
)
def test_comparison(
self, quotation_left: Quotation, quotation_right: Quotation, comparator
):
decimal_left = quotation_to_decimal(quotation_left)
decimal_right = quotation_to_decimal(quotation_right)
actual_comparison = comparator(quotation_left, quotation_right)
expected_comparison = comparator(decimal_left, decimal_right)
assert actual_comparison == expected_comparison
@pytest.mark.parametrize(
"quotation",
[
Quotation(
units=randrange(-MAX_UNITS, MAX_UNITS),
nano=randrange(-MAX_NANO, MAX_NANO),
),
Quotation(
units=randrange(-MAX_UNITS, 0),
nano=randrange(-MAX_NANO, 0),
),
Quotation(
units=randrange(0, MAX_UNITS),
nano=randrange(0, MAX_NANO),
),
],
)
def test_abs(self, quotation: Quotation):
decimal = quotation_to_decimal(quotation)
actual = abs(decimal)
expected = abs(decimal)
assert actual == expected
@pytest.mark.parametrize(
("units", "nano"),
[
(-MAX_UNITS, MAX_NANO * 1000),
(MAX_UNITS, -MAX_NANO + 1123123),
(0, MAX_NANO + 1121201203123),
(MAX_UNITS * 100, -MAX_UNITS - 121201203123),
],
)
def test_nano_overfill_transfers(self, units: int, nano: int):
quotation = Quotation(units=units, nano=nano)
if abs(nano) >= 1e9:
assert quotation.nano < 1e9
assert quotation.units - units == nano // 1_000_000_000
assert quotation.nano == nano % 1_000_000_000

View File

@@ -0,0 +1,334 @@
# pylint: disable=redefined-outer-name,unused-variable
import os
import uuid
from datetime import datetime
import pytest
from _decimal import Decimal
from examples.sandbox.sandbox_cancel_stop_order import cancel_stop_order
from examples.sandbox.sandbox_get_order_price import get_order_price
from examples.sandbox.sandbox_get_stop_orders import get_stop_orders
from examples.sandbox.sandbox_post_stop_order import post_stop_order
from t_tech.invest import (
Account,
CloseSandboxAccountResponse,
MoneyValue,
OperationState,
OrderDirection,
OrderType,
Quotation,
RequestError,
utils,
)
from t_tech.invest.sandbox.client import SandboxClient
from t_tech.invest.schemas import (
GetOrderPriceResponse,
OrderExecutionReportStatus,
PostOrderAsyncRequest,
StopOrderDirection,
StopOrderStatusOption,
)
from t_tech.invest.utils import money_to_decimal
from tests.utils import skip_when
@pytest.fixture()
def sandbox_service():
with SandboxClient(token=os.environ["INVEST_SANDBOX_TOKEN"]) as client:
yield client
@pytest.fixture()
def initial_balance_pay_in() -> MoneyValue:
return MoneyValue(currency="rub", units=1000000, nano=0)
@pytest.fixture()
def account_id(sandbox_service, initial_balance_pay_in: MoneyValue):
response = sandbox_service.sandbox.open_sandbox_account()
sandbox_service.sandbox.sandbox_pay_in(
account_id=response.account_id,
amount=initial_balance_pay_in,
)
yield response.account_id
sandbox_service.sandbox.close_sandbox_account(
account_id=response.account_id,
)
@pytest.fixture()
def figi() -> str:
return "BBG333333333"
@pytest.fixture()
def instrument_id() -> str:
return "f509af83-6e71-462f-901f-bcb073f6773b"
@pytest.fixture()
def quantity() -> int:
return 10
@pytest.fixture()
def price() -> Quotation:
return Quotation(units=6, nano=500000000)
@pytest.fixture()
def direction() -> OrderDirection:
return OrderDirection.ORDER_DIRECTION_BUY
@pytest.fixture()
def stop_order_direction() -> StopOrderDirection:
return StopOrderDirection.STOP_ORDER_DIRECTION_BUY
@pytest.fixture()
def stop_order_status() -> StopOrderStatusOption:
return StopOrderStatusOption.STOP_ORDER_STATUS_ACTIVE
@pytest.fixture()
def order_type() -> OrderType:
return OrderType.ORDER_TYPE_LIMIT
@pytest.fixture()
def order_id() -> str:
return ""
@pytest.fixture()
def async_order_id() -> str:
return str(uuid.uuid4())
@pytest.fixture()
def order(instrument_id, quantity, price, direction, account_id, order_type, order_id):
return {
"instrument_id": instrument_id,
"quantity": quantity,
"price": price,
"direction": direction,
"account_id": account_id,
"order_type": order_type,
"order_id": order_id,
}
@pytest.fixture()
def async_order(
instrument_id, quantity, price, direction, account_id, order_type, async_order_id
):
return {
"instrument_id": instrument_id,
"quantity": quantity,
"price": price,
"direction": direction,
"account_id": account_id,
"order_type": order_type,
"order_id": async_order_id,
}
skip_when_exchange_closed = skip_when(
RequestError,
lambda msg: "Instrument is not available for trading" in msg,
reason="Skipping during closed exchange",
)
@pytest.mark.skipif(
os.environ.get("INVEST_SANDBOX_TOKEN") is None,
reason="INVEST_SANDBOX_TOKEN should be specified",
)
class TestSandboxOperations:
def test_open_sandbox_account(self, sandbox_service):
response = sandbox_service.sandbox.open_sandbox_account()
assert isinstance(response.account_id, str)
sandbox_service.sandbox.close_sandbox_account(
account_id=response.account_id,
)
def test_get_sandbox_accounts(self, sandbox_service, account_id):
response = sandbox_service.users.get_accounts()
assert isinstance(response.accounts, list)
assert isinstance(response.accounts[0], Account)
assert (
len(
[
_account
for _account in response.accounts
if _account.id == account_id
]
)
== 1
)
def test_close_sandbox_account(self, sandbox_service):
response = sandbox_service.sandbox.open_sandbox_account()
response = sandbox_service.sandbox.close_sandbox_account(
account_id=response.account_id,
)
assert isinstance(response, CloseSandboxAccountResponse)
@skip_when_exchange_closed
def test_post_sandbox_order(
self, sandbox_service, order, instrument_id, direction, quantity
):
response = sandbox_service.orders.post_order(**order)
assert isinstance(response.order_id, str)
assert response.instrument_uid == instrument_id
assert response.direction == direction
assert response.lots_requested == quantity
@skip_when_exchange_closed
def test_post_sandbox_order_async(
self,
sandbox_service,
async_order,
instrument_id,
direction,
quantity,
async_order_id,
):
request = PostOrderAsyncRequest(**async_order)
response = sandbox_service.orders.post_order_async(request)
assert isinstance(response.order_request_id, str)
assert (
response.execution_report_status
== OrderExecutionReportStatus.EXECUTION_REPORT_STATUS_NEW
)
assert response.order_request_id == async_order_id
@skip_when_exchange_closed
def test_get_sandbox_orders(self, sandbox_service, order, account_id):
response = sandbox_service.orders.post_order(**order)
assert response
@skip_when_exchange_closed
@pytest.mark.skip(reason="Order executes faster than cancel")
def test_cancel_sandbox_order(self, sandbox_service, order, account_id):
response = sandbox_service.orders.post_order(**order)
response = sandbox_service.orders.cancel_order(
account_id=account_id,
order_id=response.order_id,
)
assert isinstance(response.time, datetime)
@skip_when_exchange_closed
def test_get_sandbox_order_state(
self, sandbox_service, order, account_id, instrument_id, direction, quantity
):
response = sandbox_service.orders.post_order(**order)
response = sandbox_service.orders.get_order_state(
account_id=account_id,
order_id=response.order_id,
)
assert response.instrument_uid == instrument_id
assert response.direction == direction
assert response.lots_requested == quantity
@pytest.mark.parametrize("order_type", [OrderType.ORDER_TYPE_MARKET])
@skip_when_exchange_closed
def test_get_sandbox_positions(
self, sandbox_service, account_id, order, order_type
):
_ = sandbox_service.orders.post_order(**order)
response = sandbox_service.operations.get_positions(account_id=account_id)
assert isinstance(response.money[0], MoneyValue)
assert response.money[0].currency == "rub"
def test_get_sandbox_operations(self, sandbox_service, account_id, order, figi):
response = sandbox_service.operations.get_operations(
account_id=account_id,
from_=datetime(2000, 2, 2),
to=datetime(2022, 2, 2),
state=OperationState.OPERATION_STATE_EXECUTED,
figi=figi,
)
assert isinstance(response.operations, list)
def test_get_sandbox_portfolio(
self, sandbox_service, account_id, initial_balance_pay_in: MoneyValue
):
response = sandbox_service.operations.get_portfolio(
account_id=account_id,
)
assert str(response.total_amount_bonds) == str(
MoneyValue(currency="rub", units=0, nano=0)
)
assert str(response.total_amount_currencies) == str(
initial_balance_pay_in,
)
assert str(response.total_amount_etf) == str(
MoneyValue(currency="rub", units=0, nano=0)
)
assert str(response.total_amount_futures) == str(
MoneyValue(currency="rub", units=0, nano=0)
)
assert str(response.total_amount_shares) == str(
MoneyValue(currency="rub", units=0, nano=0)
)
def test_sandbox_pay_in(
self, sandbox_service, account_id, initial_balance_pay_in: MoneyValue
):
amount = MoneyValue(currency="rub", units=1234, nano=0)
response = sandbox_service.sandbox.sandbox_pay_in(
account_id=account_id,
amount=amount,
)
assert money_to_decimal(response.balance) == (
money_to_decimal(initial_balance_pay_in) + money_to_decimal(amount)
)
@skip_when_exchange_closed
def test_sandbox_post_stop_order(
self,
sandbox_service,
account_id,
instrument_id,
stop_order_direction,
quantity,
price,
):
response = post_stop_order(
sandbox_service,
account_id,
instrument_id,
stop_order_direction,
quantity,
price,
)
assert response.order_request_id is not None
assert response.stop_order_id is not None
def test_sandbox_get_stop_orders(
self, sandbox_service, account_id, stop_order_status
):
response = get_stop_orders(sandbox_service, account_id, stop_order_status)
assert isinstance(response.stop_orders, list)
def test_sandbox_cancel_stop_order(self, sandbox_service, account_id):
stop_orders = get_stop_orders(
sandbox_service, account_id, StopOrderStatusOption.STOP_ORDER_STATUS_ACTIVE
)
if len(stop_orders.stop_orders) > 0:
stop_order_id = stop_orders.stop_orders[0].stop_order_id
response = cancel_stop_order(sandbox_service, account_id, stop_order_id)
assert isinstance(response.time, datetime)
def test_sandbox_get_order_prices(self, sandbox_service, account_id, instrument_id):
response = get_order_price(sandbox_service, account_id, instrument_id, 100)
assert isinstance(response, GetOrderPriceResponse)
assert utils.money_to_decimal(response.total_order_amount) == Decimal(100)

View File

@@ -0,0 +1,22 @@
# pylint: disable=redefined-outer-name,unused-variable
from unittest import mock
import pytest
from t_tech.invest.services import SignalService
@pytest.fixture()
def signals_service():
return mock.create_autospec(spec=SignalService)
def test_get_signals(signals_service):
response = signals_service.get_signals(request=mock.Mock()) # noqa: F841
signals_service.get_signals.assert_called_once_with(request=mock.ANY)
def test_get_strategies(signals_service):
response = signals_service.get_strategies(request=mock.Mock()) # noqa: F841
signals_service.get_strategies.assert_called_once_with(request=mock.ANY)

View File

@@ -0,0 +1,43 @@
# pylint: disable=redefined-outer-name,unused-variable
from unittest import mock
import pytest
from t_tech.invest.services import StopOrdersService
@pytest.fixture()
def stop_orders_service():
return mock.create_autospec(spec=StopOrdersService)
def test_post_stop_order(stop_orders_service):
response = stop_orders_service.post_stop_order( # noqa: F841
figi=mock.Mock(),
quantity=mock.Mock(),
price=mock.Mock(),
stop_price=mock.Mock(),
direction=mock.Mock(),
account_id=mock.Mock(),
expiration_type=mock.Mock(),
stop_order_type=mock.Mock(),
expire_date=mock.Mock(),
order_id=mock.Mock(),
)
stop_orders_service.post_stop_order.assert_called_once()
def test_get_stop_orders(stop_orders_service):
response = stop_orders_service.get_stop_orders( # noqa: F841
account_id=mock.Mock(),
)
stop_orders_service.get_stop_orders.assert_called_once()
def test_cancel_stop_order(stop_orders_service):
response = stop_orders_service.cancel_stop_order( # noqa: F841
account_id=mock.Mock(),
stop_order_id=mock.Mock(),
)
stop_orders_service.cancel_stop_order.assert_called_once()

View File

@@ -0,0 +1,385 @@
import logging
from datetime import timedelta
from decimal import Decimal
from math import exp, sqrt
from random import gauss, seed
from typing import Callable, Dict, Iterable, Iterator, List, Optional
import pytest
from grpc import StatusCode
from t_tech.invest import (
Candle,
Client,
GetCandlesResponse,
GetMarginAttributesResponse,
HistoricCandle,
MarketDataResponse,
MoneyValue,
OrderDirection,
OrderType,
PortfolioPosition,
PortfolioResponse,
Quotation,
RequestError,
)
from t_tech.invest.services import Services
from t_tech.invest.strategies.base.account_manager import AccountManager
from t_tech.invest.strategies.moving_average.plotter import MovingAverageStrategyPlotter
from t_tech.invest.strategies.moving_average.signal_executor import (
MovingAverageSignalExecutor,
)
from t_tech.invest.strategies.moving_average.strategy import MovingAverageStrategy
from t_tech.invest.strategies.moving_average.strategy_settings import (
MovingAverageStrategySettings,
)
from t_tech.invest.strategies.moving_average.strategy_state import (
MovingAverageStrategyState,
)
from t_tech.invest.strategies.moving_average.supervisor import (
MovingAverageStrategySupervisor,
)
from t_tech.invest.strategies.moving_average.trader import MovingAverageStrategyTrader
from t_tech.invest.utils import (
candle_interval_to_subscription_interval,
candle_interval_to_timedelta,
decimal_to_quotation,
now,
quotation_to_decimal,
)
logging.basicConfig(format="%(asctime)s %(levelname)s:%(message)s", level=logging.INFO)
logger = logging.getLogger(__name__)
seed(1338)
def create_noise(s0, mu, sigma) -> Callable[[int], Iterable[float]]:
"""Create Noise.
Generates a price following a geometric brownian motion process based on the input.
- s0: Asset initial price.
- mu: Interest rate expressed annual terms.
- sigma: Volatility expressed annual terms.
"""
st = s0
def generate_range(limit: int):
nonlocal st
for _ in range(limit):
st *= exp(
(mu - 0.5 * sigma**2) * (1.0 / 365.0)
+ sigma * sqrt(1.0 / 365.0) * gauss(mu=0, sigma=1)
)
yield st
return generate_range
@pytest.fixture()
def stock_prices_generator() -> Callable[[int], Iterable[float]]:
return create_noise(100, 0.01, 0.1)
@pytest.fixture()
def stock_volume_generator() -> Callable[[int], Iterable[float]]:
return create_noise(1000, 0.9, 1.1)
@pytest.fixture()
def initial_candles(
settings: MovingAverageStrategySettings,
stock_prices_generator: Callable[[int], Iterable[float]],
stock_volume_generator: Callable[[int], Iterable[float]],
) -> Iterable[HistoricCandle]:
candles = []
intervals = 365
interval_delta = candle_interval_to_timedelta(settings.candle_interval)
base = now() - interval_delta * intervals
(close,) = stock_prices_generator(1)
for i in range(intervals):
open_ = close
low, high, close = stock_prices_generator(3)
low, high = min(low, high, open_, close), max(low, high, open_, close)
(volume,) = stock_volume_generator(1)
candle = HistoricCandle(
open=decimal_to_quotation(Decimal(open_)),
high=decimal_to_quotation(Decimal(high)),
low=decimal_to_quotation(Decimal(low)),
close=decimal_to_quotation(Decimal(close)),
volume=int(volume),
time=base + interval_delta * i,
is_complete=True,
)
candles.append(candle)
return candles
@pytest.fixture()
def real_services(token: str) -> Iterator[Services]:
with Client(token) as services:
yield services
@pytest.fixture()
def mock_market_data_service(
real_services: Services,
mocker,
initial_candles: List[HistoricCandle],
) -> Services:
real_services.market_data = mocker.Mock(wraps=real_services.market_data)
real_services.market_data.get_candles = mocker.Mock()
real_services.market_data.get_candles.return_value = GetCandlesResponse(
candles=initial_candles
)
return real_services
@pytest.fixture()
def current_market_data() -> List[Candle]:
return []
@pytest.fixture()
def mock_market_data_stream_service(
real_services: Services,
mocker,
figi: str,
stock_prices_generator: Callable[[int], Iterable[float]],
stock_volume_generator: Callable[[int], Iterable[float]],
settings: MovingAverageStrategySettings,
current_market_data: List[Candle],
freezer,
) -> Services:
real_services.market_data_stream = mocker.Mock(
wraps=real_services.market_data_stream
)
def _market_data_stream(*args, **kwargs):
yield MarketDataResponse(candle=None) # type: ignore
(close,) = stock_prices_generator(1)
while True:
open_ = close
low, high, close = stock_prices_generator(3)
low, high = min(low, high, open_, close), max(low, high, open_, close)
(volume,) = stock_volume_generator(1)
candle = Candle(
figi=figi,
interval=candle_interval_to_subscription_interval(
settings.candle_interval
),
open=decimal_to_quotation(Decimal(open_)),
high=decimal_to_quotation(Decimal(high)),
low=decimal_to_quotation(Decimal(low)),
close=decimal_to_quotation(Decimal(close)),
volume=int(volume),
time=now(),
)
current_market_data.append(candle)
yield MarketDataResponse(candle=candle)
freezer.move_to(now() + timedelta(minutes=1))
real_services.market_data_stream.market_data_stream = _market_data_stream
return real_services
@pytest.fixture()
def mock_operations_service(
real_services: Services,
mocker,
portfolio_response: PortfolioResponse,
) -> Services:
real_services.operations = mocker.Mock(wraps=real_services.operations)
real_services.operations.get_portfolio.return_value = portfolio_response
return real_services
@pytest.fixture()
def mock_users_service(
real_services: Services,
mocker,
marginal_trade_active: bool,
) -> Services:
real_services.users = mocker.Mock(wraps=real_services.users)
if marginal_trade_active:
real_services.users.get_margin_attributes.return_value = (
GetMarginAttributesResponse(
liquid_portfolio=MoneyValue(currency="", units=0, nano=0),
starting_margin=MoneyValue(currency="", units=0, nano=0),
minimal_margin=MoneyValue(currency="", units=0, nano=0),
funds_sufficiency_level=Quotation(units=322, nano=0),
amount_of_missing_funds=MoneyValue(currency="", units=0, nano=0),
)
)
else:
real_services.users.get_margin_attributes.side_effect = RequestError(
code=StatusCode.PERMISSION_DENIED,
details="Marginal trade disabled",
metadata=None,
)
return real_services
@pytest.fixture()
def marginal_trade_active() -> bool:
return True
@pytest.fixture()
def mock_orders_service(
real_services: Services,
mocker,
portfolio_positions: Dict[str, PortfolioPosition],
balance: MoneyValue,
current_market_data: List[Candle],
settings: MovingAverageStrategySettings,
marginal_trade_active: bool,
) -> Services:
real_services.orders = mocker.Mock(wraps=real_services.orders)
def _post_order(
*,
figi: str = "",
quantity: int = 0,
price: Optional[Quotation] = None,
direction: OrderDirection = OrderDirection(0),
account_id: str = "",
order_type: OrderType = OrderType(0),
order_id: str = "",
):
assert figi == settings.share_id
assert quantity > 0
assert account_id == settings.account_id
assert order_type.ORDER_TYPE_MARKET
last_candle = current_market_data[-1]
last_market_price = quotation_to_decimal(last_candle.close)
position = portfolio_positions.get(figi)
if position is None:
position = PortfolioPosition(
figi=figi,
quantity=decimal_to_quotation(Decimal(0)),
)
if direction == OrderDirection.ORDER_DIRECTION_SELL:
quantity_delta = -quantity
balance_delta = last_market_price * quantity
elif direction == OrderDirection.ORDER_DIRECTION_BUY:
quantity_delta = +quantity
balance_delta = -(last_market_price * quantity)
else:
raise AssertionError("Incorrect direction")
logger.warning("Operation: %s, %s", direction, balance_delta)
old_quantity = quotation_to_decimal(position.quantity)
new_quantity = decimal_to_quotation(old_quantity + quantity_delta)
position.quantity.units = new_quantity.units
position.quantity.nano = new_quantity.nano
old_balance = quotation_to_decimal(
Quotation(units=balance.units, nano=balance.nano)
)
new_balance = decimal_to_quotation(old_balance + balance_delta)
balance.units = new_balance.units
balance.nano = new_balance.nano
portfolio_positions[figi] = position
real_services.orders.post_order = _post_order
return real_services
@pytest.fixture()
def mocked_services(
real_services: Services,
mock_market_data_service,
mock_market_data_stream_service,
mock_operations_service,
mock_users_service,
mock_orders_service,
) -> Services:
return real_services
@pytest.fixture()
def account_manager(
mocked_services: Services, settings: MovingAverageStrategySettings
) -> AccountManager:
return AccountManager(services=mocked_services, strategy_settings=settings)
@pytest.fixture()
def state() -> MovingAverageStrategyState:
return MovingAverageStrategyState()
@pytest.fixture()
def strategy(
settings: MovingAverageStrategySettings,
account_manager: AccountManager,
state: MovingAverageStrategyState,
) -> MovingAverageStrategy:
return MovingAverageStrategy(
settings=settings,
account_manager=account_manager,
state=state,
)
@pytest.fixture()
def signal_executor(
mocked_services: Services,
state: MovingAverageStrategyState,
settings: MovingAverageStrategySettings,
) -> MovingAverageSignalExecutor:
return MovingAverageSignalExecutor(
services=mocked_services,
state=state,
settings=settings,
)
@pytest.fixture()
def supervisor() -> MovingAverageStrategySupervisor:
return MovingAverageStrategySupervisor()
@pytest.fixture()
def moving_average_strategy_trader(
strategy: MovingAverageStrategy,
settings: MovingAverageStrategySettings,
mocked_services: Services,
state: MovingAverageStrategyState,
signal_executor: MovingAverageSignalExecutor,
account_manager: AccountManager,
supervisor: MovingAverageStrategySupervisor,
) -> MovingAverageStrategyTrader:
return MovingAverageStrategyTrader(
strategy=strategy,
settings=settings,
services=mocked_services,
state=state,
signal_executor=signal_executor,
account_manager=account_manager,
supervisor=supervisor,
)
@pytest.fixture()
def plotter(
settings: MovingAverageStrategySettings,
) -> MovingAverageStrategyPlotter:
return MovingAverageStrategyPlotter(settings=settings)

View File

@@ -0,0 +1,133 @@
import logging
from datetime import timedelta
from decimal import Decimal
from typing import Dict
import matplotlib.pyplot as plt
import pytest
from t_tech.invest import (
CandleInterval,
MoneyValue,
PortfolioPosition,
PortfolioResponse,
Quotation,
)
from t_tech.invest.strategies.base.account_manager import AccountManager
from t_tech.invest.strategies.moving_average.plotter import MovingAverageStrategyPlotter
from t_tech.invest.strategies.moving_average.signal_executor import (
MovingAverageSignalExecutor,
)
from t_tech.invest.strategies.moving_average.strategy import MovingAverageStrategy
from t_tech.invest.strategies.moving_average.strategy_settings import (
MovingAverageStrategySettings,
)
from t_tech.invest.strategies.moving_average.supervisor import (
MovingAverageStrategySupervisor,
)
from t_tech.invest.strategies.moving_average.trader import MovingAverageStrategyTrader
from t_tech.invest.typedefs import AccountId, ShareId
logging.basicConfig(format="%(asctime)s %(levelname)s:%(message)s", level=logging.INFO)
logger = logging.getLogger(__name__)
@pytest.fixture()
def token() -> str:
return "some"
@pytest.fixture()
def portfolio_positions() -> Dict[str, PortfolioPosition]:
return {
"BBG004730N88": PortfolioPosition(
figi="BBG004730N88",
instrument_type="share",
quantity=Quotation(units=110, nano=0),
average_position_price=MoneyValue(
currency="rub", units=261, nano=800000000
),
expected_yield=Quotation(units=-106, nano=-700000000),
current_nkd=MoneyValue(currency="", units=0, nano=0),
average_position_price_pt=Quotation(units=0, nano=0),
current_price=MoneyValue(currency="rub", units=260, nano=830000000),
)
}
@pytest.fixture()
def balance() -> MoneyValue:
return MoneyValue(currency="rub", units=20050, nano=690000000)
@pytest.fixture()
def portfolio_response(
portfolio_positions: Dict[str, PortfolioPosition],
balance: MoneyValue,
) -> PortfolioResponse:
return PortfolioResponse(
total_amount_shares=MoneyValue(currency="rub", units=28691, nano=300000000),
total_amount_bonds=MoneyValue(currency="rub", units=0, nano=0),
total_amount_etf=MoneyValue(currency="rub", units=0, nano=0),
total_amount_currencies=balance,
total_amount_futures=MoneyValue(currency="rub", units=0, nano=0),
expected_yield=Quotation(units=0, nano=-350000000),
positions=list(portfolio_positions.values()),
)
@pytest.fixture()
def figi() -> str:
return "BBG0047315Y7"
@pytest.fixture()
def account_id() -> str:
return AccountId("1337007228")
@pytest.fixture()
def settings(figi: str, account_id: AccountId) -> MovingAverageStrategySettings:
return MovingAverageStrategySettings(
share_id=ShareId(figi),
account_id=account_id,
max_transaction_price=Decimal(10000),
candle_interval=CandleInterval.CANDLE_INTERVAL_1_MIN,
long_period=timedelta(minutes=100),
short_period=timedelta(minutes=20),
std_period=timedelta(minutes=30),
)
class TestMovingAverageStrategyTrader:
@pytest.mark.freeze_time()
def test_trade(
self,
moving_average_strategy_trader: MovingAverageStrategyTrader,
strategy: MovingAverageStrategy,
account_manager: AccountManager,
signal_executor: MovingAverageSignalExecutor,
plotter: MovingAverageStrategyPlotter,
supervisor: MovingAverageStrategySupervisor,
caplog,
freezer,
):
caplog.set_level(logging.DEBUG)
caplog.set_level(logging.INFO)
initial_balance = account_manager.get_current_balance()
for i in range(50):
logger.info("Trade %s", i)
moving_average_strategy_trader.trade()
current_balance = account_manager.get_current_balance()
assert initial_balance != current_balance
logger.info("Initial balance %s", initial_balance)
logger.info("Current balance %s", current_balance)
events = supervisor.get_events()
plotter.plot(events)
plt.show(block=False)
plt.pause(1)
plt.close()

View File

@@ -0,0 +1,216 @@
import logging
import os
from datetime import timedelta
from decimal import Decimal
from typing import Iterator
import matplotlib.pyplot as plt
import pytest
from t_tech.invest import (
CandleInterval,
Client,
GetMarginAttributesResponse,
InstrumentIdType,
MoneyValue,
OpenSandboxAccountResponse,
Quotation,
ShareResponse,
TradingSchedulesResponse,
)
from t_tech.invest.exceptions import UnauthenticatedError
from t_tech.invest.services import SandboxService, Services
from t_tech.invest.strategies.base.account_manager import AccountManager
from t_tech.invest.strategies.base.errors import MarketDataNotAvailableError
from t_tech.invest.strategies.moving_average.plotter import MovingAverageStrategyPlotter
from t_tech.invest.strategies.moving_average.signal_executor import (
MovingAverageSignalExecutor,
)
from t_tech.invest.strategies.moving_average.strategy import MovingAverageStrategy
from t_tech.invest.strategies.moving_average.strategy_settings import (
MovingAverageStrategySettings,
)
from t_tech.invest.strategies.moving_average.supervisor import (
MovingAverageStrategySupervisor,
)
from t_tech.invest.strategies.moving_average.trader import MovingAverageStrategyTrader
from t_tech.invest.typedefs import AccountId, ShareId
from t_tech.invest.utils import now
logging.basicConfig(format="%(asctime)s %(levelname)s:%(message)s", level=logging.DEBUG)
logger = logging.getLogger(__name__)
@pytest.fixture()
def token() -> str:
return os.environ["INVEST_SANDBOX_TOKEN"]
@pytest.fixture()
def account(
token: str, real_services: Services, balance: MoneyValue
) -> Iterator[OpenSandboxAccountResponse]:
sandbox: SandboxService = real_services.sandbox
account_response = sandbox.open_sandbox_account()
account_id = account_response.account_id
sandbox.sandbox_pay_in(account_id=account_id, amount=balance)
yield account_response
sandbox.close_sandbox_account(account_id=account_id)
@pytest.fixture()
def account_id(account: OpenSandboxAccountResponse) -> str:
return account.account_id
@pytest.fixture()
def mock_market_data_service(real_services: Services) -> Services:
return real_services
@pytest.fixture()
def mock_market_data_stream_service(real_services: Services) -> Services:
return real_services
@pytest.fixture()
def mock_operations_service(real_services: Services) -> Services:
real_services.operations.get_portfolio = real_services.sandbox.get_sandbox_portfolio
real_services.operations.get_operations = (
real_services.sandbox.get_sandbox_operations
)
return real_services
@pytest.fixture()
def mock_users_service(
real_services: Services,
mocker,
) -> Services:
real_services.users = mocker.Mock(wraps=real_services.users)
real_services.users.get_margin_attributes.return_value = (
GetMarginAttributesResponse(
liquid_portfolio=MoneyValue(currency="", units=0, nano=0),
starting_margin=MoneyValue(currency="", units=0, nano=0),
minimal_margin=MoneyValue(currency="", units=0, nano=0),
funds_sufficiency_level=Quotation(units=322, nano=0),
amount_of_missing_funds=MoneyValue(currency="", units=0, nano=0),
)
)
return real_services
@pytest.fixture()
def mock_orders_service(real_services: Services) -> Services:
real_services.orders.post_order = real_services.sandbox.post_sandbox_order
real_services.orders.get_orders = real_services.sandbox.get_sandbox_orders
real_services.orders.cancel_order = real_services.sandbox.cancel_sandbox_order
real_services.orders.get_order_state = real_services.sandbox.get_sandbox_order_state
return real_services
@pytest.fixture()
def mocked_services(
real_services: Services,
mock_market_data_service,
mock_market_data_stream_service,
mock_operations_service,
mock_users_service,
mock_orders_service,
) -> Services:
return real_services
@pytest.fixture()
def figi() -> str:
return "BBG004730N88"
@pytest.fixture()
def balance() -> MoneyValue:
return MoneyValue(currency="rub", units=20050, nano=690000000)
@pytest.fixture()
def settings(figi: str, account_id: AccountId) -> MovingAverageStrategySettings:
return MovingAverageStrategySettings(
share_id=ShareId(figi),
account_id=account_id,
max_transaction_price=Decimal(10000),
candle_interval=CandleInterval.CANDLE_INTERVAL_HOUR,
long_period=timedelta(hours=200),
short_period=timedelta(hours=50),
std_period=timedelta(hours=30),
)
@pytest.fixture(autouse=True)
def _ensure_is_market_active(
settings: MovingAverageStrategySettings,
):
token = os.environ.get("INVEST_SANDBOX_TOKEN")
if token is None:
pytest.skip("INVEST_SANDBOX_TOKEN should be specified")
with Client(token) as client:
share_response: ShareResponse = client.instruments.share_by(
id_type=InstrumentIdType.INSTRUMENT_ID_TYPE_FIGI, id=settings.share_id
)
logger.debug("Instrument = %s", share_response.instrument.name)
response: TradingSchedulesResponse = client.instruments.trading_schedules(
exchange=share_response.instrument.exchange,
from_=now(),
to=now(),
)
(exchange,) = response.exchanges
logger.debug("Exchange = %s", exchange.exchange)
(day,) = exchange.days
if day.is_trading_day and day.start_time < now() < day.end_time:
return
pytest.skip("test skipped because market is closed")
@pytest.mark.test_sandbox()
@pytest.mark.skipif(
os.environ.get("INVEST_SANDBOX_TOKEN") is None,
reason="INVEST_SANDBOX_TOKEN should be specified",
)
@pytest.mark.xfail(
raises=UnauthenticatedError,
reason="INVEST_SANDBOX_TOKEN is incorrect",
)
class TestMovingAverageStrategyTraderInSandbox:
def test_trade(
self,
moving_average_strategy_trader: MovingAverageStrategyTrader,
strategy: MovingAverageStrategy,
account_manager: AccountManager,
signal_executor: MovingAverageSignalExecutor,
plotter: MovingAverageStrategyPlotter,
supervisor: MovingAverageStrategySupervisor,
caplog,
):
caplog.set_level(logging.DEBUG)
caplog.set_level(logging.INFO)
initial_balance = account_manager.get_current_balance()
try:
for i in range(50):
logger.info("Trade %s", i)
moving_average_strategy_trader.trade()
except MarketDataNotAvailableError:
pass
current_balance = account_manager.get_current_balance()
logger.info("Initial balance %s", initial_balance)
logger.info("Current balance %s", current_balance)
events = supervisor.get_events()
plotter.plot(events)
plt.show(block=False)
plt.pause(1)
plt.close()

View File

@@ -0,0 +1,267 @@
import logging
import os
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from typing import Dict, Iterable, List
import pytest
from matplotlib import pyplot as plt
from t_tech.invest import (
Candle,
CandleInterval,
HistoricCandle,
MarketDataResponse,
MoneyValue,
PortfolioPosition,
PortfolioResponse,
Quotation,
)
from t_tech.invest.services import Services
from t_tech.invest.strategies.base.account_manager import AccountManager
from t_tech.invest.strategies.moving_average.plotter import MovingAverageStrategyPlotter
from t_tech.invest.strategies.moving_average.signal_executor import (
MovingAverageSignalExecutor,
)
from t_tech.invest.strategies.moving_average.strategy import MovingAverageStrategy
from t_tech.invest.strategies.moving_average.strategy_settings import (
MovingAverageStrategySettings,
)
from t_tech.invest.strategies.moving_average.supervisor import (
MovingAverageStrategySupervisor,
)
from t_tech.invest.strategies.moving_average.trader import MovingAverageStrategyTrader
from t_tech.invest.typedefs import AccountId, ShareId
from t_tech.invest.utils import candle_interval_to_subscription_interval, now
logging.basicConfig(format="%(asctime)s %(levelname)s:%(message)s", level=logging.INFO)
logger = logging.getLogger(__name__)
@pytest.fixture()
def token() -> str:
return os.environ["INVEST_SANDBOX_TOKEN"]
@pytest.fixture()
def real_market_data_test_from(request) -> datetime:
return request.param
@pytest.fixture()
def real_market_data_test_start(request) -> datetime:
return request.param
@pytest.fixture()
def real_market_data_test_end(request) -> datetime:
return request.param
@pytest.fixture()
def real_market_data(
real_services: Services,
real_market_data_test_from: datetime,
real_market_data_test_end: datetime,
figi: str,
settings: MovingAverageStrategySettings,
) -> Iterable[HistoricCandle]:
candles = []
for candle in real_services.get_all_candles(
figi=figi,
from_=real_market_data_test_from,
to=real_market_data_test_end,
interval=settings.candle_interval,
):
candles.append(candle)
return candles
@pytest.fixture()
def initial_candles(
real_market_data_test_start: datetime,
real_market_data: Iterable[HistoricCandle],
) -> Iterable[HistoricCandle]:
return [
candle
for candle in real_market_data
if candle.time < real_market_data_test_start
]
@pytest.fixture()
def after_start_candles(
real_market_data_test_start: datetime,
real_market_data: Iterable[HistoricCandle],
) -> Iterable[HistoricCandle]:
return [
candle
for candle in real_market_data
if candle.time >= real_market_data_test_start
]
@pytest.fixture()
def current_market_data() -> List[Candle]:
return []
@pytest.fixture()
def mock_market_data_stream_service(
real_services: Services,
mocker,
figi: str,
settings: MovingAverageStrategySettings,
current_market_data: List[Candle],
freezer,
after_start_candles: Iterable[HistoricCandle],
real_market_data_test_from: datetime,
real_market_data_test_start: datetime,
real_market_data_test_end: datetime,
) -> Services:
real_services.market_data_stream = mocker.Mock(
wraps=real_services.market_data_stream
)
freezer.move_to(real_market_data_test_start)
def _market_data_stream(*args, **kwargs):
yield MarketDataResponse(candle=None) # type: ignore
interval = candle_interval_to_subscription_interval(settings.candle_interval)
for historic_candle in after_start_candles:
candle = Candle(
figi=figi,
interval=interval,
open=historic_candle.open,
high=historic_candle.high,
low=historic_candle.low,
close=historic_candle.close,
volume=historic_candle.volume,
time=historic_candle.time,
)
current_market_data.append(candle)
yield MarketDataResponse(candle=candle)
freezer.move_to(now() + timedelta(minutes=1))
real_services.market_data_stream.market_data_stream = _market_data_stream
return real_services
@pytest.fixture()
def portfolio_positions() -> Dict[str, PortfolioPosition]:
return {
"BBG004730N88": PortfolioPosition(
figi="BBG004730N88",
instrument_type="share",
quantity=Quotation(units=110, nano=0),
average_position_price=MoneyValue(
currency="rub", units=261, nano=800000000
),
expected_yield=Quotation(units=-106, nano=-700000000),
current_nkd=MoneyValue(currency="", units=0, nano=0),
average_position_price_pt=Quotation(units=0, nano=0),
current_price=MoneyValue(currency="rub", units=260, nano=830000000),
)
}
@pytest.fixture()
def balance() -> MoneyValue:
return MoneyValue(currency="rub", units=20050, nano=690000000)
@pytest.fixture()
def portfolio_response(
portfolio_positions: Dict[str, PortfolioPosition],
balance: MoneyValue,
) -> PortfolioResponse:
return PortfolioResponse(
total_amount_shares=MoneyValue(currency="rub", units=28691, nano=300000000),
total_amount_bonds=MoneyValue(currency="rub", units=0, nano=0),
total_amount_etf=MoneyValue(currency="rub", units=0, nano=0),
total_amount_currencies=balance,
total_amount_futures=MoneyValue(currency="rub", units=0, nano=0),
expected_yield=Quotation(units=0, nano=-350000000),
positions=list(portfolio_positions.values()),
)
@pytest.fixture()
def figi() -> str:
return "BBG0047315Y7"
@pytest.fixture()
def account_id() -> str:
return AccountId("1337007228")
@pytest.fixture()
def settings(figi: str, account_id: AccountId) -> MovingAverageStrategySettings:
return MovingAverageStrategySettings(
share_id=ShareId(figi),
account_id=account_id,
max_transaction_price=Decimal(10000),
candle_interval=CandleInterval.CANDLE_INTERVAL_1_MIN,
long_period=timedelta(minutes=100),
short_period=timedelta(minutes=50),
std_period=timedelta(minutes=30),
)
def start_datetime() -> datetime:
return datetime(year=2022, month=2, day=16, hour=17, tzinfo=timezone.utc)
@pytest.mark.skipif(
os.environ.get("INVEST_SANDBOX_TOKEN") is None,
reason="INVEST_SANDBOX_TOKEN should be specified",
)
class TestMovingAverageStrategyTraderRealMarketData:
@pytest.mark.freeze_time()
@pytest.mark.parametrize(
(
"real_market_data_test_from",
"real_market_data_test_start",
"real_market_data_test_end",
),
[
(
start_datetime() - timedelta(days=1),
start_datetime(),
start_datetime() + timedelta(days=3),
)
],
indirect=True,
)
def test_trade(
self,
moving_average_strategy_trader: MovingAverageStrategyTrader,
strategy: MovingAverageStrategy,
account_manager: AccountManager,
signal_executor: MovingAverageSignalExecutor,
plotter: MovingAverageStrategyPlotter,
supervisor: MovingAverageStrategySupervisor,
caplog,
freezer,
):
caplog.set_level(logging.DEBUG)
caplog.set_level(logging.INFO)
initial_balance = account_manager.get_current_balance()
for i in range(50):
logger.info("Trade %s", i)
moving_average_strategy_trader.trade()
current_balance = account_manager.get_current_balance()
assert initial_balance != current_balance
logger.info("Initial balance %s", initial_balance)
logger.info("Current balance %s", current_balance)
events = supervisor.get_events()
plotter.plot(events)
plt.show(block=False)
plt.pause(1)
plt.close()

View File

@@ -0,0 +1,44 @@
# pylint: disable=redefined-outer-name,unused-variable
from unittest import mock
import pytest
from t_tech.invest.services import UsersService
@pytest.fixture()
def users_service():
return mock.create_autospec(spec=UsersService)
def test_get_accounts(users_service):
response = users_service.get_accounts() # noqa: F841
users_service.get_accounts.assert_called_once()
def test_get_margin_attributes(users_service):
response = users_service.get_margin_attributes( # noqa: F841
account_id=mock.Mock(),
)
users_service.get_margin_attributes.assert_called_once()
def test_get_user_tariff(users_service):
response = users_service.get_user_tariff() # noqa: F841
users_service.get_user_tariff.assert_called_once()
def test_get_info(users_service):
response = users_service.get_info() # noqa: F841
users_service.get_info.assert_called_once()
def test_get_bank_accounts(users_service):
users_service.get_bank_accounts()
users_service.get_bank_accounts.assert_called_once()
def test_currency_transfer(users_service):
response = users_service.currency_transfer(request=mock.Mock()) # noqa: F841
users_service.currency_transfer.assert_called_once()

View File

@@ -0,0 +1,85 @@
# pylint:disable=protected-access
from datetime import datetime
import pytest
from t_tech.invest.schemas import CandleInterval
from t_tech.invest.utils import empty_or_uuid, get_intervals
@pytest.mark.parametrize(
("candle_interval", "interval", "intervals"),
[
(
CandleInterval.CANDLE_INTERVAL_DAY,
(datetime(2021, 1, 25, 0, 0), datetime(2022, 1, 25, 0, 1)),
[
(
datetime(2021, 1, 25, 0, 0),
datetime(2022, 1, 25, 0, 0),
)
],
),
(
CandleInterval.CANDLE_INTERVAL_DAY,
(datetime(2021, 1, 25, 0, 0), datetime(2023, 2, 26, 0, 1)),
[
(
datetime(2021, 1, 25, 0, 0),
datetime(2022, 1, 25, 0, 0),
),
(
datetime(2022, 1, 26, 0, 0),
datetime(2023, 1, 26, 0, 0),
),
(
datetime(2023, 1, 27, 0, 0),
datetime(2023, 2, 26, 0, 1),
),
],
),
(
CandleInterval.CANDLE_INTERVAL_DAY,
(datetime(2021, 1, 25, 0, 0), datetime(2022, 1, 25, 0, 0)),
[
(
datetime(2021, 1, 25, 0, 0),
datetime(2022, 1, 25, 0, 0),
),
],
),
(
CandleInterval.CANDLE_INTERVAL_DAY,
(datetime(2021, 1, 25, 0, 0), datetime(2022, 1, 24, 0, 0)),
[
(
datetime(2021, 1, 25, 0, 0),
datetime(2022, 1, 24, 0, 0),
),
],
),
],
)
def test_get_intervals(candle_interval, interval, intervals):
result = list(
get_intervals(
candle_interval,
*interval,
)
)
assert result == intervals
@pytest.mark.parametrize(
"s, expected",
[
("", True),
("123", False),
("1234567890", False),
("12345678-1234-1234-1234-abcdabcdabcd", True),
("12345678-12g4-1234-1234-abcdabcdabcd", False),
],
)
def test_is_empty_or_uuid(s: str, expected: bool):
assert expected == empty_or_uuid(s)

View File

@@ -0,0 +1,24 @@
from functools import wraps
import pytest
def skip_when(
exception_type,
is_error_message_expected,
reason="Skipping because of the exception",
):
def decorator_func(f):
@wraps(f)
def wrapper(*args, **kwargs):
try:
return f(*args, **kwargs)
except exception_type as error:
if is_error_message_expected(str(error)):
pytest.skip(reason)
else:
raise error
return wrapper
return decorator_func