├── jitpack.yml ├── example ├── src │ └── main │ │ └── resources │ │ └── simplelogger.properties └── pom.xml ├── .gitignore ├── core ├── src │ ├── main │ │ ├── java │ │ │ └── ru │ │ │ │ └── tinkoff │ │ │ │ └── piapi │ │ │ │ └── core │ │ │ │ ├── stream │ │ │ │ ├── StreamProcessor.java │ │ │ │ ├── StreamObserverWithProcessor.java │ │ │ │ ├── MarketDataStreamService.java │ │ │ │ ├── OrdersStreamService.java │ │ │ │ ├── OperationsStreamService.java │ │ │ │ └── MarketDataSubscriptionService.java │ │ │ │ ├── exception │ │ │ │ ├── SandboxModeViolationException.java │ │ │ │ ├── ReadonlyModeViolationException.java │ │ │ │ └── ApiRuntimeException.java │ │ │ │ ├── models │ │ │ │ ├── Money.java │ │ │ │ ├── FuturePosition.java │ │ │ │ ├── SecurityPosition.java │ │ │ │ ├── Portfolio.java │ │ │ │ ├── VirtualPosition.java │ │ │ │ ├── WithdrawLimits.java │ │ │ │ ├── Positions.java │ │ │ │ └── Position.java │ │ │ │ ├── utils │ │ │ │ ├── ValidationUtils.java │ │ │ │ ├── DateUtils.java │ │ │ │ ├── MapperUtils.java │ │ │ │ └── Helpers.java │ │ │ │ ├── UsersService.java │ │ │ │ ├── StopOrdersService.java │ │ │ │ ├── OrdersService.java │ │ │ │ ├── SandboxService.java │ │ │ │ └── MarketDataService.java │ │ └── resources │ │ │ └── config.properties │ └── test │ │ ├── resources │ │ └── config.properties │ │ └── java │ │ └── ru │ │ └── tinkoff │ │ └── piapi │ │ └── core │ │ ├── GrpcClientTester.java │ │ ├── HelpersTest.java │ │ ├── InvestApiTest.java │ │ ├── MapperTest.java │ │ ├── UsersServiceTest.java │ │ ├── MarketDataServiceTest.java │ │ ├── OrdersServiceTest.java │ │ └── StopOrdersServiceTest.java └── pom.xml ├── .editorconfig ├── .github └── workflows │ └── maven.yml ├── contract ├── src │ └── main │ │ └── proto │ │ ├── signals.proto │ │ ├── common.proto │ │ ├── sandbox.proto │ │ ├── stoporders.proto │ │ ├── users.proto │ │ └── orders.proto └── pom.xml ├── CHANGELOG.md ├── README.md ├── pom.xml └── LICENSE /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk11 -------------------------------------------------------------------------------- /example/src/main/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | org.slf4j.simpleLogger.defaultLogLevel=debug 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .gradle/ 3 | .idea/ 4 | logs/ 5 | .settings/ 6 | .project 7 | .classpath 8 | bin/ 9 | .vscode/ 10 | target/ 11 | 12 | gradle.properties 13 | *.iml 14 | .DS_Store -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/stream/StreamProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.stream; 2 | 3 | public interface StreamProcessor { 4 | 5 | void process(T response); 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [*.java] 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /core/src/test/resources/config.properties: -------------------------------------------------------------------------------- 1 | ru.tinkoff.piapi.core.api.target=localhost:8080 2 | ru.tinkoff.piapi.core.sandbox.target=localhost:8080 3 | ru.tinkoff.piapi.core.connection-timeout=PT1S 4 | ru.tinkoff.piapi.core.request-timeout=PT2S 5 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/exception/SandboxModeViolationException.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.exception; 2 | 3 | public class SandboxModeViolationException extends RuntimeException { 4 | public SandboxModeViolationException() { 5 | super("Это действие нельзя совершить в режиме \"песочницы\"."); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/exception/ReadonlyModeViolationException.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.exception; 2 | 3 | public class ReadonlyModeViolationException extends RuntimeException { 4 | public ReadonlyModeViolationException() { 5 | super("Это действие нельзя совершить в режиме \"только для чтения\"."); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/resources/config.properties: -------------------------------------------------------------------------------- 1 | # Can be managed by environment variable TINKOFF_INVEST_API_TARGET 2 | ru.tinkoff.piapi.core.api.target=invest-public-api.tinkoff.ru:443 3 | # Can be managed by environment variable TINKOFF_INVEST_API_TARGET_SANDBOX 4 | ru.tinkoff.piapi.core.sandbox.target=sandbox-invest-public-api.tinkoff.ru:443 5 | # Can be managed by environment variable TINKOFF_INVEST_API_CONNECTION_TIMEOUT 6 | ru.tinkoff.piapi.core.connection-timeout=PT1S 7 | # Can be managed by environment variable TINKOFF_INVEST_API_REQUEST_TIMEOUT 8 | ru.tinkoff.piapi.core.request-timeout=PT60S 9 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/exception/ApiRuntimeException.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.exception; 2 | 3 | import io.grpc.Metadata; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | public class ApiRuntimeException extends RuntimeException { 8 | 9 | private final Throwable throwable; 10 | private final String code; 11 | private final String message; 12 | private final String trackingId; 13 | private final Metadata metadata; 14 | 15 | public ApiRuntimeException(String message, String code, String trackingId, Throwable throwable, Metadata metadata) { 16 | super(code + " " + message + " tracking_id " + trackingId, throwable); 17 | this.metadata = metadata; 18 | this.throwable = throwable; 19 | this.message = message; 20 | this.code = code; 21 | this.trackingId = trackingId; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | branches: 12 | - main 13 | types: [opened, synchronize, reopened, ready_for_review] 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up JDK 11 23 | uses: actions/setup-java@v2 24 | with: 25 | java-version: '11' 26 | distribution: 'temurin' 27 | cache: maven 28 | - name: Build with Maven 29 | run: mvn -B package --file pom.xml 30 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/models/Money.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.models; 2 | 3 | import lombok.Builder; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import ru.tinkoff.piapi.contract.v1.MoneyValue; 7 | import ru.tinkoff.piapi.core.utils.MapperUtils; 8 | 9 | import javax.annotation.Nonnull; 10 | import java.math.BigDecimal; 11 | 12 | @Getter 13 | @EqualsAndHashCode 14 | @Builder 15 | public class Money { 16 | private final String currency; 17 | private final BigDecimal value; 18 | 19 | private Money(@Nonnull String currency, @Nonnull BigDecimal value) { 20 | this.currency = currency; 21 | this.value = value; 22 | } 23 | 24 | public static Money fromResponse(@Nonnull MoneyValue moneyValue) { 25 | return Money.builder() 26 | .currency(moneyValue.getCurrency()) 27 | .value(MapperUtils.moneyValueToBigDecimal(moneyValue)) 28 | .build(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/stream/StreamObserverWithProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.stream; 2 | 3 | import io.grpc.stub.StreamObserver; 4 | 5 | import javax.annotation.Nonnull; 6 | import javax.annotation.Nullable; 7 | import java.util.function.Consumer; 8 | 9 | public class StreamObserverWithProcessor implements StreamObserver { 10 | 11 | private final StreamProcessor streamProcessor; 12 | private final Consumer onErrorCallback; 13 | 14 | public StreamObserverWithProcessor(@Nonnull StreamProcessor streamProcessor, 15 | @Nullable Consumer onErrorCallback) { 16 | this.streamProcessor = streamProcessor; 17 | this.onErrorCallback = onErrorCallback; 18 | } 19 | 20 | @Override 21 | public void onNext(T value) { 22 | streamProcessor.process(value); 23 | } 24 | 25 | @Override 26 | public void onError(Throwable t) { 27 | if (onErrorCallback != null) { 28 | onErrorCallback.accept(t); 29 | } 30 | } 31 | 32 | @Override 33 | public void onCompleted() { 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/utils/ValidationUtils.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.utils; 2 | 3 | import ru.tinkoff.piapi.core.exception.ReadonlyModeViolationException; 4 | import ru.tinkoff.piapi.core.exception.SandboxModeViolationException; 5 | 6 | import java.time.Instant; 7 | 8 | public class ValidationUtils { 9 | private static final String TO_IS_NOT_AFTER_FROM_MESSAGE = "Окончание периода не может быть раньше начала."; 10 | private static final String WRONG_PAGE_MESSAGE = "Номерами страниц могут быть только положительные числа."; 11 | 12 | 13 | public static void checkPage(int page) { 14 | if (page < 0) { 15 | throw new IllegalArgumentException(WRONG_PAGE_MESSAGE); 16 | } 17 | } 18 | 19 | public static void checkFromTo(Instant from, Instant to) { 20 | if (from.isAfter(to)) { 21 | throw new IllegalArgumentException(TO_IS_NOT_AFTER_FROM_MESSAGE); 22 | } 23 | } 24 | 25 | public static void checkReadonly(boolean readonlyMode) { 26 | if (readonlyMode) { 27 | throw new ReadonlyModeViolationException(); 28 | } 29 | } 30 | 31 | public static void checkSandbox(boolean sandboxMode) { 32 | if (sandboxMode) { 33 | throw new SandboxModeViolationException(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /example/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | ru.tinkoff.piapi 7 | java-sdk 8 | 1.6-SNAPSHOT 9 | 10 | 11 | java-sdk-example 12 | 1.6-SNAPSHOT 13 | Tinkoff Invest API Java SDK - Example 14 | jar 15 | 16 | 17 | UTF-8 18 | 11 19 | 11 20 | 11 21 | 22 | 23 | 24 | 25 | ru.tinkoff.piapi 26 | java-sdk-core 27 | 1.6-SNAPSHOT 28 | 29 | 30 | 31 | org.slf4j 32 | slf4j-simple 33 | 1.7.33 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/models/FuturePosition.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.models; 2 | 3 | import ru.tinkoff.piapi.contract.v1.PositionsFutures; 4 | 5 | import javax.annotation.Nonnull; 6 | import java.util.Objects; 7 | 8 | public class FuturePosition { 9 | private final String figi; 10 | private final long blocked; 11 | private final long balance; 12 | 13 | private FuturePosition(@Nonnull String figi, long blocked, long balance) { 14 | this.figi = figi; 15 | this.blocked = blocked; 16 | this.balance = balance; 17 | } 18 | 19 | @Nonnull 20 | public static FuturePosition fromResponse(@Nonnull PositionsFutures positionsFutures) { 21 | return new FuturePosition( 22 | positionsFutures.getFigi(), 23 | positionsFutures.getBlocked(), 24 | positionsFutures.getBalance() 25 | ); 26 | } 27 | 28 | @Nonnull 29 | public String getFigi() { 30 | return figi; 31 | } 32 | 33 | public long getBlocked() { 34 | return blocked; 35 | } 36 | 37 | public long getBalance() { 38 | return balance; 39 | } 40 | 41 | @Override 42 | public boolean equals(Object o) { 43 | if (this == o) { 44 | return true; 45 | } 46 | if (o == null || getClass() != o.getClass()) { 47 | return false; 48 | } 49 | FuturePosition that = (FuturePosition) o; 50 | return blocked == that.blocked && balance == that.balance && figi.equals(that.figi); 51 | } 52 | 53 | @Override 54 | public int hashCode() { 55 | return Objects.hash(figi, blocked, balance); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/models/SecurityPosition.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.models; 2 | 3 | import ru.tinkoff.piapi.contract.v1.PositionsSecurities; 4 | 5 | import javax.annotation.Nonnull; 6 | import java.util.Objects; 7 | 8 | public class SecurityPosition { 9 | private final String figi; 10 | private final long blocked; 11 | private final long balance; 12 | 13 | private SecurityPosition(@Nonnull String figi, long blocked, long balance) { 14 | this.figi = figi; 15 | this.blocked = blocked; 16 | this.balance = balance; 17 | } 18 | 19 | @Nonnull 20 | public static SecurityPosition fromResponse(@Nonnull PositionsSecurities positionsSecurities) { 21 | return new SecurityPosition( 22 | positionsSecurities.getFigi(), 23 | positionsSecurities.getBlocked(), 24 | positionsSecurities.getBalance() 25 | ); 26 | } 27 | 28 | @Nonnull 29 | public String getFigi() { 30 | return figi; 31 | } 32 | 33 | public long getBlocked() { 34 | return blocked; 35 | } 36 | 37 | public long getBalance() { 38 | return balance; 39 | } 40 | 41 | @Override 42 | public boolean equals(Object o) { 43 | if (this == o) { 44 | return true; 45 | } 46 | if (o == null || getClass() != o.getClass()) { 47 | return false; 48 | } 49 | SecurityPosition that = (SecurityPosition) o; 50 | return blocked == that.blocked && balance == that.balance && figi.equals(that.figi); 51 | } 52 | 53 | @Override 54 | public int hashCode() { 55 | return Objects.hash(figi, blocked, balance); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/stream/MarketDataStreamService.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.stream; 2 | 3 | import ru.tinkoff.piapi.contract.v1.MarketDataResponse; 4 | import ru.tinkoff.piapi.contract.v1.MarketDataStreamServiceGrpc; 5 | 6 | import javax.annotation.Nonnull; 7 | import javax.annotation.Nullable; 8 | import java.util.Map; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | import java.util.function.Consumer; 11 | 12 | public class MarketDataStreamService { 13 | 14 | private final MarketDataStreamServiceGrpc.MarketDataStreamServiceStub stub; 15 | private final Map streamMap = new ConcurrentHashMap<>(); 16 | 17 | public MarketDataStreamService(MarketDataStreamServiceGrpc.MarketDataStreamServiceStub stub) { 18 | this.stub = stub; 19 | } 20 | 21 | public int streamCount() { 22 | return streamMap.size(); 23 | } 24 | 25 | public MarketDataSubscriptionService getStreamById(String id) { 26 | return streamMap.get(id); 27 | } 28 | 29 | public Map getAllStreams() { 30 | return streamMap; 31 | } 32 | 33 | public MarketDataSubscriptionService newStream(@Nonnull String id, 34 | @Nonnull StreamProcessor streamProcessor, 35 | @Nullable Consumer onErrorCallback) { 36 | if (streamMap.containsKey(id)) { 37 | var existSubscriptionService = streamMap.get(id); 38 | existSubscriptionService.cancel(); 39 | } 40 | var subscriptionService = new MarketDataSubscriptionService(stub, streamProcessor, onErrorCallback); 41 | streamMap.put(id, subscriptionService); 42 | return subscriptionService; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /core/src/test/java/ru/tinkoff/piapi/core/GrpcClientTester.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core; 2 | 3 | import io.grpc.BindableService; 4 | import io.grpc.Channel; 5 | import io.grpc.inprocess.InProcessChannelBuilder; 6 | import io.grpc.inprocess.InProcessServerBuilder; 7 | import io.grpc.testing.GrpcCleanupRule; 8 | import org.junit.Rule; 9 | 10 | import java.io.IOException; 11 | import java.util.function.Function; 12 | 13 | abstract class GrpcClientTester { 14 | @Rule 15 | public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); 16 | 17 | abstract protected T createClient(Channel channel); 18 | 19 | final protected T mkClientBasedOnServer(BindableService grpcService, Function customCreator) { 20 | var serverName = InProcessServerBuilder.generateName(); 21 | try { 22 | grpcCleanup.register( 23 | InProcessServerBuilder.forName(serverName) 24 | .directExecutor() 25 | .addService(grpcService) 26 | .build() 27 | .start()); 28 | } catch (IOException e) { 29 | System.err.println(e.getLocalizedMessage()); 30 | System.exit(1); 31 | } 32 | 33 | var channel = grpcCleanup.register( 34 | InProcessChannelBuilder.forName(serverName) 35 | .directExecutor() 36 | .build()); 37 | 38 | return customCreator.apply(channel); 39 | } 40 | 41 | final protected T mkClientBasedOnServer(BindableService grpcService) { 42 | var serverName = InProcessServerBuilder.generateName(); 43 | try { 44 | grpcCleanup.register( 45 | InProcessServerBuilder.forName(serverName) 46 | .directExecutor() 47 | .addService(grpcService) 48 | .build() 49 | .start()); 50 | } catch (IOException e) { 51 | System.err.println(e.getLocalizedMessage()); 52 | System.exit(1); 53 | } 54 | 55 | var channel = grpcCleanup.register( 56 | InProcessChannelBuilder.forName(serverName) 57 | .directExecutor() 58 | .build()); 59 | 60 | return createClient(channel); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/models/Portfolio.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.models; 2 | 3 | import lombok.Builder; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import ru.tinkoff.piapi.contract.v1.PortfolioResponse; 7 | import ru.tinkoff.piapi.core.utils.MapperUtils; 8 | 9 | import javax.annotation.Nonnull; 10 | import java.math.BigDecimal; 11 | import java.util.List; 12 | 13 | @Getter 14 | @EqualsAndHashCode 15 | @Builder 16 | public class Portfolio { 17 | private final Money totalAmountShares; 18 | private final Money totalAmountBonds; 19 | private final Money totalAmountEtfs; 20 | private final Money totalAmountCurrencies; 21 | private final Money totalAmountFutures; 22 | private final Money totalAmountPortfolio; 23 | private final BigDecimal expectedYield; 24 | private final List positions; 25 | private final Money totalAmountSp; 26 | private final Money totalAmountOptions; 27 | private final List virtualPositions; 28 | 29 | public static Portfolio fromResponse(@Nonnull PortfolioResponse portfolioResponse) { 30 | return Portfolio.builder() 31 | .totalAmountShares(Money.fromResponse(portfolioResponse.getTotalAmountShares())) 32 | .totalAmountBonds(Money.fromResponse(portfolioResponse.getTotalAmountBonds())) 33 | .totalAmountEtfs(Money.fromResponse(portfolioResponse.getTotalAmountEtf())) 34 | .totalAmountCurrencies(Money.fromResponse(portfolioResponse.getTotalAmountCurrencies())) 35 | .totalAmountFutures(Money.fromResponse(portfolioResponse.getTotalAmountFutures())) 36 | .totalAmountOptions(Money.fromResponse(portfolioResponse.getTotalAmountOptions())) 37 | .totalAmountSp(Money.fromResponse(portfolioResponse.getTotalAmountSp())) 38 | .totalAmountPortfolio(Money.fromResponse(portfolioResponse.getTotalAmountPortfolio())) 39 | .expectedYield(MapperUtils.quotationToBigDecimal(portfolioResponse.getExpectedYield())) 40 | .virtualPositions(VirtualPosition.fromResponse(portfolioResponse.getVirtualPositionsList())) 41 | .positions(Position.fromResponse(portfolioResponse.getPositionsList())) 42 | .build(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /contract/src/main/proto/signals.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tinkoff.public.invest.api.contract.v1; 4 | 5 | option go_package = "./;investapi"; 6 | option java_package = "ru.tinkoff.piapi.contract.v1"; 7 | option java_multiple_files = true; 8 | option csharp_namespace = "Tinkoff.InvestApi.V1"; 9 | option objc_class_prefix = "TIAPI"; 10 | option php_namespace = "Tinkoff\\Invest\\V1"; 11 | 12 | import "google/protobuf/timestamp.proto"; 13 | 14 | service SignalService { 15 | rpc GetStrategies(GetStrategiesRequest) returns (GetStrategiesResponse); 16 | 17 | rpc GetSignals(GetSignalsRequest) returns (GetSignalsResponse); 18 | } 19 | 20 | message GetStrategiesRequest { 21 | string strategy_id = 1; //Идентификатор стратегии 22 | } 23 | 24 | message GetStrategiesResponse { 25 | repeated Strategy strategies = 1; 26 | } 27 | 28 | message Strategy { 29 | string strategy_id = 1; // Идентификатор стратегии 30 | string strategy_name = 2; // Название стратегии 31 | string strategy_description = 3; // Описание стратегии 32 | string strategy_url = 4; // Ссылка на страницу с описанием стратегии 33 | } 34 | 35 | message GetSignalsRequest { 36 | string strategy_id = 1; // Идентификатор стратегии 37 | google.protobuf.Timestamp from = 2; // Дата начала запрашиваемого интервала в часовом поясе UTC. 38 | string instrument_uid = 3; // Идентификатор бумаги 39 | google.protobuf.Timestamp to = 4; // Дата конца запрашиваемого интервала в часовом поясе UTC. 40 | bool archive = 5; // Только архивные сигналы, по умолчанию false 41 | } 42 | 43 | message GetSignalsResponse { 44 | repeated Signal signals = 1; 45 | } 46 | 47 | message Signal { 48 | string signal_id = 1; // идентификатор сигнала 49 | string strategy_id = 2; // Идентификатор стратегии 50 | string instrument_uid = 3; // Идентификатор бумаги 51 | google.protobuf.Timestamp create_dt = 4; // Датавремя создания сигнала в часовом поясе UTC. 52 | int64 lifetime = 5; // Срок действия сигнала, секунд 53 | string direction = 6; // "buy" / "sell" 54 | float price = 7; // Цена бумаги на момент формирования сигнала 55 | float profit = 8; // Расчетная прибыль сигнала 56 | string info = 9; // Произвольная информация о сигнале 57 | } 58 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/models/VirtualPosition.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.models; 2 | 3 | import lombok.Builder; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import ru.tinkoff.piapi.contract.v1.VirtualPortfolioPosition; 7 | import ru.tinkoff.piapi.core.utils.DateUtils; 8 | import ru.tinkoff.piapi.core.utils.MapperUtils; 9 | 10 | import javax.annotation.Nonnull; 11 | import java.math.BigDecimal; 12 | import java.time.Instant; 13 | import java.util.List; 14 | import java.util.stream.Collectors; 15 | 16 | @Getter 17 | @EqualsAndHashCode 18 | @Builder 19 | public class VirtualPosition { 20 | private final String figi; 21 | private final String positionUid; 22 | private final String instrumentUid; 23 | private final String instrumentType; 24 | private final BigDecimal quantity; 25 | private final Money averagePositionPrice; 26 | private final BigDecimal expectedYield; 27 | private final BigDecimal expectedYieldFifo; 28 | private final Money currentPrice; 29 | private final Money averagePositionPriceFifo; 30 | private final BigDecimal quantityLots; 31 | private Instant expireDate; 32 | 33 | @Nonnull 34 | public static VirtualPosition fromResponse(@Nonnull VirtualPortfolioPosition virtualPosition) { 35 | return VirtualPosition.builder() 36 | .figi(virtualPosition.getFigi()) 37 | .instrumentUid(virtualPosition.getInstrumentUid()) 38 | .positionUid(virtualPosition.getPositionUid()) 39 | .figi(virtualPosition.getFigi()) 40 | .instrumentType(virtualPosition.getInstrumentType()) 41 | .quantity(MapperUtils.quotationToBigDecimal(virtualPosition.getQuantity())) 42 | .averagePositionPrice(Money.fromResponse(virtualPosition.getAveragePositionPrice())) 43 | .expectedYield(MapperUtils.quotationToBigDecimal(virtualPosition.getExpectedYield())) 44 | .expectedYieldFifo(MapperUtils.quotationToBigDecimal(virtualPosition.getExpectedYieldFifo())) 45 | .expireDate(DateUtils.timestampToInstant(virtualPosition.getExpireDate())) 46 | .currentPrice(Money.fromResponse(virtualPosition.getCurrentPrice())) 47 | .averagePositionPriceFifo(Money.fromResponse(virtualPosition.getAveragePositionPriceFifo())) 48 | .build(); 49 | } 50 | 51 | public static List fromResponse(@Nonnull List virtualPositions) { 52 | return virtualPositions.stream().map(VirtualPosition::fromResponse).collect(Collectors.toList()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/models/WithdrawLimits.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.models; 2 | 3 | import ru.tinkoff.piapi.contract.v1.WithdrawLimitsResponse; 4 | 5 | import javax.annotation.Nonnull; 6 | import java.util.List; 7 | import java.util.Objects; 8 | import java.util.stream.Collectors; 9 | 10 | /** 11 | * Доступный для вывода остаток. 12 | */ 13 | public class WithdrawLimits { 14 | private final List money; 15 | private final List blocked; 16 | private final List blockedGuarantee; 17 | 18 | private WithdrawLimits(@Nonnull List money, 19 | @Nonnull List blocked, 20 | @Nonnull List blockedGuarantee) { 21 | this.money = money; 22 | this.blocked = blocked; 23 | this.blockedGuarantee = blockedGuarantee; 24 | } 25 | 26 | public static WithdrawLimits fromResponse(@Nonnull WithdrawLimitsResponse withdrawLimitsResponse) { 27 | return new WithdrawLimits( 28 | withdrawLimitsResponse.getMoneyList().stream().map(Money::fromResponse).collect(Collectors.toList()), 29 | withdrawLimitsResponse.getBlockedList().stream().map(Money::fromResponse).collect(Collectors.toList()), 30 | withdrawLimitsResponse.getBlockedGuaranteeList().stream().map(Money::fromResponse).collect(Collectors.toList()) 31 | ); 32 | } 33 | 34 | /** 35 | * Получение валютных позиций портфеля. 36 | * 37 | * @return Валютные позиции портфеля. 38 | */ 39 | @Nonnull 40 | public List getMoney() { 41 | return money; 42 | } 43 | 44 | /** 45 | * Получение заблокированных валютных позиций портфеля. 46 | * 47 | * @return Заблокированные валютные позиций портфеля. 48 | */ 49 | @Nonnull 50 | public List getBlocked() { 51 | return blocked; 52 | } 53 | 54 | /** 55 | * Получение средств заблокированных под гарантийное обеспечение фьючерсов. 56 | * 57 | * @return Средства заблокированные под гарантийное обеспечение фьючерсов. 58 | */ 59 | @Nonnull 60 | public List getBlockedGuarantee() { 61 | return blockedGuarantee; 62 | } 63 | 64 | @Override 65 | public boolean equals(Object o) { 66 | if (this == o) { 67 | return true; 68 | } 69 | if (o == null || getClass() != o.getClass()) { 70 | return false; 71 | } 72 | WithdrawLimits that = (WithdrawLimits) o; 73 | return money.equals(that.money) && blocked.equals(that.blocked) && blockedGuarantee.equals(that.blockedGuarantee); 74 | } 75 | 76 | @Override 77 | public int hashCode() { 78 | return Objects.hash(money, blocked, blockedGuarantee); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/models/Positions.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.models; 2 | 3 | import ru.tinkoff.piapi.contract.v1.PositionsResponse; 4 | 5 | import javax.annotation.Nonnull; 6 | import java.util.List; 7 | import java.util.Objects; 8 | import java.util.stream.Collectors; 9 | 10 | public class Positions { 11 | private final List money; 12 | private final List blocked; 13 | private final List securities; 14 | private final boolean limitsLoadingInProgress; 15 | private final List futures; 16 | 17 | @Nonnull 18 | public static Positions fromResponse(@Nonnull PositionsResponse positionsResponse) { 19 | return new Positions( 20 | positionsResponse.getMoneyList().stream().map(Money::fromResponse).collect(Collectors.toList()), 21 | positionsResponse.getBlockedList().stream().map(Money::fromResponse).collect(Collectors.toList()), 22 | positionsResponse.getSecuritiesList().stream().map(SecurityPosition::fromResponse).collect(Collectors.toList()), 23 | positionsResponse.getLimitsLoadingInProgress(), 24 | positionsResponse.getFuturesList().stream().map(FuturePosition::fromResponse).collect(Collectors.toList()) 25 | ); 26 | } 27 | 28 | private Positions(@Nonnull List money, 29 | @Nonnull List blocked, 30 | @Nonnull List securities, 31 | boolean limitsLoadingInProgress, 32 | @Nonnull List futures) { 33 | this.money = money; 34 | this.blocked = blocked; 35 | this.securities = securities; 36 | this.limitsLoadingInProgress = limitsLoadingInProgress; 37 | this.futures = futures; 38 | } 39 | 40 | @Nonnull 41 | public List getMoney() { 42 | return money; 43 | } 44 | 45 | @Nonnull 46 | public List getBlocked() { 47 | return blocked; 48 | } 49 | 50 | @Nonnull 51 | public List getSecurities() { 52 | return securities; 53 | } 54 | 55 | public boolean isLimitsLoadingInProgress() { 56 | return limitsLoadingInProgress; 57 | } 58 | 59 | @Nonnull 60 | public List getFutures() { 61 | return futures; 62 | } 63 | 64 | @Override 65 | public boolean equals(Object o) { 66 | if (this == o) return true; 67 | if (o == null || getClass() != o.getClass()) return false; 68 | Positions positions = (Positions) o; 69 | return limitsLoadingInProgress == positions.limitsLoadingInProgress && money.equals(positions.money) && 70 | blocked.equals(positions.blocked) && securities.equals(positions.securities) && 71 | futures.equals(positions.futures); 72 | } 73 | 74 | @Override 75 | public int hashCode() { 76 | return Objects.hash(money, blocked, securities, limitsLoadingInProgress, futures); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /contract/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | ru.tinkoff.piapi 7 | java-sdk 8 | 1.6-SNAPSHOT 9 | 10 | 11 | java-sdk-grpc-contract 12 | 1.6-SNAPSHOT 13 | Tinkoff Invest API Java SDK - GRPC contracts 14 | jar 15 | 16 | 17 | UTF-8 18 | 11 19 | 11 20 | 11 21 | 22 | 23 | 24 | 25 | 26 | kr.motd.maven 27 | os-maven-plugin 28 | 1.6.2 29 | 30 | 31 | 32 | 33 | org.xolstice.maven.plugins 34 | protobuf-maven-plugin 35 | 0.6.1 36 | 37 | com.google.protobuf:protoc:3.22.3:exe:${os.detected.classifier} 38 | grpc-java 39 | io.grpc:protoc-gen-grpc-java:${io.grpc.version}:exe:${os.detected.classifier} 40 | 41 | 42 | 43 | 44 | compile 45 | compile-custom 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | io.grpc 56 | grpc-protobuf 57 | ${io.grpc.version} 58 | 59 | 60 | io.grpc 61 | grpc-stub 62 | ${io.grpc.version} 63 | 64 | 65 | org.apache.tomcat 66 | annotations-api 67 | 6.0.53 68 | provided 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /core/src/test/java/ru/tinkoff/piapi/core/HelpersTest.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import java.time.Instant; 6 | import java.util.LinkedList; 7 | import java.util.List; 8 | import java.util.stream.Stream; 9 | 10 | import com.google.protobuf.Timestamp; 11 | 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.params.ParameterizedTest; 14 | import org.junit.jupiter.params.provider.Arguments; 15 | import org.junit.jupiter.params.provider.MethodSource; 16 | 17 | import io.smallrye.mutiny.Multi; 18 | import ru.tinkoff.piapi.core.utils.DateUtils; 19 | import ru.tinkoff.piapi.core.utils.Helpers; 20 | 21 | public class HelpersTest { 22 | 23 | @Test 24 | void wrapWithFuture_Test() { 25 | var expected = "sample"; 26 | 27 | var future = Helpers.unaryAsyncCall(observer -> { 28 | observer.onNext(expected); 29 | observer.onCompleted(); 30 | }); 31 | 32 | assertEquals(expected, future.join()); 33 | } 34 | 35 | @Test 36 | void instantToTimestamp_Test() { 37 | var input = Instant.ofEpochSecond(1234567890, 111222333); 38 | var expected = Timestamp.newBuilder() 39 | .setSeconds(input.getEpochSecond()) 40 | .setNanos(input.getNano()) 41 | .build(); 42 | 43 | var actual = DateUtils.instantToTimestamp(input); 44 | 45 | assertEquals(expected, actual); 46 | } 47 | 48 | @Test 49 | void timestampToInstant_Test() { 50 | var input = Timestamp.newBuilder() 51 | .setSeconds(1234567890) 52 | .setNanos(111222333) 53 | .build(); 54 | var expected = Instant.ofEpochSecond(input.getSeconds(), input.getNanos()); 55 | 56 | var actual = DateUtils.timestampToInstant(input); 57 | 58 | assertEquals(expected, actual); 59 | } 60 | 61 | @Test 62 | void wrapEmitterWithStreamObserver_Test() { 63 | var expected = List.of("sample"); 64 | var actual = new LinkedList(); 65 | 66 | Multi.createFrom() 67 | .emitter(emitter -> { 68 | var observer = Helpers.wrapEmitterWithStreamObserver(emitter); 69 | for (var item : expected) { 70 | observer.onNext(item); 71 | } 72 | }) 73 | .subscribe() 74 | .with(actual::add); 75 | 76 | assertIterableEquals(expected, actual); 77 | } 78 | 79 | @ParameterizedTest 80 | @MethodSource("stringAndStringProvider") 81 | void preprocessInputOrderId_Test(String input, String expected) { 82 | assertEquals(expected, Helpers.preprocessInputOrderId(input)); 83 | } 84 | 85 | static Stream stringAndStringProvider() { 86 | return Stream.of( 87 | Arguments.of("test", "test"), 88 | Arguments.of("abcdefghijklmnopqrstuvwxyz1234567890extra", "abcdefghijklmnopqrstuvwxyz1234567890"), 89 | Arguments.of("abcdefghijklmnopqrstuvwxyz1234567890", "abcdefghijklmnopqrstuvwxyz1234567890"), 90 | Arguments.of("", ""), 91 | Arguments.of(" ", ""), 92 | Arguments.of(" ", "") 93 | ); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/models/Position.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.models; 2 | 3 | import lombok.Builder; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import ru.tinkoff.piapi.contract.v1.PortfolioPosition; 7 | import ru.tinkoff.piapi.core.utils.MapperUtils; 8 | 9 | import javax.annotation.Nonnull; 10 | import java.math.BigDecimal; 11 | import java.util.List; 12 | import java.util.stream.Collectors; 13 | 14 | @Getter 15 | @EqualsAndHashCode 16 | @Builder 17 | public class Position { 18 | private final String figi; 19 | private final String instrumentType; 20 | private final BigDecimal quantity; 21 | private final Money averagePositionPrice; 22 | private final BigDecimal expectedYield; 23 | private final Money currentNkd; 24 | private final BigDecimal averagePositionPricePt; 25 | private final Money currentPrice; 26 | private final Money averagePositionPriceFifo; 27 | private final BigDecimal quantityLots; 28 | 29 | private Position(@Nonnull String figi, 30 | @Nonnull String instrumentType, 31 | @Nonnull BigDecimal quantity, 32 | @Nonnull Money averagePositionPrice, 33 | @Nonnull BigDecimal expectedYield, 34 | @Nonnull Money currentNkd, 35 | @Nonnull BigDecimal averagePositionPricePt, 36 | @Nonnull Money currentPrice, 37 | @Nonnull Money averagePositionPriceFifo, 38 | @Nonnull BigDecimal quantityLots) { 39 | this.figi = figi; 40 | this.instrumentType = instrumentType; 41 | this.quantity = quantity; 42 | this.averagePositionPrice = averagePositionPrice; 43 | this.expectedYield = expectedYield; 44 | this.currentNkd = currentNkd; 45 | this.averagePositionPricePt = averagePositionPricePt; 46 | this.currentPrice = currentPrice; 47 | this.averagePositionPriceFifo = averagePositionPriceFifo; 48 | this.quantityLots = quantityLots; 49 | } 50 | 51 | @Nonnull 52 | public static Position fromResponse(@Nonnull PortfolioPosition portfolioPosition) { 53 | return new Position( 54 | portfolioPosition.getFigi(), 55 | portfolioPosition.getInstrumentType(), 56 | MapperUtils.quotationToBigDecimal(portfolioPosition.getQuantity()), 57 | Money.fromResponse(portfolioPosition.getAveragePositionPrice()), 58 | MapperUtils.quotationToBigDecimal(portfolioPosition.getExpectedYield()), 59 | Money.fromResponse(portfolioPosition.getCurrentNkd()), 60 | MapperUtils.quotationToBigDecimal(portfolioPosition.getAveragePositionPricePt()), 61 | Money.fromResponse(portfolioPosition.getCurrentPrice()), 62 | Money.fromResponse(portfolioPosition.getAveragePositionPriceFifo()), 63 | MapperUtils.quotationToBigDecimal(portfolioPosition.getQuantityLots()) 64 | ); 65 | } 66 | 67 | public static List fromResponse(@Nonnull List portfolioPositions) { 68 | return portfolioPositions.stream().map(Position::fromResponse).collect(Collectors.toList()); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /contract/src/main/proto/common.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tinkoff.public.invest.api.contract.v1; 4 | 5 | option go_package = "./;investapi"; 6 | option java_package = "ru.tinkoff.piapi.contract.v1"; 7 | option java_multiple_files = true; 8 | option csharp_namespace = "Tinkoff.InvestApi.V1"; 9 | option objc_class_prefix = "TIAPI"; 10 | option php_namespace = "Tinkoff\\Invest\\V1"; 11 | 12 | import "google/protobuf/timestamp.proto"; 13 | 14 | //Тип инструмента. 15 | enum InstrumentType { 16 | INSTRUMENT_TYPE_UNSPECIFIED = 0; 17 | INSTRUMENT_TYPE_BOND = 1; //Облигация. 18 | INSTRUMENT_TYPE_SHARE = 2; //Акция. 19 | INSTRUMENT_TYPE_CURRENCY = 3; //Валюта. 20 | INSTRUMENT_TYPE_ETF = 4; //Exchange-traded fund. Фонд. 21 | INSTRUMENT_TYPE_FUTURES = 5; //Фьючерс. 22 | INSTRUMENT_TYPE_SP = 6; //Структурная нота. 23 | INSTRUMENT_TYPE_OPTION = 7; //Опцион. 24 | INSTRUMENT_TYPE_CLEARING_CERTIFICATE = 8; //Clearing certificate. 25 | } 26 | 27 | //Денежная сумма в определенной валюте 28 | message MoneyValue { 29 | 30 | // строковый ISO-код валюты 31 | string currency = 1; 32 | 33 | // целая часть суммы, может быть отрицательным числом 34 | int64 units = 2; 35 | 36 | // дробная часть суммы, может быть отрицательным числом 37 | int32 nano = 3; 38 | } 39 | 40 | //Котировка - денежная сумма без указания валюты 41 | message Quotation { 42 | 43 | // целая часть суммы, может быть отрицательным числом 44 | int64 units = 1; 45 | 46 | // дробная часть суммы, может быть отрицательным числом 47 | int32 nano = 2; 48 | } 49 | 50 | //Режим торгов инструмента 51 | enum SecurityTradingStatus { 52 | SECURITY_TRADING_STATUS_UNSPECIFIED = 0; //Торговый статус не определён 53 | SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING = 1; //Недоступен для торгов 54 | SECURITY_TRADING_STATUS_OPENING_PERIOD = 2; //Период открытия торгов 55 | SECURITY_TRADING_STATUS_CLOSING_PERIOD = 3; //Период закрытия торгов 56 | SECURITY_TRADING_STATUS_BREAK_IN_TRADING = 4; //Перерыв в торговле 57 | SECURITY_TRADING_STATUS_NORMAL_TRADING = 5; //Нормальная торговля 58 | SECURITY_TRADING_STATUS_CLOSING_AUCTION = 6; //Аукцион закрытия 59 | SECURITY_TRADING_STATUS_DARK_POOL_AUCTION = 7; //Аукцион крупных пакетов 60 | SECURITY_TRADING_STATUS_DISCRETE_AUCTION = 8; //Дискретный аукцион 61 | SECURITY_TRADING_STATUS_OPENING_AUCTION_PERIOD = 9; //Аукцион открытия 62 | SECURITY_TRADING_STATUS_TRADING_AT_CLOSING_AUCTION_PRICE = 10; //Период торгов по цене аукциона закрытия 63 | SECURITY_TRADING_STATUS_SESSION_ASSIGNED = 11; //Сессия назначена 64 | SECURITY_TRADING_STATUS_SESSION_CLOSE = 12; //Сессия закрыта 65 | SECURITY_TRADING_STATUS_SESSION_OPEN = 13; //Сессия открыта 66 | SECURITY_TRADING_STATUS_DEALER_NORMAL_TRADING = 14; //Доступна торговля в режиме внутренней ликвидности брокера 67 | SECURITY_TRADING_STATUS_DEALER_BREAK_IN_TRADING = 15; //Перерыв торговли в режиме внутренней ликвидности брокера 68 | SECURITY_TRADING_STATUS_DEALER_NOT_AVAILABLE_FOR_TRADING = 16; //Недоступна торговля в режиме внутренней ликвидности брокера 69 | } 70 | 71 | //Проверка активности стрима. 72 | message Ping { 73 | 74 | //Время проверки. 75 | google.protobuf.Timestamp time = 1; 76 | } -------------------------------------------------------------------------------- /core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | ru.tinkoff.piapi 7 | java-sdk 8 | 1.6-SNAPSHOT 9 | 10 | 11 | java-sdk-core 12 | 1.6-SNAPSHOT 13 | Tinkoff Invest API Java SDK - Core 14 | jar 15 | 16 | 17 | UTF-8 18 | 11 19 | 11 20 | 11 21 | 22 | 23 | 24 | 25 | ru.tinkoff.piapi 26 | java-sdk-grpc-contract 27 | 1.6-SNAPSHOT 28 | 29 | 30 | 31 | io.grpc 32 | grpc-netty-shaded 33 | ${io.grpc.version} 34 | 35 | 36 | org.slf4j 37 | slf4j-api 38 | 1.7.33 39 | 40 | 41 | io.smallrye.reactive 42 | mutiny 43 | 1.3.1 44 | 45 | 46 | 47 | org.junit.jupiter 48 | junit-jupiter 49 | 5.8.2 50 | test 51 | 52 | 53 | org.mockito 54 | mockito-core 55 | 4.2.0 56 | test 57 | 58 | 59 | org.slf4j 60 | slf4j-simple 61 | 1.7.33 62 | test 63 | 64 | 65 | io.grpc 66 | grpc-testing 67 | ${io.grpc.version} 68 | test 69 | 70 | 71 | com.fasterxml.jackson.core 72 | jackson-databind 73 | ${jackson.version} 74 | 75 | 76 | org.projectlombok 77 | lombok 78 | 1.18.24 79 | provided 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /core/src/test/java/ru/tinkoff/piapi/core/InvestApiTest.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | public class InvestApiTest { 8 | 9 | // TODO Как проверить, что в настройках по умолчанию задан TARGET из конфига? 10 | // @Test 11 | // void defaultChannelUsesTargetFromConfig() { 12 | // 13 | // } 14 | 15 | // TODO Как проверить, что в настройках по умолчанию задан CONNECTION_TIMEOUT из конфига? 16 | // @Test 17 | // void defaultChannelUsesConnectionTimeoutFromConfig() { 18 | // 19 | // } 20 | 21 | // TODO Как проверить, что в настройках по умолчанию задан REQUEST_TIMEOUT из конфига? 22 | // @Test 23 | // void defaultChannelUsesRequestTimeoutFromConfig() { 24 | // 25 | // } 26 | 27 | @Test 28 | void creationAlwaysUsesPassedChannel() { 29 | var channel = InvestApi.defaultChannel("token", null); 30 | 31 | var api = InvestApi.create(channel); 32 | assertSame(channel, api.getChannel(), "Simple creation doesn't use passed Channel."); 33 | 34 | var readonlyApi = InvestApi.createReadonly(channel); 35 | assertSame(channel, readonlyApi.getChannel(), "Readonly creation doesn't use passed Channel."); 36 | 37 | var sandboxApi = InvestApi.createSandbox(channel); 38 | assertSame(channel, sandboxApi.getChannel(), "Sandbox creation doesn't use passed Channel."); 39 | } 40 | 41 | @Test 42 | void simpleCreationProducesNotReadonlyNorSandbox() { 43 | var channel = InvestApi.defaultChannel("token", null); 44 | 45 | var api = InvestApi.create(channel); 46 | assertFalse(api.isReadonlyMode(), "Simple creation produces readonly mode."); 47 | } 48 | 49 | @Test 50 | void readonlyCreationProducesReadonlyOnly() { 51 | var channel = InvestApi.defaultChannel("token", null); 52 | 53 | var readonlyApi = InvestApi.createReadonly(channel); 54 | assertTrue(readonlyApi.isReadonlyMode(), "Readonly creation doesn't produce readonly mode."); 55 | } 56 | 57 | @Test 58 | void sandboxCreationProducesSandboxOnly() { 59 | var channel = InvestApi.defaultChannel("token", null); 60 | 61 | var sandboxApi = InvestApi.createSandbox(channel); 62 | assertFalse(sandboxApi.isReadonlyMode(), "Sandbox creation produces readonly mode."); 63 | } 64 | 65 | @Test 66 | void instrumentsServiceIsAlwaysAllowed() { 67 | var channel = InvestApi.defaultChannel("token", null); 68 | 69 | var api = InvestApi.create(channel); 70 | assertDoesNotThrow(api::getInstrumentsService); 71 | var readonlyApi = InvestApi.createReadonly(channel); 72 | assertDoesNotThrow(readonlyApi::getInstrumentsService); 73 | var sandboxApi = InvestApi.createReadonly(channel); 74 | assertDoesNotThrow(sandboxApi::getInstrumentsService); 75 | } 76 | 77 | @Test 78 | void marketDataServiceIsAlwaysAllowed() { 79 | var channel = InvestApi.defaultChannel("token", null); 80 | 81 | var api = InvestApi.create(channel); 82 | assertDoesNotThrow(api::getMarketDataService); 83 | var readonlyApi = InvestApi.createReadonly(channel); 84 | assertDoesNotThrow(readonlyApi::getMarketDataService); 85 | var sandboxApi = InvestApi.createReadonly(channel); 86 | assertDoesNotThrow(sandboxApi::getMarketDataService); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /core/src/test/java/ru/tinkoff/piapi/core/MapperTest.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import ru.tinkoff.piapi.contract.v1.GetFuturesMarginResponse; 5 | import ru.tinkoff.piapi.contract.v1.MoneyValue; 6 | import ru.tinkoff.piapi.contract.v1.Quotation; 7 | import ru.tinkoff.piapi.core.utils.MapperUtils; 8 | 9 | import java.math.BigDecimal; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | 13 | public class MapperTest { 14 | 15 | @Test 16 | public void moneyValueToBigDecimalTest() { 17 | var value = MoneyValue.newBuilder().setUnits(10).setNano(100000000).build(); 18 | var actualValue = MapperUtils.moneyValueToBigDecimal(value); 19 | var expectedValue = BigDecimal.valueOf(10.1); 20 | assertEquals(0, actualValue.compareTo(expectedValue)); 21 | } 22 | 23 | @Test 24 | public void quotationToBigDecimalTest() { 25 | var value = Quotation.newBuilder().setUnits(10).setNano(100000000).build(); 26 | var actualValue = MapperUtils.quotationToBigDecimal(value); 27 | var expectedValue = BigDecimal.valueOf(10.1); 28 | assertEquals(0, actualValue.compareTo(expectedValue)); 29 | } 30 | 31 | @Test 32 | public void bigDecimalToMoneyValueTest() { 33 | var value = BigDecimal.valueOf(10.1); 34 | var actualValue = MapperUtils.bigDecimalToMoneyValue(value); 35 | var expectedValue = MoneyValue.newBuilder().setUnits(10).setNano(100000000).build(); 36 | assertEquals(expectedValue, actualValue); 37 | } 38 | 39 | @Test 40 | public void bigDecimalToMoney2ValueTest() { 41 | var value = BigDecimal.valueOf(10.1); 42 | var actualValue = MapperUtils.bigDecimalToMoneyValue(value, "RUB"); 43 | var expectedValue = MoneyValue.newBuilder().setUnits(10).setNano(100000000).setCurrency("rub").build(); 44 | assertEquals(expectedValue, actualValue); 45 | } 46 | 47 | @Test 48 | public void bigDecimalToQuotationTest() { 49 | var value = BigDecimal.valueOf(10.1); 50 | var actualValue = MapperUtils.bigDecimalToQuotation(value); 51 | var expectedValue = Quotation.newBuilder().setUnits(10).setNano(100000000).build(); 52 | assertEquals(expectedValue, actualValue); 53 | } 54 | 55 | @Test 56 | public void futuresPriceBigDecimalTest() { 57 | var value = BigDecimal.valueOf(30); 58 | var response = GetFuturesMarginResponse 59 | .newBuilder() 60 | .setMinPriceIncrement(Quotation.newBuilder().setUnits(10).setNano(0).build()) 61 | .setMinPriceIncrementAmount(Quotation.newBuilder().setUnits(20).setNano(0).build()) 62 | .build(); 63 | var actualValue = MapperUtils.futuresPrice(value, response); 64 | var expectedValue = BigDecimal.valueOf(60); //30 / 10 * 20 65 | assertEquals(0, actualValue.compareTo(expectedValue)); 66 | } 67 | 68 | @Test 69 | public void futuresPriceQuotationTest() { 70 | var value = Quotation.newBuilder().setUnits(30).setNano(0).build(); 71 | var response = GetFuturesMarginResponse 72 | .newBuilder() 73 | .setMinPriceIncrement(Quotation.newBuilder().setUnits(10).setNano(0).build()) 74 | .setMinPriceIncrementAmount(Quotation.newBuilder().setUnits(20).setNano(0).build()) 75 | .build(); 76 | var actualValue = MapperUtils.futuresPrice(value, response); 77 | var expectedValue = BigDecimal.valueOf(60); //30 / 10 * 20 78 | assertEquals(0, actualValue.compareTo(expectedValue)); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /contract/src/main/proto/sandbox.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tinkoff.public.invest.api.contract.v1; 4 | 5 | option go_package = "./;investapi"; 6 | option java_package = "ru.tinkoff.piapi.contract.v1"; 7 | option java_multiple_files = true; 8 | option csharp_namespace = "Tinkoff.InvestApi.V1"; 9 | option objc_class_prefix = "TIAPI"; 10 | option php_namespace = "Tinkoff\\Invest\\V1"; 11 | 12 | import "common.proto"; 13 | import "orders.proto"; 14 | import "operations.proto"; 15 | import "users.proto"; 16 | 17 | service SandboxService { //Сервис для работы с песочницей TINKOFF INVEST API 18 | 19 | //Метод регистрации счёта в песочнице. 20 | rpc OpenSandboxAccount(OpenSandboxAccountRequest) returns (OpenSandboxAccountResponse); 21 | 22 | //Метод получения счетов в песочнице. 23 | rpc GetSandboxAccounts(GetAccountsRequest) returns (GetAccountsResponse); 24 | 25 | //Метод закрытия счёта в песочнице. 26 | rpc CloseSandboxAccount(CloseSandboxAccountRequest) returns (CloseSandboxAccountResponse); 27 | 28 | //Метод выставления торгового поручения в песочнице. 29 | rpc PostSandboxOrder(PostOrderRequest) returns (PostOrderResponse); 30 | 31 | //Метод изменения выставленной заявки. 32 | rpc ReplaceSandboxOrder(ReplaceOrderRequest) returns (PostOrderResponse); 33 | 34 | //Метод получения списка активных заявок по счёту в песочнице. 35 | rpc GetSandboxOrders(GetOrdersRequest) returns (GetOrdersResponse); 36 | 37 | //Метод отмены торгового поручения в песочнице. 38 | rpc CancelSandboxOrder(CancelOrderRequest) returns (CancelOrderResponse); 39 | 40 | //Метод получения статуса заявки в песочнице. 41 | rpc GetSandboxOrderState(GetOrderStateRequest) returns (OrderState); 42 | 43 | //Метод получения позиций по виртуальному счёту песочницы. 44 | rpc GetSandboxPositions(PositionsRequest) returns (PositionsResponse); 45 | 46 | //Метод получения операций в песочнице по номеру счёта. 47 | rpc GetSandboxOperations(OperationsRequest) returns (OperationsResponse); 48 | 49 | //Метод получения операций в песочнице по номеру счета с пагинацией. 50 | rpc GetSandboxOperationsByCursor(GetOperationsByCursorRequest) returns (GetOperationsByCursorResponse); 51 | 52 | //Метод получения портфолио в песочнице. 53 | rpc GetSandboxPortfolio(PortfolioRequest) returns (PortfolioResponse); 54 | 55 | //Метод пополнения счёта в песочнице. 56 | rpc SandboxPayIn(SandboxPayInRequest) returns (SandboxPayInResponse); 57 | 58 | //Метод получения доступного остатка для вывода средств в песочнице. 59 | rpc GetSandboxWithdrawLimits(WithdrawLimitsRequest) returns (WithdrawLimitsResponse); 60 | } 61 | 62 | //Запрос открытия счёта в песочнице. 63 | message OpenSandboxAccountRequest { 64 | //пустой запрос 65 | } 66 | 67 | //Номер открытого счёта в песочнице. 68 | message OpenSandboxAccountResponse { 69 | string account_id = 1; //Номер счёта 70 | } 71 | 72 | //Запрос закрытия счёта в песочнице. 73 | message CloseSandboxAccountRequest { 74 | string account_id = 1; //Номер счёта 75 | } 76 | 77 | //Результат закрытия счёта в песочнице. 78 | message CloseSandboxAccountResponse { 79 | //пустой ответ 80 | } 81 | 82 | //Запрос пополнения счёта в песочнице. 83 | message SandboxPayInRequest { 84 | string account_id = 1; //Номер счёта 85 | MoneyValue amount = 2; //Сумма пополнения счёта в рублях 86 | } 87 | 88 | //Результат пополнения счёта, текущий баланс. 89 | message SandboxPayInResponse { 90 | MoneyValue balance = 1; //Текущий баланс счёта 91 | } 92 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # История изменений Invest API Java SDK v1.0 2 | 3 | Invest API Java SDK v1.0 требует требует JDK 11 и выше. 4 | 5 | ## v1.0.14 6 | Новый способ получения marketdata через instrument_uid 7 | 8 | ## v1.0.12 9 | 10 | Новый стрим позиций 11 | Новые методы для работы с опционами - Options, OptionBy 12 | Поиск инструментов по uid/positionUid 13 | Метод запроса цен закрытия торговой сессии по инструментам getClosePrices 14 | 15 | ## v1.0.11.1 16 | 17 | Исправлен урл для новой песочницы 18 | Добавлены примеры работы с новой песочницей 19 | 20 | ## v1.0.11 21 | 22 | Новый метод получения операций OperationByCursor 23 | Новый метод выставления заявок ReplaceOrder 24 | 25 | ## v1.0.10 26 | 27 | Новый стрим для работы с портфолио 28 | Возвращение режима песочницы и связанных методов 29 | 30 | ## v1.0-M9 31 | 32 | Новые методы в сервисе инструментов - Brands, Countries, FindInstrument 33 | Возможность указать target в channel при создании экземпляра Channel 34 | 35 | ## v1.0-M8 36 | 37 | Добавлена возможность указать appName 38 | 39 | ## v1.0-M7 40 | 41 | Новые методы для работы с избранными инструментами - InstrumentsService.GetFavorites, InstrumentsService.EditFavorites 42 | 43 | Добавлена возможность указать счет в tradesStream, по которому нужно получать сделки. Если счет не указан - будут получены сделки по всем счетам 44 | 45 | Версия библиотеки jackson-databind была исправлена из-за критической уязвимости CVE-2020-36518 46 | 47 | ## v1.0-M6 48 | 49 | Новые методы для получения активов - InstrumentsService.GetAssets, InstrumentsService.GetAssetBy 50 | 51 | Новый метод получения последних сделок по инструменту. MarketDataService.GetLastTrades 52 | 53 | Убраны deprecated методы для стримов 54 | 55 | ## v1.0-M5 56 | 57 | Новые методы для получения инструментов по InstrumentStatus 58 | 59 | Исправлен баг с запуском 1.0-M4 версии (файл errors.json не найден) 60 | 61 | Исправлен баг с java.util.Currency, если валюта приходила в lowercase 62 | 63 | ## v1.0-M4 64 | 65 | ### Основное 66 | 67 | Новое API для работы со стримами. 68 | 69 | Обработка ошибок в unary методах. 70 | 71 | Обновлены контракты 72 | 73 | Новые методы GetBondCoupons, GetDividendsForeignIssuer, GetBrokerReport 74 | 75 | 76 | ## v1.0-M3 77 | 78 | ### Основное 79 | 80 | Добавлен метод для расчета стоимости инструмента с типом 'Futures'. 81 | 82 | В OperationsService почти все входные/выходные данные переведены на собственные классы 83 | в замен сгенерированных из proto-файлов. 84 | 85 | В OperationsService добавлены методы для получения брокерского отчёта. 86 | 87 | ## v1.0-M2 88 | 89 | ### Основное 90 | 91 | Завершены работы по `InstrumentsService`. 92 | 93 | ### Исправления 94 | 95 | Синхронизована версия grpc-библиотек внутри модуля `grpc-contract` 96 | 97 | ## v1.0-M1 98 | 99 | ### Основное 100 | 101 | Выделен отдельный модуль c GRPC-контрактом - `grpc-contract`. Его можно подключить отдельно и на его основе создать 102 | собственный SDK. 103 | 104 | Выделен модуль для SDK низкого уровня - `core`. Это базовый слой, который предоставляет собой обёртку над 105 | GRPC-контрактом. Содержит как блокирующие методы, так и неблокирующие (асинхронные). На основе этого 106 | модуля можно строить более высокоуровневые реализации систем. 107 | 108 | Добавлен модуль с примером использования SDK - `example`. Это законченная программа, поэтому данный модуль не нужно 109 | подключать в другие проекты. 110 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/stream/OrdersStreamService.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.stream; 2 | 3 | import io.grpc.Context; 4 | import ru.tinkoff.piapi.contract.v1.OrdersStreamServiceGrpc; 5 | import ru.tinkoff.piapi.contract.v1.TradesStreamRequest; 6 | import ru.tinkoff.piapi.contract.v1.TradesStreamResponse; 7 | 8 | import javax.annotation.Nonnull; 9 | import javax.annotation.Nullable; 10 | import java.util.Collections; 11 | import java.util.Map; 12 | import java.util.UUID; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | import java.util.function.Consumer; 15 | 16 | public class OrdersStreamService { 17 | private final OrdersStreamServiceGrpc.OrdersStreamServiceStub stub; 18 | private final Map disposeMap = new ConcurrentHashMap<>(); 19 | 20 | public OrdersStreamService(@Nonnull OrdersStreamServiceGrpc.OrdersStreamServiceStub stub) { 21 | this.stub = stub; 22 | } 23 | 24 | public String subscribeTrades(@Nonnull StreamProcessor streamProcessor, 25 | @Nullable Consumer onErrorCallback) { 26 | return tradesStream(streamProcessor, onErrorCallback, Collections.emptyList()); 27 | } 28 | 29 | public void closeStream(String streamKey) { 30 | disposeMap.computeIfPresent(streamKey, (k, dispose) -> { 31 | dispose.run(); 32 | return null; 33 | }); 34 | } 35 | 36 | /** 37 | * Подписка на стрим сделок 38 | * 39 | * @param streamProcessor обработчик пришедших сообщений в стриме 40 | * @param onErrorCallback обработчик ошибок в стриме 41 | * @param accounts Идентификаторы счетов 42 | */ 43 | public String subscribeTrades(@Nonnull StreamProcessor streamProcessor, 44 | @Nullable Consumer onErrorCallback, 45 | @Nonnull Iterable accounts) { 46 | return tradesStream(streamProcessor, onErrorCallback, accounts); 47 | } 48 | 49 | /** 50 | * Подписка на стрим сделок 51 | * 52 | * @param streamProcessor обработчик пришедших сообщений в стриме 53 | */ 54 | public String subscribeTrades(@Nonnull StreamProcessor streamProcessor) { 55 | return tradesStream(streamProcessor, null, Collections.emptyList()); 56 | } 57 | 58 | /** 59 | * Подписка на стрим сделок 60 | * 61 | * @param streamProcessor обработчик пришедших сообщений в стриме 62 | * @param accounts Идентификаторы счетов 63 | */ 64 | public String subscribeTrades(@Nonnull StreamProcessor streamProcessor, 65 | @Nonnull Iterable accounts) { 66 | return tradesStream(streamProcessor, null, accounts); 67 | } 68 | 69 | private String tradesStream(@Nonnull StreamProcessor streamProcessor, 70 | @Nullable Consumer onErrorCallback, 71 | @Nonnull Iterable accounts) { 72 | var request = TradesStreamRequest 73 | .newBuilder() 74 | .addAllAccounts(accounts) 75 | .build(); 76 | 77 | String streamKey = UUID.randomUUID().toString(); 78 | var context = Context.current().fork().withCancellation(); 79 | disposeMap.put(streamKey, () -> context.cancel(new RuntimeException("canceled by user"))); 80 | context.run(() -> stub.tradesStream( 81 | request, 82 | new StreamObserverWithProcessor<>(streamProcessor, onErrorCallback) 83 | )); 84 | 85 | return streamKey; 86 | 87 | 88 | } 89 | 90 | 91 | } 92 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/UsersService.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core; 2 | 3 | import ru.tinkoff.piapi.contract.v1.Account; 4 | import ru.tinkoff.piapi.contract.v1.GetAccountsRequest; 5 | import ru.tinkoff.piapi.contract.v1.GetAccountsResponse; 6 | import ru.tinkoff.piapi.contract.v1.GetInfoRequest; 7 | import ru.tinkoff.piapi.contract.v1.GetInfoResponse; 8 | import ru.tinkoff.piapi.contract.v1.GetMarginAttributesRequest; 9 | import ru.tinkoff.piapi.contract.v1.GetMarginAttributesResponse; 10 | import ru.tinkoff.piapi.contract.v1.GetUserTariffRequest; 11 | import ru.tinkoff.piapi.contract.v1.GetUserTariffResponse; 12 | import ru.tinkoff.piapi.contract.v1.UsersServiceGrpc.UsersServiceBlockingStub; 13 | import ru.tinkoff.piapi.contract.v1.UsersServiceGrpc.UsersServiceStub; 14 | import ru.tinkoff.piapi.core.utils.Helpers; 15 | 16 | import javax.annotation.Nonnull; 17 | import java.util.List; 18 | import java.util.concurrent.CompletableFuture; 19 | 20 | import static ru.tinkoff.piapi.core.utils.Helpers.unaryCall; 21 | import static ru.tinkoff.piapi.core.utils.ValidationUtils.checkSandbox; 22 | 23 | public class UsersService { 24 | private final UsersServiceBlockingStub userBlockingStub; 25 | private final UsersServiceStub userStub; 26 | private final boolean sandboxMode; 27 | 28 | UsersService(@Nonnull UsersServiceBlockingStub userBlockingStub, 29 | @Nonnull UsersServiceStub userStub, 30 | boolean sandboxMode) { 31 | this.sandboxMode = sandboxMode; 32 | this.userBlockingStub = userBlockingStub; 33 | this.userStub = userStub; 34 | } 35 | 36 | @Nonnull 37 | public List getAccountsSync() { 38 | return unaryCall(() -> userBlockingStub.getAccounts( 39 | GetAccountsRequest.newBuilder() 40 | .build()) 41 | .getAccountsList()); 42 | } 43 | 44 | 45 | @Nonnull 46 | public CompletableFuture> getAccounts() { 47 | return Helpers.unaryAsyncCall( 48 | observer -> userStub.getAccounts( 49 | GetAccountsRequest.newBuilder().build(), 50 | observer)) 51 | .thenApply(GetAccountsResponse::getAccountsList); 52 | } 53 | 54 | @Nonnull 55 | public GetMarginAttributesResponse getMarginAttributesSync(@Nonnull String accountId) { 56 | return unaryCall(() -> userBlockingStub.getMarginAttributes( 57 | GetMarginAttributesRequest.newBuilder().setAccountId(accountId).build())); 58 | } 59 | 60 | 61 | @Nonnull 62 | public CompletableFuture getMarginAttributes(@Nonnull String accountId) { 63 | checkSandbox(sandboxMode); 64 | 65 | return Helpers.unaryAsyncCall( 66 | observer -> userStub.getMarginAttributes( 67 | GetMarginAttributesRequest.newBuilder().setAccountId(accountId).build(), 68 | observer)); 69 | } 70 | 71 | @Nonnull 72 | public GetUserTariffResponse getUserTariffSync() { 73 | return unaryCall(() -> userBlockingStub.getUserTariff(GetUserTariffRequest.newBuilder().build())); 74 | } 75 | 76 | @Nonnull 77 | public CompletableFuture getUserTariff() { 78 | return Helpers.unaryAsyncCall( 79 | observer -> userStub.getUserTariff( 80 | GetUserTariffRequest.newBuilder().build(), 81 | observer)); 82 | } 83 | 84 | @Nonnull 85 | public GetInfoResponse getInfoSync() { 86 | return unaryCall(() -> userBlockingStub.getInfo(GetInfoRequest.newBuilder().build())); 87 | } 88 | 89 | @Nonnull 90 | public CompletableFuture getInfo() { 91 | return Helpers.unaryAsyncCall( 92 | observer -> userStub.getInfo( 93 | GetInfoRequest.newBuilder().build(), 94 | observer)); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/utils/DateUtils.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.utils; 2 | 3 | import com.google.protobuf.Timestamp; 4 | 5 | import java.time.Instant; 6 | import java.time.LocalDate; 7 | import java.time.OffsetDateTime; 8 | import java.time.ZoneId; 9 | import java.time.ZoneOffset; 10 | import java.time.format.DateTimeFormatter; 11 | 12 | public class DateUtils { 13 | 14 | private static final ZoneId DEFAULT_ZONE_ID = ZoneId.of("Etc/GMT"); 15 | private static final String PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'"; 16 | private static final ZoneOffset DEFAULT_ZONE_OFFSET = ZoneOffset.UTC; 17 | 18 | /** 19 | * Преобразование java {@link Long} в google {@link Timestamp}. 20 | * 21 | * @param epochSeconds Экземпляр {@link Long}. 22 | * @return Эквивалентный {@link Timestamp}. 23 | */ 24 | public static Timestamp epochToTimestamp(Long epochSeconds) { 25 | return Timestamp.newBuilder().setSeconds(epochSeconds).build(); 26 | } 27 | 28 | /** 29 | * Преобразование java {@link OffsetDateTime} в google {@link Timestamp}. 30 | * 31 | * @param offsetDateTime Экземпляр {@link OffsetDateTime}. 32 | * @return Эквивалентный {@link Timestamp}. 33 | */ 34 | public static Timestamp offsetDateTimeToTimestamp(OffsetDateTime offsetDateTime) { 35 | Instant instant = offsetDateTime.toInstant(); 36 | 37 | return Timestamp.newBuilder() 38 | .setSeconds(instant.getEpochSecond()) 39 | .setNanos(instant.getNano()) 40 | .build(); 41 | } 42 | 43 | /** 44 | * Преобразование java {@link Timestamp} в java {@link LocalDate}. 45 | * 46 | * @param timestamp Экземпляр google {@link Timestamp}. 47 | * @return Эквивалентный {@link LocalDate}. 48 | */ 49 | public static LocalDate epochSecondToLocalDate(Timestamp timestamp) { 50 | return Instant.ofEpochMilli(timestamp.getSeconds() * 1000).atZone(DEFAULT_ZONE_OFFSET).toLocalDate(); 51 | } 52 | 53 | /** 54 | * Преобразование java {@link OffsetDateTime} в java {@link Long}. 55 | * Количество секунд в формате epoch будет по часовому поясу UTC 56 | * 57 | * @param offsetDateTime Экземпляр {@link OffsetDateTime}. 58 | * @return Эквивалентный {@link Long}. 59 | */ 60 | public static Long offsetDateTimeToLong(OffsetDateTime offsetDateTime) { 61 | if (offsetDateTime == null) { 62 | return null; 63 | } 64 | 65 | return offsetDateTime.toInstant().getEpochSecond(); 66 | } 67 | 68 | /** 69 | * Преобразование java {@link Instant} в google {@link Timestamp}. 70 | * 71 | * @param i Экземпляр {@link Instant}. 72 | * @return Эквивалентный {@link Timestamp}. 73 | */ 74 | public static Timestamp instantToTimestamp(Instant i) { 75 | return Timestamp.newBuilder() 76 | .setSeconds(i.getEpochSecond()) 77 | .setNanos(i.getNano()) 78 | .build(); 79 | } 80 | 81 | /** 82 | * Преобразование google {@link Timestamp} в java {@link Instant}. 83 | * 84 | * @param t Экземпляр {@link Timestamp}. 85 | * @return Эквивалентный {@link Instant}. 86 | */ 87 | public static Instant timestampToInstant(Timestamp t) { 88 | return Instant.ofEpochSecond(t.getSeconds(), t.getNanos()); 89 | } 90 | 91 | /** 92 | * Возвращает текстовое представление даты в виде 2021-09-27T11:05:27Z (GMT) 93 | * @param epochMillis Время в миллисекундах в epoch формате 94 | * @return текстовое представление даты в виде 2021-09-27T11:05:27Z 95 | */ 96 | public static String epochMillisToString(long epochMillis) { 97 | var zonedDateTime = Instant.ofEpochMilli(epochMillis).atZone(DEFAULT_ZONE_ID); 98 | return zonedDateTime.format(DateTimeFormatter.ofPattern(PATTERN)); 99 | } 100 | 101 | /** 102 | * Возвращает текстовое представление даты в виде 2021-09-27T11:05:27Z (GMT) 103 | * @param timestamp Время в формате Timestamp (google) 104 | * @return текстовое представление даты в виде 2021-09-27T11:05:27Z 105 | */ 106 | public static String timestampToString(Timestamp timestamp) { 107 | return epochMillisToString(timestamp.getSeconds() * 1_000); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/utils/MapperUtils.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.utils; 2 | 3 | import ru.tinkoff.piapi.contract.v1.GetFuturesMarginResponse; 4 | import ru.tinkoff.piapi.contract.v1.MoneyValue; 5 | import ru.tinkoff.piapi.contract.v1.Quotation; 6 | 7 | import java.math.BigDecimal; 8 | import java.math.RoundingMode; 9 | 10 | public class MapperUtils { 11 | 12 | /** 13 | * Расчет реальной стоимости фьючерса. Подробнее в документации 14 | * 15 | * @param pricePoints цена в пунктах для инструмента с типом Futures 16 | * @param futuresMarginResponse ответ при вызове unary метода InstrumentsService.GetFuturesMargin 17 | * @return реальная стоимость фьючерса 18 | */ 19 | public static BigDecimal futuresPrice(Quotation pricePoints, GetFuturesMarginResponse futuresMarginResponse) { 20 | var minPriceIncrement = quotationToBigDecimal(futuresMarginResponse.getMinPriceIncrement()); 21 | var minPriceIncrementAmount = quotationToBigDecimal(futuresMarginResponse.getMinPriceIncrementAmount()); 22 | return quotationToBigDecimal(pricePoints).multiply(minPriceIncrementAmount).divide(minPriceIncrement, RoundingMode.HALF_UP) ; 23 | } 24 | 25 | /** 26 | * Расчет реальной стоимости фьючерса. Подробнее в документации 27 | * 28 | * @param pricePoints цена в пунктах для инструмента с типом Futures 29 | * @param futuresMarginResponse ответ при вызове unary метода InstrumentsService.GetFuturesMargin 30 | * @return реальная стоимость фьючерса 31 | */ 32 | public static BigDecimal futuresPrice(BigDecimal pricePoints, GetFuturesMarginResponse futuresMarginResponse) { 33 | var minPriceIncrement = quotationToBigDecimal(futuresMarginResponse.getMinPriceIncrement()); 34 | var minPriceIncrementAmount = quotationToBigDecimal(futuresMarginResponse.getMinPriceIncrementAmount()); 35 | return pricePoints.multiply(minPriceIncrementAmount).divide(minPriceIncrement, RoundingMode.HALF_UP) ; 36 | } 37 | 38 | public static Quotation bigDecimalToQuotation(BigDecimal value) { 39 | return Quotation.newBuilder() 40 | .setUnits(getUnits(value)) 41 | .setNano(getNano(value)) 42 | .build(); 43 | } 44 | 45 | public static MoneyValue bigDecimalToMoneyValue(BigDecimal value, String currency) { 46 | return MoneyValue.newBuilder() 47 | .setUnits(getUnits(value)) 48 | .setNano(getNano(value)) 49 | .setCurrency(toLowerCaseNullable(currency)) 50 | .build(); 51 | } 52 | 53 | public static long getUnits(BigDecimal value) { 54 | return value != null ? value.longValue() : 0; 55 | } 56 | 57 | public static int getNano(BigDecimal value) { 58 | return value != null ? value.remainder(BigDecimal.ONE).multiply(BigDecimal.valueOf(1_000_000_000L)).intValue() : 0; 59 | } 60 | 61 | private static String toLowerCaseNullable(String value) { 62 | return value != null ? value.toLowerCase() : ""; 63 | } 64 | 65 | public static MoneyValue bigDecimalToMoneyValue(BigDecimal value) { 66 | return bigDecimalToMoneyValue(value, null); 67 | } 68 | 69 | /** 70 | * Конвертирует Quotation в BigDecimal. Например {units: 10, nanos: 900000000} -> 10.9 71 | * 72 | * @param value значение в формате Quotation 73 | * @return Значение в формате BigDecimal 74 | */ 75 | public static BigDecimal quotationToBigDecimal(Quotation value) { 76 | if (value == null) { 77 | return null; 78 | } 79 | return mapUnitsAndNanos(value.getUnits(), value.getNano()); 80 | } 81 | 82 | /** 83 | * Конвертирует MoneyValue в BigDecimal. Например {units: 10, nanos: 900000000, currency: 'rub'} -> 10.9 84 | * 85 | * @param value значение в формате MoneyValue 86 | * @return Значение в формате BigDecimal 87 | */ 88 | public static BigDecimal moneyValueToBigDecimal(MoneyValue value) { 89 | if (value == null) { 90 | return null; 91 | } 92 | return mapUnitsAndNanos(value.getUnits(), value.getNano()); 93 | } 94 | 95 | public static BigDecimal mapUnitsAndNanos(long units, int nanos) { 96 | if (units == 0 && nanos == 0) { 97 | return BigDecimal.ZERO; 98 | } 99 | return BigDecimal.valueOf(units).add(BigDecimal.valueOf(nanos, 9)); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Version](https://img.shields.io/maven-central/v/ru.tinkoff.piapi/java-sdk?logo=apache-maven&style=flat-square)](https://search.maven.org/artifact/ru.tinkoff.piapi/java-sdk) 2 | [![Release](https://jitpack.io/v/Tinkoff/invest-api-java-sdk.svg?style=flat-square)](https://jitpack.io/#Tinkoff/invest-api-java-sdk) 3 | [![License](https://img.shields.io/github/license/Tinkoff/invest-api-java-sdk?style=flat-square&logo=apache)](https://www.apache.org/licenses/LICENSE-2.0) 4 | [![GitHub Actions Status]()](https://github.com/Tinkoff/invest-api-java-sdk/actions?query=workflow%3A"Java+CI+with+Maven") 5 | 6 | # Java SDK для Tinkoff Invest API 7 | 8 | Данный проект представляет собой инструментарий на языке Java для работы с API Тинькофф Инвестиции, который можно 9 | использовать для создания торговых роботов. 10 | 11 | ## Пререквизиты 12 | - Java версии не ниже 11 13 | - Maven версии не ниже 3, либо Gradle версии не ниже 5.0 14 | 15 | 16 | ## Использование 17 | 18 | Для начала работы подключите к вашему проекту core-модуль 19 | 20 | | Система сборки | Код | 21 | |:----------------------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 22 | | Maven | \
      \ru.tinkoff.piapi\
      \java-sdk-core\
      \1.5\
\ | 23 | | Gradle with Groovy DSL | implementation 'ru.tinkoff.piapi:java-sdk-core:1.5' | 24 | | Gradle with Kotlin DSL | implementation("ru.tinkoff.piapi:java-sdk-core:1.5") | 25 | 26 | 27 | 28 | После этого можно пользоваться инструментарием 29 | 30 | ```java 31 | import ru.tinkoff.piapi.core.InvestApi; 32 | 33 | var token = ""; 34 | var api = InvestApi.create(token); 35 | 36 | var order = api.getOrdersService().postOrderSync(...) 37 | ``` 38 | 39 | ## Сборка 40 | ### JVM 41 | Для сборки перейдите в директорию проекта и выполните одну из следующих команд 42 | 43 | | Система сборки | Код | 44 | |:----------------------:|--------------------| 45 | | Maven | mvn clean package | 46 | | Gradle | gradle clean build | 47 | 48 | ### Native 49 | Для сборки native образа [потребуется добавить зависимость](https://github.com/Tinkoff/invest-api-java-sdk/issues/61) от `native-image-support` в свой проект: 50 | 51 | Maven: 52 | ```xml 53 | 54 | com.google.cloud 55 | native-image-support 56 | 0.14.1 57 | 58 | ``` 59 | Gradle: 60 | ```groovy 61 | implementation 'com.google.cloud:native-image-support:0.14.1' 62 | ``` 63 | 64 | К аргументам сборки [GraalVM](https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/README.md) необходимо добавить: 65 | ``` 66 | --initialize-at-build-time=ch.qos.logback,org.slf4j.LoggerFactory,org.slf4j.simple.SimpleLogger,org.slf4j.impl.StaticLoggerBinder,org.slf4j.MDC 67 | ``` 68 | 69 | ## Предложения и пожелания к SDK 70 | 71 | Смело выносите свои предложения в Issues, задавайте вопросы. Pull Request'ы также принимаются. 72 | 73 | ## У меня есть вопрос по работе API 74 | 75 | Документация к API находится в [отдельном репозитории](https://github.com/Tinkoff/investAPI). Там вы можете задать 76 | вопрос в Issues. 77 | -------------------------------------------------------------------------------- /contract/src/main/proto/stoporders.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tinkoff.public.invest.api.contract.v1; 4 | 5 | option go_package = "./;investapi"; 6 | option java_package = "ru.tinkoff.piapi.contract.v1"; 7 | option java_multiple_files = true; 8 | option csharp_namespace = "Tinkoff.InvestApi.V1"; 9 | option objc_class_prefix = "TIAPI"; 10 | option php_namespace = "Tinkoff\\Invest\\V1"; 11 | 12 | import "google/protobuf/timestamp.proto"; 13 | import "common.proto"; 14 | 15 | service StopOrdersService { /* Сервис предназначен для работы со стоп-заявками:
**1**. 16 | выставление;
**2**. отмена;
**3**. получение списка стоп-заявок.*/ 17 | 18 | //Метод выставления стоп-заявки. 19 | rpc PostStopOrder(PostStopOrderRequest) returns (PostStopOrderResponse); 20 | 21 | //Метод получения списка активных стоп заявок по счёту. 22 | rpc GetStopOrders(GetStopOrdersRequest) returns (GetStopOrdersResponse); 23 | 24 | //Метод отмены стоп-заявки. 25 | rpc CancelStopOrder(CancelStopOrderRequest) returns (CancelStopOrderResponse); 26 | } 27 | 28 | //Запрос выставления стоп-заявки. 29 | message PostStopOrderRequest { 30 | string figi = 1 [ deprecated = true ]; //Deprecated Figi-идентификатор инструмента. Необходимо использовать instrument_id. 31 | int64 quantity = 2; //Количество лотов. 32 | Quotation price = 3; //Цена за 1 инструмент. Для получения стоимости лота требуется умножить на лотность инструмента. 33 | Quotation stop_price = 4; //Стоп-цена заявки за 1 инструмент. Для получения стоимости лота требуется умножить на лотность инструмента. 34 | StopOrderDirection direction = 5; //Направление операции. 35 | string account_id = 6; //Номер счёта. 36 | StopOrderExpirationType expiration_type = 7; //Тип экспирации заявки. 37 | StopOrderType stop_order_type = 8; //Тип заявки. 38 | google.protobuf.Timestamp expire_date = 9; //Дата и время окончания действия стоп-заявки в часовом поясе UTC. **Для ExpirationType = GoodTillDate заполнение обязательно**. 39 | string instrument_id = 10; //Идентификатор инструмента, принимает значения Figi или instrument_uid. 40 | } 41 | 42 | //Результат выставления стоп-заявки. 43 | message PostStopOrderResponse { 44 | string stop_order_id = 1; //Уникальный идентификатор стоп-заявки. 45 | } 46 | 47 | //Запрос получения списка активных стоп-заявок. 48 | message GetStopOrdersRequest { 49 | string account_id = 1; //Идентификатор счёта клиента. 50 | } 51 | 52 | //Список активных стоп-заявок. 53 | message GetStopOrdersResponse { 54 | repeated StopOrder stop_orders = 1; //Массив стоп-заявок по счёту. 55 | } 56 | 57 | //Запрос отмены выставленной стоп-заявки. 58 | message CancelStopOrderRequest { 59 | string account_id = 1; //Идентификатор счёта клиента. 60 | string stop_order_id = 2; //Уникальный идентификатор стоп-заявки. 61 | } 62 | 63 | //Результат отмены выставленной стоп-заявки. 64 | message CancelStopOrderResponse { 65 | google.protobuf.Timestamp time = 1; //Время отмены заявки в часовом поясе UTC. 66 | } 67 | 68 | //Информация о стоп-заявке. 69 | message StopOrder { 70 | string stop_order_id = 1; //Идентификатор-идентификатор стоп-заявки. 71 | int64 lots_requested = 2; //Запрошено лотов. 72 | string figi = 3; //Figi-идентификатор инструмента. 73 | StopOrderDirection direction = 4; //Направление операции. 74 | string currency = 5; //Валюта стоп-заявки. 75 | StopOrderType order_type = 6; //Тип стоп-заявки. 76 | google.protobuf.Timestamp create_date = 7; //Дата и время выставления заявки в часовом поясе UTC. 77 | google.protobuf.Timestamp activation_date_time = 8; //Дата и время конвертации стоп-заявки в биржевую в часовом поясе UTC. 78 | google.protobuf.Timestamp expiration_time = 9; //Дата и время снятия заявки в часовом поясе UTC. 79 | MoneyValue price = 10; //Цена заявки за 1 инструмент. Для получения стоимости лота требуется умножить на лотность инструмента. 80 | MoneyValue stop_price = 11; //Цена активации стоп-заявки за 1 инструмент. Для получения стоимости лота требуется умножить на лотность инструмента. 81 | string instrument_uid = 12; //instrument_uid идентификатор инструмента. 82 | } 83 | 84 | //Направление сделки стоп-заявки. 85 | enum StopOrderDirection { 86 | STOP_ORDER_DIRECTION_UNSPECIFIED = 0; //Значение не указано. 87 | STOP_ORDER_DIRECTION_BUY = 1; //Покупка. 88 | STOP_ORDER_DIRECTION_SELL = 2; //Продажа. 89 | } 90 | 91 | //Тип экспирации стоп-заявке. 92 | enum StopOrderExpirationType { 93 | STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED = 0; //Значение не указано. 94 | STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL = 1; //Действительно до отмены. 95 | STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE = 2; //Действительно до даты снятия. 96 | } 97 | 98 | //Тип стоп-заявки. 99 | enum StopOrderType { 100 | STOP_ORDER_TYPE_UNSPECIFIED = 0; //Значение не указано. 101 | STOP_ORDER_TYPE_TAKE_PROFIT = 1; //Take-profit заявка. 102 | STOP_ORDER_TYPE_STOP_LOSS = 2; //Stop-loss заявка. 103 | STOP_ORDER_TYPE_STOP_LIMIT = 3; //Stop-limit заявка. 104 | } 105 | -------------------------------------------------------------------------------- /core/src/test/java/ru/tinkoff/piapi/core/UsersServiceTest.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core; 2 | 3 | import io.grpc.Channel; 4 | import io.grpc.stub.StreamObserver; 5 | import org.junit.jupiter.api.Test; 6 | import ru.tinkoff.piapi.contract.v1.*; 7 | 8 | import java.util.List; 9 | 10 | import static org.junit.jupiter.api.Assertions.*; 11 | import static org.mockito.AdditionalAnswers.*; 12 | import static org.mockito.Mockito.*; 13 | 14 | public class UsersServiceTest extends GrpcClientTester { 15 | 16 | static MoneyValue someMoney = MoneyValue.newBuilder().setCurrency("RUB").setUnits(1).setNano(2).build(); 17 | static Quotation someQuote = Quotation.newBuilder().setUnits(1).setNano(2).build(); 18 | 19 | @Override 20 | protected UsersService createClient(Channel channel) { 21 | return new UsersService(UsersServiceGrpc.newBlockingStub(channel), UsersServiceGrpc.newStub(channel), false); 22 | } 23 | 24 | @Test 25 | void getAccounts_Test() { 26 | var expected = List.of( 27 | Account.newBuilder().setId("1").build(), 28 | Account.newBuilder().setId("2").build()); 29 | var grpcService = mock(UsersServiceGrpc.UsersServiceImplBase.class, delegatesTo( 30 | new UsersServiceGrpc.UsersServiceImplBase() { 31 | @Override 32 | public void getAccounts(GetAccountsRequest request, 33 | StreamObserver responseObserver) { 34 | responseObserver.onNext(GetAccountsResponse.newBuilder().addAllAccounts(expected).build()); 35 | responseObserver.onCompleted(); 36 | } 37 | })); 38 | var service = mkClientBasedOnServer(grpcService); 39 | 40 | var actualSync = service.getAccountsSync(); 41 | var actualAsync = service.getAccounts().join(); 42 | 43 | assertIterableEquals(expected, actualSync); 44 | assertIterableEquals(expected, actualAsync); 45 | 46 | var inArg = GetAccountsRequest.newBuilder().build(); 47 | verify(grpcService, times(2)).getAccounts(eq(inArg), any()); 48 | } 49 | 50 | @Test 51 | void getMarginAttributes_Test() { 52 | var accountId = "accountId"; 53 | var expected = GetMarginAttributesResponse.newBuilder() 54 | .setAmountOfMissingFunds(someMoney) 55 | .setFundsSufficiencyLevel(someQuote) 56 | .setMinimalMargin(someMoney) 57 | .setStartingMargin(someMoney) 58 | .setLiquidPortfolio(someMoney) 59 | .build(); 60 | var grpcService = mock(UsersServiceGrpc.UsersServiceImplBase.class, delegatesTo( 61 | new UsersServiceGrpc.UsersServiceImplBase() { 62 | @Override 63 | public void getMarginAttributes(GetMarginAttributesRequest request, 64 | StreamObserver responseObserver) { 65 | responseObserver.onNext(expected); 66 | responseObserver.onCompleted(); 67 | } 68 | })); 69 | var service = mkClientBasedOnServer(grpcService); 70 | 71 | var actualSync = service.getMarginAttributesSync(accountId); 72 | var actualAsync = service.getMarginAttributes(accountId).join(); 73 | 74 | assertEquals(expected, actualSync); 75 | assertEquals(expected, actualAsync); 76 | 77 | var inArg = GetMarginAttributesRequest.newBuilder().setAccountId(accountId).build(); 78 | verify(grpcService, times(2)).getMarginAttributes(eq(inArg), any()); 79 | } 80 | 81 | @Test 82 | void getUserTariff_Test() { 83 | var expected = GetUserTariffResponse.newBuilder() 84 | .addUnaryLimits(UnaryLimit.newBuilder().setLimitPerMinute(1).addMethods("method").build()) 85 | .addStreamLimits(StreamLimit.newBuilder().setLimit(1).addStreams("stream").build()) 86 | .build(); 87 | var grpcService = mock(UsersServiceGrpc.UsersServiceImplBase.class, delegatesTo( 88 | new UsersServiceGrpc.UsersServiceImplBase() { 89 | @Override 90 | public void getUserTariff(GetUserTariffRequest request, 91 | StreamObserver responseObserver) { 92 | responseObserver.onNext(expected); 93 | responseObserver.onCompleted(); 94 | } 95 | })); 96 | var service = mkClientBasedOnServer(grpcService); 97 | 98 | var actualSync = service.getUserTariffSync(); 99 | var actualAsync = service.getUserTariff().join(); 100 | 101 | assertEquals(expected, actualSync); 102 | assertEquals(expected, actualAsync); 103 | 104 | var inArg = GetUserTariffRequest.newBuilder().build(); 105 | verify(grpcService, times(2)).getUserTariff(eq(inArg), any()); 106 | } 107 | 108 | @Test 109 | void getInfo_Test() { 110 | var expected = GetInfoResponse.newBuilder() 111 | .setPremStatus(true) 112 | .setQualStatus(true) 113 | .addQualifiedForWorkWith("instrument") 114 | .build(); 115 | var grpcService = mock(UsersServiceGrpc.UsersServiceImplBase.class, delegatesTo( 116 | new UsersServiceGrpc.UsersServiceImplBase() { 117 | @Override 118 | public void getInfo(GetInfoRequest request, 119 | StreamObserver responseObserver) { 120 | responseObserver.onNext(expected); 121 | responseObserver.onCompleted(); 122 | } 123 | })); 124 | var service = mkClientBasedOnServer(grpcService); 125 | 126 | var actualSync = service.getInfoSync(); 127 | var actualAsync = service.getInfo().join(); 128 | 129 | assertEquals(expected, actualSync); 130 | assertEquals(expected, actualAsync); 131 | 132 | var inArg = GetInfoRequest.newBuilder().build(); 133 | verify(grpcService, times(2)).getInfo(eq(inArg), any()); 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /contract/src/main/proto/users.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tinkoff.public.invest.api.contract.v1; 4 | 5 | option go_package = "./;investapi"; 6 | option java_package = "ru.tinkoff.piapi.contract.v1"; 7 | option java_multiple_files = true; 8 | option csharp_namespace = "Tinkoff.InvestApi.V1"; 9 | option objc_class_prefix = "TIAPI"; 10 | option php_namespace = "Tinkoff\\Invest\\V1"; 11 | 12 | import "google/protobuf/timestamp.proto"; 13 | import "common.proto"; 14 | 15 | service UsersService { /*Сервис предназначен для получения:
**1**. 16 | списка счетов пользователя;
**2**. маржинальных показателей по счёту.*/ 17 | 18 | //Метод получения счетов пользователя. 19 | rpc GetAccounts (GetAccountsRequest) returns (GetAccountsResponse); 20 | 21 | //Расчёт маржинальных показателей по счёту. 22 | rpc GetMarginAttributes (GetMarginAttributesRequest) returns (GetMarginAttributesResponse); 23 | 24 | //Запрос тарифа пользователя. 25 | rpc GetUserTariff (GetUserTariffRequest) returns (GetUserTariffResponse); 26 | 27 | //Метод получения информации о пользователе. 28 | rpc GetInfo (GetInfoRequest) returns (GetInfoResponse); 29 | } 30 | 31 | //Запрос получения счетов пользователя. 32 | message GetAccountsRequest {} 33 | 34 | //Список счетов пользователя. 35 | message GetAccountsResponse { 36 | // Массив счетов клиента. 37 | repeated Account accounts = 1; 38 | } 39 | 40 | //Информация о счёте. 41 | message Account { 42 | 43 | // Идентификатор счёта. 44 | string id = 1; 45 | 46 | // Тип счёта. 47 | AccountType type = 2; 48 | 49 | // Название счёта. 50 | string name = 3; 51 | 52 | // Статус счёта. 53 | AccountStatus status = 4; 54 | 55 | // Дата открытия счёта в часовом поясе UTC. 56 | google.protobuf.Timestamp opened_date = 5; 57 | 58 | // Дата закрытия счёта в часовом поясе UTC. 59 | google.protobuf.Timestamp closed_date = 6; 60 | 61 | // Уровень доступа к текущему счёту (определяется токеном). 62 | AccessLevel access_level = 7; 63 | } 64 | 65 | //Тип счёта. 66 | enum AccountType { 67 | ACCOUNT_TYPE_UNSPECIFIED = 0; //Тип аккаунта не определён. 68 | ACCOUNT_TYPE_TINKOFF = 1; //Брокерский счёт Тинькофф. 69 | ACCOUNT_TYPE_TINKOFF_IIS = 2; //ИИС счёт. 70 | ACCOUNT_TYPE_INVEST_BOX = 3; //Инвесткопилка. 71 | } 72 | 73 | //Статус счёта. 74 | enum AccountStatus { 75 | ACCOUNT_STATUS_UNSPECIFIED = 0; //Статус счёта не определён. 76 | ACCOUNT_STATUS_NEW = 1; //Новый, в процессе открытия. 77 | ACCOUNT_STATUS_OPEN = 2; //Открытый и активный счёт. 78 | ACCOUNT_STATUS_CLOSED = 3; //Закрытый счёт. 79 | } 80 | 81 | //Запрос маржинальных показателей по счёту 82 | message GetMarginAttributesRequest { 83 | 84 | // Идентификатор счёта пользователя. 85 | string account_id = 1; 86 | } 87 | 88 | //Маржинальные показатели по счёту. 89 | message GetMarginAttributesResponse { 90 | 91 | // Ликвидная стоимость портфеля. Подробнее: [что такое ликвидный портфель?](https://help.tinkoff.ru/margin-trade/short/liquid-portfolio/). 92 | MoneyValue liquid_portfolio = 1; 93 | 94 | // Начальная маржа — начальное обеспечение для совершения новой сделки. Подробнее: [начальная и минимальная маржа](https://help.tinkoff.ru/margin-trade/short/initial-and-maintenance-margin/). 95 | MoneyValue starting_margin = 2; 96 | 97 | // Минимальная маржа — это минимальное обеспечение для поддержания позиции, которую вы уже открыли. Подробнее: [начальная и минимальная маржа](https://help.tinkoff.ru/margin-trade/short/initial-and-maintenance-margin/). 98 | MoneyValue minimal_margin = 3; 99 | 100 | // Уровень достаточности средств. Соотношение стоимости ликвидного портфеля к начальной марже. 101 | Quotation funds_sufficiency_level = 4; 102 | 103 | // Объем недостающих средств. Разница между стартовой маржой и ликвидной стоимости портфеля. 104 | MoneyValue amount_of_missing_funds = 5; 105 | 106 | // Скорректированная маржа.Начальная маржа, в которой плановые позиции рассчитываются с учётом активных заявок на покупку позиций лонг или продажу позиций шорт. 107 | MoneyValue corrected_margin = 6; 108 | } 109 | 110 | //Запрос текущих лимитов пользователя. 111 | message GetUserTariffRequest { 112 | } 113 | 114 | //Текущие лимиты пользователя. 115 | message GetUserTariffResponse { 116 | repeated UnaryLimit unary_limits = 1; //Массив лимитов пользователя по unary-запросам. 117 | repeated StreamLimit stream_limits = 2; //Массив лимитов пользователей для stream-соединений. 118 | } 119 | 120 | //Лимит unary-методов. 121 | message UnaryLimit { 122 | int32 limit_per_minute = 1; //Количество unary-запросов в минуту. 123 | repeated string methods = 2; //Названия методов. 124 | } 125 | 126 | //Лимит stream-соединений. 127 | message StreamLimit { 128 | int32 limit = 1; //Максимальное количество stream-соединений. 129 | repeated string streams = 2; //Названия stream-методов. 130 | int32 open = 3; //Текущее количество открытых stream-соединений. 131 | } 132 | 133 | //Запрос информации о пользователе. 134 | message GetInfoRequest { 135 | } 136 | 137 | //Информация о пользователе. 138 | message GetInfoResponse { 139 | bool prem_status = 1; //Признак премиум клиента. 140 | bool qual_status = 2; //Признак квалифицированного инвестора. 141 | repeated string qualified_for_work_with = 3; //Набор требующих тестирования инструментов и возможностей, с которыми может работать пользователь. [Подробнее](https://tinkoff.github.io/investAPI/faq_users/). 142 | string tariff = 4; //Наименование тарифа пользователя. 143 | } 144 | 145 | //Уровень доступа к счёту. 146 | enum AccessLevel { 147 | ACCOUNT_ACCESS_LEVEL_UNSPECIFIED = 0; //Уровень доступа не определён. 148 | ACCOUNT_ACCESS_LEVEL_FULL_ACCESS = 1; //Полный доступ к счёту. 149 | ACCOUNT_ACCESS_LEVEL_READ_ONLY = 2; //Доступ с уровнем прав "только чтение". 150 | ACCOUNT_ACCESS_LEVEL_NO_ACCESS = 3; //Доступ отсутствует. 151 | } 152 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/utils/Helpers.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.utils; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import io.grpc.Metadata; 6 | import io.grpc.Status; 7 | import io.grpc.StatusRuntimeException; 8 | import io.grpc.stub.StreamObserver; 9 | import io.smallrye.mutiny.subscription.MultiEmitter; 10 | import ru.tinkoff.piapi.core.exception.ApiRuntimeException; 11 | 12 | import java.io.IOException; 13 | import java.nio.charset.StandardCharsets; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | import java.util.Objects; 17 | import java.util.concurrent.CompletableFuture; 18 | import java.util.function.Consumer; 19 | import java.util.function.Supplier; 20 | 21 | public class Helpers { 22 | 23 | private static final Map> errorsMap = new HashMap<>(); 24 | private static final String DEFAULT_ERROR_ID = "70001"; 25 | private static final String DEFAULT_ERROR_DESCRIPTION = "unknown error"; 26 | 27 | static { 28 | try { 29 | var resourceAsStream = Helpers.class.getClassLoader().getResourceAsStream("errors.json"); 30 | if (resourceAsStream == null) { 31 | throw new RuntimeException("Не найден файл errors.json"); 32 | } 33 | var json = new String(resourceAsStream.readAllBytes(), StandardCharsets.UTF_8); 34 | errorsMap.putAll(new ObjectMapper().readValue(json, new TypeReference>>() { 35 | })); 36 | } catch (IOException e) { 37 | throw new RuntimeException("Не найден файл errors.json"); 38 | } 39 | } 40 | 41 | public static T unaryCall(Supplier supplier) { 42 | try { 43 | return supplier.get(); 44 | } catch (Exception exception) { 45 | throw apiRuntimeException(exception); 46 | } 47 | } 48 | 49 | private static ApiRuntimeException apiRuntimeException(Throwable exception) { 50 | var status = Status.fromThrowable(exception); 51 | var code = getErrorId(status); 52 | var description = getErrorDescription(code); 53 | var metadata = getMetadata(exception); 54 | var trackingId = getHeader("x-tracking-id", metadata); 55 | return new ApiRuntimeException(description, code, trackingId, exception, metadata); 56 | } 57 | 58 | public static String getHeader(String headerName, Metadata metadata) { 59 | return metadata.get(Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER)); 60 | } 61 | 62 | public static Metadata getMetadata(Throwable exception) { 63 | if (!(exception instanceof StatusRuntimeException)) { 64 | return null; 65 | } 66 | 67 | return ((StatusRuntimeException) exception).getTrailers(); 68 | } 69 | 70 | private static String getErrorId(Status status) { 71 | if ("RESOURCE_EXHAUSTED".equals(status.getCode().name())) { 72 | return "80002"; 73 | } 74 | if ("UNAUTHENTICATED".equals(status.getCode().name())) { 75 | return "40003"; 76 | } 77 | var error = status.getDescription(); 78 | return Objects.requireNonNullElse(error, DEFAULT_ERROR_ID); 79 | } 80 | 81 | /** 82 | * Связывание асинхронного Unary-вызова с {@link CompletableFuture}. 83 | * 84 | * @param callPerformer Асинхронный Unary-вызов. 85 | * @param Тип результата вызова. 86 | * @return {@link CompletableFuture} с результатом вызова. 87 | */ 88 | public static CompletableFuture unaryAsyncCall(Consumer> callPerformer) { 89 | var cf = new CompletableFuture(); 90 | callPerformer.accept(mkStreamObserverWithFuture(cf)); 91 | return cf; 92 | } 93 | 94 | /** 95 | * Создание StreamObserver, который связывает свой результат с CompletableFuture. 96 | *

97 | * Только для Unary-вызовов! 98 | */ 99 | private static StreamObserver mkStreamObserverWithFuture(CompletableFuture cf) { 100 | return new StreamObserver<>() { 101 | @Override 102 | public void onNext(T value) { 103 | cf.complete(value); 104 | } 105 | 106 | @Override 107 | public void onError(Throwable t) { 108 | var throwable = apiRuntimeException(t); 109 | cf.completeExceptionally(throwable); 110 | } 111 | 112 | @Override 113 | public void onCompleted() { 114 | } 115 | }; 116 | } 117 | 118 | /** 119 | * Связывание {@link MultiEmitter} со {@link StreamObserver}. 120 | * 121 | * @param emitter Экземпляр {@link MultiEmitter}. 122 | * @param Тип оперируемый {@link MultiEmitter}. 123 | * @return Связанный {@link StreamObserver}. 124 | */ 125 | public static StreamObserver wrapEmitterWithStreamObserver(MultiEmitter emitter) { 126 | return new StreamObserver<>() { 127 | @Override 128 | public void onNext(T value) { 129 | emitter.emit(value); 130 | } 131 | 132 | @Override 133 | public void onError(Throwable t) { 134 | emitter.fail(t); 135 | } 136 | 137 | @Override 138 | public void onCompleted() { 139 | emitter.complete(); 140 | } 141 | }; 142 | } 143 | 144 | /** 145 | * Проведение необходимых преобразований для пользовательского идентификатора поручения. 146 | * 147 | * @param orderId Пользовательский идентификатор поручения. 148 | * @return Преобразованный идентификатор поручения. 149 | */ 150 | public static String preprocessInputOrderId(String orderId) { 151 | var maxLength = Math.min(orderId.length(), 36); 152 | return orderId.isBlank() ? orderId.trim() : orderId.substring(0, maxLength); 153 | } 154 | 155 | private static String getErrorDescription(String id) { 156 | var error = errorsMap.get(id); 157 | if (error == null) { 158 | return DEFAULT_ERROR_DESCRIPTION; 159 | } 160 | return error.get("description"); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | ru.tinkoff.piapi 6 | java-sdk 7 | pom 8 | 1.6-SNAPSHOT 9 | 10 | Tinkoff Invest API Java SDK 11 | Java SDK for Tinkoff Invest API 12 | https://tinkoff.github.io/investAPI/ 13 | 14 | 15 | 16 | Vladimir Ivanov 17 | v.ivanov8@tinkoff.ru 18 | Tinkoff Bank 19 | https://www.tinkoff.ru/ 20 | 21 | 22 | Arty G 23 | a.gaynanov@tinkoff.ru 24 | Tinkoff Bank 25 | https://www.tinkoff.ru/ 26 | 27 | 28 | 29 | 30 | 31 | Apache License, Version 2.0 32 | https://www.apache.org/licenses/LICENSE-2.0.txt 33 | repo 34 | 35 | 36 | 37 | 38 | https://github.com/Tinkoff/invest-api-java-sdk 39 | scm:git:ssh://git@github.com/Tinkoff/invest-api-java-sdk.git 40 | scm:git:ssh://git@github.com/Tinkoff/invest-api-java-sdk.git 41 | v1.5 42 | 43 | 44 | 45 | UTF-8 46 | 11 47 | 11 48 | 11 49 | 2.13.2.2 50 | 1.56.1 51 | 52 | 53 | 54 | contract 55 | core 56 | example 57 | 58 | 59 | 60 | 61 | ossrh 62 | https://oss.sonatype.org/content/repositories/snapshots 63 | 64 | 65 | ossrh 66 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 67 | 68 | 69 | 70 | 71 | 72 | 73 | org.apache.maven.plugins 74 | maven-source-plugin 75 | 3.2.1 76 | 77 | 78 | attach-sources 79 | 80 | jar-no-fork 81 | 82 | 83 | 84 | 85 | 86 | org.apache.maven.plugins 87 | maven-surefire-plugin 88 | 2.22.2 89 | 90 | 91 | org.apache.maven.plugins 92 | maven-javadoc-plugin 93 | 3.3.1 94 | 95 | 96 | attach-javadocs 97 | 98 | jar 99 | 100 | 101 | 102 | 103 | 104 | org.apache.maven.plugins 105 | maven-gpg-plugin 106 | 3.0.1 107 | 108 | 109 | sign-artifacts 110 | verify 111 | 112 | sign 113 | 114 | 115 | ${gpg.keyname} 116 | ${gpg.keyname} 117 | 118 | 119 | 120 | 121 | 122 | org.apache.maven.plugins 123 | maven-deploy-plugin 124 | 3.0.0-M2 125 | 126 | 127 | org.apache.maven.plugins 128 | maven-release-plugin 129 | 3.0.0-M5 130 | 131 | v@{project.version} 132 | true 133 | releases 134 | 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/stream/OperationsStreamService.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.stream; 2 | 3 | import io.grpc.Context; 4 | import ru.tinkoff.piapi.contract.v1.OperationsStreamServiceGrpc; 5 | import ru.tinkoff.piapi.contract.v1.PortfolioStreamRequest; 6 | import ru.tinkoff.piapi.contract.v1.PortfolioStreamResponse; 7 | import ru.tinkoff.piapi.contract.v1.PositionsStreamRequest; 8 | import ru.tinkoff.piapi.contract.v1.PositionsStreamResponse; 9 | 10 | import javax.annotation.Nonnull; 11 | import javax.annotation.Nullable; 12 | import java.util.List; 13 | import java.util.concurrent.atomic.AtomicReference; 14 | import java.util.function.Consumer; 15 | 16 | public class OperationsStreamService { 17 | 18 | private final OperationsStreamServiceGrpc.OperationsStreamServiceStub stub; 19 | private final AtomicReference positionsCtx = new AtomicReference<>(); 20 | private final AtomicReference portfolioCtx = new AtomicReference<>(); 21 | 22 | public OperationsStreamService(OperationsStreamServiceGrpc.OperationsStreamServiceStub stub) { 23 | this.stub = stub; 24 | } 25 | 26 | /** 27 | * Подписка на стрим позиций 28 | * 29 | * @param streamProcessor обработчик пришедших сообщений в стриме 30 | * @param account Идентификатор счета 31 | */ 32 | public void subscribePositions(@Nonnull StreamProcessor streamProcessor, 33 | @Nonnull String account) { 34 | subscribePositions(streamProcessor, null, List.of(account)); 35 | } 36 | 37 | /** 38 | * Подписка на стрим позиций 39 | * 40 | * @param streamProcessor обработчик пришедших сообщений в стриме 41 | * @param onErrorCallback обработчик ошибок в стриме 42 | * @param account Идентификатор счета 43 | */ 44 | public void subscribePositions(@Nonnull StreamProcessor streamProcessor, 45 | @Nullable Consumer onErrorCallback, 46 | @Nonnull String account) { 47 | subscribePositions(streamProcessor, onErrorCallback, List.of(account)); 48 | } 49 | 50 | /** 51 | * Подписка на стрим позиций 52 | * 53 | * @param streamProcessor обработчик пришедших сообщений в стриме 54 | * @param accounts Идентификаторы счетов 55 | */ 56 | public void subscribePositions(@Nonnull StreamProcessor streamProcessor, 57 | @Nonnull Iterable accounts) { 58 | subscribePositions(streamProcessor, null, accounts); 59 | } 60 | 61 | /** 62 | * Подписка на стрим позиций 63 | * 64 | * @param streamProcessor обработчик пришедших сообщений в стриме 65 | * @param onErrorCallback обработчик ошибок в стриме 66 | * @param accounts Идентификаторы счетов 67 | */ 68 | public void subscribePositions(@Nonnull StreamProcessor streamProcessor, 69 | @Nullable Consumer onErrorCallback, 70 | @Nonnull Iterable accounts) { 71 | cancelPositionSubscription(); 72 | var request = PositionsStreamRequest 73 | .newBuilder() 74 | .addAllAccounts(accounts) 75 | .build(); 76 | var context = Context.current().fork().withCancellation(); 77 | var ctx = context.attach(); 78 | try { 79 | stub.positionsStream(request, new StreamObserverWithProcessor<>(streamProcessor, onErrorCallback)); 80 | positionsCtx.set(context); 81 | } finally { 82 | context.detach(ctx); 83 | } 84 | } 85 | 86 | /** 87 | * Подписка на стрим портфеля 88 | * 89 | * @param streamProcessor обработчик пришедших сообщений в стриме 90 | * @param account Идентификатор счета 91 | */ 92 | public void subscribePortfolio(@Nonnull StreamProcessor streamProcessor, 93 | @Nonnull String account) { 94 | subscribePortfolio(streamProcessor, null, List.of(account)); 95 | } 96 | 97 | /** 98 | * Подписка на стрим портфеля 99 | * 100 | * @param streamProcessor обработчик пришедших сообщений в стриме 101 | * @param onErrorCallback обработчик ошибок в стриме 102 | * @param account Идентификатор счета 103 | */ 104 | public void subscribePortfolio(@Nonnull StreamProcessor streamProcessor, 105 | @Nullable Consumer onErrorCallback, 106 | @Nonnull String account) { 107 | subscribePortfolio(streamProcessor, onErrorCallback, List.of(account)); 108 | } 109 | 110 | /** 111 | * Подписка на стрим портфеля 112 | * 113 | * @param streamProcessor обработчик пришедших сообщений в стриме 114 | * @param accounts Идентификаторы счетов 115 | */ 116 | public void subscribePortfolio(@Nonnull StreamProcessor streamProcessor, 117 | @Nonnull Iterable accounts) { 118 | subscribePortfolio(streamProcessor, null, accounts); 119 | } 120 | 121 | /** 122 | * Подписка на стрим портфеля 123 | * 124 | * @param streamProcessor обработчик пришедших сообщений в стриме 125 | * @param onErrorCallback обработчик ошибок в стриме 126 | * @param accounts Идентификаторы счетов 127 | */ 128 | public void subscribePortfolio(@Nonnull StreamProcessor streamProcessor, 129 | @Nullable Consumer onErrorCallback, 130 | @Nonnull Iterable accounts) { 131 | cancelPortfolioSubscription(); 132 | var request = PortfolioStreamRequest 133 | .newBuilder() 134 | .addAllAccounts(accounts) 135 | .build(); 136 | var context = Context.current().fork().withCancellation(); 137 | var ctx = context.attach(); 138 | try { 139 | stub.portfolioStream(request, new StreamObserverWithProcessor<>(streamProcessor, onErrorCallback)); 140 | portfolioCtx.set(context); 141 | } finally { 142 | context.detach(ctx); 143 | } 144 | } 145 | 146 | public void cancelPortfolioSubscription() { 147 | cancelContext(portfolioCtx.get()); 148 | } 149 | 150 | public void cancelPositionSubscription() { 151 | cancelContext(positionsCtx.get()); 152 | } 153 | 154 | void cancelContext(Context.CancellableContext context) { 155 | if (context != null) 156 | context.cancel(new RuntimeException("canceled by user")); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/stream/MarketDataSubscriptionService.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core.stream; 2 | 3 | import io.grpc.Context; 4 | import io.grpc.stub.ClientCallStreamObserver; 5 | import io.grpc.stub.StreamObserver; 6 | import ru.tinkoff.piapi.contract.v1.*; 7 | 8 | import javax.annotation.Nonnull; 9 | import javax.annotation.Nullable; 10 | import java.util.List; 11 | import java.util.concurrent.atomic.AtomicReference; 12 | import java.util.function.Consumer; 13 | 14 | public class MarketDataSubscriptionService { 15 | private final StreamObserver observer; 16 | private final AtomicReference contextRef = new AtomicReference<>(); 17 | 18 | public MarketDataSubscriptionService( 19 | @Nonnull MarketDataStreamServiceGrpc.MarketDataStreamServiceStub stub, 20 | @Nonnull StreamProcessor streamProcessor, 21 | @Nullable Consumer onErrorCallback) { 22 | var context = Context.current().fork().withCancellation(); 23 | var ctx = context.attach(); 24 | try { 25 | this.observer = stub.marketDataStream(new StreamObserverWithProcessor<>(streamProcessor, onErrorCallback)); 26 | contextRef.set(context); 27 | } finally { 28 | context.detach(ctx); 29 | } 30 | } 31 | 32 | public void subscribeTrades(@Nonnull List instrumentIds) { 33 | tradesStream(instrumentIds, SubscriptionAction.SUBSCRIPTION_ACTION_SUBSCRIBE); 34 | } 35 | 36 | public void unsubscribeTrades(@Nonnull List instrumentIds) { 37 | tradesStream(instrumentIds, SubscriptionAction.SUBSCRIPTION_ACTION_UNSUBSCRIBE); 38 | } 39 | 40 | public void subscribeOrderbook(@Nonnull List instrumentIds, 41 | int depth) { 42 | orderBookStream(instrumentIds, SubscriptionAction.SUBSCRIPTION_ACTION_SUBSCRIBE, depth); 43 | } 44 | 45 | public void subscribeOrderbook(@Nonnull List instrumentIds) { 46 | orderBookStream(instrumentIds, SubscriptionAction.SUBSCRIPTION_ACTION_SUBSCRIBE, 1); 47 | } 48 | 49 | public void unsubscribeOrderbook(@Nonnull List instrumentIds, 50 | int depth) { 51 | orderBookStream(instrumentIds, SubscriptionAction.SUBSCRIPTION_ACTION_UNSUBSCRIBE, depth); 52 | } 53 | 54 | public void unsubscribeOrderbook(@Nonnull List instrumentIds) { 55 | orderBookStream(instrumentIds, SubscriptionAction.SUBSCRIPTION_ACTION_UNSUBSCRIBE, 1); 56 | } 57 | 58 | public void subscribeInfo(@Nonnull List instrumentIds) { 59 | infoStream(instrumentIds, SubscriptionAction.SUBSCRIPTION_ACTION_SUBSCRIBE); 60 | } 61 | 62 | public void unsubscribeInfo(@Nonnull List instrumentIds) { 63 | infoStream(instrumentIds, SubscriptionAction.SUBSCRIPTION_ACTION_UNSUBSCRIBE); 64 | } 65 | 66 | 67 | public void subscribeCandles(@Nonnull List instrumentIds) { 68 | candlesStream(instrumentIds, SubscriptionAction.SUBSCRIPTION_ACTION_SUBSCRIBE, 69 | SubscriptionInterval.SUBSCRIPTION_INTERVAL_ONE_MINUTE); 70 | } 71 | 72 | public void subscribeCandles(@Nonnull List instrumentIds, SubscriptionInterval interval) { 73 | candlesStream(instrumentIds, SubscriptionAction.SUBSCRIPTION_ACTION_SUBSCRIBE, interval); 74 | } 75 | 76 | public void unsubscribeCandles(@Nonnull List instrumentIds) { 77 | candlesStream(instrumentIds, SubscriptionAction.SUBSCRIPTION_ACTION_UNSUBSCRIBE, 78 | SubscriptionInterval.SUBSCRIPTION_INTERVAL_ONE_MINUTE); 79 | } 80 | 81 | public void unsubscribeCandles(@Nonnull List instrumentIds, SubscriptionInterval interval) { 82 | candlesStream(instrumentIds, SubscriptionAction.SUBSCRIPTION_ACTION_UNSUBSCRIBE, interval); 83 | } 84 | 85 | 86 | public void subscribeLastPrices(@Nonnull List instrumentIds) { 87 | lastPricesStream(instrumentIds, SubscriptionAction.SUBSCRIPTION_ACTION_SUBSCRIBE); 88 | } 89 | 90 | public void unsubscribeLastPrices(@Nonnull List instrumentIds) { 91 | lastPricesStream(instrumentIds, SubscriptionAction.SUBSCRIPTION_ACTION_UNSUBSCRIBE); 92 | } 93 | 94 | public void cancel() { 95 | var context = contextRef.get(); 96 | if (context != null) context.cancel(new RuntimeException("canceled by user")); 97 | } 98 | 99 | 100 | private void candlesStream(@Nonnull List instrumentIds, 101 | @Nonnull SubscriptionAction action, 102 | @Nonnull SubscriptionInterval interval) { 103 | var builder = SubscribeCandlesRequest 104 | .newBuilder() 105 | .setSubscriptionAction(action); 106 | for (var instrumentId : instrumentIds) { 107 | builder.addInstruments(CandleInstrument 108 | .newBuilder() 109 | .setInterval(interval) 110 | .setInstrumentId(instrumentId) 111 | .build()); 112 | } 113 | var request = MarketDataRequest 114 | .newBuilder() 115 | .setSubscribeCandlesRequest(builder) 116 | .build(); 117 | observer.onNext(request); 118 | } 119 | 120 | private void lastPricesStream(@Nonnull List instrumentIds, 121 | @Nonnull SubscriptionAction action) { 122 | var builder = SubscribeLastPriceRequest 123 | .newBuilder() 124 | .setSubscriptionAction(action); 125 | for (var instrumentId : instrumentIds) { 126 | builder.addInstruments(LastPriceInstrument 127 | .newBuilder() 128 | .setInstrumentId(instrumentId) 129 | .build()); 130 | } 131 | var request = MarketDataRequest 132 | .newBuilder() 133 | .setSubscribeLastPriceRequest(builder) 134 | .build(); 135 | observer.onNext(request); 136 | } 137 | 138 | private void tradesStream(@Nonnull List instrumentIds, 139 | @Nonnull SubscriptionAction action) { 140 | var builder = SubscribeTradesRequest 141 | .newBuilder() 142 | .setSubscriptionAction(action); 143 | for (String instrumentId : instrumentIds) { 144 | builder.addInstruments(TradeInstrument 145 | .newBuilder() 146 | .setInstrumentId(instrumentId) 147 | .build()); 148 | } 149 | var request = MarketDataRequest 150 | .newBuilder() 151 | .setSubscribeTradesRequest(builder) 152 | .build(); 153 | observer.onNext(request); 154 | } 155 | 156 | private void orderBookStream(@Nonnull List instrumentIds, 157 | @Nonnull SubscriptionAction action, 158 | int depth) { 159 | var builder = SubscribeOrderBookRequest 160 | .newBuilder() 161 | .setSubscriptionAction(action); 162 | for (var instrumentId : instrumentIds) { 163 | builder.addInstruments(OrderBookInstrument 164 | .newBuilder() 165 | .setDepth(depth) 166 | .setInstrumentId(instrumentId) 167 | .build()); 168 | } 169 | var request = MarketDataRequest 170 | .newBuilder() 171 | .setSubscribeOrderBookRequest(builder) 172 | .build(); 173 | observer.onNext(request); 174 | } 175 | 176 | private void infoStream(@Nonnull List instrumentIds, 177 | @Nonnull SubscriptionAction action) { 178 | var builder = SubscribeInfoRequest 179 | .newBuilder() 180 | .setSubscriptionAction(action); 181 | for (var instrumentId : instrumentIds) { 182 | builder.addInstruments(InfoInstrument.newBuilder().setInstrumentId(instrumentId).build()); 183 | } 184 | var request = MarketDataRequest 185 | .newBuilder() 186 | .setSubscribeInfoRequest(builder) 187 | .build(); 188 | observer.onNext(request); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /core/src/test/java/ru/tinkoff/piapi/core/MarketDataServiceTest.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core; 2 | 3 | import com.google.protobuf.Timestamp; 4 | import io.grpc.Channel; 5 | import io.grpc.stub.StreamObserver; 6 | import org.junit.jupiter.api.Test; 7 | import ru.tinkoff.piapi.contract.v1.CandleInterval; 8 | import ru.tinkoff.piapi.contract.v1.GetCandlesRequest; 9 | import ru.tinkoff.piapi.contract.v1.GetCandlesResponse; 10 | import ru.tinkoff.piapi.contract.v1.GetLastPricesRequest; 11 | import ru.tinkoff.piapi.contract.v1.GetLastPricesResponse; 12 | import ru.tinkoff.piapi.contract.v1.GetLastTradesRequest; 13 | import ru.tinkoff.piapi.contract.v1.GetLastTradesResponse; 14 | import ru.tinkoff.piapi.contract.v1.GetOrderBookRequest; 15 | import ru.tinkoff.piapi.contract.v1.GetOrderBookResponse; 16 | import ru.tinkoff.piapi.contract.v1.GetTradingStatusRequest; 17 | import ru.tinkoff.piapi.contract.v1.GetTradingStatusResponse; 18 | import ru.tinkoff.piapi.contract.v1.HistoricCandle; 19 | import ru.tinkoff.piapi.contract.v1.LastPrice; 20 | import ru.tinkoff.piapi.contract.v1.MarketDataServiceGrpc; 21 | import ru.tinkoff.piapi.contract.v1.Trade; 22 | import ru.tinkoff.piapi.contract.v1.TradeDirection; 23 | import ru.tinkoff.piapi.core.utils.DateUtils; 24 | 25 | import java.util.List; 26 | 27 | import static org.junit.jupiter.api.Assertions.assertEquals; 28 | import static org.junit.jupiter.api.Assertions.assertIterableEquals; 29 | import static org.mockito.AdditionalAnswers.delegatesTo; 30 | import static org.mockito.ArgumentMatchers.any; 31 | import static org.mockito.ArgumentMatchers.eq; 32 | import static org.mockito.Mockito.mock; 33 | import static org.mockito.Mockito.times; 34 | import static org.mockito.Mockito.verify; 35 | 36 | public class MarketDataServiceTest extends GrpcClientTester { 37 | 38 | @Override 39 | protected MarketDataService createClient(Channel channel) { 40 | return new MarketDataService( 41 | MarketDataServiceGrpc.newBlockingStub(channel), 42 | MarketDataServiceGrpc.newStub(channel)); 43 | } 44 | 45 | @Test 46 | void getCandles_Test() { 47 | var expected = GetCandlesResponse.newBuilder() 48 | .addCandles(HistoricCandle.newBuilder().setVolume(1).build()) 49 | .build(); 50 | var grpcService = mock(MarketDataServiceGrpc.MarketDataServiceImplBase.class, delegatesTo( 51 | new MarketDataServiceGrpc.MarketDataServiceImplBase() { 52 | @Override 53 | public void getCandles(GetCandlesRequest request, 54 | StreamObserver responseObserver) { 55 | responseObserver.onNext(expected); 56 | responseObserver.onCompleted(); 57 | } 58 | })); 59 | var service = mkClientBasedOnServer(grpcService); 60 | 61 | var inArg = GetCandlesRequest.newBuilder() 62 | .setInstrumentId("figi") 63 | .setFrom(Timestamp.newBuilder().setSeconds(1234567890).build()) 64 | .setTo(Timestamp.newBuilder().setSeconds(1234567890).setNanos(111222333).build()) 65 | .setInterval(CandleInterval.CANDLE_INTERVAL_1_MIN) 66 | .build(); 67 | var actualSync = service.getCandlesSync(inArg.getInstrumentId(), DateUtils.timestampToInstant(inArg.getFrom()), 68 | DateUtils.timestampToInstant(inArg.getTo()), inArg.getInterval()); 69 | var actualAsync = service.getCandles(inArg.getInstrumentId(), DateUtils.timestampToInstant(inArg.getFrom()), 70 | DateUtils.timestampToInstant(inArg.getTo()), inArg.getInterval()).join(); 71 | 72 | assertIterableEquals(expected.getCandlesList(), actualSync); 73 | assertIterableEquals(expected.getCandlesList(), actualAsync); 74 | 75 | verify(grpcService, times(2)).getCandles(eq(inArg), any()); 76 | } 77 | 78 | @Test 79 | void getLastTrades_Test() { 80 | var figi = "my_figi"; 81 | var uid = "my_uid"; 82 | var expected = GetLastTradesResponse.newBuilder() 83 | .addTrades(Trade.newBuilder().setFigi(figi).setInstrumentUid(uid).setDirection(TradeDirection.TRADE_DIRECTION_BUY).setQuantity(1).build()) 84 | .build(); 85 | var grpcService = mock(MarketDataServiceGrpc.MarketDataServiceImplBase.class, delegatesTo( 86 | new MarketDataServiceGrpc.MarketDataServiceImplBase() { 87 | 88 | @Override 89 | public void getLastTrades(GetLastTradesRequest request, StreamObserver responseObserver) { 90 | responseObserver.onNext(expected); 91 | responseObserver.onCompleted(); 92 | } 93 | })); 94 | var service = mkClientBasedOnServer(grpcService); 95 | 96 | var inArg = GetLastTradesRequest.newBuilder() 97 | .setInstrumentId(figi) 98 | .setFrom(Timestamp.newBuilder().setSeconds(1234567890).build()) 99 | .setTo(Timestamp.newBuilder().setSeconds(1234567890).setNanos(111222333).build()) 100 | .build(); 101 | var actualSync = service.getLastTradesSync(inArg.getInstrumentId(), DateUtils.timestampToInstant(inArg.getFrom()), 102 | DateUtils.timestampToInstant(inArg.getTo())); 103 | var actualAsync = service.getLastTrades(inArg.getInstrumentId(), DateUtils.timestampToInstant(inArg.getFrom()), 104 | DateUtils.timestampToInstant(inArg.getTo())).join(); 105 | 106 | assertIterableEquals(expected.getTradesList(), actualSync); 107 | assertIterableEquals(expected.getTradesList(), actualAsync); 108 | 109 | verify(grpcService, times(2)).getLastTrades(eq(inArg), any()); 110 | } 111 | 112 | @Test 113 | void getLastPrices_Test() { 114 | var expected = GetLastPricesResponse.newBuilder() 115 | .addLastPrices(LastPrice.newBuilder().setFigi("figi1").build()) 116 | .addLastPrices(LastPrice.newBuilder().setFigi("figi2").build()) 117 | .build(); 118 | var grpcService = mock(MarketDataServiceGrpc.MarketDataServiceImplBase.class, delegatesTo( 119 | new MarketDataServiceGrpc.MarketDataServiceImplBase() { 120 | @Override 121 | public void getLastPrices(GetLastPricesRequest request, 122 | StreamObserver responseObserver) { 123 | responseObserver.onNext(expected); 124 | responseObserver.onCompleted(); 125 | } 126 | })); 127 | var service = mkClientBasedOnServer(grpcService); 128 | 129 | var inArg = GetLastPricesRequest.newBuilder() 130 | .addInstrumentId("figi1") 131 | .addInstrumentId("figi2") 132 | .build(); 133 | var actualSync = service.getLastPricesSync(List.of("figi1", "figi2")); 134 | var actualAsync = service.getLastPrices(List.of("figi1", "figi2")).join(); 135 | 136 | assertIterableEquals(expected.getLastPricesList(), actualSync); 137 | assertIterableEquals(expected.getLastPricesList(), actualAsync); 138 | 139 | verify(grpcService, times(2)).getLastPrices(eq(inArg), any()); 140 | } 141 | 142 | @Test 143 | void getOrderBook_Test() { 144 | var expected = GetOrderBookResponse.newBuilder() 145 | .setFigi("figi") 146 | .setDepth(10) 147 | .build(); 148 | var grpcService = mock(MarketDataServiceGrpc.MarketDataServiceImplBase.class, delegatesTo( 149 | new MarketDataServiceGrpc.MarketDataServiceImplBase() { 150 | @Override 151 | public void getOrderBook(GetOrderBookRequest request, 152 | StreamObserver responseObserver) { 153 | responseObserver.onNext(expected); 154 | responseObserver.onCompleted(); 155 | } 156 | })); 157 | var service = mkClientBasedOnServer(grpcService); 158 | 159 | var inArg = GetOrderBookRequest.newBuilder() 160 | .setInstrumentId(expected.getFigi()) 161 | .setDepth(expected.getDepth()) 162 | .build(); 163 | var actualSync = service.getOrderBookSync(inArg.getInstrumentId(), inArg.getDepth()); 164 | var actualAsync = service.getOrderBook(inArg.getInstrumentId(), inArg.getDepth()).join(); 165 | 166 | assertEquals(expected, actualSync); 167 | assertEquals(expected, actualAsync); 168 | 169 | verify(grpcService, times(2)).getOrderBook(eq(inArg), any()); 170 | } 171 | 172 | @Test 173 | void getTradingStatus_Test() { 174 | var expected = GetTradingStatusResponse.newBuilder() 175 | .setFigi("figi") 176 | .build(); 177 | var grpcService = mock(MarketDataServiceGrpc.MarketDataServiceImplBase.class, delegatesTo( 178 | new MarketDataServiceGrpc.MarketDataServiceImplBase() { 179 | @Override 180 | public void getTradingStatus(GetTradingStatusRequest request, 181 | StreamObserver responseObserver) { 182 | responseObserver.onNext(expected); 183 | responseObserver.onCompleted(); 184 | } 185 | })); 186 | var service = mkClientBasedOnServer(grpcService); 187 | 188 | var inArg = GetTradingStatusRequest.newBuilder() 189 | .setInstrumentId(expected.getFigi()) 190 | .build(); 191 | var actualSync = service.getTradingStatusSync(inArg.getInstrumentId()); 192 | var actualAsync = service.getTradingStatus(inArg.getInstrumentId()).join(); 193 | 194 | assertEquals(expected, actualSync); 195 | assertEquals(expected, actualAsync); 196 | 197 | verify(grpcService, times(2)).getTradingStatus(eq(inArg), any()); 198 | } 199 | 200 | } 201 | -------------------------------------------------------------------------------- /core/src/test/java/ru/tinkoff/piapi/core/OrdersServiceTest.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core; 2 | 3 | import com.google.protobuf.Timestamp; 4 | import io.grpc.Channel; 5 | import io.grpc.stub.StreamObserver; 6 | import org.hamcrest.core.IsInstanceOf; 7 | import org.junit.Rule; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.rules.ExpectedException; 10 | import ru.tinkoff.piapi.contract.v1.CancelOrderRequest; 11 | import ru.tinkoff.piapi.contract.v1.CancelOrderResponse; 12 | import ru.tinkoff.piapi.contract.v1.GetOrderStateRequest; 13 | import ru.tinkoff.piapi.contract.v1.GetOrdersRequest; 14 | import ru.tinkoff.piapi.contract.v1.GetOrdersResponse; 15 | import ru.tinkoff.piapi.contract.v1.OrderDirection; 16 | import ru.tinkoff.piapi.contract.v1.OrderState; 17 | import ru.tinkoff.piapi.contract.v1.OrderType; 18 | import ru.tinkoff.piapi.contract.v1.OrdersServiceGrpc; 19 | import ru.tinkoff.piapi.contract.v1.PostOrderRequest; 20 | import ru.tinkoff.piapi.contract.v1.PostOrderResponse; 21 | import ru.tinkoff.piapi.contract.v1.Quotation; 22 | import ru.tinkoff.piapi.core.exception.ReadonlyModeViolationException; 23 | import ru.tinkoff.piapi.core.utils.DateUtils; 24 | 25 | import java.util.concurrent.CompletionException; 26 | 27 | import static org.junit.jupiter.api.Assertions.assertEquals; 28 | import static org.junit.jupiter.api.Assertions.assertIterableEquals; 29 | import static org.junit.jupiter.api.Assertions.assertThrows; 30 | import static org.mockito.AdditionalAnswers.delegatesTo; 31 | import static org.mockito.ArgumentMatchers.any; 32 | import static org.mockito.ArgumentMatchers.eq; 33 | import static org.mockito.Mockito.mock; 34 | import static org.mockito.Mockito.times; 35 | import static org.mockito.Mockito.verify; 36 | 37 | public class OrdersServiceTest extends GrpcClientTester { 38 | 39 | @Rule 40 | public ExpectedException futureThrown = ExpectedException.none(); 41 | 42 | @Override 43 | protected OrdersService createClient(Channel channel) { 44 | return new OrdersService( 45 | OrdersServiceGrpc.newBlockingStub(channel), 46 | OrdersServiceGrpc.newStub(channel), 47 | false); 48 | } 49 | 50 | 51 | @Test 52 | void postOrder_Test() { 53 | var expected = PostOrderResponse.newBuilder() 54 | .setOrderId("orderId") 55 | .setFigi("figi") 56 | .setDirection(OrderDirection.ORDER_DIRECTION_BUY) 57 | .build(); 58 | var grpcService = mock(OrdersServiceGrpc.OrdersServiceImplBase.class, delegatesTo( 59 | new OrdersServiceGrpc.OrdersServiceImplBase() { 60 | @Override 61 | public void postOrder(PostOrderRequest request, 62 | StreamObserver responseObserver) { 63 | responseObserver.onNext(expected); 64 | responseObserver.onCompleted(); 65 | } 66 | })); 67 | var service = mkClientBasedOnServer(grpcService); 68 | 69 | var inArg = PostOrderRequest.newBuilder() 70 | .setAccountId("accountId") 71 | .setInstrumentId(expected.getFigi()) 72 | .setDirection(expected.getDirection()) 73 | .setPrice(Quotation.newBuilder().build()) 74 | .build(); 75 | var actualSync = service.postOrderSync( 76 | inArg.getInstrumentId(), inArg.getQuantity(), inArg.getPrice(), inArg.getDirection(), 77 | inArg.getAccountId(), inArg.getOrderType(), inArg.getOrderId()); 78 | var actualAsync = service.postOrder( 79 | inArg.getInstrumentId(), inArg.getQuantity(), inArg.getPrice(), inArg.getDirection(), 80 | inArg.getAccountId(), inArg.getOrderType(), inArg.getOrderId()) 81 | .join(); 82 | 83 | assertEquals(expected, actualSync); 84 | assertEquals(expected, actualAsync); 85 | 86 | verify(grpcService, times(2)).postOrder(eq(inArg), any()); 87 | } 88 | 89 | @Test 90 | void postOrder_forbiddenInReadonly_Test() { 91 | var grpcService = mock(OrdersServiceGrpc.OrdersServiceImplBase.class); 92 | var readonlyService = mkClientBasedOnServer( 93 | grpcService, 94 | channel -> new OrdersService( 95 | OrdersServiceGrpc.newBlockingStub(channel), 96 | OrdersServiceGrpc.newStub(channel), 97 | true)); 98 | 99 | assertThrows( 100 | ReadonlyModeViolationException.class, 101 | () -> readonlyService.postOrderSync( 102 | "", 0, Quotation.getDefaultInstance(), OrderDirection.ORDER_DIRECTION_UNSPECIFIED, 103 | "", OrderType.ORDER_TYPE_UNSPECIFIED, "")); 104 | futureThrown.expect(CompletionException.class); 105 | futureThrown.expectCause(IsInstanceOf.instanceOf(ReadonlyModeViolationException.class)); 106 | assertThrows(ReadonlyModeViolationException.class, () -> readonlyService.postOrder( 107 | "", 0, Quotation.getDefaultInstance(), OrderDirection.ORDER_DIRECTION_UNSPECIFIED, 108 | "", OrderType.ORDER_TYPE_UNSPECIFIED, "")); 109 | } 110 | 111 | @Test 112 | void getOrders_Test() { 113 | var accountId = "accountId"; 114 | var expected = GetOrdersResponse.newBuilder() 115 | .addOrders(OrderState.newBuilder().setOrderId("orderId").build()) 116 | .build(); 117 | var grpcService = mock(OrdersServiceGrpc.OrdersServiceImplBase.class, delegatesTo( 118 | new OrdersServiceGrpc.OrdersServiceImplBase() { 119 | @Override 120 | public void getOrders(GetOrdersRequest request, 121 | StreamObserver responseObserver) { 122 | responseObserver.onNext(expected); 123 | responseObserver.onCompleted(); 124 | } 125 | })); 126 | var service = mkClientBasedOnServer(grpcService); 127 | 128 | var actualSync = service.getOrdersSync(accountId); 129 | var actualAsync = service.getOrders(accountId).join(); 130 | 131 | assertIterableEquals(expected.getOrdersList(), actualSync); 132 | assertIterableEquals(expected.getOrdersList(), actualAsync); 133 | 134 | var inArg = GetOrdersRequest.newBuilder() 135 | .setAccountId(accountId) 136 | .build(); 137 | verify(grpcService, times(2)).getOrders(eq(inArg), any()); 138 | } 139 | 140 | @Test 141 | void cancelOrder_Test() { 142 | var accountId = "accountId"; 143 | var orderId = "orderId"; 144 | var expected = CancelOrderResponse.newBuilder() 145 | .setTime(Timestamp.newBuilder().setSeconds(1234567890).setNanos(0).build()) 146 | .build(); 147 | var grpcService = mock(OrdersServiceGrpc.OrdersServiceImplBase.class, delegatesTo( 148 | new OrdersServiceGrpc.OrdersServiceImplBase() { 149 | @Override 150 | public void cancelOrder(CancelOrderRequest request, 151 | StreamObserver responseObserver) { 152 | responseObserver.onNext(expected); 153 | responseObserver.onCompleted(); 154 | } 155 | })); 156 | var service = mkClientBasedOnServer(grpcService); 157 | 158 | var actualSync = service.cancelOrderSync(accountId, orderId); 159 | var actualAsync = service.cancelOrder(accountId, orderId).join(); 160 | 161 | assertEquals(DateUtils.timestampToInstant(expected.getTime()), actualSync); 162 | assertEquals(DateUtils.timestampToInstant(expected.getTime()), actualAsync); 163 | 164 | var inArg = CancelOrderRequest.newBuilder() 165 | .setAccountId(accountId) 166 | .setOrderId(orderId) 167 | .build(); 168 | verify(grpcService, times(2)).cancelOrder(eq(inArg), any()); 169 | } 170 | 171 | @Test 172 | void cancelOrder_forbiddenInReadonly_Test() { 173 | var grpcService = mock(OrdersServiceGrpc.OrdersServiceImplBase.class); 174 | var readonlyService = mkClientBasedOnServer( 175 | grpcService, 176 | channel -> new OrdersService( 177 | OrdersServiceGrpc.newBlockingStub(channel), 178 | OrdersServiceGrpc.newStub(channel), 179 | true)); 180 | 181 | assertThrows( 182 | ReadonlyModeViolationException.class, 183 | () -> readonlyService.cancelOrderSync("", "")); 184 | futureThrown.expect(CompletionException.class); 185 | futureThrown.expectCause(IsInstanceOf.instanceOf(ReadonlyModeViolationException.class)); 186 | assertThrows(ReadonlyModeViolationException.class, () -> readonlyService.cancelOrder("", "")); 187 | } 188 | 189 | @Test 190 | void getOrderState_Test() { 191 | var accountId = "accountId"; 192 | var orderId = "orderId"; 193 | var expected = OrderState.newBuilder() 194 | .setOrderId(orderId) 195 | .build(); 196 | var grpcService = mock(OrdersServiceGrpc.OrdersServiceImplBase.class, delegatesTo( 197 | new OrdersServiceGrpc.OrdersServiceImplBase() { 198 | @Override 199 | public void getOrderState(GetOrderStateRequest request, 200 | StreamObserver responseObserver) { 201 | responseObserver.onNext(expected); 202 | responseObserver.onCompleted(); 203 | } 204 | })); 205 | var service = mkClientBasedOnServer(grpcService); 206 | 207 | var actualSync = service.getOrderStateSync(accountId, orderId); 208 | var actualAsync = service.getOrderState(accountId, orderId).join(); 209 | 210 | assertEquals(expected, actualSync); 211 | assertEquals(expected, actualAsync); 212 | 213 | var inArg = GetOrderStateRequest.newBuilder() 214 | .setAccountId(accountId) 215 | .setOrderId(orderId) 216 | .build(); 217 | verify(grpcService, times(2)).getOrderState(eq(inArg), any()); 218 | } 219 | 220 | } 221 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/StopOrdersService.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core; 2 | 3 | import ru.tinkoff.piapi.contract.v1.CancelStopOrderRequest; 4 | import ru.tinkoff.piapi.contract.v1.CancelStopOrderResponse; 5 | import ru.tinkoff.piapi.contract.v1.GetStopOrdersRequest; 6 | import ru.tinkoff.piapi.contract.v1.GetStopOrdersResponse; 7 | import ru.tinkoff.piapi.contract.v1.PostStopOrderRequest; 8 | import ru.tinkoff.piapi.contract.v1.PostStopOrderResponse; 9 | import ru.tinkoff.piapi.contract.v1.Quotation; 10 | import ru.tinkoff.piapi.contract.v1.StopOrder; 11 | import ru.tinkoff.piapi.contract.v1.StopOrderDirection; 12 | import ru.tinkoff.piapi.contract.v1.StopOrderExpirationType; 13 | import ru.tinkoff.piapi.contract.v1.StopOrderType; 14 | import ru.tinkoff.piapi.contract.v1.StopOrdersServiceGrpc.StopOrdersServiceBlockingStub; 15 | import ru.tinkoff.piapi.contract.v1.StopOrdersServiceGrpc.StopOrdersServiceStub; 16 | import ru.tinkoff.piapi.core.utils.DateUtils; 17 | import ru.tinkoff.piapi.core.utils.Helpers; 18 | 19 | import javax.annotation.Nonnull; 20 | import java.time.Instant; 21 | import java.util.List; 22 | import java.util.concurrent.CompletableFuture; 23 | 24 | import static ru.tinkoff.piapi.core.utils.Helpers.unaryCall; 25 | import static ru.tinkoff.piapi.core.utils.ValidationUtils.checkReadonly; 26 | import static ru.tinkoff.piapi.core.utils.ValidationUtils.checkSandbox; 27 | 28 | public class StopOrdersService { 29 | private final StopOrdersServiceBlockingStub stopOrdersBlockingStub; 30 | private final StopOrdersServiceStub stopOrdersStub; 31 | private final boolean readonlyMode; 32 | private final boolean sandboxMode; 33 | 34 | StopOrdersService(@Nonnull StopOrdersServiceBlockingStub stopOrdersBlockingStub, 35 | @Nonnull StopOrdersServiceStub stopOrdersStub, 36 | boolean readonlyMode, 37 | boolean sandboxMode) { 38 | this.sandboxMode = sandboxMode; 39 | this.stopOrdersBlockingStub = stopOrdersBlockingStub; 40 | this.stopOrdersStub = stopOrdersStub; 41 | this.readonlyMode = readonlyMode; 42 | } 43 | 44 | @Nonnull 45 | public String postStopOrderGoodTillCancelSync(@Nonnull String instrumentId, 46 | long quantity, 47 | @Nonnull Quotation price, 48 | @Nonnull Quotation stopPrice, 49 | @Nonnull StopOrderDirection direction, 50 | @Nonnull String accountId, 51 | @Nonnull StopOrderType type) { 52 | checkReadonly(readonlyMode); 53 | checkSandbox(sandboxMode); 54 | 55 | return unaryCall(() -> stopOrdersBlockingStub.postStopOrder( 56 | PostStopOrderRequest.newBuilder() 57 | .setInstrumentId(instrumentId) 58 | .setQuantity(quantity) 59 | .setPrice(price) 60 | .setStopPrice(stopPrice) 61 | .setDirection(direction) 62 | .setAccountId(accountId) 63 | .setExpirationType(StopOrderExpirationType.STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL) 64 | .setStopOrderType(type) 65 | .build()) 66 | .getStopOrderId()); 67 | } 68 | 69 | @Nonnull 70 | public String postStopOrderGoodTillDateSync(@Nonnull String instrumentId, 71 | long quantity, 72 | @Nonnull Quotation price, 73 | @Nonnull Quotation stopPrice, 74 | @Nonnull StopOrderDirection direction, 75 | @Nonnull String accountId, 76 | @Nonnull StopOrderType type, 77 | @Nonnull Instant expireDate) { 78 | checkReadonly(readonlyMode); 79 | checkSandbox(sandboxMode); 80 | 81 | return unaryCall(() -> stopOrdersBlockingStub.postStopOrder( 82 | PostStopOrderRequest.newBuilder() 83 | .setInstrumentId(instrumentId) 84 | .setQuantity(quantity) 85 | .setPrice(price) 86 | .setStopPrice(stopPrice) 87 | .setDirection(direction) 88 | .setAccountId(accountId) 89 | .setExpirationType(StopOrderExpirationType.STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE) 90 | .setStopOrderType(type) 91 | .setExpireDate(DateUtils.instantToTimestamp(expireDate)) 92 | .build()) 93 | .getStopOrderId()); 94 | } 95 | 96 | @Nonnull 97 | public List getStopOrdersSync(@Nonnull String accountId) { 98 | checkSandbox(sandboxMode); 99 | 100 | return unaryCall(() -> stopOrdersBlockingStub.getStopOrders( 101 | GetStopOrdersRequest.newBuilder() 102 | .setAccountId(accountId) 103 | .build()) 104 | .getStopOrdersList()); 105 | } 106 | 107 | @Nonnull 108 | public Instant cancelStopOrderSync(@Nonnull String accountId, 109 | @Nonnull String stopOrderId) { 110 | checkReadonly(readonlyMode); 111 | checkSandbox(sandboxMode); 112 | 113 | var responseTime = unaryCall(() -> stopOrdersBlockingStub.cancelStopOrder( 114 | CancelStopOrderRequest.newBuilder() 115 | .setAccountId(accountId) 116 | .setStopOrderId(stopOrderId) 117 | .build()) 118 | .getTime()); 119 | 120 | return DateUtils.timestampToInstant(responseTime); 121 | } 122 | 123 | @Nonnull 124 | public CompletableFuture postStopOrderGoodTillCancel(@Nonnull String instrumentId, 125 | long quantity, 126 | @Nonnull Quotation price, 127 | @Nonnull Quotation stopPrice, 128 | @Nonnull StopOrderDirection direction, 129 | @Nonnull String accountId, 130 | @Nonnull StopOrderType type) { 131 | checkReadonly(readonlyMode); 132 | checkSandbox(sandboxMode); 133 | 134 | return Helpers.unaryAsyncCall( 135 | observer -> stopOrdersStub.postStopOrder( 136 | PostStopOrderRequest.newBuilder() 137 | .setInstrumentId(instrumentId) 138 | .setQuantity(quantity) 139 | .setPrice(price) 140 | .setStopPrice(stopPrice) 141 | .setDirection(direction) 142 | .setAccountId(accountId) 143 | .setExpirationType(StopOrderExpirationType.STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL) 144 | .setStopOrderType(type) 145 | .build(), 146 | observer)) 147 | .thenApply(PostStopOrderResponse::getStopOrderId); 148 | } 149 | 150 | @Nonnull 151 | public CompletableFuture postStopOrderGoodTillDate(@Nonnull String instrumentId, 152 | long quantity, 153 | @Nonnull Quotation price, 154 | @Nonnull Quotation stopPrice, 155 | @Nonnull StopOrderDirection direction, 156 | @Nonnull String accountId, 157 | @Nonnull StopOrderType type, 158 | @Nonnull Instant expireDate) { 159 | checkReadonly(readonlyMode); 160 | checkSandbox(sandboxMode); 161 | 162 | return Helpers.unaryAsyncCall( 163 | observer -> stopOrdersStub.postStopOrder( 164 | PostStopOrderRequest.newBuilder() 165 | .setInstrumentId(instrumentId) 166 | .setQuantity(quantity) 167 | .setPrice(price) 168 | .setStopPrice(stopPrice) 169 | .setDirection(direction) 170 | .setAccountId(accountId) 171 | .setExpirationType(StopOrderExpirationType.STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE) 172 | .setStopOrderType(type) 173 | .setExpireDate(DateUtils.instantToTimestamp(expireDate)) 174 | .build(), 175 | observer)) 176 | .thenApply(PostStopOrderResponse::getStopOrderId); 177 | } 178 | 179 | @Nonnull 180 | public CompletableFuture> getStopOrders(@Nonnull String accountId) { 181 | checkSandbox(sandboxMode); 182 | 183 | return Helpers.unaryAsyncCall( 184 | observer -> stopOrdersStub.getStopOrders( 185 | GetStopOrdersRequest.newBuilder() 186 | .setAccountId(accountId) 187 | .build(), 188 | observer)) 189 | .thenApply(GetStopOrdersResponse::getStopOrdersList); 190 | } 191 | 192 | @Nonnull 193 | public CompletableFuture cancelStopOrder(@Nonnull String accountId, 194 | @Nonnull String stopOrderId) { 195 | checkReadonly(readonlyMode); 196 | checkSandbox(sandboxMode); 197 | 198 | return Helpers.unaryAsyncCall( 199 | observer -> stopOrdersStub.cancelStopOrder( 200 | CancelStopOrderRequest.newBuilder() 201 | .setAccountId(accountId) 202 | .setStopOrderId(stopOrderId) 203 | .build(), 204 | observer)) 205 | .thenApply(response -> DateUtils.timestampToInstant(response.getTime())); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /contract/src/main/proto/orders.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tinkoff.public.invest.api.contract.v1; 4 | 5 | option go_package = "./;investapi"; 6 | option java_package = "ru.tinkoff.piapi.contract.v1"; 7 | option java_multiple_files = true; 8 | option csharp_namespace = "Tinkoff.InvestApi.V1"; 9 | option objc_class_prefix = "TIAPI"; 10 | option php_namespace = "Tinkoff\\Invest\\V1"; 11 | 12 | import "common.proto"; 13 | import "google/protobuf/timestamp.proto"; 14 | 15 | service OrdersStreamService { 16 | //Stream сделок пользователя 17 | rpc TradesStream(TradesStreamRequest) returns (stream TradesStreamResponse); 18 | } 19 | 20 | service OrdersService {/* Сервис предназначен для работы с торговыми поручениями:
**1**. 21 | выставление;
**2**. отмена;
**3**. получение статуса;
**4**. 22 | расчёт полной стоимости;
**5**. получение списка заявок.*/ 23 | //Метод выставления заявки. 24 | rpc PostOrder(PostOrderRequest) returns (PostOrderResponse); 25 | 26 | //Метод отмены биржевой заявки. 27 | rpc CancelOrder(CancelOrderRequest) returns (CancelOrderResponse); 28 | 29 | //Метод получения статуса торгового поручения. 30 | rpc GetOrderState(GetOrderStateRequest) returns (OrderState); 31 | 32 | //Метод получения списка активных заявок по счёту. 33 | rpc GetOrders(GetOrdersRequest) returns (GetOrdersResponse); 34 | 35 | //Метод изменения выставленной заявки. 36 | rpc ReplaceOrder(ReplaceOrderRequest) returns (PostOrderResponse); 37 | } 38 | 39 | //Запрос установки соединения. 40 | message TradesStreamRequest { 41 | repeated string accounts = 1; //Идентификаторы счетов. 42 | } 43 | 44 | //Информация о торговых поручениях. 45 | message TradesStreamResponse { 46 | oneof payload { 47 | OrderTrades order_trades = 1; //Информация об исполнении торгового поручения. 48 | Ping ping = 2; //Проверка активности стрима. 49 | } 50 | } 51 | 52 | //Информация об исполнении торгового поручения. 53 | message OrderTrades { 54 | string order_id = 1; //Идентификатор торгового поручения. 55 | google.protobuf.Timestamp created_at = 2; //Дата и время создания сообщения в часовом поясе UTC. 56 | OrderDirection direction = 3; //Направление сделки. 57 | string figi = 4; //Figi-идентификатор инструмента. 58 | repeated OrderTrade trades = 5; //Массив сделок. 59 | string account_id = 6; //Идентификатор счёта. 60 | string instrument_uid = 7; //UID идентификатор инструмента. 61 | } 62 | 63 | //Информация о сделке. 64 | message OrderTrade { 65 | google.protobuf.Timestamp date_time = 1; //Дата и время совершения сделки в часовом поясе UTC. 66 | Quotation price = 2; //Цена за 1 инструмент, по которой совершена сделка. 67 | int64 quantity = 3; //Количество штук в сделке. 68 | string trade_id = 4; //Идентификатор сделки. 69 | } 70 | 71 | //Запрос выставления торгового поручения. 72 | message PostOrderRequest { 73 | string figi = 1 [ deprecated = true ]; //Deprecated Figi-идентификатор инструмента. Необходимо использовать instrument_id. 74 | int64 quantity = 2; //Количество лотов. 75 | Quotation price = 3; //Цена за 1 инструмент. Для получения стоимости лота требуется умножить на лотность инструмента. Игнорируется для рыночных поручений. 76 | OrderDirection direction = 4; //Направление операции. 77 | string account_id = 5; //Номер счёта. 78 | OrderType order_type = 6; //Тип заявки. 79 | string order_id = 7; //Идентификатор запроса выставления поручения для целей идемпотентности в формате UID. Максимальная длина 36 символов. 80 | string instrument_id = 8; //Идентификатор инструмента, принимает значения Figi или Instrument_uid. 81 | } 82 | 83 | //Прочитайте про ключ идемпотентности [здесь](https://tinkoff.github.io/investAPI/head-orders/) 84 | 85 | //Информация о выставлении поручения. 86 | message PostOrderResponse { 87 | string order_id = 1; //Биржевой идентификатор заявки. 88 | OrderExecutionReportStatus execution_report_status = 2; //Текущий статус заявки. 89 | int64 lots_requested = 3; //Запрошено лотов. 90 | int64 lots_executed = 4; //Исполнено лотов. 91 | 92 | MoneyValue initial_order_price = 5; //Начальная цена заявки. Произведение количества запрошенных лотов на цену. 93 | MoneyValue executed_order_price = 6; //Исполненная средняя цена 1 одного инструмента в заявки. 94 | MoneyValue total_order_amount = 7; //Итоговая стоимость заявки, включающая все комиссии. 95 | MoneyValue initial_commission = 8; //Начальная комиссия. Комиссия рассчитанная при выставлении заявки. 96 | MoneyValue executed_commission = 9; //Фактическая комиссия по итогам исполнения заявки. 97 | MoneyValue aci_value = 10; //Значение НКД (накопленного купонного дохода) на дату. Подробнее: [НКД при выставлении торговых поручений](https://tinkoff.github.io/investAPI/head-orders#coupon) 98 | 99 | string figi = 11; // Figi-идентификатор инструмента. 100 | OrderDirection direction = 12; //Направление сделки. 101 | MoneyValue initial_security_price = 13; //Начальная цена за 1 инструмент. Для получения стоимости лота требуется умножить на лотность инструмента. 102 | OrderType order_type = 14; //Тип заявки. 103 | string message = 15; //Дополнительные данные об исполнении заявки. 104 | Quotation initial_order_price_pt = 16; //Начальная цена заявки в пунктах (для фьючерсов). 105 | string instrument_uid = 17; //UID идентификатор инструмента. 106 | } 107 | 108 | //Запрос отмены торгового поручения. 109 | message CancelOrderRequest { 110 | string account_id = 1; //Номер счёта. 111 | string order_id = 2; //Идентификатор заявки. 112 | } 113 | 114 | //Результат отмены торгового поручения. 115 | message CancelOrderResponse { 116 | google.protobuf.Timestamp time = 1; //Дата и время отмены заявки в часовом поясе UTC. 117 | } 118 | 119 | //Запрос получения статуса торгового поручения. 120 | message GetOrderStateRequest { 121 | string account_id = 1; //Номер счёта. 122 | string order_id = 2; //Идентификатор заявки. 123 | } 124 | 125 | //Запрос получения списка активных торговых поручений. 126 | message GetOrdersRequest { 127 | string account_id = 1; //Номер счёта. 128 | } 129 | 130 | //Список активных торговых поручений. 131 | message GetOrdersResponse { 132 | repeated OrderState orders = 1; //Массив активных заявок. 133 | } 134 | 135 | //Информация о торговом поручении. 136 | message OrderState { 137 | string order_id = 1; //Биржевой идентификатор заявки. 138 | OrderExecutionReportStatus execution_report_status = 2; //Текущий статус заявки. 139 | int64 lots_requested = 3; //Запрошено лотов. 140 | int64 lots_executed = 4; //Исполнено лотов. 141 | MoneyValue initial_order_price = 5; //Начальная цена заявки. Произведение количества запрошенных лотов на цену. 142 | MoneyValue executed_order_price = 6; //Исполненная цена заявки. Произведение средней цены покупки на количество лотов. 143 | MoneyValue total_order_amount = 7; //Итоговая стоимость заявки, включающая все комиссии. 144 | MoneyValue average_position_price = 8; //Средняя цена позиции по сделке. 145 | MoneyValue initial_commission = 9; //Начальная комиссия. Комиссия, рассчитанная на момент подачи заявки. 146 | MoneyValue executed_commission = 10; //Фактическая комиссия по итогам исполнения заявки. 147 | string figi = 11; //Figi-идентификатор инструмента. 148 | OrderDirection direction = 12; //Направление заявки. 149 | MoneyValue initial_security_price = 13; //Начальная цена за 1 инструмент. Для получения стоимости лота требуется умножить на лотность инструмента. 150 | repeated OrderStage stages = 14; //Стадии выполнения заявки. 151 | MoneyValue service_commission = 15; //Сервисная комиссия. 152 | string currency = 16; //Валюта заявки. 153 | OrderType order_type = 17; //Тип заявки. 154 | google.protobuf.Timestamp order_date = 18; //Дата и время выставления заявки в часовом поясе UTC. 155 | string instrument_uid = 19; //UID идентификатор инструмента. 156 | string order_request_id = 20; //Идентификатор ключа идемпотентности, переданный клиентом, в формате UID. Максимальная длина 36 символов. 157 | } 158 | 159 | //Сделки в рамках торгового поручения. 160 | message OrderStage { 161 | MoneyValue price = 1; //Цена за 1 инструмент. Для получения стоимости лота требуется умножить на лотность инструмента. 162 | int64 quantity = 2; //Количество лотов. 163 | string trade_id = 3; //Идентификатор сделки. 164 | } 165 | 166 | //Запрос изменения выставленной заявки. 167 | message ReplaceOrderRequest { 168 | string account_id = 1; //Номер счета. 169 | string order_id = 6; //Идентификатор заявки на бирже. 170 | string idempotency_key = 7; //Новый идентификатор запроса выставления поручения для целей идемпотентности. Максимальная длина 36 символов. Перезатирает старый ключ. 171 | int64 quantity = 11; //Количество лотов. 172 | Quotation price = 12; //Цена за 1 инструмент. 173 | PriceType price_type = 13; //Тип цены. 174 | } 175 | 176 | //Направление операции. 177 | enum OrderDirection { 178 | ORDER_DIRECTION_UNSPECIFIED = 0; //Значение не указано 179 | ORDER_DIRECTION_BUY = 1; //Покупка 180 | ORDER_DIRECTION_SELL = 2; //Продажа 181 | } 182 | 183 | //Тип заявки. 184 | enum OrderType { 185 | ORDER_TYPE_UNSPECIFIED = 0; //Значение не указано 186 | ORDER_TYPE_LIMIT = 1; //Лимитная 187 | ORDER_TYPE_MARKET = 2; //Рыночная 188 | ORDER_TYPE_BESTPRICE = 3; //Лучшая цена 189 | } 190 | 191 | //Текущий статус заявки (поручения) 192 | enum OrderExecutionReportStatus { 193 | EXECUTION_REPORT_STATUS_UNSPECIFIED = 0; 194 | EXECUTION_REPORT_STATUS_FILL = 1; //Исполнена 195 | EXECUTION_REPORT_STATUS_REJECTED = 2; //Отклонена 196 | EXECUTION_REPORT_STATUS_CANCELLED = 3; //Отменена пользователем 197 | EXECUTION_REPORT_STATUS_NEW = 4; //Новая 198 | EXECUTION_REPORT_STATUS_PARTIALLYFILL = 5; //Частично исполнена 199 | } 200 | 201 | //Тип цены. 202 | enum PriceType { 203 | PRICE_TYPE_UNSPECIFIED = 0; //Значение не определено. 204 | PRICE_TYPE_POINT = 1; //Цена в пунктах (только для фьючерсов и облигаций). 205 | PRICE_TYPE_CURRENCY = 2; //Цена в валюте расчётов по инструменту. 206 | } -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/OrdersService.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core; 2 | 3 | import ru.tinkoff.piapi.contract.v1.*; 4 | import ru.tinkoff.piapi.contract.v1.OrdersServiceGrpc.OrdersServiceBlockingStub; 5 | import ru.tinkoff.piapi.contract.v1.OrdersServiceGrpc.OrdersServiceStub; 6 | import ru.tinkoff.piapi.core.utils.DateUtils; 7 | import ru.tinkoff.piapi.core.utils.Helpers; 8 | 9 | import javax.annotation.Nonnull; 10 | import javax.annotation.Nullable; 11 | import java.time.Instant; 12 | import java.util.List; 13 | import java.util.UUID; 14 | import java.util.concurrent.CompletableFuture; 15 | 16 | import static ru.tinkoff.piapi.core.utils.Helpers.unaryCall; 17 | import static ru.tinkoff.piapi.core.utils.ValidationUtils.checkReadonly; 18 | 19 | public class OrdersService { 20 | private final OrdersServiceBlockingStub ordersBlockingStub; 21 | private final OrdersServiceStub ordersStub; 22 | private final boolean readonlyMode; 23 | 24 | OrdersService(@Nonnull OrdersServiceBlockingStub ordersBlockingStub, 25 | @Nonnull OrdersServiceStub ordersStub, 26 | boolean readonlyMode) { 27 | this.ordersBlockingStub = ordersBlockingStub; 28 | this.ordersStub = ordersStub; 29 | this.readonlyMode = readonlyMode; 30 | } 31 | 32 | 33 | /** 34 | * 35 | * @param instrumentId figi / instrument_uid инструмента 36 | * @param quantity количество лотов 37 | * @param price цена (для лимитной заявки) 38 | * @param direction покупка/продажа 39 | * @param accountId id аккаунта 40 | * @param type рыночная / лимитная заявка 41 | * @param orderId уникальный идентификатор заявки 42 | * @return 43 | */ 44 | @Nonnull 45 | public PostOrderResponse postOrderSync(@Nonnull String instrumentId, 46 | long quantity, 47 | @Nonnull Quotation price, 48 | @Nonnull OrderDirection direction, 49 | @Nonnull String accountId, 50 | @Nonnull OrderType type, 51 | @Nullable String orderId) { 52 | checkReadonly(readonlyMode); 53 | var finalOrderId = orderId == null ? UUID.randomUUID().toString() : orderId; 54 | 55 | return unaryCall(() -> ordersBlockingStub.postOrder( 56 | PostOrderRequest.newBuilder() 57 | .setInstrumentId(instrumentId) 58 | .setQuantity(quantity) 59 | .setPrice(price) 60 | .setDirection(direction) 61 | .setAccountId(accountId) 62 | .setOrderType(type) 63 | .setOrderId(Helpers.preprocessInputOrderId(finalOrderId)) 64 | .build())); 65 | } 66 | 67 | @Nonnull 68 | public Instant cancelOrderSync(@Nonnull String accountId, 69 | @Nonnull String orderId) { 70 | checkReadonly(readonlyMode); 71 | 72 | var responseTime = unaryCall(() -> ordersBlockingStub.cancelOrder( 73 | CancelOrderRequest.newBuilder() 74 | .setAccountId(accountId) 75 | .setOrderId(orderId) 76 | .build()) 77 | .getTime()); 78 | 79 | return DateUtils.timestampToInstant(responseTime); 80 | } 81 | 82 | @Nonnull 83 | public OrderState getOrderStateSync(@Nonnull String accountId, 84 | @Nonnull String orderId) { 85 | return unaryCall(() -> ordersBlockingStub.getOrderState( 86 | GetOrderStateRequest.newBuilder() 87 | .setAccountId(accountId) 88 | .setOrderId(orderId) 89 | .build())); 90 | } 91 | 92 | @Nonnull 93 | public List getOrdersSync(@Nonnull String accountId) { 94 | return unaryCall(() -> ordersBlockingStub.getOrders( 95 | GetOrdersRequest.newBuilder() 96 | .setAccountId(accountId) 97 | .build()) 98 | .getOrdersList()); 99 | } 100 | 101 | /** 102 | * 103 | * @param instrumentId figi / instrument_uid инструмента 104 | * @param quantity количество лотов 105 | * @param price цена (для лимитной заявки) 106 | * @param direction покупка/продажа 107 | * @param accountId id аккаунта 108 | * @param type рыночная / лимитная заявка 109 | * @param orderId уникальный идентификатор заявки 110 | * @return 111 | */ 112 | @Nonnull 113 | public CompletableFuture postOrder(@Nonnull String instrumentId, 114 | long quantity, 115 | @Nonnull Quotation price, 116 | @Nonnull OrderDirection direction, 117 | @Nonnull String accountId, 118 | @Nonnull OrderType type, 119 | @Nullable String orderId) { 120 | checkReadonly(readonlyMode); 121 | var finalOrderId = orderId == null ? UUID.randomUUID().toString() : orderId; 122 | 123 | return Helpers.unaryAsyncCall( 124 | observer -> ordersStub.postOrder( 125 | PostOrderRequest.newBuilder() 126 | .setInstrumentId(instrumentId) 127 | .setQuantity(quantity) 128 | .setPrice(price) 129 | .setDirection(direction) 130 | .setAccountId(accountId) 131 | .setOrderType(type) 132 | .setOrderId(Helpers.preprocessInputOrderId(finalOrderId)) 133 | .build(), 134 | observer)); 135 | } 136 | 137 | @Nonnull 138 | public CompletableFuture cancelOrder(@Nonnull String accountId, 139 | @Nonnull String orderId) { 140 | checkReadonly(readonlyMode); 141 | 142 | return Helpers.unaryAsyncCall( 143 | observer -> ordersStub.cancelOrder( 144 | CancelOrderRequest.newBuilder() 145 | .setAccountId(accountId) 146 | .setOrderId(orderId) 147 | .build(), 148 | observer)) 149 | .thenApply(response -> DateUtils.timestampToInstant(response.getTime())); 150 | } 151 | 152 | @Nonnull 153 | public CompletableFuture getOrderState(@Nonnull String accountId, 154 | @Nonnull String orderId) { 155 | return Helpers.unaryAsyncCall( 156 | observer -> ordersStub.getOrderState( 157 | GetOrderStateRequest.newBuilder() 158 | .setAccountId(accountId) 159 | .setOrderId(orderId) 160 | .build(), 161 | observer)); 162 | } 163 | 164 | @Nonnull 165 | public CompletableFuture> getOrders(@Nonnull String accountId) { 166 | return Helpers.unaryAsyncCall( 167 | observer -> ordersStub.getOrders( 168 | GetOrdersRequest.newBuilder() 169 | .setAccountId(accountId) 170 | .build(), 171 | observer)) 172 | .thenApply(GetOrdersResponse::getOrdersList); 173 | } 174 | 175 | /** Последовательное выполнение 2 операций - отмены и выставления нового ордера 176 | * 177 | * @param accountId Номер счета 178 | * @param quantity Количество лотов 179 | * @param price Цена за 1 инструмент 180 | * @param idempotencyKey Новый идентификатор запроса выставления поручения для целей идемпотентности. Максимальная длина 36 символов. Перезатирает старый ключ 181 | * @param orderId Идентификатор заявки на бирже 182 | * @param priceType Тип цены. Пока не используется (можно передавать null) 183 | * @return Информация о выставлении поручения 184 | */ 185 | @Nonnull 186 | public CompletableFuture replaceOrder(@Nonnull String accountId, 187 | long quantity, 188 | @Nonnull Quotation price, 189 | @Nullable String idempotencyKey, 190 | @Nonnull String orderId, 191 | @Nullable PriceType priceType) { 192 | var request = ReplaceOrderRequest.newBuilder() 193 | .setAccountId(accountId) 194 | .setPrice(price) 195 | .setQuantity(quantity) 196 | .setIdempotencyKey(idempotencyKey == null ? "" : idempotencyKey) 197 | .setOrderId(orderId) 198 | .setPriceType(priceType == null ? PriceType.PRICE_TYPE_UNSPECIFIED : priceType) 199 | .build(); 200 | return Helpers.unaryAsyncCall( 201 | observer -> ordersStub.replaceOrder(request, observer)); 202 | } 203 | 204 | /** Последовательное выполнение 2 операций - отмены и выставления нового ордера 205 | * 206 | * @param accountId Номер счета 207 | * @param quantity Количество лотов 208 | * @param price Цена за 1 инструмент 209 | * @param idempotencyKey Новый идентификатор запроса выставления поручения для целей идемпотентности. Максимальная длина 36 символов. Перезатирает старый ключ 210 | * @param orderId Идентификатор заявки на бирже 211 | * @param priceType Тип цены. Пока не используется (можно передавать null) 212 | * @return Информация о выставлении поручения 213 | */ 214 | @Nonnull 215 | public PostOrderResponse replaceOrderSync(@Nonnull String accountId, 216 | long quantity, 217 | @Nonnull Quotation price, 218 | @Nullable String idempotencyKey, 219 | @Nonnull String orderId, 220 | @Nullable PriceType priceType) { 221 | var request = ReplaceOrderRequest.newBuilder() 222 | .setAccountId(accountId) 223 | .setPrice(price) 224 | .setQuantity(quantity) 225 | .setIdempotencyKey(idempotencyKey == null ? "" : idempotencyKey) 226 | .setOrderId(orderId) 227 | .setPriceType(priceType == null ? PriceType.PRICE_TYPE_UNSPECIFIED : priceType) 228 | .build(); 229 | return unaryCall(() -> ordersBlockingStub.replaceOrder(request)); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2022 Tinkoff Bank 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/SandboxService.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core; 2 | 3 | import ru.tinkoff.piapi.contract.v1.Account; 4 | import ru.tinkoff.piapi.contract.v1.CancelOrderRequest; 5 | import ru.tinkoff.piapi.contract.v1.CancelOrderResponse; 6 | import ru.tinkoff.piapi.contract.v1.CloseSandboxAccountRequest; 7 | import ru.tinkoff.piapi.contract.v1.CloseSandboxAccountResponse; 8 | import ru.tinkoff.piapi.contract.v1.GetAccountsRequest; 9 | import ru.tinkoff.piapi.contract.v1.GetAccountsResponse; 10 | import ru.tinkoff.piapi.contract.v1.GetOrderStateRequest; 11 | import ru.tinkoff.piapi.contract.v1.GetOrdersRequest; 12 | import ru.tinkoff.piapi.contract.v1.GetOrdersResponse; 13 | import ru.tinkoff.piapi.contract.v1.MoneyValue; 14 | import ru.tinkoff.piapi.contract.v1.OpenSandboxAccountRequest; 15 | import ru.tinkoff.piapi.contract.v1.OpenSandboxAccountResponse; 16 | import ru.tinkoff.piapi.contract.v1.Operation; 17 | import ru.tinkoff.piapi.contract.v1.OperationState; 18 | import ru.tinkoff.piapi.contract.v1.OperationsRequest; 19 | import ru.tinkoff.piapi.contract.v1.OperationsResponse; 20 | import ru.tinkoff.piapi.contract.v1.OrderDirection; 21 | import ru.tinkoff.piapi.contract.v1.OrderState; 22 | import ru.tinkoff.piapi.contract.v1.OrderType; 23 | import ru.tinkoff.piapi.contract.v1.PortfolioRequest; 24 | import ru.tinkoff.piapi.contract.v1.PortfolioResponse; 25 | import ru.tinkoff.piapi.contract.v1.PositionsRequest; 26 | import ru.tinkoff.piapi.contract.v1.PositionsResponse; 27 | import ru.tinkoff.piapi.contract.v1.PostOrderRequest; 28 | import ru.tinkoff.piapi.contract.v1.PostOrderResponse; 29 | import ru.tinkoff.piapi.contract.v1.Quotation; 30 | import ru.tinkoff.piapi.contract.v1.SandboxPayInRequest; 31 | import ru.tinkoff.piapi.contract.v1.SandboxPayInResponse; 32 | import ru.tinkoff.piapi.contract.v1.SandboxServiceGrpc.SandboxServiceBlockingStub; 33 | import ru.tinkoff.piapi.contract.v1.SandboxServiceGrpc.SandboxServiceStub; 34 | import ru.tinkoff.piapi.core.utils.DateUtils; 35 | import ru.tinkoff.piapi.core.utils.Helpers; 36 | 37 | import javax.annotation.Nonnull; 38 | import javax.annotation.Nullable; 39 | import java.time.Instant; 40 | import java.util.List; 41 | import java.util.concurrent.CompletableFuture; 42 | 43 | import static ru.tinkoff.piapi.core.utils.Helpers.unaryCall; 44 | 45 | public class SandboxService { 46 | private final SandboxServiceBlockingStub sandboxBlockingStub; 47 | private final SandboxServiceStub sandboxStub; 48 | 49 | SandboxService(@Nonnull SandboxServiceBlockingStub sandboxBlockingStub, 50 | @Nonnull SandboxServiceStub sandboxStub) { 51 | this.sandboxBlockingStub = sandboxBlockingStub; 52 | this.sandboxStub = sandboxStub; 53 | } 54 | 55 | @Nonnull 56 | public String openAccountSync() { 57 | return unaryCall(() -> sandboxBlockingStub.openSandboxAccount( 58 | OpenSandboxAccountRequest.newBuilder() 59 | .build()) 60 | .getAccountId()); 61 | } 62 | 63 | @Nonnull 64 | public List getAccountsSync() { 65 | return unaryCall(() -> sandboxBlockingStub.getSandboxAccounts( 66 | GetAccountsRequest.newBuilder() 67 | .build()) 68 | .getAccountsList()); 69 | } 70 | 71 | public void closeAccountSync(@Nonnull String accountId) { 72 | unaryCall(() -> sandboxBlockingStub.closeSandboxAccount( 73 | CloseSandboxAccountRequest.newBuilder() 74 | .setAccountId(accountId) 75 | .build())); 76 | } 77 | 78 | @Nonnull 79 | public PostOrderResponse postOrderSync(@Nonnull String figi, 80 | long quantity, 81 | @Nonnull Quotation price, 82 | @Nonnull OrderDirection direction, 83 | @Nonnull String accountId, 84 | @Nonnull OrderType type, 85 | @Nonnull String orderId) { 86 | return unaryCall(() -> sandboxBlockingStub.postSandboxOrder( 87 | PostOrderRequest.newBuilder() 88 | .setFigi(figi) 89 | .setQuantity(quantity) 90 | .setPrice(price) 91 | .setDirection(direction) 92 | .setAccountId(accountId) 93 | .setOrderType(type) 94 | .setOrderId(Helpers.preprocessInputOrderId(orderId)) 95 | .build())); 96 | } 97 | 98 | @Nonnull 99 | public List getOrdersSync(@Nonnull String accountId) { 100 | return unaryCall(() -> sandboxBlockingStub.getSandboxOrders( 101 | GetOrdersRequest.newBuilder() 102 | .setAccountId(accountId) 103 | .build()) 104 | .getOrdersList()); 105 | } 106 | 107 | @Nonnull 108 | public Instant cancelOrderSync(@Nonnull String accountId, 109 | @Nonnull String orderId) { 110 | var responseTime = unaryCall(() -> sandboxBlockingStub.cancelSandboxOrder( 111 | CancelOrderRequest.newBuilder() 112 | .setAccountId(accountId) 113 | .setOrderId(orderId) 114 | .build()) 115 | .getTime()); 116 | 117 | return DateUtils.timestampToInstant(responseTime); 118 | } 119 | 120 | @Nonnull 121 | public OrderState getOrderStateSync(@Nonnull String accountId, 122 | @Nonnull String orderId) { 123 | return unaryCall(() -> sandboxBlockingStub.getSandboxOrderState( 124 | GetOrderStateRequest.newBuilder() 125 | .setAccountId(accountId) 126 | .setOrderId(orderId) 127 | .build())); 128 | } 129 | 130 | @Nonnull 131 | public PositionsResponse getPositionsSync(@Nonnull String accountId) { 132 | return unaryCall(() -> sandboxBlockingStub.getSandboxPositions( 133 | PositionsRequest.newBuilder().setAccountId(accountId).build())); 134 | } 135 | 136 | @Nonnull 137 | public List getOperationsSync(@Nonnull String accountId, 138 | @Nonnull Instant from, 139 | @Nonnull Instant to, 140 | @Nonnull OperationState operationState, 141 | @Nullable String figi) { 142 | return unaryCall(() -> sandboxBlockingStub.getSandboxOperations( 143 | OperationsRequest.newBuilder() 144 | .setAccountId(accountId) 145 | .setFrom(DateUtils.instantToTimestamp(from)) 146 | .setTo(DateUtils.instantToTimestamp(to)) 147 | .setState(operationState) 148 | .setFigi(figi == null ? "" : figi) 149 | .build()) 150 | .getOperationsList()); 151 | } 152 | 153 | @Nonnull 154 | public PortfolioResponse getPortfolioSync(@Nonnull String accountId) { 155 | return unaryCall(() -> sandboxBlockingStub.getSandboxPortfolio( 156 | PortfolioRequest.newBuilder().setAccountId(accountId).build())); 157 | } 158 | 159 | @Nonnull 160 | public MoneyValue payInSync(@Nonnull String accountId, @Nonnull MoneyValue moneyValue) { 161 | return unaryCall(() -> sandboxBlockingStub.sandboxPayIn( 162 | SandboxPayInRequest.newBuilder() 163 | .setAccountId(accountId) 164 | .setAmount(moneyValue) 165 | .build()) 166 | .getBalance()); 167 | } 168 | 169 | @Nonnull 170 | public CompletableFuture openAccount() { 171 | return Helpers.unaryAsyncCall( 172 | observer -> sandboxStub.openSandboxAccount( 173 | OpenSandboxAccountRequest.newBuilder() 174 | .build(), 175 | observer)) 176 | .thenApply(OpenSandboxAccountResponse::getAccountId); 177 | } 178 | 179 | @Nonnull 180 | public CompletableFuture> getAccounts() { 181 | return Helpers.unaryAsyncCall( 182 | observer -> sandboxStub.getSandboxAccounts( 183 | GetAccountsRequest.newBuilder().build(), 184 | observer)) 185 | .thenApply(GetAccountsResponse::getAccountsList); 186 | } 187 | 188 | @Nonnull 189 | public CompletableFuture closeAccount(@Nonnull String accountId) { 190 | return Helpers.unaryAsyncCall( 191 | observer -> sandboxStub.closeSandboxAccount( 192 | CloseSandboxAccountRequest.newBuilder() 193 | .setAccountId(accountId) 194 | .build(), 195 | observer)) 196 | .thenApply(r -> null); 197 | } 198 | 199 | @Nonnull 200 | public CompletableFuture postOrder(@Nonnull String figi, 201 | long quantity, 202 | @Nonnull Quotation price, 203 | @Nonnull OrderDirection direction, 204 | @Nonnull String accountId, 205 | @Nonnull OrderType type, 206 | @Nonnull String orderId) { 207 | return Helpers.unaryAsyncCall( 208 | observer -> sandboxStub.postSandboxOrder( 209 | PostOrderRequest.newBuilder() 210 | .setFigi(figi) 211 | .setQuantity(quantity) 212 | .setPrice(price) 213 | .setDirection(direction) 214 | .setAccountId(accountId) 215 | .setOrderType(type) 216 | .setOrderId(Helpers.preprocessInputOrderId(orderId)) 217 | .build(), 218 | observer)); 219 | } 220 | 221 | @Nonnull 222 | public CompletableFuture> getOrders(@Nonnull String accountId) { 223 | return Helpers.unaryAsyncCall( 224 | observer -> sandboxStub.getSandboxOrders( 225 | GetOrdersRequest.newBuilder() 226 | .setAccountId(accountId) 227 | .build(), 228 | observer)) 229 | .thenApply(GetOrdersResponse::getOrdersList); 230 | } 231 | 232 | @Nonnull 233 | public CompletableFuture cancelOrder(@Nonnull String accountId, 234 | @Nonnull String orderId) { 235 | return Helpers.unaryAsyncCall( 236 | observer -> sandboxStub.cancelSandboxOrder( 237 | CancelOrderRequest.newBuilder() 238 | .setAccountId(accountId) 239 | .setOrderId(orderId) 240 | .build(), 241 | observer)) 242 | .thenApply(response -> DateUtils.timestampToInstant(response.getTime())); 243 | } 244 | 245 | @Nonnull 246 | public CompletableFuture getOrderState(@Nonnull String accountId, 247 | @Nonnull String orderId) { 248 | return Helpers.unaryAsyncCall( 249 | observer -> sandboxStub.getSandboxOrderState( 250 | GetOrderStateRequest.newBuilder() 251 | .setAccountId(accountId) 252 | .setOrderId(orderId) 253 | .build(), 254 | observer)); 255 | } 256 | 257 | @Nonnull 258 | public CompletableFuture getPositions(@Nonnull String accountId) { 259 | return Helpers.unaryAsyncCall( 260 | observer -> sandboxStub.getSandboxPositions( 261 | PositionsRequest.newBuilder().setAccountId(accountId).build(), 262 | observer)); 263 | } 264 | 265 | @Nonnull 266 | public CompletableFuture> getOperations(@Nonnull String accountId, 267 | @Nonnull Instant from, 268 | @Nonnull Instant to, 269 | @Nonnull OperationState operationState, 270 | @Nullable String figi) { 271 | return Helpers.unaryAsyncCall( 272 | observer -> sandboxStub.getSandboxOperations( 273 | OperationsRequest.newBuilder() 274 | .setAccountId(accountId) 275 | .setFrom(DateUtils.instantToTimestamp(from)) 276 | .setTo(DateUtils.instantToTimestamp(to)) 277 | .setState(operationState) 278 | .setFigi(figi == null ? "" : figi) 279 | .build(), 280 | observer)) 281 | .thenApply(OperationsResponse::getOperationsList); 282 | } 283 | 284 | @Nonnull 285 | public CompletableFuture getPortfolio(@Nonnull String accountId) { 286 | return Helpers.unaryAsyncCall( 287 | observer -> sandboxStub.getSandboxPortfolio( 288 | PortfolioRequest.newBuilder().setAccountId(accountId).build(), 289 | observer)); 290 | } 291 | 292 | @Nonnull 293 | public CompletableFuture payIn(@Nonnull String accountId, 294 | @Nonnull MoneyValue moneyValue) { 295 | return Helpers.unaryAsyncCall( 296 | observer -> sandboxStub.sandboxPayIn( 297 | SandboxPayInRequest.newBuilder() 298 | .setAccountId(accountId) 299 | .setAmount(moneyValue) 300 | .build(), 301 | observer)) 302 | .thenApply(SandboxPayInResponse::getBalance); 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /core/src/main/java/ru/tinkoff/piapi/core/MarketDataService.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core; 2 | 3 | import ru.tinkoff.piapi.contract.v1.*; 4 | import ru.tinkoff.piapi.core.utils.DateUtils; 5 | import ru.tinkoff.piapi.core.utils.Helpers; 6 | 7 | import javax.annotation.Nonnull; 8 | import java.time.Instant; 9 | import java.time.temporal.ChronoUnit; 10 | import java.util.ArrayList; 11 | import java.util.Collection; 12 | import java.util.List; 13 | import java.util.concurrent.CompletableFuture; 14 | 15 | import static ru.tinkoff.piapi.contract.v1.MarketDataServiceGrpc.MarketDataServiceBlockingStub; 16 | import static ru.tinkoff.piapi.contract.v1.MarketDataServiceGrpc.MarketDataServiceStub; 17 | import static ru.tinkoff.piapi.core.utils.Helpers.unaryCall; 18 | import static ru.tinkoff.piapi.core.utils.ValidationUtils.checkFromTo; 19 | 20 | public class MarketDataService { 21 | private final MarketDataServiceBlockingStub marketDataBlockingStub; 22 | private final MarketDataServiceStub marketDataStub; 23 | 24 | MarketDataService(@Nonnull MarketDataServiceBlockingStub marketDataBlockingStub, 25 | @Nonnull MarketDataServiceStub marketDataStub) { 26 | this.marketDataBlockingStub = marketDataBlockingStub; 27 | this.marketDataStub = marketDataStub; 28 | } 29 | 30 | /** 31 | * Получение (синхронное) списка обезличенных сделок по инструменту. 32 | * 33 | * @param instrumentId FIGI-идентификатор / uid инструмента. 34 | * @param from Начало периода (по UTC). 35 | * @param to Окончание периода (по UTC). 36 | * @return Список обезличенных сделок по инструменту. 37 | */ 38 | @Nonnull 39 | public List getLastTradesSync(@Nonnull String instrumentId, 40 | @Nonnull Instant from, 41 | @Nonnull Instant to) { 42 | checkFromTo(from, to); 43 | 44 | return unaryCall(() -> marketDataBlockingStub.getLastTrades( 45 | GetLastTradesRequest.newBuilder() 46 | .setInstrumentId(instrumentId) 47 | .setFrom(DateUtils.instantToTimestamp(from)) 48 | .setTo(DateUtils.instantToTimestamp(to)) 49 | .build()) 50 | .getTradesList()); 51 | } 52 | 53 | /** 54 | * Получение (синхронное) списка обезличенных сделок по инструменту за последний час. 55 | * 56 | * @param instrumentId FIGI-идентификатор / uid инструмента. 57 | * @return Список обезличенных сделок по инструменту. 58 | */ 59 | @Nonnull 60 | public List getLastTradesSync(@Nonnull String instrumentId) { 61 | var to = Instant.now(); 62 | var from = to.minus(60, ChronoUnit.MINUTES); 63 | return getLastTradesSync(instrumentId, from, to); 64 | } 65 | 66 | /** 67 | * Получение (синхронное) списка свечей по инструменту. 68 | * 69 | * @param instrumentId идентификатор инструмента. Может принимать значение FIGI или uid 70 | * @param from Начало периода (по UTC). 71 | * @param to Окончание периода (по UTC). 72 | * @param interval Интервал свечей 73 | * @return Список свечей 74 | */ 75 | @Nonnull 76 | public List getCandlesSync(@Nonnull String instrumentId, 77 | @Nonnull Instant from, 78 | @Nonnull Instant to, 79 | @Nonnull CandleInterval interval) { 80 | checkFromTo(from, to); 81 | 82 | return unaryCall(() -> marketDataBlockingStub.getCandles( 83 | GetCandlesRequest.newBuilder() 84 | .setInstrumentId(instrumentId) 85 | .setFrom(DateUtils.instantToTimestamp(from)) 86 | .setTo(DateUtils.instantToTimestamp(to)) 87 | .setInterval(interval) 88 | .build()) 89 | .getCandlesList()); 90 | } 91 | 92 | /** 93 | * Получение (синхронное) списка последних цен по инструментам 94 | * 95 | * @param instrumentIds FIGI-идентификатор / uid инструмента. 96 | * @return Список последний цен 97 | */ 98 | @Nonnull 99 | public List getLastPricesSync(@Nonnull Iterable instrumentIds) { 100 | return unaryCall(() -> marketDataBlockingStub.getLastPrices( 101 | GetLastPricesRequest.newBuilder() 102 | .addAllInstrumentId(instrumentIds) 103 | .build()) 104 | .getLastPricesList()); 105 | } 106 | 107 | /** 108 | * Получение (синхронное) информации о стакане 109 | * 110 | * @param instrumentId FIGI-идентификатор / uid инструмента. 111 | * @param depth глубина стакана. Может принимать значения 1, 10, 20, 30, 40, 50 112 | * @return стакан для инструмента 113 | */ 114 | @Nonnull 115 | public GetOrderBookResponse getOrderBookSync(@Nonnull String instrumentId, int depth) { 116 | return unaryCall(() -> marketDataBlockingStub.getOrderBook( 117 | GetOrderBookRequest.newBuilder() 118 | .setInstrumentId(instrumentId) 119 | .setDepth(depth) 120 | .build())); 121 | } 122 | 123 | /** 124 | * Получение (синхронное) текущего торгового статуса инструмента 125 | * 126 | * @param instrumentId FIGI-идентификатор / uid инструмента. 127 | * @return текущий торговый статус инструмента 128 | */ 129 | @Nonnull 130 | public GetTradingStatusResponse getTradingStatusSync(@Nonnull String instrumentId) { 131 | return unaryCall(() -> marketDataBlockingStub.getTradingStatus( 132 | GetTradingStatusRequest.newBuilder() 133 | .setInstrumentId(instrumentId) 134 | .build())); 135 | } 136 | 137 | /** 138 | * Получение (синхронное) текущего торгового статуса инструментов 139 | * 140 | * @param instrumentIds FIGI-идентификатор / uid инструментов. 141 | * @return текущий торговый статус инструмента 142 | */ 143 | @Nonnull 144 | public GetTradingStatusesResponse getTradingStatusesSync(@Nonnull Iterable instrumentIds) { 145 | return unaryCall(() -> marketDataBlockingStub.getTradingStatuses( 146 | GetTradingStatusesRequest.newBuilder() 147 | .addAllInstrumentId(instrumentIds) 148 | .build())); 149 | } 150 | 151 | /** 152 | * Получение (асинхронное) списка свечей по инструменту. 153 | * 154 | * @param instrumentId идентификатор инструмента. Может принимать значение FIGI или uid 155 | * @param from Начало периода (по UTC). 156 | * @param to Окончание периода (по UTC). 157 | * @param interval Интервал свечей 158 | * @return Список свечей 159 | */ 160 | @Nonnull 161 | public CompletableFuture> getCandles(@Nonnull String instrumentId, 162 | @Nonnull Instant from, 163 | @Nonnull Instant to, 164 | @Nonnull CandleInterval interval) { 165 | checkFromTo(from, to); 166 | 167 | return Helpers.unaryAsyncCall( 168 | observer -> marketDataStub.getCandles( 169 | GetCandlesRequest.newBuilder() 170 | .setInstrumentId(instrumentId) 171 | .setFrom(DateUtils.instantToTimestamp(from)) 172 | .setTo(DateUtils.instantToTimestamp(to)) 173 | .setInterval(interval) 174 | .build(), 175 | observer)) 176 | .thenApply(GetCandlesResponse::getCandlesList); 177 | } 178 | 179 | /** 180 | * Получение (асинхронное) списка обезличенных сделок по инструменту. 181 | * 182 | * @param instrumentId FIGI-идентификатор / uid инструмента. 183 | * @param from Начало периода (по UTC). 184 | * @param to Окончание периода (по UTC). 185 | * @return Список обезличенных сделок по инструменту. 186 | */ 187 | @Nonnull 188 | public CompletableFuture> getLastTrades(@Nonnull String instrumentId, 189 | @Nonnull Instant from, 190 | @Nonnull Instant to) { 191 | checkFromTo(from, to); 192 | 193 | return Helpers.unaryAsyncCall( 194 | observer -> marketDataStub.getLastTrades( 195 | GetLastTradesRequest.newBuilder() 196 | .setInstrumentId(instrumentId) 197 | .setFrom(DateUtils.instantToTimestamp(from)) 198 | .setTo(DateUtils.instantToTimestamp(to)) 199 | .build(), 200 | observer)) 201 | .thenApply(GetLastTradesResponse::getTradesList); 202 | } 203 | 204 | /** 205 | * Получение (асинхронное) списка обезличенных сделок по инструменту за последний час. 206 | * 207 | * @param instrumentId FIGI-идентификатор / uid инструмента.. 208 | * @return Список обезличенных сделок по инструменту. 209 | */ 210 | @Nonnull 211 | public CompletableFuture> getLastTrades(@Nonnull String instrumentId) { 212 | var to = Instant.now(); 213 | var from = to.minus(60, ChronoUnit.MINUTES); 214 | return getLastTrades(instrumentId, from, to); 215 | } 216 | 217 | /** 218 | * Получение (асинхронное) списка цен закрытия торговой сессии по инструменту. 219 | * 220 | * @param instrumentId FIGI-идентификатор / uid инструмента. 221 | * @return Цена закрытия торговой сессии по инструменту. 222 | */ 223 | @Nonnull 224 | public CompletableFuture> getClosePrices(@Nonnull String instrumentId) { 225 | var instruments = InstrumentClosePriceRequest.newBuilder().setInstrumentId(instrumentId).build(); 226 | 227 | return Helpers.unaryAsyncCall( 228 | observer -> marketDataStub.getClosePrices( 229 | GetClosePricesRequest.newBuilder() 230 | .addAllInstruments(List.of(instruments)) 231 | .build(), 232 | observer)) 233 | .thenApply(GetClosePricesResponse::getClosePricesList); 234 | } 235 | 236 | /** 237 | * Получение (асинхронное) списка цен закрытия торговой сессии по инструментам. 238 | * 239 | * @param instrumentIds FIGI-идентификатор / uid инструментов. 240 | * @return Цена закрытия торговой сессии по инструментам. 241 | */ 242 | @Nonnull 243 | public CompletableFuture> getClosePrices(@Nonnull Iterable instrumentIds) { 244 | var instruments = new ArrayList(); 245 | for (String instrumentId : instrumentIds) { 246 | instruments.add(InstrumentClosePriceRequest.newBuilder().setInstrumentId(instrumentId).build()); 247 | } 248 | 249 | return Helpers.unaryAsyncCall( 250 | observer -> marketDataStub.getClosePrices( 251 | GetClosePricesRequest.newBuilder() 252 | .addAllInstruments(instruments) 253 | .build(), 254 | observer)) 255 | .thenApply(GetClosePricesResponse::getClosePricesList); 256 | } 257 | 258 | /** 259 | * Получение (асинхронное) списка цен закрытия торговой сессии по инструменту. 260 | * 261 | * @param instrumentId FIGI-идентификатор / uid инструмента. 262 | * @return Цена закрытия торговой сессии по инструменту. 263 | */ 264 | @Nonnull 265 | public List getClosePricesSync(@Nonnull String instrumentId) { 266 | var instruments = InstrumentClosePriceRequest.newBuilder().setInstrumentId(instrumentId).build(); 267 | 268 | return unaryCall(() -> marketDataBlockingStub.getClosePrices( 269 | GetClosePricesRequest.newBuilder() 270 | .addAllInstruments(List.of(instruments)) 271 | .build()).getClosePricesList()); 272 | } 273 | 274 | /** 275 | * Получение (асинхронное) списка цен закрытия торговой сессии по инструментам. 276 | * 277 | * @param instrumentIds FIGI-идентификатор / uid инструментов. 278 | * @return Цена закрытия торговой сессии по инструментам. 279 | */ 280 | @Nonnull 281 | public List getClosePricesSync(@Nonnull Iterable instrumentIds) { 282 | var instruments = new ArrayList(); 283 | for (String instrumentId : instrumentIds) { 284 | instruments.add(InstrumentClosePriceRequest.newBuilder().setInstrumentId(instrumentId).build()); 285 | } 286 | 287 | return unaryCall(() -> marketDataBlockingStub.getClosePrices( 288 | GetClosePricesRequest.newBuilder() 289 | .addAllInstruments(instruments) 290 | .build()).getClosePricesList()); 291 | } 292 | 293 | /** 294 | * Получение (асинхронное) списка последних цен по инструментам 295 | * 296 | * @param instrumentIds FIGI-идентификатор / uid инструмента. 297 | * @return Список последний цен 298 | */ 299 | @Nonnull 300 | public CompletableFuture> getLastPrices(@Nonnull Iterable instrumentIds) { 301 | return Helpers.unaryAsyncCall( 302 | observer -> marketDataStub.getLastPrices( 303 | GetLastPricesRequest.newBuilder() 304 | .addAllInstrumentId(instrumentIds) 305 | .build(), 306 | observer)) 307 | .thenApply(GetLastPricesResponse::getLastPricesList); 308 | } 309 | 310 | /** 311 | * Получение (асинхронное) информации о стакане 312 | * 313 | * @param instrumentId FIGI-идентификатор / uid инструмента. 314 | * @param depth глубина стакана 315 | * @return 316 | */ 317 | @Nonnull 318 | public CompletableFuture getOrderBook(@Nonnull String instrumentId, int depth) { 319 | return Helpers.unaryAsyncCall( 320 | observer -> marketDataStub.getOrderBook( 321 | GetOrderBookRequest.newBuilder() 322 | .setInstrumentId(instrumentId) 323 | .setDepth(depth) 324 | .build(), 325 | observer)); 326 | } 327 | 328 | /** 329 | * Получение (асинхронное) информации о торговом статусе инструмента 330 | * 331 | * @param instrumentId FIGI-идентификатор / uid инструмента. 332 | * @return Информация о торговом статусе 333 | */ 334 | @Nonnull 335 | public CompletableFuture getTradingStatus(@Nonnull String instrumentId) { 336 | return Helpers.unaryAsyncCall( 337 | observer -> marketDataStub.getTradingStatus( 338 | GetTradingStatusRequest.newBuilder() 339 | .setInstrumentId(instrumentId) 340 | .build(), 341 | observer)); 342 | } 343 | 344 | /** 345 | * Получение (асинхронное) информации о торговом статусе инструментов 346 | * 347 | * @param instrumentIds FIGI-идентификатор / uid инструментов. 348 | * @return Информация о торговом статусе 349 | */ 350 | @Nonnull 351 | public CompletableFuture getTradingStatuses(@Nonnull Iterable instrumentIds) { 352 | return Helpers.unaryAsyncCall( 353 | observer -> marketDataStub.getTradingStatuses( 354 | GetTradingStatusesRequest.newBuilder() 355 | .addAllInstrumentId(instrumentIds) 356 | .build(), 357 | observer)); 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /core/src/test/java/ru/tinkoff/piapi/core/StopOrdersServiceTest.java: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.piapi.core; 2 | 3 | import com.google.protobuf.Timestamp; 4 | import io.grpc.Channel; 5 | import io.grpc.stub.StreamObserver; 6 | import org.hamcrest.core.IsInstanceOf; 7 | import org.junit.Rule; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.rules.ExpectedException; 10 | import ru.tinkoff.piapi.contract.v1.*; 11 | import ru.tinkoff.piapi.core.exception.ReadonlyModeViolationException; 12 | import ru.tinkoff.piapi.core.exception.SandboxModeViolationException; 13 | import ru.tinkoff.piapi.core.utils.DateUtils; 14 | 15 | import java.time.Instant; 16 | import java.util.concurrent.CompletionException; 17 | 18 | import static org.junit.jupiter.api.Assertions.*; 19 | import static org.mockito.AdditionalAnswers.delegatesTo; 20 | import static org.mockito.ArgumentMatchers.any; 21 | import static org.mockito.ArgumentMatchers.eq; 22 | import static org.mockito.Mockito.*; 23 | 24 | public class StopOrdersServiceTest extends GrpcClientTester { 25 | 26 | @Rule 27 | public ExpectedException futureThrown = ExpectedException.none(); 28 | 29 | @Override 30 | protected StopOrdersService createClient(Channel channel) { 31 | return new StopOrdersService( 32 | StopOrdersServiceGrpc.newBlockingStub(channel), 33 | StopOrdersServiceGrpc.newStub(channel), 34 | false, 35 | false); 36 | } 37 | 38 | private StopOrdersService createReadonlyClient(Channel channel) { 39 | return new StopOrdersService( 40 | StopOrdersServiceGrpc.newBlockingStub(channel), 41 | StopOrdersServiceGrpc.newStub(channel), 42 | true, 43 | false); 44 | } 45 | 46 | private StopOrdersService createSandboxClient(Channel channel) { 47 | return new StopOrdersService( 48 | StopOrdersServiceGrpc.newBlockingStub(channel), 49 | StopOrdersServiceGrpc.newStub(channel), 50 | false, 51 | true); 52 | } 53 | 54 | @Test 55 | void postStopOrderGoodTillCancel_Test() { 56 | var expected = PostStopOrderResponse.newBuilder() 57 | .setStopOrderId("stopOrderId") 58 | .build(); 59 | var grpcService = mock(StopOrdersServiceGrpc.StopOrdersServiceImplBase.class, delegatesTo( 60 | new StopOrdersServiceGrpc.StopOrdersServiceImplBase() { 61 | @Override 62 | public void postStopOrder(PostStopOrderRequest request, 63 | StreamObserver responseObserver) { 64 | responseObserver.onNext(expected); 65 | responseObserver.onCompleted(); 66 | } 67 | })); 68 | var service = mkClientBasedOnServer(grpcService); 69 | 70 | var inArg = PostStopOrderRequest.newBuilder() 71 | .setInstrumentId("figi") 72 | .setQuantity(1) 73 | .setPrice(Quotation.newBuilder().setUnits(100).setNano(0).build()) 74 | .setStopPrice(Quotation.newBuilder().setUnits(110).setNano(0).build()) 75 | .setDirection(StopOrderDirection.STOP_ORDER_DIRECTION_SELL) 76 | .setAccountId("accountId") 77 | .setExpirationType(StopOrderExpirationType.STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL) 78 | .setStopOrderType(StopOrderType.STOP_ORDER_TYPE_TAKE_PROFIT) 79 | .build(); 80 | var actualSync = service.postStopOrderGoodTillCancelSync( 81 | inArg.getInstrumentId(), 82 | inArg.getQuantity(), 83 | inArg.getPrice(), 84 | inArg.getStopPrice(), 85 | inArg.getDirection(), 86 | inArg.getAccountId(), 87 | inArg.getStopOrderType() 88 | ); 89 | var actualAsync = service.postStopOrderGoodTillCancel( 90 | inArg.getInstrumentId(), 91 | inArg.getQuantity(), 92 | inArg.getPrice(), 93 | inArg.getStopPrice(), 94 | inArg.getDirection(), 95 | inArg.getAccountId(), 96 | inArg.getStopOrderType() 97 | ).join(); 98 | 99 | assertEquals(expected.getStopOrderId(), actualSync); 100 | assertEquals(expected.getStopOrderId(), actualAsync); 101 | 102 | verify(grpcService, times(2)).postStopOrder(eq(inArg), any()); 103 | } 104 | 105 | @Test 106 | void postStopOrderGoodTillDate_Test() { 107 | var expected = PostStopOrderResponse.newBuilder() 108 | .setStopOrderId("stopOrderId") 109 | .build(); 110 | var grpcService = mock(StopOrdersServiceGrpc.StopOrdersServiceImplBase.class, delegatesTo( 111 | new StopOrdersServiceGrpc.StopOrdersServiceImplBase() { 112 | @Override 113 | public void postStopOrder(PostStopOrderRequest request, 114 | StreamObserver responseObserver) { 115 | responseObserver.onNext(expected); 116 | responseObserver.onCompleted(); 117 | } 118 | })); 119 | var service = mkClientBasedOnServer(grpcService); 120 | 121 | var inArg = PostStopOrderRequest.newBuilder() 122 | .setInstrumentId("figi") 123 | .setQuantity(1) 124 | .setPrice(Quotation.newBuilder().setUnits(100).setNano(0).build()) 125 | .setStopPrice(Quotation.newBuilder().setUnits(110).setNano(0).build()) 126 | .setDirection(StopOrderDirection.STOP_ORDER_DIRECTION_SELL) 127 | .setAccountId("accountId") 128 | .setExpirationType(StopOrderExpirationType.STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE) 129 | .setStopOrderType(StopOrderType.STOP_ORDER_TYPE_TAKE_PROFIT) 130 | .setExpireDate(Timestamp.newBuilder().setSeconds(1234567890).setNanos(0).build()) 131 | .build(); 132 | var actualSync = service.postStopOrderGoodTillDateSync( 133 | inArg.getInstrumentId(), 134 | inArg.getQuantity(), 135 | inArg.getPrice(), 136 | inArg.getStopPrice(), 137 | inArg.getDirection(), 138 | inArg.getAccountId(), 139 | inArg.getStopOrderType(), 140 | DateUtils.timestampToInstant(inArg.getExpireDate()) 141 | ); 142 | var actualAsync = service.postStopOrderGoodTillDate( 143 | inArg.getInstrumentId(), 144 | inArg.getQuantity(), 145 | inArg.getPrice(), 146 | inArg.getStopPrice(), 147 | inArg.getDirection(), 148 | inArg.getAccountId(), 149 | inArg.getStopOrderType(), 150 | DateUtils.timestampToInstant(inArg.getExpireDate()) 151 | ).join(); 152 | 153 | assertEquals(expected.getStopOrderId(), actualSync); 154 | assertEquals(expected.getStopOrderId(), actualAsync); 155 | 156 | verify(grpcService, times(2)).postStopOrder(eq(inArg), any()); 157 | } 158 | 159 | @Test 160 | void getStopOrders_Test() { 161 | var accountId = "accountId"; 162 | var expected = GetStopOrdersResponse.newBuilder() 163 | .addStopOrders(StopOrder.newBuilder() 164 | .setStopOrderId("stopOrderId") 165 | .setFigi("figi") 166 | .build()) 167 | .build(); 168 | var grpcService = mock(StopOrdersServiceGrpc.StopOrdersServiceImplBase.class, delegatesTo( 169 | new StopOrdersServiceGrpc.StopOrdersServiceImplBase() { 170 | @Override 171 | public void getStopOrders(GetStopOrdersRequest request, 172 | StreamObserver responseObserver) { 173 | responseObserver.onNext(expected); 174 | responseObserver.onCompleted(); 175 | } 176 | })); 177 | var service = mkClientBasedOnServer(grpcService); 178 | 179 | var actualSync = service.getStopOrdersSync(accountId); 180 | var actualAsync = service.getStopOrders(accountId).join(); 181 | 182 | assertIterableEquals(expected.getStopOrdersList(), actualSync); 183 | assertIterableEquals(expected.getStopOrdersList(), actualAsync); 184 | 185 | var inArg = GetStopOrdersRequest.newBuilder() 186 | .setAccountId(accountId) 187 | .build(); 188 | verify(grpcService, times(2)).getStopOrders(eq(inArg), any()); 189 | } 190 | 191 | @Test 192 | void cancelStopOrder_Test() { 193 | var accountId = "accountId"; 194 | var stopOrderId = "stopOrderId"; 195 | var expected = CancelStopOrderResponse.newBuilder() 196 | .setTime(Timestamp.newBuilder() 197 | .setSeconds(1234567890) 198 | .setNanos(0) 199 | .build()) 200 | .build(); 201 | var grpcService = mock(StopOrdersServiceGrpc.StopOrdersServiceImplBase.class, delegatesTo( 202 | new StopOrdersServiceGrpc.StopOrdersServiceImplBase() { 203 | @Override 204 | public void cancelStopOrder(CancelStopOrderRequest request, 205 | StreamObserver responseObserver) { 206 | responseObserver.onNext(expected); 207 | responseObserver.onCompleted(); 208 | } 209 | })); 210 | var service = mkClientBasedOnServer(grpcService); 211 | 212 | var actualSync = service.cancelStopOrderSync(accountId, stopOrderId); 213 | var actualAsync = service.cancelStopOrder(accountId, stopOrderId).join(); 214 | 215 | assertEquals(DateUtils.timestampToInstant(expected.getTime()), actualSync); 216 | assertEquals(DateUtils.timestampToInstant(expected.getTime()), actualAsync); 217 | 218 | var inArg = CancelStopOrderRequest.newBuilder() 219 | .setAccountId(accountId) 220 | .setStopOrderId(stopOrderId) 221 | .build(); 222 | verify(grpcService, times(2)).cancelStopOrder(eq(inArg), any()); 223 | } 224 | 225 | @Test 226 | void postStopOrderGoodTillCancel_forbiddenInReadonly_Test() { 227 | var grpcService = mock(StopOrdersServiceGrpc.StopOrdersServiceImplBase.class); 228 | var readonlyService = mkClientBasedOnServer(grpcService, this::createReadonlyClient); 229 | 230 | assertThrows( 231 | ReadonlyModeViolationException.class, 232 | () -> readonlyService.postStopOrderGoodTillCancelSync( 233 | "", 0, Quotation.getDefaultInstance(), Quotation.getDefaultInstance(), 234 | StopOrderDirection.STOP_ORDER_DIRECTION_UNSPECIFIED, "", StopOrderType.STOP_ORDER_TYPE_UNSPECIFIED) 235 | ); 236 | futureThrown.expect(CompletionException.class); 237 | futureThrown.expectCause(IsInstanceOf.instanceOf(ReadonlyModeViolationException.class)); 238 | assertThrows(ReadonlyModeViolationException.class, () -> readonlyService.postStopOrderGoodTillCancel( 239 | "", 0, Quotation.getDefaultInstance(), Quotation.getDefaultInstance(), 240 | StopOrderDirection.STOP_ORDER_DIRECTION_UNSPECIFIED, "", StopOrderType.STOP_ORDER_TYPE_UNSPECIFIED)); 241 | } 242 | 243 | @Test 244 | void postStopOrderGoodTillCancel_forbiddenInSandbox_Test() { 245 | var grpcService = mock(StopOrdersServiceGrpc.StopOrdersServiceImplBase.class); 246 | var sandboxService = mkClientBasedOnServer(grpcService, this::createSandboxClient); 247 | 248 | assertThrows( 249 | SandboxModeViolationException.class, 250 | () -> sandboxService.postStopOrderGoodTillCancelSync( 251 | "", 0, Quotation.getDefaultInstance(), Quotation.getDefaultInstance(), 252 | StopOrderDirection.STOP_ORDER_DIRECTION_UNSPECIFIED, "", StopOrderType.STOP_ORDER_TYPE_UNSPECIFIED) 253 | ); 254 | futureThrown.expect(CompletionException.class); 255 | futureThrown.expectCause(IsInstanceOf.instanceOf(SandboxModeViolationException.class)); 256 | assertThrows(SandboxModeViolationException.class, () -> sandboxService.postStopOrderGoodTillCancel( 257 | "", 0, Quotation.getDefaultInstance(), Quotation.getDefaultInstance(), 258 | StopOrderDirection.STOP_ORDER_DIRECTION_UNSPECIFIED, "", StopOrderType.STOP_ORDER_TYPE_UNSPECIFIED)); 259 | } 260 | 261 | @Test 262 | void postStopOrderGoodTillDate_forbiddenInReadonly_Test() { 263 | var grpcService = mock(StopOrdersServiceGrpc.StopOrdersServiceImplBase.class); 264 | var readonlyService = mkClientBasedOnServer(grpcService, this::createReadonlyClient); 265 | 266 | assertThrows( 267 | ReadonlyModeViolationException.class, 268 | () -> readonlyService.postStopOrderGoodTillDateSync( 269 | "", 0, Quotation.getDefaultInstance(), Quotation.getDefaultInstance(), 270 | StopOrderDirection.STOP_ORDER_DIRECTION_UNSPECIFIED, "", StopOrderType.STOP_ORDER_TYPE_UNSPECIFIED, 271 | Instant.EPOCH)); 272 | futureThrown.expect(CompletionException.class); 273 | futureThrown.expectCause(IsInstanceOf.instanceOf(ReadonlyModeViolationException.class)); 274 | assertThrows(ReadonlyModeViolationException.class, () -> readonlyService.postStopOrderGoodTillDate( 275 | "", 0, Quotation.getDefaultInstance(), Quotation.getDefaultInstance(), 276 | StopOrderDirection.STOP_ORDER_DIRECTION_UNSPECIFIED, "", StopOrderType.STOP_ORDER_TYPE_UNSPECIFIED, 277 | Instant.EPOCH)); 278 | } 279 | 280 | @Test 281 | void postStopOrderGoodTillDate_forbiddenInSandbox_Test() { 282 | var grpcService = mock(StopOrdersServiceGrpc.StopOrdersServiceImplBase.class); 283 | var sandboxService = mkClientBasedOnServer(grpcService, this::createSandboxClient); 284 | 285 | assertThrows( 286 | SandboxModeViolationException.class, 287 | () -> sandboxService.postStopOrderGoodTillDateSync( 288 | "", 0, Quotation.getDefaultInstance(), Quotation.getDefaultInstance(), 289 | StopOrderDirection.STOP_ORDER_DIRECTION_UNSPECIFIED, "", StopOrderType.STOP_ORDER_TYPE_UNSPECIFIED, 290 | Instant.EPOCH)); 291 | futureThrown.expect(CompletionException.class); 292 | futureThrown.expectCause(IsInstanceOf.instanceOf(SandboxModeViolationException.class)); 293 | assertThrows(SandboxModeViolationException.class, () -> sandboxService.postStopOrderGoodTillDate( 294 | "", 0, Quotation.getDefaultInstance(), Quotation.getDefaultInstance(), 295 | StopOrderDirection.STOP_ORDER_DIRECTION_UNSPECIFIED, "", StopOrderType.STOP_ORDER_TYPE_UNSPECIFIED, 296 | Instant.EPOCH)); 297 | } 298 | 299 | @Test 300 | void cancelStopOrder_forbiddenInReadonly_Test() { 301 | var grpcService = mock(StopOrdersServiceGrpc.StopOrdersServiceImplBase.class); 302 | var readonlyService = mkClientBasedOnServer(grpcService, this::createReadonlyClient); 303 | 304 | assertThrows( 305 | ReadonlyModeViolationException.class, 306 | () -> readonlyService.cancelStopOrderSync("", "")); 307 | futureThrown.expect(CompletionException.class); 308 | futureThrown.expectCause(IsInstanceOf.instanceOf(ReadonlyModeViolationException.class)); 309 | assertThrows(ReadonlyModeViolationException.class, () -> readonlyService.cancelStopOrder("", "")); 310 | } 311 | 312 | @Test 313 | void cancelStopOrder_forbiddenInSandbox_Test() { 314 | var grpcService = mock(StopOrdersServiceGrpc.StopOrdersServiceImplBase.class); 315 | var sandboxService = mkClientBasedOnServer(grpcService, this::createSandboxClient); 316 | 317 | assertThrows( 318 | SandboxModeViolationException.class, 319 | () -> sandboxService.cancelStopOrderSync("", "")); 320 | futureThrown.expect(CompletionException.class); 321 | futureThrown.expectCause(IsInstanceOf.instanceOf(SandboxModeViolationException.class)); 322 | assertThrows(SandboxModeViolationException.class, () -> sandboxService.cancelStopOrder("", "")); 323 | } 324 | } 325 | --------------------------------------------------------------------------------