t-tech-investments 0.1.0 → 0.2.0

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/rules/git-flow.mdc +49 -5
  3. data/.cursor/rules/less-code.mdc +6 -0
  4. data/.cursor/rules/only-important-steps.mdc +5 -0
  5. data/.cursor/rules/sdk-architecture.mdc +31 -0
  6. data/.cursor/rules/sdk-code-standards.mdc +22 -0
  7. data/.cursor/rules/sdk-patterns.mdc +22 -0
  8. data/.cursor/rules/sdk-testing.mdc +19 -0
  9. data/CHANGELOG.md +14 -0
  10. data/README.md +320 -7
  11. data/Rakefile +4 -0
  12. data/contracts.lock +6 -0
  13. data/lib/t/tech/investments/client.rb +54 -0
  14. data/lib/t/tech/investments/coercers.rb +171 -0
  15. data/lib/t/tech/investments/configuration.rb +74 -0
  16. data/lib/t/tech/investments/errors.rb +27 -0
  17. data/lib/t/tech/investments/proto/common_pb.rb +42 -0
  18. data/lib/t/tech/investments/proto/google/api/field_behavior_pb.rb +19 -0
  19. data/lib/t/tech/investments/proto/instruments_pb.rb +157 -0
  20. data/lib/t/tech/investments/proto/instruments_services_pb.rb +115 -0
  21. data/lib/t/tech/investments/proto/marketdata_pb.rb +99 -0
  22. data/lib/t/tech/investments/proto/marketdata_services_pb.rb +68 -0
  23. data/lib/t/tech/investments/proto/operations_pb.rb +78 -0
  24. data/lib/t/tech/investments/proto/operations_services_pb.rb +67 -0
  25. data/lib/t/tech/investments/proto/orders_pb.rb +64 -0
  26. data/lib/t/tech/investments/proto/orders_services_pb.rb +69 -0
  27. data/lib/t/tech/investments/proto/sandbox_pb.rb +37 -0
  28. data/lib/t/tech/investments/proto/sandbox_services_pb.rb +75 -0
  29. data/lib/t/tech/investments/proto/signals_pb.rb +37 -0
  30. data/lib/t/tech/investments/proto/signals_services_pb.rb +36 -0
  31. data/lib/t/tech/investments/proto/stoporders_pb.rb +45 -0
  32. data/lib/t/tech/investments/proto/stoporders_services_pb.rb +38 -0
  33. data/lib/t/tech/investments/proto/users_pb.rb +47 -0
  34. data/lib/t/tech/investments/proto/users_services_pb.rb +51 -0
  35. data/lib/t/tech/investments/proto_loader.rb +31 -0
  36. data/lib/t/tech/investments/services/contracts/market_data_stream_contract.rb +51 -0
  37. data/lib/t/tech/investments/services/mappers/market_data_event_mapper.rb +68 -0
  38. data/lib/t/tech/investments/services/market_data_facade.rb +46 -0
  39. data/lib/t/tech/investments/services/registry.rb +80 -0
  40. data/lib/t/tech/investments/services/stores/local_subscription_store.rb +54 -0
  41. data/lib/t/tech/investments/services/stores/subscription.rb +82 -0
  42. data/lib/t/tech/investments/services/stream/market_data_kinds.rb +24 -0
  43. data/lib/t/tech/investments/services/stream/market_data_stream_session.rb +131 -0
  44. data/lib/t/tech/investments/services/stream/subscription_reconciler.rb +46 -0
  45. data/lib/t/tech/investments/services/unary_adapter.rb +64 -0
  46. data/lib/t/tech/investments/services.rb +12 -0
  47. data/lib/t/tech/investments/transport.rb +137 -0
  48. data/lib/t/tech/investments/version.rb +1 -1
  49. data/lib/t/tech/investments.rb +30 -1
  50. data/script/regenerate_proto +119 -0
  51. data/sig/t/tech/investments.rbs +154 -1
  52. metadata +75 -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: 20123e15a4902951a5ee22661123c8a69ec7a55573203f505ec3f2ffd0016fc4
4
+ data.tar.gz: 24cd3848684453f36079b82c8398c9f69e8d29d422375e6396654b2b55eadd09
5
5
  SHA512:
