t-tech-investments 0.1.1 → 0.2.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e68074e95141d63fe02be0f024fdba50ec74b935b61f95cdc35754dd368484f
4
- data.tar.gz: adcdccd09aed1a6bcc83e274f7c02cf954a5f05645740aad949027af7fe8352d
3
+ metadata.gz: 4bf8867c825875c3ae7f360a619639d0a4625a9f32f3a068e1c3636f649fbff5
4
+ data.tar.gz: c062acc25e8df6ffe8503331b054a481390e051c0be0cb99a895d7dbf8bd2be8
5
5
  SHA512:
6
- metadata.gz: 83bb6a2f390c88557029f0b349670f393faed7da128e368bec3829154490ee9921e0af9c7532278ddd34b72f800444408380ce373bdfb57827f253cb11871fb3
7
- data.tar.gz: 832db0d5502ac1d24562d9703ada3f224248778857bc551e261a88351620f8977cbb16c6e95d5b1279349209cc827cf5500f5e8edf162a241d1c32ce84aaf89d
6
+ metadata.gz: b9e479135ca5169147c694d1c0e10369a5dda50d81e783b1a0ddb18c5cf385734e40ff1ffe31b3df12db2d9033e23ef2ebd80389d1733161b6028ff93d04d08d
7
+ data.tar.gz: d808c48241f5af9c48ce21709443c44555d9efec9719947c32a6aded19cb862b9611559ceaa4ed354dc83e7e9db433f661c9f932aa2f8ece215cb93cbb7de48f
@@ -0,0 +1,6 @@
1
+ ---
2
+ description: Пожалуйста, решите эту задачу с минимально возможным количеством строк кода.
3
+ alwaysApply: false
4
+ ---
5
+ Пожалуйста, решите эту задачу с минимально возможным количеством строк кода.
6
+ Убедитесь, что результат функционально эквивалентен исходной цели и проходит существующие тесты или сценарии использовани
@@ -0,0 +1,5 @@
1
+ ---
2
+ alwaysApply: false
3
+ ---
4
+ Разбей это только на действительно необходимые шаги. Исключи любые необязательные или умозрительные функции.
5
+ Убедись, что каждый шаг напрямую соответствует текущей цели. Предоставь объяснение, почему каждый шаг необходим.
@@ -7,14 +7,15 @@ alwaysApply: true
7
7
 
8
8
  ## Слои
9
9
 
10
- - `Client` (facade) → `Services` (adapters) → `Transport` (gRPC)
10
+ - `Client` (facade) → `Services` → `Transport` (gRPC)
11
+ - `Services` подслои: `contracts` (proto-обвязка), `mappers`, `stores`, `stream`
11
12
  - Вспомогательные: `Coercers/Mappers`, `Errors`, `Proto` (сгенерёнка)
12
13
 
13
14
  ## Зависимости (жёсткие запреты)
14
15
 
15
16
  - `Services` не вызывают gRPC stub напрямую — только через `Transport`.
16
17
  - Публичный API не возвращает protobuf по умолчанию (только `raw:`/debug, если нужен).
17
- - `Transport` без бизнес-логики; только: metadata, deadlines, retries-policy, error mapping.
18
+ - `Transport` без бизнес-логики; только: metadata, deadlines, error mapping. Retry и управление состоянием соединения — **не** зона ответственности библиотеки; пользователь сам реализует повторные попытки при необходимости.
18
19
  - `Coercers/Mappers` — чистые и детерминированные (без IO/сети).
19
20
 
20
21
  ## Контракты (invest-contracts)
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.2] - Unreleased
4
+
5
+ - Version bump for next development cycle
6
+
7
+ ## [0.2.1] - 2026-02-06
8
+
9
+ - ProtoLoader: harden loading order and market data signatures; add specs
10
+ - Docs: clarify retry responsibility and client cleanup; remove stray gem artifact
11
+
12
+ ## [0.2.0] - 2026-02-06
13
+
14
+ - Client: services exposed via Registry (method_missing), no hardcoded list; facades (e.g. market_data) discovered dynamically
15
+ - ProtoLoader: load proto files dynamically from directory; common_pb first, then rest + services
16
+ - Market data stream: session stop/close, reconciliation (GetMySubscriptions → restart + resubscribe on drift), max_restarts limit and StreamRestartLimitError
17
+ - Market data: contract/mapper/store/reconciler split; subscription kinds single source of truth; candle interval validation
18
+ - Transport: allow custom CA certs for gRPC
19
+ - Dev: SimpleCov coverage (COVERAGE=1), codecov badge in README
20
+ - Docs: README stream stopping, reconciliation example, installation and usage
21
+
3
22
  ## [0.1.1] - 2026-02-05
