All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ru.tinkoff.piapi.core.InvestApi Maven / Gradle / Ivy

There is a newer version: 1.25
Show newest version
package ru.tinkoff.piapi.core;

import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ClientInterceptors;
import io.grpc.ForwardingClientCall;
import io.grpc.ForwardingClientCallListener;
import io.grpc.ManagedChannel;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
import io.grpc.netty.shaded.io.netty.channel.ChannelOption;
import io.grpc.stub.MetadataUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ru.tinkoff.piapi.core.stream.MarketDataStreamService;
import ru.tinkoff.piapi.core.stream.OperationsStreamService;
import ru.tinkoff.piapi.core.stream.OrdersStreamService;
import ru.tinkoff.piapi.contract.v1.*;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.time.Duration;
import java.time.format.DateTimeParseException;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * Интерфейс API реальной торговли.
 * 

* При использовании токена с правами только на чтение * вызов модифицирующих методов приводит к ошибке. */ public class InvestApi { private static final String configResourceName = "config.properties"; private static final String defaultAppName = "tinkoff.invest-api-java-sdk"; private static final Properties props; static { props = loadProps(); } private final Channel channel; private final UsersService userService; private final OperationsService operationsService; private final InstrumentsService instrumentsService; private final StopOrdersService stopOrdersService; private final OrdersService ordersService; private final OrdersStreamService ordersStreamService; private final OperationsStreamService operationsStreamService; private final MarketDataService marketDataService; private final MarketDataStreamService marketDataStreamService; private final SandboxService sandboxService; private final boolean readonlyMode; private final boolean sandboxMode; private InvestApi(@Nonnull Channel channel, boolean readonlyMode, boolean sandboxMode) { this.readonlyMode = readonlyMode; this.sandboxMode = sandboxMode; this.channel = channel; this.instrumentsService = new InstrumentsService( InstrumentsServiceGrpc.newBlockingStub(channel), InstrumentsServiceGrpc.newStub(channel)); this.marketDataService = new MarketDataService( MarketDataServiceGrpc.newBlockingStub(channel), MarketDataServiceGrpc.newStub(channel)); this.marketDataStreamService = new MarketDataStreamService(MarketDataStreamServiceGrpc.newStub(channel)); this.ordersStreamService = new OrdersStreamService(OrdersStreamServiceGrpc.newStub(channel)); this.operationsStreamService = new OperationsStreamService(OperationsStreamServiceGrpc.newStub(channel)); this.userService = new UsersService( UsersServiceGrpc.newBlockingStub(channel), UsersServiceGrpc.newStub(channel), sandboxMode); this.operationsService = new OperationsService( OperationsServiceGrpc.newBlockingStub(channel), OperationsServiceGrpc.newStub(channel), sandboxMode); this.stopOrdersService = new StopOrdersService( StopOrdersServiceGrpc.newBlockingStub(channel), StopOrdersServiceGrpc.newStub(channel), readonlyMode, sandboxMode); this.ordersService = new OrdersService( OrdersServiceGrpc.newBlockingStub(channel), OrdersServiceGrpc.newStub(channel), readonlyMode); this.sandboxService = new SandboxService( SandboxServiceGrpc.newBlockingStub(channel), SandboxServiceGrpc.newStub(channel)); } /** * Создаёт экземпляр API для реальной торговли с использованием * готовой конфигурации GRPC-соединения. *

* ВНИМАНИЕ! Конфигурация должна включать в себя авторизацию * с использованием необходимого токена для реальной торговли. * * @param channel Конфигурация GRPC-соединения. * @return Экземпляр API для реальной торговли. */ @Nonnull public static InvestApi create(@Nonnull Channel channel) { return new InvestApi(channel, false, false); } /** * Создаёт экземпляр API для реальной торговли с использованием * готовой конфигурации GRPC-соединения. *

* * @param token Токен для торговли. * @return Экземпляр API для реальной торговли. */ @Nonnull public static InvestApi create(@Nonnull String token) { return new InvestApi(defaultChannel(token, null), false, false); } /** * Создаёт экземпляр API для реальной торговли с использованием * готовой конфигурации GRPC-соединения. *

* * @param token Токен для торговли. * @param appName Application name для сбора статистики. * Подробности в документации. * @return Экземпляр API для реальной торговли. */ @Nonnull public static InvestApi create(@Nonnull String token, @Nonnull String appName) { return new InvestApi(defaultChannel(token, appName), false, false); } /** * Создаёт экземпляр API в режиме "только для чтения" * с использованием готовой конфигурации GRPC-соединения. *

* ВНИМАНИЕ! Конфигурация должна включать в себя авторизацию * с использованием необходимого токена для режима "только для чтения". * * @param channel Конфигурация GRPC-соединения. * @return Экземпляр API для реальной торговли. */ @Nonnull public static InvestApi createReadonly(@Nonnull Channel channel) { return new InvestApi(channel, true, false); } /** * Создаёт экземпляр API в режиме "только для чтения" * с использованием готовой конфигурации GRPC-соединения. *

* * @param token Токен для торговли. * @return Экземпляр API для реальной торговли. */ @Nonnull public static InvestApi createReadonly(@Nonnull String token) { return new InvestApi(defaultChannel(token, null), true, false); } /** * Создаёт экземпляр API в режиме "только для чтения" * с использованием готовой конфигурации GRPC-соединения. *

* * @param token Токен для торговли. * @param appName Application name для сбора статистики. * Подробности в документации. * @return Экземпляр API для реальной торговли. */ @Nonnull public static InvestApi createReadonly(@Nonnull String token, @Nonnull String appName) { return new InvestApi(defaultChannel(token, appName), true, false); } /** * Создаёт экземпляр API для работы в "песочнице" с использованием * готовой конфигурации GRPC-соединения. *

* ВНИМАНИЕ! Конфигурация должна включать в себя авторизацию * с использованием необходимого токена для "песочницы". * * @param channel Конфигурация GRPC-соединения. * @return Экземпляр API "песочницы". */ @Nonnull public static InvestApi createSandbox(@Nonnull Channel channel) { return new InvestApi(channel, false, true); } /** * Создаёт экземпляр API для работы в "песочнице" с использованием * готовой конфигурации GRPC-соединения. *

* * @param token Токен для торговли. * @return Экземпляр API "песочницы". */ @Nonnull public static InvestApi createSandbox(@Nonnull String token) { var target = Optional.ofNullable(System.getenv("TINKOFF_INVEST_API_TARGET_SANDBOX")) .orElseGet(() -> props.getProperty("ru.tinkoff.piapi.core.sandbox.target")); return new InvestApi(defaultChannel(token, defaultAppName, target), false, true); } /** * Создаёт экземпляр API для работы в "песочнице" с использованием * готовой конфигурации GRPC-соединения. * Подробности про appName в документации. *

* * @param token Токен для торговли. * @param appName Application name для сбора статистики. * Подробности в документации. * @return Экземпляр API "песочницы". */ @Nonnull public static InvestApi createSandbox(@Nonnull String token, @Nonnull String appName) { var target = Optional.ofNullable(System.getenv("TINKOFF_INVEST_API_TARGET_SANDBOX")) .orElseGet(() -> props.getProperty("ru.tinkoff.piapi.core.sandbox.target")); return new InvestApi(defaultChannel(token, appName, target), false, true); } @Nonnull public static Channel defaultChannel(String token, String appName, String target) { var headers = new Metadata(); addAuthHeader(headers, token); addAppNameHeader(headers, appName); Duration connectionTimeout; try { var availableTimeOutValue = Optional.ofNullable(System.getenv("TINKOFF_INVEST_API_CONNECTION_TIMEOUT")) .orElseGet(() -> props.getProperty("ru.tinkoff.piapi.core.connection-timeout")); connectionTimeout = Duration.parse(availableTimeOutValue); } catch (DateTimeParseException e) { connectionTimeout = Duration.parse(props.getProperty("ru.tinkoff.piapi.core.connection-timeout")); } Duration requestTimeout; try { var availableTimeOutValue = Optional.ofNullable(System.getenv("TINKOFF_INVEST_API_REQUEST_TIMEOUT")) .orElseGet(() -> props.getProperty("ru.tinkoff.piapi.core.request-timeout")); requestTimeout = Duration.parse(availableTimeOutValue); } catch (DateTimeParseException e) { requestTimeout = Duration.parse(props.getProperty("ru.tinkoff.piapi.core.request-timeout")); } return NettyChannelBuilder .forTarget(target) .intercept( new LoggingInterceptor(), MetadataUtils.newAttachHeadersInterceptor(headers), new TimeoutInterceptor(requestTimeout)) .withOption( ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) connectionTimeout.toMillis()) // Намерено сужаем тип - предполагается, // что таймаут имеет разумную величину. .useTransportSecurity() .keepAliveTimeout(60, TimeUnit.SECONDS) .maxInboundMessageSize(16777216) // 16 Mb .build(); } @Nonnull public static Channel defaultChannel(String token, String appName) { var target = Optional.ofNullable(System.getenv("TINKOFF_INVEST_API_TARGET")) .orElseGet(() -> props.getProperty("ru.tinkoff.piapi.core.api.target")); return defaultChannel(token, appName, target); } @Nonnull public static Channel defaultChannel(String token) { var target = Optional.ofNullable(System.getenv("TINKOFF_INVEST_API_TARGET")) .orElseGet(() -> props.getProperty("ru.tinkoff.piapi.core.api.target")); return defaultChannel(token, defaultAppName, target); } public static void addAppNameHeader(@Nonnull Metadata metadata, @Nullable String appName) { var key = Metadata.Key.of("x-app-name", Metadata.ASCII_STRING_MARSHALLER); metadata.put(key, appName == null ? defaultAppName : appName); } public static void addAuthHeader(@Nonnull Metadata metadata, @Nonnull String token) { var authKey = Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER); metadata.put(authKey, "Bearer " + token); } /** * Запуск функции апи с поддержкой получения заголовков grpc * * @param api - фукнция (InvestApi call, HeadersWrapper headers) -> T * При вызове через обертку call в функции, после выполнения грпц вызова, * получить заголовки сервера headers.get("x-tracing-id") * @param - тип возвращаемого значения * @return результат выполнения api */ public T runWithHeaders(BiFunction api) { var headersWrapper = new HeadersWrapper(); var metadataCatch = MetadataUtils.newCaptureMetadataInterceptor(headersWrapper.headersRef, headersWrapper.trailersRef); var intercepted = ClientInterceptors.intercept(channel, metadataCatch); return api.apply(new InvestApi(intercepted, readonlyMode, sandboxMode), headersWrapper); } private static Properties loadProps() { var loader = Thread.currentThread().getContextClassLoader(); var props = new Properties(); try (var resourceStream = loader.getResourceAsStream(configResourceName)) { props.load(resourceStream); } catch (IOException e) { throw new RuntimeException(e); } return props; } /** * Получение сервиса котировок. * * @return Сервис котировок. */ @Nonnull public MarketDataService getMarketDataService() { return marketDataService; } /** * Получение сервиса стримов котировок. * * @return Сервис стримов котировок. */ @Nonnull public MarketDataStreamService getMarketDataStreamService() { return marketDataStreamService; } /** * Получение сервиса торговых поручений. * * @return Сервис торговых поручений. */ @Nonnull public OrdersService getOrdersService() { return ordersService; } /** * Получение сервиса стримов торговых поручений. * * @return Сервис стримов торговых поручений. */ @Nonnull public OrdersStreamService getOrdersStreamService() { return ordersStreamService; } /** * Получение сервиса стоп-заявок. * * @return Севис стоп-заявок. */ @Nonnull public StopOrdersService getStopOrdersService() { return stopOrdersService; } /** * Получение сервиса инструментов. * * @return Сервис инструментов. */ @Nonnull public InstrumentsService getInstrumentsService() { return instrumentsService; } /** * Получение сервиса операций. * * @return Сервис операций. */ @Nonnull public OperationsService getOperationsService() { return operationsService; } /** * Получение сервиса аккаунтов. * * @return Сервис аккаунтов. */ @Nonnull public UsersService getUserService() { return userService; } /** * Получение сервиса "песочница". * * @return Сервис "песочница". */ @Nonnull public SandboxService getSandboxService() { return sandboxService; } /** * Получение GRPC-подключения. * * @return GRPC-подключения. */ @Nonnull public Channel getChannel() { return channel; } /** * остановка подключение к api * * @param waitChannelTerminationSec - ожидание терминирования канала сек */ public void destroy(int waitChannelTerminationSec) { try { ((ManagedChannel) getChannel()) .shutdownNow() .awaitTermination(waitChannelTerminationSec, TimeUnit.SECONDS); } catch (InterruptedException e) { throw new RuntimeException(e); } } public boolean isReadonlyMode() { return readonlyMode; } /** * Получение флага режима "песочницы". * * @return Флаг режима "песочницы". */ public boolean isSandboxMode() { return sandboxMode; } public OperationsStreamService getOperationsStreamService() { return operationsStreamService; } static class TimeoutInterceptor implements ClientInterceptor { private final Duration timeout; public TimeoutInterceptor(Duration timeout) { this.timeout = timeout; } @Override public ClientCall interceptCall( MethodDescriptor method, CallOptions callOptions, Channel next) { if (method.getType() == MethodDescriptor.MethodType.UNARY) { callOptions = callOptions.withDeadlineAfter(this.timeout.toMillis(), TimeUnit.MILLISECONDS); } return next.newCall(method, callOptions); } } static class LoggingInterceptor implements ClientInterceptor { private final Logger logger = LoggerFactory.getLogger(LoggingInterceptor.class); @Override public ClientCall interceptCall( MethodDescriptor method, CallOptions callOptions, Channel next) { return new LoggingClientCall<>( next.newCall(method, callOptions), logger, method); } } static class LoggingClientCall extends ForwardingClientCall.SimpleForwardingClientCall { private final Logger logger; private final MethodDescriptor method; LoggingClientCall( ClientCall call, Logger logger, MethodDescriptor method) { super(call); this.logger = logger; this.method = method; } @Override public void start(ClientCall.Listener responseListener, Metadata headers) { logger.debug( "Готовится вызов метода {} сервиса {}.", method.getBareMethodName(), method.getServiceName()); super.start( new LoggingClientCallListener<>(responseListener, logger, method), headers); } } static class LoggingClientCallListener extends ForwardingClientCallListener.SimpleForwardingClientCallListener { private static final Metadata.Key trackingIdKey = Metadata.Key.of("x-tracking-id", Metadata.ASCII_STRING_MARSHALLER); private final Logger logger; private final MethodDescriptor method; volatile private String lastTrackingId; LoggingClientCallListener( ClientCall.Listener listener, Logger logger, MethodDescriptor method) { super(listener); this.logger = logger; this.method = method; } @Override public void onHeaders(Metadata headers) { lastTrackingId = headers.get(trackingIdKey); delegate().onHeaders(headers); } @Override public void onMessage(RespT message) { if (method.getType() == MethodDescriptor.MethodType.UNARY) { logger.debug( "Пришёл ответ от метода {} сервиса {}. (x-tracking-id = {})", method.getBareMethodName(), method.getServiceName(), lastTrackingId); } else { logger.debug( "Пришло сообщение от потока {} сервиса {}.", method.getBareMethodName(), method.getServiceName()); } delegate().onMessage(message); } } private final static Map> METADATA_KEYS = Set.of( "x-tracking-id", "x-ratelimit-limit", "x-ratelimit-remaining", "x-ratelimit-reset", "message" ) .stream() .collect(Collectors.toUnmodifiableMap( Function.identity(), key -> Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER) )); public static class HeadersWrapper { private final AtomicReference headersRef; private final AtomicReference trailersRef; HeadersWrapper() { headersRef = new AtomicReference<>(); trailersRef = new AtomicReference<>(); } public Optional headersMetadata() { return Optional.ofNullable(headersRef.get()); } public Optional trailersMetadata() { return Optional.ofNullable(trailersRef.get()); } public Optional get(String headerNm) { var headerMetaKey = Optional.ofNullable(METADATA_KEYS.get(headerNm)) .or(() -> Optional.of(Metadata.Key.of(headerNm, Metadata.ASCII_STRING_MARSHALLER))) .orElseThrow(); return headersMetadata() .map(metadata -> metadata.get(headerMetaKey)) .or(() -> trailersMetadata().map(metadata -> metadata.get(headerMetaKey))) ; } @Override public String toString() { return "headers=" + headersRef.get() + ", trailers=" + trailersRef.get(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy