RAPTOR v18.4: Исправлена отчетность, активированы выходные
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user