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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/rules/git-flow.mdc +49 -5
  3. data/.cursor/rules/sdk-architecture.mdc +30 -0
  4. data/.cursor/rules/sdk-code-standards.mdc +22 -0
  5. data/.cursor/rules/sdk-patterns.mdc +22 -0
  6. data/.cursor/rules/sdk-testing.mdc +19 -0
  7. data/CHANGELOG.md +4 -0
  8. data/README.md +195 -4
  9. data/Rakefile +4 -0
  10. data/contracts.lock +6 -0
  11. data/lib/t/tech/investments/client.rb +70 -0
  12. data/lib/t/tech/investments/coercers.rb +171 -0
  13. data/lib/t/tech/investments/configuration.rb +74 -0
  14. data/lib/t/tech/investments/errors.rb +24 -0
  15. data/lib/t/tech/investments/proto/common_pb.rb +42 -0
  16. data/lib/t/tech/investments/proto/google/api/field_behavior_pb.rb +19 -0
  17. data/lib/t/tech/investments/proto/instruments_pb.rb +157 -0
  18. data/lib/t/tech/investments/proto/instruments_services_pb.rb +115 -0
  19. data/lib/t/tech/investments/proto/marketdata_pb.rb +99 -0
  20. data/lib/t/tech/investments/proto/marketdata_services_pb.rb +68 -0
  21. data/lib/t/tech/investments/proto/operations_pb.rb +78 -0
  22. data/lib/t/tech/investments/proto/operations_services_pb.rb +67 -0
  23. data/lib/t/tech/investments/proto/orders_pb.rb +64 -0
  24. data/lib/t/tech/investments/proto/orders_services_pb.rb +69 -0
  25. data/lib/t/tech/investments/proto/sandbox_pb.rb +37 -0
  26. data/lib/t/tech/investments/proto/sandbox_services_pb.rb +75 -0
  27. data/lib/t/tech/investments/proto/signals_pb.rb +37 -0
  28. data/lib/t/tech/investments/proto/signals_services_pb.rb +36 -0
  29. data/lib/t/tech/investments/proto/stoporders_pb.rb +45 -0
  30. data/lib/t/tech/investments/proto/stoporders_services_pb.rb +38 -0
  31. data/lib/t/tech/investments/proto/users_pb.rb +47 -0
  32. data/lib/t/tech/investments/proto/users_services_pb.rb +51 -0
  33. data/lib/t/tech/investments/proto_loader.rb +49 -0
  34. data/lib/t/tech/investments/services/market_data_facade.rb +35 -0
  35. data/lib/t/tech/investments/services/market_data_stream_session.rb +148 -0
  36. data/lib/t/tech/investments/services/registry.rb +38 -0
  37. data/lib/t/tech/investments/services/unary_adapter.rb +64 -0
  38. data/lib/t/tech/investments/services.rb +6 -0
  39. data/lib/t/tech/investments/transport.rb +137 -0
  40. data/lib/t/tech/investments/version.rb +1 -1
  41. data/lib/t/tech/investments.rb +30 -1
  42. data/script/regenerate_proto +119 -0
  43. data/sig/t/tech/investments.rbs +114 -1
  44. metadata +67 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cff97b258ff7384f55f88ee890266d106003ba1e61ccaf0f140db45b6f981f55
4
- data.tar.gz: 3d03d9e98cedc667ef2462e6aaba1824c5dc54b85fd34601241ba6aae90c911a
3
+ metadata.gz: 2e68074e95141d63fe02be0f024fdba50ec74b935b61f95cdc35754dd368484f
4
+ data.tar.gz: adcdccd09aed1a6bcc83e274f7c02cf954a5f05645740aad949027af7fe8352d
5
5
  SHA512:
6
- metadata.gz: 49373e6e1b2b729b41701a778f2b1f2e7d9ab302b2231aaaa9d9cacb30816a8eba8784ca428f1ddce8abea1cd27d5e980df38fbd29f157ced37fafe135236a39
7
- data.tar.gz: a0bb1a988d286d8bfe2117c1b482b36a29bda4b090c355cdbe9acc98dd1aa663d38384c17af4a77ce07b21df262cea51c2c26daccea37963f5f8f8ea70a6fdc5
6
+ metadata.gz: 83bb6a2f390c88557029f0b349670f393faed7da128e368bec3829154490ee9921e0af9c7532278ddd34b72f800444408380ce373bdfb57827f253cb11871fb3
7
+ data.tar.gz: 832db0d5502ac1d24562d9703ada3f224248778857bc551e261a88351620f8977cbb16c6e95d5b1279349209cc827cf5500f5e8edf162a241d1c32ce84aaf89d
@@ -1,12 +1,56 @@
1
1
  ---
2
- description: Git Flow: ветки и слияния
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
- # Git Flow
35
+ Рекомендованный формат commit message:
36
+ - feat: добавление новой функциональности
37
+ - fix: исправление ошибки
38
+ - refactor: рефакторинг без изменения поведения
39
+ - chore: инфраструктурные изменения
40
+ - test: добавление или изменение тестов
41
+ - docs: документация
7
42
 
8
- **Ветки:** main — только прод (через release/hotfix). develop — интеграция, от неё feature.
43
+ Примеры:
44
+ - feat(strategy): add breakout strategy
45
+ - fix(risk): prevent position size overflow
46
+ - refactor(execution): simplify order state machine
9
47
 
10
- **Именование:** `feature/имя` ← develop → develop; `release/1.2.0` ← develop → main + develop; `hotfix/имя` ← main → main + develop.
48
+ Merge:
49
+ - Предпочтителен merge commit или squash (осознанно).
50
+ - Избегать rebase на общих ветках (develop, main).
51
+ - Конфликты решаются до merge, не после.
11
52
 
12
- Предлагать эти шаблоны веток, feature сливать в develop, не в main.
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
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.1] - 2026-02-05
4
+
5
+ - Docs: update README with correct links and env options
6
+
3
7
  ## [0.1.0] - 2026-02-05
4
8
 
5
9
  - Initial release
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # T::Tech::Investments
2
2
 
3
- Ruby-обёртка над gRPC API [T-Bank Invest API](https://opensource.tbank.ru/invest/invest-contracts). Контракты (proto-файлы) берутся из репозитория [invest-contracts](https://opensource.tbank.ru/invest/invest-contracts/-/tree/master/src/docs/contracts); гем даёт удобный Ruby-клиент для работы с брокерским счётом, инструментами, заявками и стримами маркет-данных.
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
- TODO: Write usage instructions here
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/[USERNAME]/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/[USERNAME]/t-tech-investments/blob/main/CODE_OF_CONDUCT.md).
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/[USERNAME]/t-tech-investments/blob/main/CODE_OF_CONDUCT.md).
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
@@ -9,4 +9,8 @@ require "rubocop/rake_task"
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
12
+ task :regenerate_proto do
13
+ sh "bundle exec ruby script/regenerate_proto"
14
+ end
15
+
12
16
  task default: %i[spec rubocop]
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