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 +4 -4
- data/.cursor/rules/less-code.mdc +6 -0
- data/.cursor/rules/only-important-steps.mdc +5 -0
- data/.cursor/rules/sdk-architecture.mdc +3 -2
- data/CHANGELOG.md +19 -0
- data/README.md +189 -65
- data/lib/t/tech/investments/client.rb +15 -31
- data/lib/t/tech/investments/errors.rb +3 -0
- data/lib/t/tech/investments/proto_loader.rb +59 -34
- data/lib/t/tech/investments/services/contracts/market_data_stream_contract.rb +51 -0
- data/lib/t/tech/investments/services/mappers/market_data_event_mapper.rb +68 -0
- data/lib/t/tech/investments/services/market_data_facade.rb +15 -2
- data/lib/t/tech/investments/services/registry.rb +56 -14
- data/lib/t/tech/investments/services/stores/local_subscription_store.rb +54 -0
- data/lib/t/tech/investments/services/stores/subscription.rb +82 -0
- data/lib/t/tech/investments/services/stream/market_data_kinds.rb +24 -0
- data/lib/t/tech/investments/services/stream/market_data_stream_session.rb +131 -0
- data/lib/t/tech/investments/services/stream/subscription_reconciler.rb +46 -0
- data/lib/t/tech/investments/services.rb +7 -1
- data/lib/t/tech/investments/version.rb +1 -1
- data/sig/t/tech/investments.rbs +51 -1
- metadata +10 -2
- data/lib/t/tech/investments/services/market_data_stream_session.rb +0 -148
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4bf8867c825875c3ae7f360a619639d0a4625a9f32f3a068e1c3636f649fbff5
|
|
4
|
+
data.tar.gz: c062acc25e8df6ffe8503331b054a481390e051c0be0cb99a895d7dbf8bd2be8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b9e479135ca5169147c694d1c0e10369a5dda50d81e783b1a0ddb18c5cf385734e40ff1ffe31b3df12db2d9033e23ef2ebd80389d1733161b6028ff93d04d08d
|
|
7
|
+
data.tar.gz: d808c48241f5af9c48ce21709443c44555d9efec9719947c32a6aded19cb862b9611559ceaa4ed354dc83e7e9db433f661c9f932aa2f8ece215cb93cbb7de48f
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Пожалуйста, решите эту задачу с минимально возможным количеством строк кода.
|
|
3
|
+
alwaysApply: false
|
|
4
|
+
---
|
|
5
|
+
Пожалуйста, решите эту задачу с минимально возможным количеством строк кода.
|
|
6
|
+
Убедитесь, что результат функционально эквивалентен исходной цели и проходит существующие тесты или сценарии использовани
|
|
@@ -7,14 +7,15 @@ alwaysApply: true
|
|
|
7
7
|
|
|
8
8
|
## Слои
|
|
9
9
|
|
|
10
|
-
- `Client` (facade) → `Services`
|
|
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,
|
|
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
|
-
|
|
3
|
+
[](https://rubygems.org/gems/t-tech-investments)
|
|
4
|
+
[](https://www.ruby-lang.org/)
|
|
5
|
+
[](https://github.com/naveroot/t-tech-investments/actions/workflows/main.yml)
|
|
6
|
+
[](https://codecov.io/gh/naveroot/t-tech-investments)
|
|
4
7
|
|
|
5
|
-
|
|
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
|
-
|
|
46
|
+
Minimum required to start using T-Invest API:
|
|
24
47
|
|
|
25
48
|
- **Ruby**: >= 3.2
|
|
26
|
-
-
|
|
27
|
-
-
|
|
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
|
-
#
|
|
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
|
-
|
|
67
|
+
Optional (ENV-first configuration):
|
|
45
68
|
|
|
46
69
|
```bash
|
|
47
|
-
#
|
|
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
|
-
#
|
|
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**
|
|
66
|
-
**When**
|
|
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
|
-
|
|
98
|
+
If you installed the gem via `gem install` and see `uninitialized constant T`, make sure your script requires the gem:
|
|
76
99
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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") #
|
|
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**
|
|
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
|
-
-
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
- **Prod vs Sandbox**:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
+
Additionally (recommended for SDK) you can pass:
|
|
128
159
|
|
|
129
|
-
- **`x-app-name`** —
|
|
160
|
+
- **`x-app-name`** — app/SDK identifier for analytics (as recommended in the docs)
|
|
130
161
|
|
|
131
|
-
|
|
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**
|
|
136
|
-
**When**
|
|
137
|
-
**Then**
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
186
|
+
# Raw protobuf (debugging)
|
|
156
187
|
raw_response = client.users.get_accounts(raw: true)
|
|
157
188
|
```
|
|
158
189
|
|
|
159
|
-
####
|
|
190
|
+
#### Timeouts (deadline)
|
|
160
191
|
|
|
161
|
-
|
|
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
|
-
####
|
|
194
|
+
#### Tracking id (x-tracking-id)
|
|
164
195
|
|
|
165
|
-
|
|
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
|
-
-
|
|
170
|
-
-
|
|
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:
|
|
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)
|
|
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)
|
|
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
|
-
#
|
|
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:
|
|
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
|
-
-
|
|
213
|
-
-
|
|
214
|
-
-
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
38
|
-
service(:operations)
|
|
27
|
+
super
|
|
39
28
|
end
|
|
40
29
|
|
|
41
|
-
def
|
|
42
|
-
|
|
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
|
-
|
|
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!
|
|
@@ -8,40 +8,65 @@ module T
|
|
|
8
8
|
module ProtoLoader
|
|
9
9
|
PROTO_DIR = File.expand_path("proto", __dir__)
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|