RAPTOR v18.4: Исправлена отчетность, активированы выходные
This commit is contained in:
0
invest-python-master/tests/__init__.py
Normal file
0
invest-python-master/tests/__init__.py
Normal file
0
invest-python-master/tests/caches/__init__.py
Normal file
0
invest-python-master/tests/caches/__init__.py
Normal file
275
invest-python-master/tests/caches/test_instrument_cache.py
Normal file
275
invest-python-master/tests/caches/test_instrument_cache.py
Normal 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()
|
||||
26
invest-python-master/tests/caches/test_ttl_cache.py
Normal file
26
invest-python-master/tests/caches/test_ttl_cache.py
Normal 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)
|
||||
0
invest-python-master/tests/data_loaders/__init__.py
Normal file
0
invest-python-master/tests/data_loaders/__init__.py
Normal file
557
invest-python-master/tests/data_loaders/test_cached_load.py
Normal file
557
invest-python-master/tests/data_loaders/test_cached_load.py
Normal 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
|
||||
)
|
||||
153
invest-python-master/tests/data_loaders/test_get_all_candles.py
Normal file
153
invest-python-master/tests/data_loaders/test_get_all_candles.py
Normal 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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
0
invest-python-master/tests/marketdata/__init__.py
Normal file
0
invest-python-master/tests/marketdata/__init__.py
Normal 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)
|
||||
]
|
||||
)
|
||||
25
invest-python-master/tests/test_datetime_utils.py
Normal file
25
invest-python-master/tests/test_datetime_utils.py
Normal 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
|
||||
221
invest-python-master/tests/test_instruments.py
Normal file
221
invest-python-master/tests/test_instruments.py
Normal 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)
|
||||
85
invest-python-master/tests/test_marketdata.py
Normal file
85
invest-python-master/tests/test_marketdata.py
Normal 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)
|
||||
44
invest-python-master/tests/test_operations.py
Normal file
44
invest-python-master/tests/test_operations.py
Normal 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()
|
||||
48
invest-python-master/tests/test_orders.py
Normal file
48
invest-python-master/tests/test_orders.py
Normal 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()
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
56
invest-python-master/tests/test_protobuf_to_dataclass.py
Normal file
56
invest-python-master/tests/test_protobuf_to_dataclass.py
Normal 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
|
||||
239
invest-python-master/tests/test_quotation_convert.py
Normal file
239
invest-python-master/tests/test_quotation_convert.py
Normal 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
|
||||
334
invest-python-master/tests/test_sandbox.py
Normal file
334
invest-python-master/tests/test_sandbox.py
Normal 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)
|
||||
22
invest-python-master/tests/test_signals.py
Normal file
22
invest-python-master/tests/test_signals.py
Normal 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)
|
||||
43
invest-python-master/tests/test_stoporders.py
Normal file
43
invest-python-master/tests/test_stoporders.py
Normal 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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
44
invest-python-master/tests/test_users.py
Normal file
44
invest-python-master/tests/test_users.py
Normal 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()
|
||||
85
invest-python-master/tests/test_utils.py
Normal file
85
invest-python-master/tests/test_utils.py
Normal 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)
|
||||
24
invest-python-master/tests/utils.py
Normal file
24
invest-python-master/tests/utils.py
Normal 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
|
||||
Reference in New Issue
Block a user