6
- metadata.gz: 49373e6e1b2b729b41701a778f2b1f2e7d9ab302b2231aaaa9d9cacb30816a8eba8784ca428f1ddce8abea1cd27d5e980df38fbd29f157ced37fafe135236a39
7
- data.tar.gz: a0bb1a988d286d8bfe2117c1b482b36a29bda4b090c355cdbe9acc98dd1aa663d38384c17af4a77ce07b21df262cea51c2c26daccea37963f5f8f8ea70a6fdc5
6
+ metadata.gz: 4c2ab86b84b0303698ccb523c981e1437da6d0efb7a111971f7682db44e8bb855c26b1582a977b047ff2c111683f718fcd08ddb88956df32745fa620de39191d
7
+ data.tar.gz: 63a2b49c538d2b9c95f204e517325630de7ac26f9e5397002a81cf8cc6f15a991f99904195241bbbc21f84faa8324a688a0bcf8c3a6664d9bb68dc42ece6143c
@@ -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,6 @@
1
+ ---
2
+ description: Пожалуйста, решите эту задачу с минимально возможным количеством строк кода.
3
+ alwaysApply: false
4
+ ---
5
+ Пожалуйста, решите эту задачу с минимально возможным количеством строк кода.
6
+ Убедитесь, что результат функционально эквивалентен исходной цели и проходит существующие тесты или сценарии использовани
@@ -0,0 +1,5 @@
1
+ ---
2
+ alwaysApply: false
3
+ ---
4
+ Разбей это только на действительно необходимые шаги. Исключи любые необязательные или умозрительные функции.
5
+ Убедись, что каждый шаг напрямую соответствует текущей цели. Предоставь объяснение, почему каждый шаг необходим.
@@ -0,0 +1,31 @@
1
+ ---
2
+ description: Минимальная архитектура SDK: слои, зависимости, контракты
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # Архитектура SDK (минимум правил)
7
+
8
+ ## Слои
9
+
10
+ - `Client` (facade) → `Services` → `Transport` (gRPC)
11
+ - `Services` подслои: `contracts` (proto-обвязка), `mappers`, `stores`, `stream`
12
+ - Вспомогательные: `Coercers/Mappers`, `Errors`, `Proto` (сгенерёнка)
13
+
14
+ ## Зависимости (жёсткие запреты)
15
+
16
+ - `Services` не вызывают gRPC stub напрямую — только через `Transport`.
17
+ - Публичный API не возвращает protobuf по умолчанию (только `raw:`/debug, если нужен).
18
+ - `Transport` без бизнес-логики; только: metadata, deadlines, retries-policy, error mapping.
19
+ - `Coercers/Mappers` — чистые и детерминированные (без IO/сети).
20
+
21
+ ## Контракты (invest-contracts)
22
+
23
+ - Контракты **пинуем по tag/SHA** (не `master`), храним в `contracts.lock`.
24
+ - Сгенерированный Ruby protobuf/gRPC код **коммитим** в `lib/**/proto/**` (установка гема без `protoc`).
25
+ - Обновление контрактов обязано включать: pin → реген → тест “все RPC экспонированы”.
26
+
27
+ ## T‑Invest инварианты (transport обязан)
28
+
29
+ - metadata каждого запроса: `Authorization: Bearer <token>`; `x-app-name` (если задан).
30
+ - endpoints по умолчанию (конфигурируемо): prod/sandbox.
31
+ - `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,19 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2026-02-06
4
+
5
+ - Client: services exposed via Registry (method_missing), no hardcoded list; facades (e.g. market_data) discovered dynamically
6
+ - ProtoLoader: load proto files dynamically from directory; common_pb first, then rest + services
7
+ - Market data stream: session stop/close, reconciliation (GetMySubscriptions → restart + resubscribe on drift), max_restarts limit and StreamRestartLimitError
8
+ - Market data: contract/mapper/store/reconciler split; subscription kinds single source of truth; candle interval validation
9
+ - Transport: allow custom CA certs for gRPC
10
+ - Dev: SimpleCov coverage (COVERAGE=1), codecov badge in README
11
+ - Docs: README stream stopping, reconciliation example, installation and usage
12
+
13
+ ## [0.1.1] - 2026-02-05
14
+
15
+ - Docs: update README with correct links and env options
16
+
3
17
  ## [0.1.0] - 2026-02-05
4
18
 
5
19
  - Initial release
data/README.md CHANGED
@@ -1,12 +1,35 @@
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
+ [![Gem Version](https://img.shields.io/gem/v/t-tech-investments.svg)](https://rubygems.org/gems/t-tech-investments)
4
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-blue)](https://www.ruby-lang.org/)
5
+ [![CI](https://img.shields.io/github/actions/workflow/status/naveroot/t-tech-investments/main.yml?branch=main&label=rspec)](https://github.com/naveroot/t-tech-investments/actions/workflows/main.yml)
6
+ [![codecov](https://codecov.io/gh/naveroot/t-tech-investments/graph/badge.svg)](https://codecov.io/gh/naveroot/t-tech-investments)
4
7
 
5
- Гем предназначен для интеграции инвестиционных сервисов T-Bank в Ruby-приложения: типизированные вызовы вместо ручной работы с gRPC, единый стиль API и опора на официальные контракты.
8
+ Ruby wrapper for the gRPC API of [T-Bank Invest](https://developer.tbank.ru/invest/intro/intro/). Contracts (proto files) are sourced from [investAPI](https://github.com/RussianInvestments/investAPI) and pinned by SHA in `contracts.lock`; generated protobuf/gRPC code is committed to `lib/**/proto/**`. The gem provides a convenient Ruby client for accounts, instruments, orders, and market data streams. Available `Client` methods depend on the loaded contract; after regenerating proto, the set of methods can change.
9
+
10
+ This gem integrates T-Bank investment services into Ruby apps: typed calls instead of manual gRPC handling, consistent API style, and adherence to official contracts.
11
+
12
+ ## Contents
13
+
14
+ - [Installation](#installation)
15
+ - [Minimal requirements (Prod / Sandbox)](#minimal-requirements-prod--sandbox)
16
+ - [Usage](#usage)
17
+ - [Development](#development)
18
+ - [Contributing](#contributing)
19
+ - [License](#license)
20
+ - [Code of Conduct](#code-of-conduct)
21
+
22
+ ## Key features
23
+
24
+ - Typed unary RPC wrappers for all services
25
+ - MarketData bidirectional stream with Ruby-friendly events
26
+ - Hash/kwargs ↔ protobuf coercers (reflection-based)
27
+ - Config snapshot per client (safe for background jobs)
28
+ - Error mapping with tracking id extraction
6
29
 
7
30
  ## Installation
8
31
 
9
- Install the gem and add to the application's Gemfile by executing:
32
+ Install the gem and add it to the application's Gemfile by executing:
10
33
 
11
34
  ```bash
12
35
  bundle add t-tech-investments
@@ -18,19 +41,309 @@ If bundler is not being used to manage dependencies, install the gem by executin
18
41
  gem install t-tech-investments
19
42
  ```
20
43
 
44
+ ## Minimal requirements (Prod / Sandbox)
45
+
46
+ Minimum required to start using T-Invest API:
47
+
48
+ - **Ruby**: >= 3.2
49
+ - **Access token**: obtain a token in T-Bank Dev Portal (“Getting started”) — [docs](https://developer.tbank.ru/invest/intro/intro/)
50
+ - **Environment**: use a **prod** or **sandbox** token
51
+ - **gRPC endpoint** (from docs):
52
+ - **Prod**: `invest-public-api.tbank.ru:443`
53
+ - **Sandbox**: `sandbox-invest-public-api.tbank.ru:443`
54
+
55
+ Recommended environment-variable configuration (names follow this gem's convention):
56
+
57
+ ```bash
58
+ # Prod
59
+ export TINVEST_TOKEN="..."
60
+ export TINVEST_ENDPOINT="invest-public-api.tbank.ru:443"
61
+
62
+ # or Sandbox
63
+ export TINVEST_TOKEN="..."
64
+ export TINVEST_ENDPOINT="sandbox-invest-public-api.tbank.ru:443"
65
+ ```
66
+
67
+ Optional (ENV-first configuration):
68
+
69
+ ```bash
70
+ # Application identifier (x-app-name)
71
+ export TINVEST_APP_NAME="my-app"
72
+
73
+ # Default timeout (seconds)
74
+ export TINVEST_TIMEOUT="10"
75
+
76
+ # Path to CA certs PEM (custom environments)
77
+ export TINVEST_SSL_CA_CERTS="/path/to/ca.pem"
78
+ ```
79
+
21
80
  ## Usage
22
81
 
23
- TODO: Write usage instructions here
82
+ The gem provides a global default configuration plus per-client overrides.
83
+
84
+ ### Configuration
85
+
86
+ #### ENV-first (quick start)
87
+
88
+ **Given** you have a token and selected environment (prod/sandbox)
89
+ **When** you set `TINVEST_TOKEN` and `TINVEST_ENDPOINT`
90
+ **Then** you can create a client without additional setup
91
+
92
+ ```ruby
93
+ require "t/tech/investments"
94
+
95
+ client = T::Tech::Investments.client
96
+ ```
97
+
98
+ If you installed the gem via `gem install` and see `uninitialized constant T`, make sure your script requires the gem:
99
+
100
+ ```ruby
101
+ require "t/tech/investments"
102
+ ```
103
+
104
+ #### Explicit configuration (Rails / monolith)
105
+
106
+ **Given** you want centralized configuration
107
+ **When** you call `T::Tech::Investments.configure`
108
+ **Then** new clients use these values by default
109
+
110
+ ```ruby
111
+ T::Tech::Investments.configure do |c|
112
+ c.token = ENV.fetch("TINVEST_TOKEN")
113
+ c.endpoint = ENV.fetch("TINVEST_ENDPOINT", "invest-public-api.tbank.ru:443") # or sandbox
114
+ c.timeout = 10
115
+
116
+ # optional
117
+ c.app_name = "my-app"
118
+ c.logger = Logger.new($stdout)
119
+ end
120
+
121
+ client = T::Tech::Investments.client
122
+ ```
123
+
124
+ #### Override for a single client
125
+
126
+ **Given** you need different settings for one call/process
127
+ **When** you pass parameters to `client(...)`
128
+ **Then** they apply only to that instance
129
+
130
+ ```ruby
131
+ client = T::Tech::Investments.client(
132
+ endpoint: "sandbox-invest-public-api.tbank.ru:443",
133
+ timeout: 2
134
+ )
135
+ ```
136
+
137
+ ### Configuration best practices
138
+
139
+ - **Validate token**: if missing/empty, raise `ConfigurationError` (or similar) with a clear message.
140
+ - **Do not log secrets**: token must not appear in logs or exceptions.
141
+ - **Snapshot config per client**: a created client should not pick up global config changes later (important for jobs/threads).
142
+ - **Prod vs Sandbox**: distinguish environments only by the endpoint from the docs:
143
+ - **Prod**: `invest-public-api.tbank.ru:443`
144
+ - **Sandbox**: `sandbox-invest-public-api.tbank.ru:443`
145
+
146
+ ### Unary requests (data fetch)
147
+
148
+ Unary requests in T-Invest API are standard gRPC calls: “one request → one response”.
149
+
150
+ #### Authorization and headers
151
+
152
+ Per T-Invest API docs, the token is passed in **request metadata** as a header:
153
+
154
+ - **`authorization: Bearer <token>`**
155
+
156
+ Additionally (recommended for SDK) you can pass:
157
+
158
+ - **`x-app-name`** — app/SDK identifier for analytics (as recommended in the docs)
159
+
160
+ Source: [gRPC — Authorization / AppName / trackingId](https://developer.tbank.ru/invest/intro/developer/protocols/grpc/).
161
+
162
+ #### Example (common pattern)
163
+
164
+ **Given** `TINVEST_TOKEN` and `TINVEST_ENDPOINT` are configured
165
+ **When** you call a unary service method
166
+ **Then** you get a Hash (default) or raw protobuf with `raw: true`
167
+
168
+ ```ruby
169
+ client = T::Tech::Investments.client
170
+
171
+ # Accounts list (Hash with :accounts etc.)
172
+ accounts = client.users.get_accounts
173
+ # With params and options
174
+ accounts = client.users.get_accounts(status: :open)
175
+
176
+ # Candles for an instrument (from/to — Time or Hash)
177
+ candles = client.market_data.get_candles(
178
+ figi: "BBG004730N88",
179
+ from: Time.now - 3600,
180
+ to: Time.now,
181
+ interval: :candle_interval_1_min
182
+ )
183
+
184
+ # Raw protobuf (debugging)
185
+ raw_response = client.users.get_accounts(raw: true)
186
+ ```
187
+
188
+ #### Timeouts (deadline)
189
+
190
+ For gRPC it is important to set a deadline/timeout to avoid hanging requests. Calls use `timeout` from configuration (or an override per client).
191
+
192
+ #### Tracking id (x-tracking-id)
193
+
194
+ Docs note that **`x-tracking-id` is included in unary responses**. This UUID should be provided to support when troubleshooting.
195
+
196
+ Recommended practice for the gem:
197
+
198
+ - log/propagate `x-tracking-id` on errors;
199
+ - optionally expose it via a response wrapper or instrumentation hooks.
200
+
201
+ ### Streaming (MarketData)
202
+
203
+ Bidirectional stream: in the block you send subscriptions, then `each_event` yields events. Events are Hashes with `:type` and `:data`; **ping is ignored** by the SDK.
204
+
205
+ #### Correct usage
206
+
207
+ - Treat the stream as **long-lived** and set a higher `timeout` (or override per call).
208
+ - Enqueue subscriptions **before** calling `each_event`.
209
+ - Keep `each_event` running; break the block when you want to stop the stream.
210
+
211
+ ```ruby
212
+ client.market_data.stream(timeout: 300) do |s|
213
+ s.subscribe_candles(
214
+ subscription_action: :SUBSCRIPTION_ACTION_SUBSCRIBE,
215
+ instruments: [
216
+ {
217
+ figi: "BBG004730N88",
218
+ interval: :SUBSCRIPTION_INTERVAL_ONE_MINUTE
219
+ }
220
+ ],
221
+ waiting_close: true,
222
+ candle_source_type: :CANDLE_SOURCE_EXCHANGE
223
+ )
224
+
225
+ s.each_event do |event|
226
+ p event
227
+ # break if you want to stop the stream
228
+ end
229
+ end
230
+ ```
231
+
232
+ #### Troubleshooting
233
+
234
+ If you see `SUBSCRIPTION_STATUS_SUBSCRIPTION_NOT_FOUND` or the server responds with an unsubscribe action, explicitly pass full enum values:
235
+
236
+ ```ruby
237
+ client.market_data.stream(timeout: 300) do |s|
238
+ s.subscribe_candles(
239
+ subscription_action: :SUBSCRIPTION_ACTION_SUBSCRIBE,
240
+ instruments: [
241
+ {
242
+ figi: "BBG004730N88",
243
+ interval: :SUBSCRIPTION_INTERVAL_ONE_MINUTE
244
+ }
245
+ ],
246
+ waiting_close: true,
247
+ candle_source_type: :CANDLE_SOURCE_EXCHANGE
248
+ )
249
+ s.each_event { |event| p event }
250
+ end
251
+ ```
252
+
253
+ Also ensure your **token and endpoint match the same environment** (prod vs sandbox).
254
+
255
+ ```ruby
256
+ client = T::Tech::Investments.client
257
+
258
+ client.market_data.stream do |s|
259
+ # 1) Subscriptions (subscription_action: :subscribe or :unsubscribe)
260
+ s.subscribe_candles(
261
+ subscription_action: :subscribe,
262
+ instruments: [{ figi: "BBG004730N88", interval: :subscription_interval_one_minute }],
263
+ waiting_close: true
264
+ )
265
+ s.subscribe_order_book(
266
+ subscription_action: :subscribe,
267
+ instruments: [{ figi: "BBG004730N88", depth: 10 }]
268
+ )
269
+
270
+ # 2) Read events (event[:type], event[:data])
271
+ s.each_event do |event|
272
+ case event[:type]
273
+ when :candle
274
+ puts "candle: #{event[:data].inspect}"
275
+ when :orderbook
276
+ puts "orderbook: #{event[:data].inspect}"
277
+ when :subscribe_candles_response, :subscribe_order_book_response
278
+ # subscription responses (tracking_id and statuses in event[:data])
279
+ warn "subscription: #{event[:data].inspect}"
280
+ when :trading_status, :last_price, :trade
281
+ # other types by contract
282
+ end
283
+ end
284
+ end
285
+ ```
286
+
287
+ Reconciliation: if subscriptions drift, call `s.get_my_subscriptions` and resubscribe or restart the stream as needed.
288
+
289
+ #### Stopping a stream (graceful)
290
+
291
+ If you need to stop a long-running stream from another thread or from a signal handler, call `stop`/`close` on the session to terminate the request stream cleanly:
292
+
293
+ ```ruby
294
+ session = nil
295
+ stream_thread = Thread.new do
296
+ client.market_data.stream(timeout: 300) do |s|
297
+ session = s
298
+ s.subscribe_candles(
299
+ subscription_action: :subscribe,
300
+ instruments: [{ figi: "BBG004730N88", interval: :subscription_interval_one_minute }]
301
+ )
302
+ s.each_event { |event| p event }
303
+ end
304
+ end
305
+
306
+ # later...
307
+ session&.stop
308
+ stream_thread.join
309
+ ```
310
+
311
+ #### Reconciliation example (server vs local subscriptions)
312
+
313
+ To detect drift, request server-side subscriptions and let the session reconcile:
314
+
315
+ ```ruby
316
+ client.market_data.stream(timeout: 300) do |s|
317
+ s.subscribe_candles(
318
+ subscription_action: :subscribe,
319
+ instruments: [{ figi: "BBG004730N88", interval: :subscription_interval_one_minute }]
320
+ )
321
+ s.get_my_subscriptions
322
+
323
+ s.each_event do |event|
324
+ case event[:type]
325
+ when :subscribe_candles_response
326
+ # local subscription responses
327
+ end
328
+ end
329
+ end
330
+ ```
331
+
332
+ Notes:
333
+
334
+ - Stream authorization uses metadata `authorization: Bearer <token>` (same as unary). See [gRPC — Authorization / AppName / trackingId](https://developer.tbank.ru/invest/intro/developer/protocols/grpc/).
335
+ - A single stream can send multiple subscription requests (candles, order book, trades, etc.) — each is enqueued separately.
336
+ - See [Market data service docs](https://developer.tbank.ru/invest/services/quotes/head-marketdata/).
24
337
 
25
338
  ## Development
26
339
 
27
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
340
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. To generate a coverage report (development only), run `COVERAGE=1 bundle exec rspec`; the report is written to `coverage/`. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
28
341
 
29
342
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
30
343
 
31
344
  ## Contributing
32
345
 
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).
346
+ 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
347
 
35
348
  ## License
36
349
 
@@ -38,4 +351,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
38
351
 
39
352
  ## Code of Conduct
40
353
 
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).
354
+ 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,54 @@
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 method_missing(method_name, *args, **kwargs, &block)
22
+ if Services::Registry.public_service_names.include?(method_name) &&
23
+ args.empty? && kwargs.empty? && block.nil?
24
+ return service(method_name)
25
+ end
26
+
27
+ super
28
+ end
29
+
30
+ def respond_to_missing?(method_name, include_private = false)
31
+ Services::Registry.public_service_names.include?(method_name) || super
32
+ end
33
+
34
+ private
35
+
36
+ def service(name)
37
+ if (facade_class = Services::Registry.facade_class(name))
38
+ @facades ||= {}
39
+ return @facades[name] ||= begin
40
+ ProtoLoader.load!
41
+ unary = Services::UnaryAdapter.new(Services::Registry.service_class(name), transport)
42
+ facade_class.new(unary, transport)
43
+ end
44
+ end
45
+
46
+ @services[name] ||= begin
47
+ ProtoLoader.load!
48
+ Services::UnaryAdapter.new(Services::Registry.service_class(name), transport)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end