4
23
 
5
24
  - Docs: update README with correct links and env options
data/README.md CHANGED
@@ -1,12 +1,35 @@
1
1
  # T::Tech::Investments
2
2
 
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-клиент для работы с брокерским счётом, инструментами, заявками и стримами маркет-данных.
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
@@ -20,51 +43,51 @@ gem install t-tech-investments
20
43
 
21
44
  ## Minimal requirements (Prod / Sandbox)
22
45
 
23
- Минимум, который нужен пользователю, чтобы начать работать с T-Invest API:
46
+ Minimum required to start using T-Invest API:
24
47
 
25
48
  - **Ruby**: >= 3.2
26
- - **Токен доступа**: получите токен в личном кабинете T-Bank Dev Portal (см. “Начало работы”) — [документация](https://developer.tbank.ru/invest/intro/intro/)
27
- - **Выбор контура**: используйте токен **для прода** или **для песочницы** (sandbox)
28
- - **gRPC endpoint** (из документации):
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):
29
52
  - **Prod**: `invest-public-api.tbank.ru:443`
30
53
  - **Sandbox**: `sandbox-invest-public-api.tbank.ru:443`
31
54
 
32
- Рекомендуемая форма конфигурации через переменные окружения (названия переменных соглашение этого гема):
55
+ Recommended environment-variable configuration (names follow this gem's convention):
33
56
 
34
57
  ```bash
35
58
  # Prod
36
59
  export TINVEST_TOKEN="..."
37
60
  export TINVEST_ENDPOINT="invest-public-api.tbank.ru:443"
38
61
 
39
- # или Sandbox
62
+ # or Sandbox
40
63
  export TINVEST_TOKEN="..."
41
64
  export TINVEST_ENDPOINT="sandbox-invest-public-api.tbank.ru:443"
42
65
  ```
43
66
 
44
- Опционально (ENV-first конфигурация):
67
+ Optional (ENV-first configuration):
45
68
 
46
69
  ```bash
47
- # Идентификатор приложения (x-app-name)
70
+ # Application identifier (x-app-name)
48
71
  export TINVEST_APP_NAME="my-app"
49
72
 
50
- # Таймаут по умолчанию (секунды)
73
+ # Default timeout (seconds)
51
74
  export TINVEST_TIMEOUT="10"
52
75
 
53
- # Путь к PEM с CA-сертификатами (для кастомных окружений)
76
+ # Path to CA certs PEM (custom environments)
54
77
  export TINVEST_SSL_CA_CERTS="/path/to/ca.pem"
55
78
  ```
56
79
 
57
80
  ## Usage
58
81
 
59
- Гем предлагает “глобальную” конфигурацию по умолчанию + возможность переопределить параметры при создании клиента.
82
+ The gem provides a global default configuration plus per-client overrides.
60
83
 
61
84
  ### Configuration
62
85
 
63
- #### ENV-first (быстрый старт)
86
+ #### ENV-first (quick start)
64
87
 
65
- **Given** у вас есть токен и выбран контур (prod/sandbox)
66
- **When** вы задаёте `TINVEST_TOKEN` и `TINVEST_ENDPOINT`
67
- **Then** вы можете создать клиента без дополнительной настройки
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
68
91
 
69
92
  ```ruby
70
93
  require "t/tech/investments"
@@ -72,19 +95,25 @@ require "t/tech/investments"
72
95
  client = T::Tech::Investments.client
73
96
  ```
74
97
 
75
- #### Явная конфигурация (Rails / monolith)
98
+ If you installed the gem via `gem install` and see `uninitialized constant T`, make sure your script requires the gem:
76
99
 
77
- **Given** вы хотите централизованную настройку
78
- **When** вы вызываете `T::Tech::Investments.configure`
79
- **Then** новые клиенты используют эти значения по умолчанию
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
80
109
 
81
110
  ```ruby
82
111
  T::Tech::Investments.configure do |c|
83
112
  c.token = ENV.fetch("TINVEST_TOKEN")
84
- c.endpoint = ENV.fetch("TINVEST_ENDPOINT", "invest-public-api.tbank.ru:443") # или sandbox
113
+ c.endpoint = ENV.fetch("TINVEST_ENDPOINT", "invest-public-api.tbank.ru:443") # or sandbox
85
114
  c.timeout = 10
86
115
 
87
- # опционально
116
+ # optional
88
117
  c.app_name = "my-app"
89
118
  c.logger = Logger.new($stdout)
90
119
  end
@@ -92,11 +121,11 @@ end
92
121
  client = T::Tech::Investments.client
93
122
  ```
94
123
 
95
- #### Override для одного клиента
124
+ #### Override for a single client
96
125
 
97
- **Given** вам нужно изменить настройки только для одного вызова/процесса
98
- **When** вы передаёте параметры в `client(...)`
99
- **Then** они применяются только к этому экземпляру клиента
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
100
129
 
101
130
  ```ruby
102
131
  client = T::Tech::Investments.client(
@@ -107,44 +136,46 @@ client = T::Tech::Investments.client(
107
136
 
108
137
  ### Configuration best practices
109
138
 
110
- - **Валидируйте токен**: если токена нет/он пустой — поднимайте `ConfigurationError` (или аналог) с понятным сообщением.
111
- - **Не логируйте секреты**: токен не должен попадать в логи/исключения.
112
- - **Снимок конфигурации при создании клиента**: созданный клиент не должен “подхватывать” изменения глобального конфига на лету (важно для потоков/фоновых задач).
113
- - **Prod vs Sandbox**: различайте контуры только endpoint’ом из документации:
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:
114
143
  - **Prod**: `invest-public-api.tbank.ru:443`
115
144
  - **Sandbox**: `sandbox-invest-public-api.tbank.ru:443`
145
+ - **Retries and connection state**: the library does not retry failed calls. The application is responsible for retries, reconnects, and connection state; implement retry logic in your code if needed.
146
+ - **Long-lived processes**: when shutting down or discarding a client, call `client.transport.close` to release gRPC channel and stubs.
116
147
 
117
- ### Unary requests (получение данных)
148
+ ### Unary requests (data fetch)
118
149
 
119
- Унарные запросы в TInvest API это обычные gRPC-вызовы “один запросодин ответ”.
150
+ Unary requests in T-Invest API are standard gRPC calls: “one request one response”.
120
151
 
121
- #### Авторизация и служебные заголовки
152
+ #### Authorization and headers
122
153
 
123
- По документации TInvest API токен нужно передавать в **metadata каждого запроса** в виде заголовка:
154
+ Per T-Invest API docs, the token is passed in **request metadata** as a header:
124
155
 
125
156
  - **`authorization: Bearer <token>`**
126
157
 
127
- Дополнительно (рекомендуется для SDK) можно передавать:
158
+ Additionally (recommended for SDK) you can pass:
128
159
 
129
- - **`x-app-name`** — идентификатор приложения/SDK для статистики (рекомендации в доке)
160
+ - **`x-app-name`** — app/SDK identifier for analytics (as recommended in the docs)
130
161
 
131
- Источник: [gRPC — Авторизация / AppName / trackingId](https://developer.tbank.ru/invest/intro/developer/protocols/grpc/).
162
+ Source: [gRPC — Authorization / AppName / trackingId](https://developer.tbank.ru/invest/intro/developer/protocols/grpc/).
132
163
 
133
- #### Пример (общий паттерн)
164
+ #### Example (common pattern)
134
165
 
135
- **Given** у вас настроены `TINVEST_TOKEN` и `TINVEST_ENDPOINT`
136
- **When** вы вызываете унарный метод сервиса
137
- **Then** вы получаете Hash (по умолчанию) или сырой protobuf при `raw: true`
166
+ **Given** `TINVEST_TOKEN` and `TINVEST_ENDPOINT` are configured
167
+ **When** you call a unary service method
168
+ **Then** you get a Hash (default) or raw protobuf with `raw: true`
138
169
 
139
170
  ```ruby
140
171
  client = T::Tech::Investments.client
141
172
 
142
- # Список аккаунтов (Hash с :accounts и т.д.)
173
+ # Accounts list (Hash with :accounts etc.)
143
174
  accounts = client.users.get_accounts
144
- # С параметрами и опциями
175
+ # With params and options
145
176
  accounts = client.users.get_accounts(status: :open)
146
177
 
147
- # Свечи по инструменту (from/to — Time или Hash)
178
+ # Candles for an instrument (from/to — Time or Hash)
148
179
  candles = client.market_data.get_candles(
149
180
  figi: "BBG004730N88",
150
181
  from: Time.now - 3600,
@@ -152,32 +183,82 @@ candles = client.market_data.get_candles(
152
183
  interval: :candle_interval_1_min
153
184
  )
154
185
 
155
- # Сырой protobuf (для отладки)
186
+ # Raw protobuf (debugging)
156
187
  raw_response = client.users.get_accounts(raw: true)
157
188
  ```
158
189
 
159
- #### Таймауты (deadline)
190
+ #### Timeouts (deadline)
160
191
 
161
- Для gRPC важно задавать deadline/timeout, чтобы запросы не зависали бесконечно. Вызов должен использовать значение `timeout` из конфигурации (или override при создании клиента).
192
+ For gRPC it is important to set a deadline/timeout to avoid hanging requests. Calls use `timeout` from configuration (or an override per client).
162
193
 
163
- #### tracking id (x-tracking-id)
194
+ #### Tracking id (x-tracking-id)
164
195
 
165
- Документация отмечает, что **`x-tracking-id` добавляется в ответы unary-методов**. Это UUID запроса, который нужно указывать при обращении в техподдержку.
196
+ Docs note that **`x-tracking-id` is included in unary responses**. This UUID should be provided to support when troubleshooting.
166
197
 
167
- Рекомендуемая практика для гема:
198
+ Recommended practice for the gem:
168
199
 
169
- - логировать/пробрасывать `x-tracking-id` при ошибках;
170
- - (опционально) предоставлять его пользователю через объект-обёртку ответа или через instrumentation hooks.
200
+ - log/propagate `x-tracking-id` on errors;
201
+ - optionally expose it via a response wrapper or instrumentation hooks.
171
202
 
172
203
  ### Streaming (MarketData)
173
204
 
174
- Bidirectional stream: в блоке задаёте подписки, затем `each_event` отдаёт события. События Hash с `:type` и `:data`; **ping игнорируется** SDK.
205
+ 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.
206
+
207
+ #### Correct usage
208
+
209
+ - Treat the stream as **long-lived** and set a higher `timeout` (or override per call).
210
+ - Enqueue subscriptions **before** calling `each_event`.
211
+ - Keep `each_event` running; break the block when you want to stop the stream.
212
+
213
+ ```ruby
214
+ client.market_data.stream(timeout: 300) do |s|
215
+ s.subscribe_candles(
216
+ subscription_action: :SUBSCRIPTION_ACTION_SUBSCRIBE,
217
+ instruments: [
218
+ {
219
+ figi: "BBG004730N88",
220
+ interval: :SUBSCRIPTION_INTERVAL_ONE_MINUTE
221
+ }
222
+ ],
223
+ waiting_close: true,
224
+ candle_source_type: :CANDLE_SOURCE_EXCHANGE
225
+ )
226
+
227
+ s.each_event do |event|
228
+ p event
229
+ # break if you want to stop the stream
230
+ end
231
+ end
232
+ ```
233
+
234
+ #### Troubleshooting
235
+
236
+ If you see `SUBSCRIPTION_STATUS_SUBSCRIPTION_NOT_FOUND` or the server responds with an unsubscribe action, explicitly pass full enum values:
237
+
238
+ ```ruby
239
+ client.market_data.stream(timeout: 300) do |s|
240
+ s.subscribe_candles(
241
+ subscription_action: :SUBSCRIPTION_ACTION_SUBSCRIBE,
242
+ instruments: [
243
+ {
244
+ figi: "BBG004730N88",
245
+ interval: :SUBSCRIPTION_INTERVAL_ONE_MINUTE
246
+ }
247
+ ],
248
+ waiting_close: true,
249
+ candle_source_type: :CANDLE_SOURCE_EXCHANGE
250
+ )
251
+ s.each_event { |event| p event }
252
+ end
253
+ ```
254
+
255
+ Also ensure your **token and endpoint match the same environment** (prod vs sandbox).
175
256
 
176
257
  ```ruby
177
258
  client = T::Tech::Investments.client
178
259
 
179
260
  client.market_data.stream do |s|
180
- # 1) Подписки (subscription_action: :subscribe или :unsubscribe)
261
+ # 1) Subscriptions (subscription_action: :subscribe or :unsubscribe)
181
262
  s.subscribe_candles(
182
263
  subscription_action: :subscribe,
183
264
  instruments: [{ figi: "BBG004730N88", interval: :subscription_interval_one_minute }],
@@ -188,7 +269,7 @@ client.market_data.stream do |s|
188
269
  instruments: [{ figi: "BBG004730N88", depth: 10 }]
189
270
  )
190
271
 
191
- # 2) Чтение событий (event[:type], event[:data])
272
+ # 2) Read events (event[:type], event[:data])
192
273
  s.each_event do |event|
193
274
  case event[:type]
194
275
  when :candle
@@ -196,26 +277,69 @@ client.market_data.stream do |s|
196
277
  when :orderbook
197
278
  puts "orderbook: #{event[:data].inspect}"
198
279
  when :subscribe_candles_response, :subscribe_order_book_response
199
- # ответы подписки (tracking_id и статусы в event[:data])
280
+ # subscription responses (tracking_id and statuses in event[:data])
200
281
  warn "subscription: #{event[:data].inspect}"
201
282
  when :trading_status, :last_price, :trade
202
- # остальные типы по контракту
283
+ # other types by contract
203
284
  end
204
285
  end
205
286
  end
206
287
  ```
207
288
 
208
- Reconciliation: при рассинхроне подписок вызовите `s.get_my_subscriptions` и при необходимости переподпишитесь или перезапустите стрим.
289
+ Reconciliation: if subscriptions drift, call `s.get_my_subscriptions` and resubscribe or restart the stream as needed.
290
+
291
+ #### Stopping a stream (graceful)
292
+
293
+ 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:
294
+
295
+ ```ruby
296
+ session = nil
297
+ stream_thread = Thread.new do
298
+ client.market_data.stream(timeout: 300) do |s|
299
+ session = s
300
+ s.subscribe_candles(
301
+ subscription_action: :subscribe,
302
+ instruments: [{ figi: "BBG004730N88", interval: :subscription_interval_one_minute }]
303
+ )
304
+ s.each_event { |event| p event }
305
+ end
306
+ end
307
+
308
+ # later...
309
+ session&.stop
310
+ stream_thread.join
311
+ ```
312
+
313
+ #### Reconciliation example (server vs local subscriptions)
314
+
315
+ To detect drift, request server-side subscriptions and let the session reconcile:
316
+
317
+ ```ruby
318
+ client.market_data.stream(timeout: 300) do |s|
319
+ s.subscribe_candles(
320
+ subscription_action: :subscribe,
321
+ instruments: [{ figi: "BBG004730N88", interval: :subscription_interval_one_minute }]
322
+ )
323
+ s.get_my_subscriptions
324
+
325
+ s.each_event do |event|
326
+ case event[:type]
327
+ when :subscribe_candles_response
328
+ # local subscription responses
329
+ end
330
+ end
331
+ end
332
+ ```
209
333
 
210
- Примечания:
334
+ Notes:
211
335
 
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/).
336
+ - Stream authorization uses metadata `authorization: Bearer <token>` (same as unary). See [gRPC — Authorization / AppName / trackingId](https://developer.tbank.ru/invest/intro/developer/protocols/grpc/).
337
+ - A single stream can send multiple subscription requests (candles, order book, trades, etc.) — each is enqueued separately.
338
+ - See [Market data service docs](https://developer.tbank.ru/invest/services/quotes/head-marketdata/).
215
339
 
216
340
  ## Development
217
341
 
218
- 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.
342
+ 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.
219
343
 
220
344
  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).
221
345
 
@@ -18,46 +18,30 @@ module T
18
18
  @transport ||= Transport.new(config)
19
19
  end
20
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)
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)
34
25
  end
35
- end
36
26
 
37
- def operations
38
- service(:operations)
27
+ super
39
28
  end
40
29
 
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)
30
+ def respond_to_missing?(method_name, include_private = false)
31
+ Services::Registry.public_service_names.include?(method_name) || super
55
32
  end
56
33
 
57
34
  private
58
35
 
59
36
  def service(name)
60
- return market_data if name == :market_data
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
61
45
 
62
46
  @services[name] ||= begin
63
47
  ProtoLoader.load!
@@ -18,6 +18,9 @@ module T
18
18
  @tracking_id = tracking_id
19
19
  end
20
20
  end
21
+
22
+ # Raised when the market data stream exceeds the configured max restarts (reconnect loop).
23
+ class StreamRestartLimitError < Base; end
21
24
  end
22
25
  end
23
26
  end
@@ -8,40 +8,65 @@ module T
8
8
  module ProtoLoader
9
9
  PROTO_DIR = File.expand_path("proto", __dir__)
10
10
 
11
- # Contract _pb.rb (messages); order respects dependencies.
12
- PB_FILES = %w[
13
- common_pb
14
- instruments_pb
15
- marketdata_pb
16
- operations_pb
17
- orders_pb
18
- sandbox_pb
19
- signals_pb
20
- stoporders_pb
21
- users_pb
22
- ].freeze
23
-
24
- # Service definitions (depend on _pb).
25
- SERVICES_FILES = %w[
26
- users_services_pb
27
- instruments_services_pb
28
- marketdata_services_pb
29
- operations_services_pb
30
- orders_services_pb
31
- sandbox_services_pb
32
- signals_services_pb
33
- stoporders_services_pb
34
- ].freeze
35
-
36
- module_function
37
-
38
- def load!
39
- return if @loaded
40
-
41
- $LOAD_PATH.unshift(PROTO_DIR) unless $LOAD_PATH.include?(PROTO_DIR)
42
- PB_FILES.each { |f| require f }
43
- SERVICES_FILES.each { |f| require f }
44
- @loaded = true
11
+ @load_mutex = Mutex.new
12
+ @load_cv = ConditionVariable.new
13
+ @loaded = false
14
+ @loading = false
15
+ @loading_thread = nil
16
+ @load_error = nil
17
+
18
+ class << self
19
+ def load!
20
+ @load_mutex.synchronize do
21
+ return if @loaded
22
+
23
+ if @loading
24
+ if @loading_thread == Thread.current
25
+ raise RuntimeError, "ProtoLoader.load! re-entrant call while loading"
26
+ end
27
+ @load_cv.wait(@load_mutex) while @loading
28
+ return if @loaded
29
+ raise @load_error if @load_error
30
+ end
31
+
32
+ @load_error = nil
33
+ @loading = true
34
+ @loading_thread = Thread.current
35
+ end
36
+
37
+ begin
38
+ load_files
39
+ @load_mutex.synchronize do
40
+ @loaded = true
41
+ @loading = false
42
+ @loading_thread = nil
43
+ @load_cv.broadcast
44
+ end
45
+ rescue StandardError => e
46
+ @load_mutex.synchronize do
47
+ @load_error = e
48
+ @loading = false
49
+ @loading_thread = nil
50
+ @load_cv.broadcast
51
+ end
52
+ raise
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def load_files
59
+ $LOAD_PATH.unshift(PROTO_DIR) unless $LOAD_PATH.include?(PROTO_DIR)
60
+ pb_files = Dir[File.join(PROTO_DIR, "*_pb.rb")]
61
+ .reject { |f| f.end_with?("_services_pb.rb") }
62
+ .map { |f| File.basename(f, ".rb") }
63
+ .sort
64
+ services_files = Dir[File.join(PROTO_DIR, "*_services_pb.rb")]
65
+ .map { |f| File.basename(f, ".rb") }
66
+ .sort
67
+ pb_files = pb_files.partition { |f| f == "common_pb" }.flatten
68
+ (pb_files + services_files).each { |f| require f }
69
+ end
45
70
  end
46
71
  end
47
72
  end