coindcx-client 0.1.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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +55 -0
- data/.github/workflows/release.yml +138 -0
- data/.rubocop.yml +56 -0
- data/AGENT.md +352 -0
- data/README.md +224 -0
- data/bin/console +59 -0
- data/docs/README.md +29 -0
- data/docs/coindcx_docs_gaps.md +3 -0
- data/docs/core.md +179 -0
- data/docs/rails_integration.md +151 -0
- data/docs/standalone_bot.md +159 -0
- data/lib/coindcx/auth/signer.rb +48 -0
- data/lib/coindcx/client.rb +44 -0
- data/lib/coindcx/configuration.rb +108 -0
- data/lib/coindcx/contracts/channel_name.rb +23 -0
- data/lib/coindcx/contracts/identifiers.rb +36 -0
- data/lib/coindcx/contracts/order_request.rb +120 -0
- data/lib/coindcx/contracts/socket_backend.rb +19 -0
- data/lib/coindcx/contracts/wallet_transfer_request.rb +46 -0
- data/lib/coindcx/errors/base_error.rb +54 -0
- data/lib/coindcx/logging/null_logger.rb +12 -0
- data/lib/coindcx/logging/structured_logger.rb +17 -0
- data/lib/coindcx/models/balance.rb +8 -0
- data/lib/coindcx/models/base_model.rb +31 -0
- data/lib/coindcx/models/instrument.rb +8 -0
- data/lib/coindcx/models/market.rb +8 -0
- data/lib/coindcx/models/order.rb +8 -0
- data/lib/coindcx/models/trade.rb +8 -0
- data/lib/coindcx/rest/base_resource.rb +35 -0
- data/lib/coindcx/rest/funding/facade.rb +18 -0
- data/lib/coindcx/rest/funding/orders.rb +46 -0
- data/lib/coindcx/rest/futures/facade.rb +29 -0
- data/lib/coindcx/rest/futures/market_data.rb +71 -0
- data/lib/coindcx/rest/futures/orders.rb +47 -0
- data/lib/coindcx/rest/futures/positions.rb +93 -0
- data/lib/coindcx/rest/futures/wallets.rb +44 -0
- data/lib/coindcx/rest/margin/facade.rb +17 -0
- data/lib/coindcx/rest/margin/orders.rb +57 -0
- data/lib/coindcx/rest/public/facade.rb +17 -0
- data/lib/coindcx/rest/public/market_data.rb +52 -0
- data/lib/coindcx/rest/spot/facade.rb +17 -0
- data/lib/coindcx/rest/spot/orders.rb +67 -0
- data/lib/coindcx/rest/transfers/facade.rb +17 -0
- data/lib/coindcx/rest/transfers/wallets.rb +40 -0
- data/lib/coindcx/rest/user/accounts.rb +17 -0
- data/lib/coindcx/rest/user/facade.rb +17 -0
- data/lib/coindcx/transport/circuit_breaker.rb +65 -0
- data/lib/coindcx/transport/http_client.rb +290 -0
- data/lib/coindcx/transport/rate_limit_registry.rb +65 -0
- data/lib/coindcx/transport/request_policy.rb +152 -0
- data/lib/coindcx/transport/response_normalizer.rb +40 -0
- data/lib/coindcx/transport/retry_policy.rb +79 -0
- data/lib/coindcx/utils/payload.rb +51 -0
- data/lib/coindcx/version.rb +5 -0
- data/lib/coindcx/ws/connection_manager.rb +423 -0
- data/lib/coindcx/ws/connection_state.rb +75 -0
- data/lib/coindcx/ws/parsers/order_book_snapshot.rb +42 -0
- data/lib/coindcx/ws/private_channels.rb +38 -0
- data/lib/coindcx/ws/public_channels.rb +92 -0
- data/lib/coindcx/ws/socket_io_client.rb +89 -0
- data/lib/coindcx/ws/socket_io_simple_backend.rb +63 -0
- data/lib/coindcx/ws/subscription_registry.rb +80 -0
- data/lib/coindcx/ws/uri_ruby3_compat.rb +13 -0
- data/lib/coindcx.rb +63 -0
- data/spec/auth_signer_spec.rb +22 -0
- data/spec/client_spec.rb +19 -0
- data/spec/contracts/order_request_spec.rb +136 -0
- data/spec/contracts/wallet_transfer_request_spec.rb +45 -0
- data/spec/models/base_model_spec.rb +18 -0
- data/spec/rest/funding/orders_spec.rb +43 -0
- data/spec/rest/futures/market_data_spec.rb +49 -0
- data/spec/rest/futures/orders_spec.rb +107 -0
- data/spec/rest/futures/positions_spec.rb +57 -0
- data/spec/rest/futures/wallets_spec.rb +44 -0
- data/spec/rest/margin/orders_spec.rb +87 -0
- data/spec/rest/public/market_data_spec.rb +31 -0
- data/spec/rest/spot/orders_spec.rb +152 -0
- data/spec/rest/transfers/wallets_spec.rb +33 -0
- data/spec/rest/user/accounts_spec.rb +21 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/transport/http_client_spec.rb +232 -0
- data/spec/transport/rate_limit_registry_spec.rb +28 -0
- data/spec/transport/request_policy_spec.rb +67 -0
- data/spec/transport/response_normalizer_spec.rb +63 -0
- data/spec/ws/connection_manager_spec.rb +339 -0
- data/spec/ws/order_book_snapshot_spec.rb +25 -0
- data/spec/ws/private_channels_spec.rb +28 -0
- data/spec/ws/public_channels_spec.rb +89 -0
- data/spec/ws/socket_io_client_spec.rb +229 -0
- data/spec/ws/socket_io_simple_backend_spec.rb +41 -0
- data/spec/ws/uri_ruby3_compat_spec.rb +12 -0
- metadata +164 -0
data/README.md
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# coindcx-client
|
|
2
|
+
|
|
3
|
+
`coindcx-client` is a CoinDCX-specific Ruby client built from a layered exchange-client architecture rather than a thin wrapper.
|
|
4
|
+
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
- [Docs index](./docs/README.md)
|
|
8
|
+
- [Core usage](./docs/core.md)
|
|
9
|
+
- [Rails integration](./docs/rails_integration.md)
|
|
10
|
+
- [Standalone trading bot](./docs/standalone_bot.md)
|
|
11
|
+
|
|
12
|
+
## Design goals
|
|
13
|
+
|
|
14
|
+
- keep transport, auth, resources, models, and websockets separate
|
|
15
|
+
- model CoinDCX namespaces explicitly: public, spot, margin, user, transfers, and futures
|
|
16
|
+
- keep the gem stateless and leave strategy, position tracking, and risk logic to the host app
|
|
17
|
+
- preserve CoinDCX websocket constraints instead of flattening them into a generic websocket abstraction
|
|
18
|
+
- enforce endpoint-aware rate limiting at the transport boundary
|
|
19
|
+
- fail with structured errors that trading code can classify cleanly
|
|
20
|
+
|
|
21
|
+
## Structure
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
lib/coindcx.rb
|
|
25
|
+
lib/coindcx/version.rb
|
|
26
|
+
lib/coindcx/configuration.rb
|
|
27
|
+
lib/coindcx/client.rb
|
|
28
|
+
lib/coindcx/transport/
|
|
29
|
+
lib/coindcx/errors/
|
|
30
|
+
lib/coindcx/auth/
|
|
31
|
+
lib/coindcx/rest/
|
|
32
|
+
lib/coindcx/ws/
|
|
33
|
+
lib/coindcx/models/
|
|
34
|
+
lib/coindcx/contracts/
|
|
35
|
+
lib/coindcx/utils/
|
|
36
|
+
docs/
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
gem 'coindcx-client'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For local development:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
bundle install
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Configuration
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
require 'logger'
|
|
55
|
+
require 'coindcx'
|
|
56
|
+
|
|
57
|
+
CoinDCX.configure do |config|
|
|
58
|
+
config.api_key = ENV.fetch('COINDCX_API_KEY')
|
|
59
|
+
config.api_secret = ENV.fetch('COINDCX_API_SECRET')
|
|
60
|
+
config.logger = Logger.new($stdout)
|
|
61
|
+
|
|
62
|
+
# HTTP retry tuning
|
|
63
|
+
config.max_retries = 2
|
|
64
|
+
config.retry_base_interval = 0.25
|
|
65
|
+
config.market_data_retry_budget = 2
|
|
66
|
+
config.private_read_retry_budget = 1
|
|
67
|
+
config.idempotent_order_retry_budget = 1
|
|
68
|
+
|
|
69
|
+
# Socket reconnect tuning
|
|
70
|
+
config.socket_reconnect_attempts = 5
|
|
71
|
+
config.socket_reconnect_interval = 1.0
|
|
72
|
+
config.socket_heartbeat_interval = 10.0
|
|
73
|
+
config.socket_liveness_timeout = 60.0
|
|
74
|
+
|
|
75
|
+
# Critical order-endpoint protection
|
|
76
|
+
config.circuit_breaker_threshold = 3
|
|
77
|
+
config.circuit_breaker_cooldown = 30.0
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
By default the websocket layer uses `socket.io-client-simple`. You can still override the backend with `socket_io_backend_factory` when you need a custom adapter.
|
|
82
|
+
|
|
83
|
+
## REST usage
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
client = CoinDCX.client
|
|
87
|
+
|
|
88
|
+
client.public.market_data.list_tickers
|
|
89
|
+
client.public.market_data.list_market_details
|
|
90
|
+
client.public.market_data.list_trades(pair: 'B-BTC_USDT', limit: 50)
|
|
91
|
+
|
|
92
|
+
client.spot.orders.create(
|
|
93
|
+
side: 'buy',
|
|
94
|
+
order_type: 'limit_order',
|
|
95
|
+
market: 'SNTBTC',
|
|
96
|
+
price_per_unit: '0.03244',
|
|
97
|
+
total_quantity: 400,
|
|
98
|
+
client_order_id: SecureRandom.uuid
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
client.user.accounts.list_balances
|
|
102
|
+
client.transfers.wallets.transfer(
|
|
103
|
+
source_wallet_type: 'spot',
|
|
104
|
+
destination_wallet_type: 'futures',
|
|
105
|
+
currency_short_name: 'USDT',
|
|
106
|
+
amount: 1
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
client.futures.market_data.list_active_instruments(margin_currency_short_names: ['USDT'])
|
|
110
|
+
client.futures.market_data.fetch_instrument(pair: 'B-BTC_USDT', margin_currency_short_name: 'USDT')
|
|
111
|
+
client.futures.market_data.current_prices
|
|
112
|
+
client.futures.market_data.stats(pair: 'B-BTC_USDT')
|
|
113
|
+
client.futures.market_data.conversions
|
|
114
|
+
client.futures.orders.list(status: 'open', margin_currency_short_name: ['USDT'])
|
|
115
|
+
client.futures.orders.list_trades(page: 1, size: 50)
|
|
116
|
+
|
|
117
|
+
client.funding.orders.list
|
|
118
|
+
client.funding.orders.lend(currency_short_name: 'USDT', amount: '10')
|
|
119
|
+
client.funding.orders.settle(id: 'funding-order-id')
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Websocket usage
|
|
123
|
+
|
|
124
|
+
CoinDCX documents Socket.io for websocket access. This gem keeps that boundary explicit and now tracks connection state, heartbeat liveness, private auth renewal, and subscription replay after reconnects.
|
|
125
|
+
|
|
126
|
+
Socket.IO often delivers **multiple data arguments** after the event name (for example a channel string plus the quote object). The client **coalesces** those into a single Hash (merging multiple Hash frames) before invoking your block, so handlers always see one payload object.
|
|
127
|
+
|
|
128
|
+
Live streams frequently wrap quotes as `{ "event" => "price-change", "data" => "<JSON string>" }`. The client **parses** that `data` string and **merges** the inner object to the top level before dispatch, so fields like `p` and `s` are directly on the hash passed to your block.
|
|
129
|
+
|
|
130
|
+
**Fan-out:** one underlying listener is registered per `event_name` (for example all `price-change` subscriptions share it). Every handler for that event runs on **every** message. For multiple instruments, filter using payload hints such as `s`, `pair`, or `market` (see `scripts/futures_ws_subscription_smoke.rb`).
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
client = CoinDCX.client
|
|
134
|
+
prices_channel = CoinDCX::WS::PublicChannels.price_stats(pair: 'B-BTC_USDT')
|
|
135
|
+
|
|
136
|
+
client.ws.connect
|
|
137
|
+
client.ws.subscribe_public(channel_name: prices_channel, event_name: 'price-change') do |payload|
|
|
138
|
+
puts payload
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Trading safety rules
|
|
143
|
+
|
|
144
|
+
- Always supply `client_order_id` when placing orders. The gem will not retry mutable order creation without it.
|
|
145
|
+
- Persist `client_order_id` in your host application so a timeout can be reconciled safely.
|
|
146
|
+
- Order create and transfer requests validate required fields before sending them to CoinDCX.
|
|
147
|
+
- WebSocket subscriptions are replayed automatically after reconnect, and private subscriptions rebuild auth payloads on every reconnect.
|
|
148
|
+
- WebSocket delivery is `at_least_once`. Consumers must tolerate duplicates after reconnect.
|
|
149
|
+
- The gem does not guarantee lossless recovery of events missed while CoinDCX was disconnected.
|
|
150
|
+
|
|
151
|
+
Private subscriptions use the documented `coindcx` channel signing flow:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
client.ws.subscribe_private(event_name: CoinDCX::WS::PrivateChannels::ORDER_UPDATE_EVENT) do |payload|
|
|
155
|
+
puts payload
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Error handling
|
|
160
|
+
|
|
161
|
+
The transport raises structured errors so calling code can branch intentionally without parsing strings:
|
|
162
|
+
|
|
163
|
+
- `CoinDCX::Errors::AuthError`
|
|
164
|
+
- `CoinDCX::Errors::RateLimitError`
|
|
165
|
+
- `CoinDCX::Errors::RetryableRateLimitError`
|
|
166
|
+
- `CoinDCX::Errors::RemoteValidationError`
|
|
167
|
+
- `CoinDCX::Errors::UpstreamServerError`
|
|
168
|
+
- `CoinDCX::Errors::TransportError`
|
|
169
|
+
- `CoinDCX::Errors::CircuitOpenError`
|
|
170
|
+
- `CoinDCX::Errors::RequestError`
|
|
171
|
+
- `CoinDCX::Errors::SocketConnectionError`
|
|
172
|
+
- `CoinDCX::Errors::SocketAuthenticationError`
|
|
173
|
+
- `CoinDCX::Errors::SocketStateError`
|
|
174
|
+
|
|
175
|
+
Every API error exposes normalized metadata through `status`, `category`, `code`, `request_context`, and `retryable`.
|
|
176
|
+
|
|
177
|
+
## Rate limiting
|
|
178
|
+
|
|
179
|
+
`CoinDCX::Configuration` ships with named buckets for authenticated endpoint families and enforces them before the request is sent.
|
|
180
|
+
|
|
181
|
+
Read and write paths are separated so market data traffic does not consume order-placement capacity. Private endpoints require an explicit bucket definition.
|
|
182
|
+
|
|
183
|
+
Spot order buckets include:
|
|
184
|
+
|
|
185
|
+
- `spot_create_order_multiple`
|
|
186
|
+
- `spot_create_order`
|
|
187
|
+
- `spot_cancel_all`
|
|
188
|
+
- `spot_order_status_multiple`
|
|
189
|
+
- `spot_order_status`
|
|
190
|
+
- `spot_cancel_multiple_by_id`
|
|
191
|
+
- `spot_cancel_order`
|
|
192
|
+
- `spot_active_order`
|
|
193
|
+
- `spot_active_order_count`
|
|
194
|
+
- `spot_trade_history`
|
|
195
|
+
- `spot_edit_price`
|
|
196
|
+
|
|
197
|
+
Additional private endpoint families ship with conservative defaults until exchange-specific limits are confirmed.
|
|
198
|
+
|
|
199
|
+
## Stateless boundary
|
|
200
|
+
|
|
201
|
+
This gem is intentionally limited to:
|
|
202
|
+
|
|
203
|
+
- API calls
|
|
204
|
+
- signing
|
|
205
|
+
- socket connection management
|
|
206
|
+
- lightweight parsing and typed facades
|
|
207
|
+
|
|
208
|
+
This gem intentionally does **not** own:
|
|
209
|
+
|
|
210
|
+
- position tracking
|
|
211
|
+
- order lifecycle orchestration outside the API response shape
|
|
212
|
+
- strategy logic
|
|
213
|
+
- risk management
|
|
214
|
+
- application caching
|
|
215
|
+
- persistence of idempotency keys
|
|
216
|
+
- reconciliation of missed websocket events after downtime
|
|
217
|
+
|
|
218
|
+
## Notes
|
|
219
|
+
|
|
220
|
+
- spot market data stays under `rest/public`
|
|
221
|
+
- futures market data lives under `rest/futures`, even when it uses public hosts
|
|
222
|
+
- websocket order book parsing is snapshot-oriented and preserves CoinDCX's "up to 50 recent orders" constraint
|
|
223
|
+
- the websocket layer uses Socket.io and does not masquerade as a plain websocket client
|
|
224
|
+
- release tags are expected to be immutable once published
|
data/bin/console
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# IRB with CoinDCX loaded and configured from ENV (and optional repo-root .env).
|
|
5
|
+
#
|
|
6
|
+
# bundle exec bin/console
|
|
7
|
+
#
|
|
8
|
+
# Typical ENV: COINDCX_API_KEY, COINDCX_API_SECRET
|
|
9
|
+
# Optional: COINDCX_API_BASE_URL, COINDCX_PUBLIC_BASE_URL, COINDCX_SOCKET_BASE_URL
|
|
10
|
+
|
|
11
|
+
require "bundler/setup"
|
|
12
|
+
require_relative "../lib/coindcx"
|
|
13
|
+
|
|
14
|
+
# Socket.IO delivers events on the WebSocket thread; IRB often delays plain puts until you press Enter.
|
|
15
|
+
$stdout.sync = true
|
|
16
|
+
$stderr.sync = true
|
|
17
|
+
|
|
18
|
+
root = File.expand_path("..", __dir__)
|
|
19
|
+
env_path = File.join(root, ".env")
|
|
20
|
+
if File.file?(env_path)
|
|
21
|
+
File.foreach(env_path) do |line|
|
|
22
|
+
line = line.strip
|
|
23
|
+
next if line.empty? || line.start_with?("#")
|
|
24
|
+
|
|
25
|
+
key, _, value = line.partition("=")
|
|
26
|
+
next if key.empty?
|
|
27
|
+
|
|
28
|
+
ENV[key.strip] = value.strip.gsub(/\A["']|["']\z/, "")
|
|
29
|
+
end
|
|
30
|
+
warn "Loaded #{env_path}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
CoinDCX.configure do |config|
|
|
34
|
+
config.api_key = ENV.fetch("COINDCX_API_KEY", nil)
|
|
35
|
+
config.api_secret = ENV.fetch("COINDCX_API_SECRET", nil)
|
|
36
|
+
unless (v = ENV["COINDCX_API_BASE_URL"].to_s.strip).empty?
|
|
37
|
+
config.api_base_url = v
|
|
38
|
+
end
|
|
39
|
+
unless (v = ENV["COINDCX_PUBLIC_BASE_URL"].to_s.strip).empty?
|
|
40
|
+
config.public_base_url = v
|
|
41
|
+
end
|
|
42
|
+
unless (v = ENV["COINDCX_SOCKET_BASE_URL"].to_s.strip).empty?
|
|
43
|
+
config.socket_base_url = v
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
client = CoinDCX.client
|
|
48
|
+
|
|
49
|
+
warn <<~BANNER
|
|
50
|
+
CoinDCX ready — locals: client, client.ws, CoinDCX.configuration
|
|
51
|
+
WebSocket: use warn(...) inside subscribe blocks (not only puts), then client.ws.connect last.
|
|
52
|
+
Spot smoke (liquid pair):
|
|
53
|
+
pc = CoinDCX::WS::PublicChannels
|
|
54
|
+
client.ws.subscribe_public(channel_name: pc.price_stats(pair: "B-BTC_USDT"), event_name: "price-change") { |p| warn(p.inspect) }
|
|
55
|
+
client.ws.connect
|
|
56
|
+
If you see a Hash with a "data" key (like the JS docs), print p["data"] || p
|
|
57
|
+
BANNER
|
|
58
|
+
|
|
59
|
+
binding.irb # rubocop:disable Lint/Debugger -- interactive entry point
|
data/docs/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# CoinDCX Ruby Client
|
|
2
|
+
|
|
3
|
+
Production-grade Ruby client for CoinDCX REST + Socket.io APIs.
|
|
4
|
+
|
|
5
|
+
## Docs
|
|
6
|
+
|
|
7
|
+
- [Core Usage](./core.md)
|
|
8
|
+
- [Rails Integration](./rails_integration.md)
|
|
9
|
+
- [Standalone Trading Bot](./standalone_bot.md)
|
|
10
|
+
|
|
11
|
+
## Philosophy
|
|
12
|
+
|
|
13
|
+
- Stateless client
|
|
14
|
+
- No trading logic
|
|
15
|
+
- Deterministic execution
|
|
16
|
+
- Event-driven compatible
|
|
17
|
+
|
|
18
|
+
## Notes
|
|
19
|
+
|
|
20
|
+
These docs use the current implemented `CoinDCX` namespace and API surface from this repository.
|
|
21
|
+
Application-level concerns like `EventBus`, position tracking, caching, and exit logic remain outside the gem.
|
|
22
|
+
|
|
23
|
+
### Runtime contract
|
|
24
|
+
|
|
25
|
+
- REST requests are validated locally before serialization when the gem knows the required boundary rules.
|
|
26
|
+
- Mutable order endpoints are never auto-retried unless the caller supplies an idempotency key such as `client_order_id`.
|
|
27
|
+
- WebSocket delivery is at-least-once across reconnects. Consumers must tolerate duplicate events.
|
|
28
|
+
- Public and private subscriptions are replayed after reconnect. Private subscriptions regenerate auth payloads on every reconnect.
|
|
29
|
+
- The gem does not guarantee durable event replay from CoinDCX. If the socket dies, missed events during downtime are the host app's responsibility.
|
data/docs/core.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Core Usage
|
|
2
|
+
|
|
3
|
+
## 1. Initialization
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
require 'logger'
|
|
7
|
+
require 'coindcx'
|
|
8
|
+
require 'securerandom'
|
|
9
|
+
|
|
10
|
+
CoinDCX.configure do |config|
|
|
11
|
+
config.api_key = ENV.fetch('COINDCX_API_KEY')
|
|
12
|
+
config.api_secret = ENV.fetch('COINDCX_API_SECRET')
|
|
13
|
+
config.logger = Logger.new($stdout)
|
|
14
|
+
config.max_retries = 2
|
|
15
|
+
config.retry_base_interval = 0.25
|
|
16
|
+
config.socket_reconnect_attempts = 3
|
|
17
|
+
config.socket_reconnect_interval = 1.0
|
|
18
|
+
config.socket_heartbeat_interval = 10.0
|
|
19
|
+
config.socket_liveness_timeout = 60.0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
client = CoinDCX.client
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 2. Public APIs
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
tickers = client.public.market_data.list_tickers
|
|
29
|
+
markets = client.public.market_data.list_markets
|
|
30
|
+
market_details = client.public.market_data.list_market_details
|
|
31
|
+
candles = client.public.market_data.list_candles(pair: 'B-BTC_USDT', interval: '1m')
|
|
32
|
+
order_book = client.public.market_data.fetch_order_book(pair: 'B-BTC_USDT')
|
|
33
|
+
trades = client.public.market_data.list_trades(pair: 'B-BTC_USDT', limit: 50)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 3. Private APIs
|
|
37
|
+
|
|
38
|
+
### Spot orders
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
order = client.spot.orders.create(
|
|
42
|
+
side: 'buy',
|
|
43
|
+
order_type: 'limit_order',
|
|
44
|
+
market: 'SNTBTC',
|
|
45
|
+
price_per_unit: '0.03244',
|
|
46
|
+
total_quantity: 400,
|
|
47
|
+
client_order_id: SecureRandom.uuid
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Balances and account info
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
balances = client.user.accounts.list_balances
|
|
55
|
+
info = client.user.accounts.fetch_info
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Wallet transfers
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
transfer = client.transfers.wallets.transfer(
|
|
62
|
+
source_wallet_type: 'spot',
|
|
63
|
+
destination_wallet_type: 'futures',
|
|
64
|
+
currency_short_name: 'USDT',
|
|
65
|
+
amount: 1
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 4. Futures APIs
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
active_instruments = client.futures.market_data.list_active_instruments(
|
|
73
|
+
margin_currency_short_names: ['USDT']
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
instrument = client.futures.market_data.fetch_instrument(
|
|
77
|
+
pair: 'B-BTC_USDT',
|
|
78
|
+
margin_currency_short_name: 'USDT'
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
futures_order_book = client.futures.market_data.fetch_order_book(
|
|
82
|
+
instrument: 'B-BTC_USDT',
|
|
83
|
+
depth: 50
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
positions = client.futures.positions.list
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 5. WebSocket Usage
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
ws = client.ws
|
|
93
|
+
channel = CoinDCX::WS::PublicChannels.price_stats(pair: 'B-BTC_USDT')
|
|
94
|
+
|
|
95
|
+
ws.connect
|
|
96
|
+
|
|
97
|
+
ws.subscribe_public(channel_name: channel, event_name: 'price-change') do |data|
|
|
98
|
+
puts data
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
To exercise **all documented Spot** public streams (and private `coindcx` events when `COINDCX_API_KEY` / `COINDCX_API_SECRET` are set), run from the repo root:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
bundle exec ruby scripts/spot_sockets_smoke.rb
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
See the script header for `COINDCX_PAIR`, candle interval, order book depth, and throttling options.
|
|
109
|
+
|
|
110
|
+
**Futures** streams use the same client and `coindcx` private channel; builders live on `CoinDCX::WS::PublicChannels` (`futures_candlestick`, `futures_order_book`, `futures_ltp`, `futures_new_trade`, `current_prices_futures`) and private event names `DF_POSITION_UPDATE_EVENT`, `DF_ORDER_UPDATE_EVENT`, plus shared `BALANCE_UPDATE_EVENT`. Run:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
bundle exec ruby scripts/futures_sockets_smoke.rb
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Use `scripts/futures_stream_eth_sol.rb` or `scripts/futures_ws_subscription_smoke.rb` for narrower checks.
|
|
117
|
+
|
|
118
|
+
The websocket client guarantees:
|
|
119
|
+
|
|
120
|
+
- bounded reconnect attempts with exponential backoff
|
|
121
|
+
- liveness checks for quiet streams
|
|
122
|
+
- automatic replay of subscription intent after reconnect
|
|
123
|
+
- at-least-once event delivery semantics after reconnect
|
|
124
|
+
|
|
125
|
+
The websocket client does **not** guarantee:
|
|
126
|
+
|
|
127
|
+
- gap-free market data during disconnect windows
|
|
128
|
+
- exactly-once event delivery
|
|
129
|
+
- application-level deduplication
|
|
130
|
+
|
|
131
|
+
### Private stream usage
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
ws.subscribe_private(
|
|
135
|
+
event_name: CoinDCX::WS::PrivateChannels::ORDER_UPDATE_EVENT
|
|
136
|
+
) do |data|
|
|
137
|
+
puts data
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## 6. Error Handling
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
begin
|
|
145
|
+
client.spot.orders.create(
|
|
146
|
+
side: 'buy',
|
|
147
|
+
order_type: 'limit_order',
|
|
148
|
+
market: 'SNTBTC',
|
|
149
|
+
price_per_unit: '0.03244',
|
|
150
|
+
total_quantity: 400,
|
|
151
|
+
client_order_id: SecureRandom.uuid
|
|
152
|
+
)
|
|
153
|
+
rescue CoinDCX::Errors::AuthError => e
|
|
154
|
+
warn("authentication failed: #{e.message}")
|
|
155
|
+
rescue CoinDCX::Errors::RateLimitError => e
|
|
156
|
+
warn("rate limited: #{e.message}")
|
|
157
|
+
rescue CoinDCX::Errors::RetryableRateLimitError => e
|
|
158
|
+
warn("rate limited, retryable: #{e.retryable}")
|
|
159
|
+
rescue CoinDCX::Errors::UpstreamServerError => e
|
|
160
|
+
warn("upstream failure: #{e.body[:error][:category]}")
|
|
161
|
+
rescue CoinDCX::Errors::RequestError => e
|
|
162
|
+
warn("request failed: #{e.message}")
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Every raised API error exposes:
|
|
167
|
+
|
|
168
|
+
- `category`
|
|
169
|
+
- `code`
|
|
170
|
+
- `retryable`
|
|
171
|
+
- `request_context`
|
|
172
|
+
- normalized `body[:error]`
|
|
173
|
+
|
|
174
|
+
## 7. Critical Rules
|
|
175
|
+
|
|
176
|
+
- Keep the gem as an API client only
|
|
177
|
+
- Keep strategy, risk, and position tracking in your app
|
|
178
|
+
- Prefer websocket feeds over polling when CoinDCX provides them
|
|
179
|
+
- Route all authenticated calls through the provided resource classes, not ad-hoc HTTP code
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Rails Integration
|
|
2
|
+
|
|
3
|
+
## Architecture
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
CoinDCX Gem -> Adapter Layer -> AlgoTradingApi -> Strategy -> Execution
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## 1. Initializer
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# config/initializers/coindcx.rb
|
|
13
|
+
CoinDCX.configure do |config|
|
|
14
|
+
config.api_key = ENV.fetch('COINDCX_API_KEY')
|
|
15
|
+
config.api_secret = ENV.fetch('COINDCX_API_SECRET')
|
|
16
|
+
config.logger = Rails.logger
|
|
17
|
+
config.max_retries = 2
|
|
18
|
+
config.retry_base_interval = 0.25
|
|
19
|
+
config.socket_reconnect_attempts = 3
|
|
20
|
+
config.socket_reconnect_interval = 1.0
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
COINDCX_CLIENT = CoinDCX.client
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 2. Adapter Layer (MANDATORY)
|
|
27
|
+
|
|
28
|
+
Do **not** call the gem directly from controllers, jobs, or domain services.
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# app/services/brokers/coindcx/client.rb
|
|
32
|
+
module Brokers
|
|
33
|
+
module Coindcx
|
|
34
|
+
class Client
|
|
35
|
+
def initialize(client: COINDCX_CLIENT)
|
|
36
|
+
@client = client
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def place_limit_buy(market:, price_per_unit:, total_quantity:)
|
|
40
|
+
@client.spot.orders.create(
|
|
41
|
+
side: 'buy',
|
|
42
|
+
order_type: 'limit_order',
|
|
43
|
+
market: market,
|
|
44
|
+
price_per_unit: price_per_unit,
|
|
45
|
+
total_quantity: total_quantity,
|
|
46
|
+
client_order_id: SecureRandom.uuid
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def fetch_ltp(market:)
|
|
51
|
+
@client.public.market_data.list_tickers.find { |ticker| ticker.market == market }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def balances
|
|
55
|
+
@client.user.accounts.list_balances
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 3. WebSocket -> Event System
|
|
63
|
+
|
|
64
|
+
Replace a polling-first flow with an event-driven flow:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
CoinDCX WS -> EventBus -> Positions::Manager -> Exit Engine
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The gem does not ship an `EventBus`; keep it in the Rails app.
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
# config/initializers/event_bus.rb
|
|
74
|
+
module EventBus
|
|
75
|
+
@listeners = Hash.new { |hash, key| hash[key] = [] }
|
|
76
|
+
|
|
77
|
+
class << self
|
|
78
|
+
def subscribe(event, &block)
|
|
79
|
+
@listeners[event] << block
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def publish(event, payload)
|
|
83
|
+
@listeners[event].each { |listener| listener.call(payload) }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
# config/initializers/coindcx_ws.rb
|
|
91
|
+
Thread.new do
|
|
92
|
+
ws = COINDCX_CLIENT.ws
|
|
93
|
+
channel = CoinDCX::WS::PublicChannels.price_stats(pair: 'B-BTC_USDT')
|
|
94
|
+
|
|
95
|
+
ws.connect
|
|
96
|
+
ws.subscribe_public(channel_name: channel, event_name: 'price-change') do |data|
|
|
97
|
+
EventBus.publish(:ltp_update, data)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 4. LTP Cache (CRITICAL)
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
# app/services/positions/ltp_cache.rb
|
|
106
|
+
module Positions
|
|
107
|
+
class LtpCache
|
|
108
|
+
def self.update(symbol, price)
|
|
109
|
+
Rails.cache.write("ltp:#{symbol}", price)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.get(symbol)
|
|
113
|
+
Rails.cache.read("ltp:#{symbol}")
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
EventBus.subscribe(:ltp_update) do |payload|
|
|
121
|
+
symbol = payload.fetch('s', 'UNKNOWN')
|
|
122
|
+
price = payload.fetch('p', payload['last_price'])
|
|
123
|
+
Positions::LtpCache.update(symbol, price)
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## 5. Risk Manager Integration
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
ltp = Positions::LtpCache.get(symbol)
|
|
131
|
+
|
|
132
|
+
if ltp && ltp <= stop_loss
|
|
133
|
+
exit_position
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## 6. Order Execution Flow
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
Signal -> Adapter -> CoinDCX -> Order ID -> Track -> WS updates -> Exit
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## 7. Critical Rules
|
|
144
|
+
|
|
145
|
+
- Never call the gem directly from controllers
|
|
146
|
+
- Always go through the adapter layer
|
|
147
|
+
- Send websocket events into your app event bus
|
|
148
|
+
- Prefer websocket market data over polling when available
|
|
149
|
+
- Keep all strategy and risk logic in Rails, not in the gem
|
|
150
|
+
- Persist `client_order_id` in your Rails app before calling create-order endpoints
|
|
151
|
+
- Treat websocket events as at-least-once delivery and deduplicate in the app if needed
|