t-tech-investments 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.cursor/rules/git-flow.mdc +49 -5
- data/.cursor/rules/sdk-architecture.mdc +30 -0
- data/.cursor/rules/sdk-code-standards.mdc +22 -0
- data/.cursor/rules/sdk-patterns.mdc +22 -0
- data/.cursor/rules/sdk-testing.mdc +19 -0
- data/CHANGELOG.md +4 -0
- data/README.md +195 -4
- data/Rakefile +4 -0
- data/contracts.lock +6 -0
- data/lib/t/tech/investments/client.rb +70 -0
- data/lib/t/tech/investments/coercers.rb +171 -0
- data/lib/t/tech/investments/configuration.rb +74 -0
- data/lib/t/tech/investments/errors.rb +24 -0
- data/lib/t/tech/investments/proto/common_pb.rb +42 -0
- data/lib/t/tech/investments/proto/google/api/field_behavior_pb.rb +19 -0
- data/lib/t/tech/investments/proto/instruments_pb.rb +157 -0
- data/lib/t/tech/investments/proto/instruments_services_pb.rb +115 -0
- data/lib/t/tech/investments/proto/marketdata_pb.rb +99 -0
- data/lib/t/tech/investments/proto/marketdata_services_pb.rb +68 -0
- data/lib/t/tech/investments/proto/operations_pb.rb +78 -0
- data/lib/t/tech/investments/proto/operations_services_pb.rb +67 -0
- data/lib/t/tech/investments/proto/orders_pb.rb +64 -0
- data/lib/t/tech/investments/proto/orders_services_pb.rb +69 -0
- data/lib/t/tech/investments/proto/sandbox_pb.rb +37 -0
- data/lib/t/tech/investments/proto/sandbox_services_pb.rb +75 -0
- data/lib/t/tech/investments/proto/signals_pb.rb +37 -0
- data/lib/t/tech/investments/proto/signals_services_pb.rb +36 -0
- data/lib/t/tech/investments/proto/stoporders_pb.rb +45 -0
- data/lib/t/tech/investments/proto/stoporders_services_pb.rb +38 -0
- data/lib/t/tech/investments/proto/users_pb.rb +47 -0
- data/lib/t/tech/investments/proto/users_services_pb.rb +51 -0
- data/lib/t/tech/investments/proto_loader.rb +49 -0
- data/lib/t/tech/investments/services/market_data_facade.rb +35 -0
- data/lib/t/tech/investments/services/market_data_stream_session.rb +148 -0
- data/lib/t/tech/investments/services/registry.rb +38 -0
- data/lib/t/tech/investments/services/unary_adapter.rb +64 -0
- data/lib/t/tech/investments/services.rb +6 -0
- data/lib/t/tech/investments/transport.rb +137 -0
- data/lib/t/tech/investments/version.rb +1 -1
- data/lib/t/tech/investments.rb +30 -1
- data/script/regenerate_proto +119 -0
- data/sig/t/tech/investments.rbs +114 -1
- metadata +67 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e68074e95141d63fe02be0f024fdba50ec74b935b61f95cdc35754dd368484f
|
|
4
|
+
data.tar.gz: adcdccd09aed1a6bcc83e274f7c02cf954a5f05645740aad949027af7fe8352d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 83bb6a2f390c88557029f0b349670f393faed7da128e368bec3829154490ee9921e0af9c7532278ddd34b72f800444408380ce373bdfb57827f253cb11871fb3
|
|
7
|
+
data.tar.gz: 832db0d5502ac1d24562d9703ada3f224248778857bc551e261a88351620f8977cbb16c6e95d5b1279349209cc827cf5500f5e8edf162a241d1c32ce84aaf89d
|
data/.cursor/rules/git-flow.mdc
CHANGED
|
@@ -1,12 +1,56 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: >
|
|
3
|
+
Правила следования Git Flow для разработки торговой системы.
|
|
4
|
+
Определяет модель веток, правила коммитов и merge-стратегии.
|
|
5
|
+
|
|
6
|
+
globs:
|
|
7
|
+
- "**/*"
|
|
8
|
+
|
|
3
9
|
alwaysApply: true
|
|
4
10
|
---
|
|
11
|
+
В проекте используется Git Flow.
|
|
12
|
+
|
|
13
|
+
Основные ветки:
|
|
14
|
+
- main — стабильная ветка, всегда в рабочем состоянии.
|
|
15
|
+
- develop — основная ветка разработки.
|
|
16
|
+
|
|
17
|
+
Рабочие ветки:
|
|
18
|
+
- feature/* — разработка новых фич и модулей
|
|
19
|
+
- bugfix/* — исправления ошибок
|
|
20
|
+
- hotfix/* — срочные исправления для main
|
|
21
|
+
- release/* — подготовка релиза
|
|
22
|
+
|
|
23
|
+
Правила работы:
|
|
24
|
+
- Прямая разработка в main запрещена.
|
|
25
|
+
- Новые фичи всегда начинаются от develop.
|
|
26
|
+
- Feature-ветки мержатся обратно только в develop.
|
|
27
|
+
- Hotfix-ветки начинаются от main и мержатся в main и develop.
|
|
28
|
+
- Release-ветки используются для стабилизации перед релизом.
|
|
29
|
+
|
|
30
|
+
Коммиты:
|
|
31
|
+
- Коммиты атомарные и логически завершённые.
|
|
32
|
+
- Один коммит — одно логическое изменение.
|
|
33
|
+
- Сообщения коммитов описывают ЧТО и ЗАЧЕМ, а не КАК.
|
|
5
34
|
|
|
6
|
-
|
|
35
|
+
Рекомендованный формат commit message:
|
|
36
|
+
- feat: добавление новой функциональности
|
|
37
|
+
- fix: исправление ошибки
|
|
38
|
+
- refactor: рефакторинг без изменения поведения
|
|
39
|
+
- chore: инфраструктурные изменения
|
|
40
|
+
- test: добавление или изменение тестов
|
|
41
|
+
- docs: документация
|
|
7
42
|
|
|
8
|
-
|
|
43
|
+
Примеры:
|
|
44
|
+
- feat(strategy): add breakout strategy
|
|
45
|
+
- fix(risk): prevent position size overflow
|
|
46
|
+
- refactor(execution): simplify order state machine
|
|
9
47
|
|
|
10
|
-
|
|
48
|
+
Merge:
|
|
49
|
+
- Предпочтителен merge commit или squash (осознанно).
|
|
50
|
+
- Избегать rebase на общих ветках (develop, main).
|
|
51
|
+
- Конфликты решаются до merge, не после.
|
|
11
52
|
|
|
12
|
-
|
|
53
|
+
Cursor должен:
|
|
54
|
+
- учитывать текущую ветку при генерации изменений
|
|
55
|
+
- не предлагать изменения напрямую в main
|
|
56
|
+
- придерживаться Git Flow при создании новых задач и файлов
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Минимальная архитектура SDK: слои, зависимости, контракты
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Архитектура SDK (минимум правил)
|
|
7
|
+
|
|
8
|
+
## Слои
|
|
9
|
+
|
|
10
|
+
- `Client` (facade) → `Services` (adapters) → `Transport` (gRPC)
|
|
11
|
+
- Вспомогательные: `Coercers/Mappers`, `Errors`, `Proto` (сгенерёнка)
|
|
12
|
+
|
|
13
|
+
## Зависимости (жёсткие запреты)
|
|
14
|
+
|
|
15
|
+
- `Services` не вызывают gRPC stub напрямую — только через `Transport`.
|
|
16
|
+
- Публичный API не возвращает protobuf по умолчанию (только `raw:`/debug, если нужен).
|
|
17
|
+
- `Transport` без бизнес-логики; только: metadata, deadlines, retries-policy, error mapping.
|
|
18
|
+
- `Coercers/Mappers` — чистые и детерминированные (без IO/сети).
|
|
19
|
+
|
|
20
|
+
## Контракты (invest-contracts)
|
|
21
|
+
|
|
22
|
+
- Контракты **пинуем по tag/SHA** (не `master`), храним в `contracts.lock`.
|
|
23
|
+
- Сгенерированный Ruby protobuf/gRPC код **коммитим** в `lib/**/proto/**` (установка гема без `protoc`).
|
|
24
|
+
- Обновление контрактов обязано включать: pin → реген → тест “все RPC экспонированы”.
|
|
25
|
+
|
|
26
|
+
## T‑Invest инварианты (transport обязан)
|
|
27
|
+
|
|
28
|
+
- metadata каждого запроса: `Authorization: Bearer <token>`; `x-app-name` (если задан).
|
|
29
|
+
- endpoints по умолчанию (конфигурируемо): prod/sandbox.
|
|
30
|
+
- `x-tracking-id`: извлекать из unary headers и прокидывать в ошибки/логи; для stream — логировать tracking id из статусных сообщений подписки.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Минимальные стандарты кода/PR для SDK
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Код и процесс (минимум правил)
|
|
7
|
+
|
|
8
|
+
## Стиль/качество
|
|
9
|
+
|
|
10
|
+
- Ruby **>= 3.2**, стиль — через `rubocop` (double quotes и т.д.).
|
|
11
|
+
- `rubocop` и `rspec` обязаны проходить.
|
|
12
|
+
- Не логировать секреты (token/credentials).
|
|
13
|
+
- Protobuf наружу не экспонировать по умолчанию (только `raw:`/debug).
|
|
14
|
+
|
|
15
|
+
## Контракты
|
|
16
|
+
|
|
17
|
+
- Любое обновление `invest-contracts` включает: обновить pin tag/SHA → реген `lib/**/proto/**` → golden test “все RPC экспонированы”.
|
|
18
|
+
|
|
19
|
+
## Git/PR
|
|
20
|
+
|
|
21
|
+
- Следовать git-flow из `.cursor/rules/git-flow.mdc` (feature от `develop` → `develop`).
|
|
22
|
+
- PR: кратко “что/зачем” + тест-план + breaking/non-breaking для публичного Ruby API.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Минимальные паттерны: auto-expose RPC, coercion, streaming API
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Паттерны (минимум правил)
|
|
7
|
+
|
|
8
|
+
## 1) “Все методы реализованы”
|
|
9
|
+
|
|
10
|
+
- RPC **экспонируются автоматически** из descriptor’ов/Stub’ов (метапрограммирование).
|
|
11
|
+
- Нельзя вручную поддерживать списки методов или дублировать proto-структуру в коде.
|
|
12
|
+
|
|
13
|
+
## 2) Преобразования Ruby ↔ protobuf
|
|
14
|
+
|
|
15
|
+
- Вход в unary/stream управление: Ruby hash/kwargs → protobuf message через reflection (descriptor).
|
|
16
|
+
- Выход наружу: protobuf response → Ruby-friendly hash/DTO; protobuf наружу только `raw:`/debug.
|
|
17
|
+
|
|
18
|
+
## 3) Streaming (block/yield)
|
|
19
|
+
|
|
20
|
+
- Streaming API наружу: `client.market_data.stream { |s| s.subscribe_*; s.each_event { |event| ... } }`.
|
|
21
|
+
- Внутри: bidi stream + очередь исходящих команд; server ping игнорируется.
|
|
22
|
+
- Для MarketDataStream: reconciliation через `GetMySubscriptions` (рассинхрон → resubscribe; пусто → restart).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Тесты без сети: golden checks, coercion, streaming через fake transport
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Тестирование (строго без сети)
|
|
7
|
+
|
|
8
|
+
- Любые тесты **без сетевых вызовов** (ни prod, ни sandbox).
|
|
9
|
+
- Тестируем только: преобразования, поведение SDK, соответствие контрактам и документации.
|
|
10
|
+
|
|
11
|
+
## Обязательные проверки
|
|
12
|
+
|
|
13
|
+
- **Golden test “все RPC экспонированы”**: после регенерации proto тест падает, если появился новый RPC, но SDK его не выставил.
|
|
14
|
+
- **Coercers/Mappers**: reflection-based Hash/kwargs ↔ protobuf (enum/time/money/oneof).
|
|
15
|
+
- **Transport**: `GRPC::BadStatus` → `Errors::*`, извлечение `x-tracking-id` (unary headers), redaction секретов.
|
|
16
|
+
- **Streaming**: тестировать через fake transport + controlled `Enumerator`:
|
|
17
|
+
- ping игнорируется
|
|
18
|
+
- статусы подписки (включая `TOO_MANY_REQUESTS`) мапятся в события/ошибки
|
|
19
|
+
- reconciliation через `GetMySubscriptions` (рассинхрон → resubscribe; пусто → restart)
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# T::Tech::Investments
|
|
2
2
|
|
|
3
|
-
Ruby-обёртка над gRPC API [T-Bank Invest API](https://
|
|
3
|
+
Ruby-обёртка над gRPC API [T-Bank Invest API](https://developer.tbank.ru/invest/intro/intro/). Контракты (proto-файлы) берутся из репозитория [investAPI](https://github.com/RussianInvestments/investAPI) и пинятся по SHA в `contracts.lock`; сгенерированный protobuf/gRPC код коммитится в `lib/**/proto/**`. Гем даёт удобный Ruby-клиент для работы с брокерским счётом, инструментами, заявками и стримами маркет-данных.
|
|
4
4
|
|
|
5
5
|
Гем предназначен для интеграции инвестиционных сервисов T-Bank в Ruby-приложения: типизированные вызовы вместо ручной работы с gRPC, единый стиль API и опора на официальные контракты.
|
|
6
6
|
|
|
@@ -18,9 +18,200 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
|
18
18
|
gem install t-tech-investments
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## Minimal requirements (Prod / Sandbox)
|
|
22
|
+
|
|
23
|
+
Минимум, который нужен пользователю, чтобы начать работать с T-Invest API:
|
|
24
|
+
|
|
25
|
+
- **Ruby**: >= 3.2
|
|
26
|
+
- **Токен доступа**: получите токен в личном кабинете T-Bank Dev Portal (см. “Начало работы”) — [документация](https://developer.tbank.ru/invest/intro/intro/)
|
|
27
|
+
- **Выбор контура**: используйте токен **для прода** или **для песочницы** (sandbox)
|
|
28
|
+
- **gRPC endpoint** (из документации):
|
|
29
|
+
- **Prod**: `invest-public-api.tbank.ru:443`
|
|
30
|
+
- **Sandbox**: `sandbox-invest-public-api.tbank.ru:443`
|
|
31
|
+
|
|
32
|
+
Рекомендуемая форма конфигурации через переменные окружения (названия переменных — соглашение этого гема):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Prod
|
|
36
|
+
export TINVEST_TOKEN="..."
|
|
37
|
+
export TINVEST_ENDPOINT="invest-public-api.tbank.ru:443"
|
|
38
|
+
|
|
39
|
+
# или Sandbox
|
|
40
|
+
export TINVEST_TOKEN="..."
|
|
41
|
+
export TINVEST_ENDPOINT="sandbox-invest-public-api.tbank.ru:443"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Опционально (ENV-first конфигурация):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Идентификатор приложения (x-app-name)
|
|
48
|
+
export TINVEST_APP_NAME="my-app"
|
|
49
|
+
|
|
50
|
+
# Таймаут по умолчанию (секунды)
|
|
51
|
+
export TINVEST_TIMEOUT="10"
|
|
52
|
+
|
|
53
|
+
# Путь к PEM с CA-сертификатами (для кастомных окружений)
|
|
54
|
+
export TINVEST_SSL_CA_CERTS="/path/to/ca.pem"
|
|
55
|
+
```
|
|
56
|
+
|
|
21
57
|
## Usage
|
|
22
58
|
|
|
23
|
-
|
|
59
|
+
Гем предлагает “глобальную” конфигурацию по умолчанию + возможность переопределить параметры при создании клиента.
|
|
60
|
+
|
|
61
|
+
### Configuration
|
|
62
|
+
|
|
63
|
+
#### ENV-first (быстрый старт)
|
|
64
|
+
|
|
65
|
+
**Given** у вас есть токен и выбран контур (prod/sandbox)
|
|
66
|
+
**When** вы задаёте `TINVEST_TOKEN` и `TINVEST_ENDPOINT`
|
|
67
|
+
**Then** вы можете создать клиента без дополнительной настройки
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
require "t/tech/investments"
|
|
71
|
+
|
|
72
|
+
client = T::Tech::Investments.client
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### Явная конфигурация (Rails / monolith)
|
|
76
|
+
|
|
77
|
+
**Given** вы хотите централизованную настройку
|
|
78
|
+
**When** вы вызываете `T::Tech::Investments.configure`
|
|
79
|
+
**Then** новые клиенты используют эти значения по умолчанию
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
T::Tech::Investments.configure do |c|
|
|
83
|
+
c.token = ENV.fetch("TINVEST_TOKEN")
|
|
84
|
+
c.endpoint = ENV.fetch("TINVEST_ENDPOINT", "invest-public-api.tbank.ru:443") # или sandbox
|
|
85
|
+
c.timeout = 10
|
|
86
|
+
|
|
87
|
+
# опционально
|
|
88
|
+
c.app_name = "my-app"
|
|
89
|
+
c.logger = Logger.new($stdout)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
client = T::Tech::Investments.client
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### Override для одного клиента
|
|
96
|
+
|
|
97
|
+
**Given** вам нужно изменить настройки только для одного вызова/процесса
|
|
98
|
+
**When** вы передаёте параметры в `client(...)`
|
|
99
|
+
**Then** они применяются только к этому экземпляру клиента
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
client = T::Tech::Investments.client(
|
|
103
|
+
endpoint: "sandbox-invest-public-api.tbank.ru:443",
|
|
104
|
+
timeout: 2
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Configuration best practices
|
|
109
|
+
|
|
110
|
+
- **Валидируйте токен**: если токена нет/он пустой — поднимайте `ConfigurationError` (или аналог) с понятным сообщением.
|
|
111
|
+
- **Не логируйте секреты**: токен не должен попадать в логи/исключения.
|
|
112
|
+
- **Снимок конфигурации при создании клиента**: созданный клиент не должен “подхватывать” изменения глобального конфига на лету (важно для потоков/фоновых задач).
|
|
113
|
+
- **Prod vs Sandbox**: различайте контуры только endpoint’ом из документации:
|
|
114
|
+
- **Prod**: `invest-public-api.tbank.ru:443`
|
|
115
|
+
- **Sandbox**: `sandbox-invest-public-api.tbank.ru:443`
|
|
116
|
+
|
|
117
|
+
### Unary requests (получение данных)
|
|
118
|
+
|
|
119
|
+
Унарные запросы в T‑Invest API — это обычные gRPC-вызовы “один запрос → один ответ”.
|
|
120
|
+
|
|
121
|
+
#### Авторизация и служебные заголовки
|
|
122
|
+
|
|
123
|
+
По документации T‑Invest API токен нужно передавать в **metadata каждого запроса** в виде заголовка:
|
|
124
|
+
|
|
125
|
+
- **`authorization: Bearer <token>`**
|
|
126
|
+
|
|
127
|
+
Дополнительно (рекомендуется для SDK) можно передавать:
|
|
128
|
+
|
|
129
|
+
- **`x-app-name`** — идентификатор приложения/SDK для статистики (рекомендации в доке)
|
|
130
|
+
|
|
131
|
+
Источник: [gRPC — Авторизация / AppName / trackingId](https://developer.tbank.ru/invest/intro/developer/protocols/grpc/).
|
|
132
|
+
|
|
133
|
+
#### Пример (общий паттерн)
|
|
134
|
+
|
|
135
|
+
**Given** у вас настроены `TINVEST_TOKEN` и `TINVEST_ENDPOINT`
|
|
136
|
+
**When** вы вызываете унарный метод сервиса
|
|
137
|
+
**Then** вы получаете Hash (по умолчанию) или сырой protobuf при `raw: true`
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
client = T::Tech::Investments.client
|
|
141
|
+
|
|
142
|
+
# Список аккаунтов (Hash с :accounts и т.д.)
|
|
143
|
+
accounts = client.users.get_accounts
|
|
144
|
+
# С параметрами и опциями
|
|
145
|
+
accounts = client.users.get_accounts(status: :open)
|
|
146
|
+
|
|
147
|
+
# Свечи по инструменту (from/to — Time или Hash)
|
|
148
|
+
candles = client.market_data.get_candles(
|
|
149
|
+
figi: "BBG004730N88",
|
|
150
|
+
from: Time.now - 3600,
|
|
151
|
+
to: Time.now,
|
|
152
|
+
interval: :candle_interval_1_min
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Сырой protobuf (для отладки)
|
|
156
|
+
raw_response = client.users.get_accounts(raw: true)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### Таймауты (deadline)
|
|
160
|
+
|
|
161
|
+
Для gRPC важно задавать deadline/timeout, чтобы запросы не зависали бесконечно. Вызов должен использовать значение `timeout` из конфигурации (или override при создании клиента).
|
|
162
|
+
|
|
163
|
+
#### tracking id (x-tracking-id)
|
|
164
|
+
|
|
165
|
+
Документация отмечает, что **`x-tracking-id` добавляется в ответы unary-методов**. Это UUID запроса, который нужно указывать при обращении в техподдержку.
|
|
166
|
+
|
|
167
|
+
Рекомендуемая практика для гема:
|
|
168
|
+
|
|
169
|
+
- логировать/пробрасывать `x-tracking-id` при ошибках;
|
|
170
|
+
- (опционально) предоставлять его пользователю через объект-обёртку ответа или через instrumentation hooks.
|
|
171
|
+
|
|
172
|
+
### Streaming (MarketData)
|
|
173
|
+
|
|
174
|
+
Bidirectional stream: в блоке задаёте подписки, затем `each_event` отдаёт события. События — Hash с `:type` и `:data`; **ping игнорируется** SDK.
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
client = T::Tech::Investments.client
|
|
178
|
+
|
|
179
|
+
client.market_data.stream do |s|
|
|
180
|
+
# 1) Подписки (subscription_action: :subscribe или :unsubscribe)
|
|
181
|
+
s.subscribe_candles(
|
|
182
|
+
subscription_action: :subscribe,
|
|
183
|
+
instruments: [{ figi: "BBG004730N88", interval: :subscription_interval_one_minute }],
|
|
184
|
+
waiting_close: true
|
|
185
|
+
)
|
|
186
|
+
s.subscribe_order_book(
|
|
187
|
+
subscription_action: :subscribe,
|
|
188
|
+
instruments: [{ figi: "BBG004730N88", depth: 10 }]
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# 2) Чтение событий (event[:type], event[:data])
|
|
192
|
+
s.each_event do |event|
|
|
193
|
+
case event[:type]
|
|
194
|
+
when :candle
|
|
195
|
+
puts "candle: #{event[:data].inspect}"
|
|
196
|
+
when :orderbook
|
|
197
|
+
puts "orderbook: #{event[:data].inspect}"
|
|
198
|
+
when :subscribe_candles_response, :subscribe_order_book_response
|
|
199
|
+
# ответы подписки (tracking_id и статусы в event[:data])
|
|
200
|
+
warn "subscription: #{event[:data].inspect}"
|
|
201
|
+
when :trading_status, :last_price, :trade
|
|
202
|
+
# остальные типы по контракту
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Reconciliation: при рассинхроне подписок вызовите `s.get_my_subscriptions` и при необходимости переподпишитесь или перезапустите стрим.
|
|
209
|
+
|
|
210
|
+
Примечания:
|
|
211
|
+
|
|
212
|
+
- Авторизация для стрима — через metadata `Authorization: Bearer <token>` (как и для unary). Подробнее: [gRPC — Авторизация / AppName / trackingId](https://developer.tbank.ru/invest/intro/developer/protocols/grpc/).
|
|
213
|
+
- В одном стриме можно отправить несколько подписок (свечи, стакан, сделки и т.д.) — каждая своим запросом в очередь.
|
|
214
|
+
- Подробнее: [Описание сервиса котировок](https://developer.tbank.ru/invest/services/quotes/head-marketdata/).
|
|
24
215
|
|
|
25
216
|
## Development
|
|
26
217
|
|
|
@@ -30,7 +221,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
|
30
221
|
|
|
31
222
|
## Contributing
|
|
32
223
|
|
|
33
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
|
224
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/naveroot/t-tech-investments. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/naveroot/t-tech-investments/blob/main/CODE_OF_CONDUCT.md).
|
|
34
225
|
|
|
35
226
|
## License
|
|
36
227
|
|
|
@@ -38,4 +229,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
38
229
|
|
|
39
230
|
## Code of Conduct
|
|
40
231
|
|
|
41
|
-
Everyone interacting in the T::Tech::Investments project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/
|
|
232
|
+
Everyone interacting in the T::Tech::Investments project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/naveroot/t-tech-investments/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
CHANGED
data/contracts.lock
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"repository": "https://github.com/RussianInvestments/investAPI.git",
|
|
3
|
+
"ref": "3eaf23a25f598fe483c913184acdbd9132bc68d2",
|
|
4
|
+
"contracts_path": "src/docs/contracts",
|
|
5
|
+
"description": "invest-contracts pin (Release 1.43). Update ref then run script/regenerate_proto and golden test."
|
|
6
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module T
|
|
4
|
+
module Tech
|
|
5
|
+
module Investments
|
|
6
|
+
# Client facade: config snapshot, transport, and service adapters (users, instruments, etc.).
|
|
7
|
+
# Services expose unary RPCs via Transport + Coercers; RPCs are defined from proto descriptors.
|
|
8
|
+
class Client
|
|
9
|
+
attr_reader :config
|
|
10
|
+
|
|
11
|
+
def initialize(config_snapshot)
|
|
12
|
+
@config = config_snapshot.validate!
|
|
13
|
+
@transport = nil
|
|
14
|
+
@services = {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def transport
|
|
18
|
+
@transport ||= Transport.new(config)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def users
|
|
22
|
+
service(:users)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def instruments
|
|
26
|
+
service(:instruments)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def market_data
|
|
30
|
+
@market_data ||= begin
|
|
31
|
+
ProtoLoader.load!
|
|
32
|
+
unary = Services::UnaryAdapter.new(Services::Registry.service_class(:market_data), transport)
|
|
33
|
+
Services::MarketDataFacade.new(unary, transport)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def operations
|
|
38
|
+
service(:operations)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def orders
|
|
42
|
+
service(:orders)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def sandbox
|
|
46
|
+
service(:sandbox)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def stop_orders
|
|
50
|
+
service(:stop_orders)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def signals
|
|
54
|
+
service(:signals)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def service(name)
|
|
60
|
+
return market_data if name == :market_data
|
|
61
|
+
|
|
62
|
+
@services[name] ||= begin
|
|
63
|
+
ProtoLoader.load!
|
|
64
|
+
Services::UnaryAdapter.new(Services::Registry.service_class(name), transport)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module T
|
|
4
|
+
module Tech
|
|
5
|
+
module Investments
|
|
6
|
+
# Reflection-based Hash/kwargs <-> protobuf. No IO; deterministic.
|
|
7
|
+
# rubocop:disable Metrics/ModuleLength, Metrics/ClassLength
|
|
8
|
+
module Coercers
|
|
9
|
+
TIMESTAMP_FULL_NAME = "google.protobuf.Timestamp"
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
# Builds a protobuf message instance from a Hash. Keys can be symbols or strings.
|
|
13
|
+
# Coerces: enum (symbol or int), Time -> Timestamp, nested hashes -> messages, repeated -> array.
|
|
14
|
+
#
|
|
15
|
+
# @param message_class [Class] Generated protobuf message class (e.g. GetAccountsRequest)
|
|
16
|
+
# @param hash [Hash] Attributes (symbol or string keys)
|
|
17
|
+
# @return [Object] Instance of message_class
|
|
18
|
+
def to_request(message_class, hash)
|
|
19
|
+
return message_class.new if hash.nil? || hash.empty?
|
|
20
|
+
|
|
21
|
+
msg = message_class.new
|
|
22
|
+
descriptor = message_class.descriptor
|
|
23
|
+
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
24
|
+
|
|
25
|
+
descriptor.entries.each do |field|
|
|
26
|
+
value = fetch_value(hash, field.name)
|
|
27
|
+
next if value.nil?
|
|
28
|
+
|
|
29
|
+
set_field(msg, field, value, pool)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
msg
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Converts a protobuf message to a Ruby Hash (symbol keys).
|
|
36
|
+
# Optionally converts Timestamp fields to Time and enums to short symbols.
|
|
37
|
+
#
|
|
38
|
+
# @param message [Object] Protobuf message instance
|
|
39
|
+
# @param ruby_friendly [Boolean] If true, timestamps become Time, enums may be shortened
|
|
40
|
+
# @return [Hash]
|
|
41
|
+
def to_hash(message, ruby_friendly: false)
|
|
42
|
+
return {} if message.nil?
|
|
43
|
+
|
|
44
|
+
h = message.to_h
|
|
45
|
+
ruby_friendly ? deep_ruby_friendly(h, message.class.descriptor, message) : h
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def fetch_value(hash, name)
|
|
51
|
+
name_sym = name.to_sym
|
|
52
|
+
return hash[name_sym] if hash.key?(name_sym)
|
|
53
|
+
return hash[name] if hash.key?(name.to_s)
|
|
54
|
+
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def set_field(msg, field, value, pool)
|
|
59
|
+
name = field.name
|
|
60
|
+
if field.label == :repeated
|
|
61
|
+
arr = Array(value).map { |v| coerce_single_value(field, v, pool) }
|
|
62
|
+
msg[name].clear
|
|
63
|
+
msg[name].concat(arr)
|
|
64
|
+
return
|
|
65
|
+
end
|
|
66
|
+
msg[name] = coerce_single_value(field, value, pool)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
70
|
+
def coerce_single_value(field, value, pool)
|
|
71
|
+
case field.type
|
|
72
|
+
when :enum
|
|
73
|
+
coerce_enum(field, value)
|
|
74
|
+
when :message
|
|
75
|
+
coerce_message(field, value, pool)
|
|
76
|
+
when :int64, :int32, :uint64, :uint32
|
|
77
|
+
value.is_a?(Integer) ? value : Integer(value, 10)
|
|
78
|
+
when :float, :double
|
|
79
|
+
value.is_a?(Numeric) ? value : Float(value)
|
|
80
|
+
when :string
|
|
81
|
+
value.to_s
|
|
82
|
+
when :bool
|
|
83
|
+
!!value
|
|
84
|
+
when :bytes
|
|
85
|
+
value.to_s.dup.force_encoding(Encoding::ASCII_8BIT)
|
|
86
|
+
else
|
|
87
|
+
value
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
91
|
+
|
|
92
|
+
# rubocop:disable Metrics/AbcSize
|
|
93
|
+
def coerce_enum(field, value)
|
|
94
|
+
return value if value.is_a?(Integer)
|
|
95
|
+
|
|
96
|
+
enum_mod = field.subtype.enummodule
|
|
97
|
+
sym = value.is_a?(Symbol) ? value : value.to_s.to_sym
|
|
98
|
+
resolved = enum_mod.resolve(sym)
|
|
99
|
+
return resolved if resolved
|
|
100
|
+
|
|
101
|
+
# Try full enum constant name (e.g. ACCOUNT_STATUS_OPEN from :open -> match suffix)
|
|
102
|
+
enum_mod.constants.each do |c|
|
|
103
|
+
return enum_mod.const_get(c) if c.to_s.downcase.end_with?(sym.to_s.downcase)
|
|
104
|
+
end
|
|
105
|
+
raise ArgumentError, "Unknown enum value for #{field.name}: #{value.inspect}"
|
|
106
|
+
end
|
|
107
|
+
# rubocop:enable Metrics/AbcSize
|
|
108
|
+
|
|
109
|
+
# rubocop:disable Metrics/MethodLength
|
|
110
|
+
def coerce_message(field, value, pool)
|
|
111
|
+
submsg_name = field.submsg_name
|
|
112
|
+
submsg_class = pool.lookup(submsg_name).msgclass
|
|
113
|
+
|
|
114
|
+
if submsg_name == TIMESTAMP_FULL_NAME
|
|
115
|
+
return time_to_timestamp(value) if value.is_a?(Time)
|
|
116
|
+
return submsg_class.new(value) if value.is_a?(Hash)
|
|
117
|
+
|
|
118
|
+
raise ArgumentError, "Timestamp expects Time or Hash, got #{value.class}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
return to_request(submsg_class, value) if value.is_a?(Hash)
|
|
122
|
+
unless value.is_a?(submsg_class)
|
|
123
|
+
raise ArgumentError, "Expected Hash or message for #{field.name}, got #{value.class}"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
value
|
|
127
|
+
end
|
|
128
|
+
# rubocop:enable Metrics/MethodLength
|
|
129
|
+
|
|
130
|
+
def time_to_timestamp(time)
|
|
131
|
+
ts = ::Google::Protobuf::Timestamp.new
|
|
132
|
+
ts.seconds = time.to_i
|
|
133
|
+
ts.nanos = time.nsec
|
|
134
|
+
ts
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def deep_ruby_friendly(hash, _descriptor, _message)
|
|
138
|
+
return hash_to_time(hash) if timestamp_like_hash?(hash)
|
|
139
|
+
|
|
140
|
+
return hash unless hash.is_a?(Hash)
|
|
141
|
+
|
|
142
|
+
hash.transform_values { |val| ruby_friendly_value(val) }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def ruby_friendly_value(val)
|
|
146
|
+
case val
|
|
147
|
+
when Hash then deep_ruby_friendly(val, nil, nil)
|
|
148
|
+
when Array then val.map { |elem| elem.is_a?(Hash) ? deep_ruby_friendly(elem, nil, nil) : elem }
|
|
149
|
+
else val
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def timestamp_like_hash?(hash)
|
|
154
|
+
return false unless hash.is_a?(Hash)
|
|
155
|
+
return false unless hash.key?(:seconds) || hash.key?("seconds")
|
|
156
|
+
return false unless hash.key?(:nanos) || hash.key?("nanos")
|
|
157
|
+
|
|
158
|
+
hash.size <= 2
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def hash_to_time(hash)
|
|
162
|
+
sec = hash[:seconds] || hash["seconds"] || 0
|
|
163
|
+
nano = hash[:nanos] || hash["nanos"] || 0
|
|
164
|
+
Time.at(sec, nano, :nsec)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
# rubocop:enable Metrics/ModuleLength, Metrics/ClassLength
|