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.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +55 -0
  3. data/.github/workflows/release.yml +138 -0
  4. data/.rubocop.yml +56 -0
  5. data/AGENT.md +352 -0
  6. data/README.md +224 -0
  7. data/bin/console +59 -0
  8. data/docs/README.md +29 -0
  9. data/docs/coindcx_docs_gaps.md +3 -0
  10. data/docs/core.md +179 -0
  11. data/docs/rails_integration.md +151 -0
  12. data/docs/standalone_bot.md +159 -0
  13. data/lib/coindcx/auth/signer.rb +48 -0
  14. data/lib/coindcx/client.rb +44 -0
  15. data/lib/coindcx/configuration.rb +108 -0
  16. data/lib/coindcx/contracts/channel_name.rb +23 -0
  17. data/lib/coindcx/contracts/identifiers.rb +36 -0
  18. data/lib/coindcx/contracts/order_request.rb +120 -0
  19. data/lib/coindcx/contracts/socket_backend.rb +19 -0
  20. data/lib/coindcx/contracts/wallet_transfer_request.rb +46 -0
  21. data/lib/coindcx/errors/base_error.rb +54 -0
  22. data/lib/coindcx/logging/null_logger.rb +12 -0
  23. data/lib/coindcx/logging/structured_logger.rb +17 -0
  24. data/lib/coindcx/models/balance.rb +8 -0
  25. data/lib/coindcx/models/base_model.rb +31 -0
  26. data/lib/coindcx/models/instrument.rb +8 -0
  27. data/lib/coindcx/models/market.rb +8 -0
  28. data/lib/coindcx/models/order.rb +8 -0
  29. data/lib/coindcx/models/trade.rb +8 -0
  30. data/lib/coindcx/rest/base_resource.rb +35 -0
  31. data/lib/coindcx/rest/funding/facade.rb +18 -0
  32. data/lib/coindcx/rest/funding/orders.rb +46 -0
  33. data/lib/coindcx/rest/futures/facade.rb +29 -0
  34. data/lib/coindcx/rest/futures/market_data.rb +71 -0
  35. data/lib/coindcx/rest/futures/orders.rb +47 -0
  36. data/lib/coindcx/rest/futures/positions.rb +93 -0
  37. data/lib/coindcx/rest/futures/wallets.rb +44 -0
  38. data/lib/coindcx/rest/margin/facade.rb +17 -0
  39. data/lib/coindcx/rest/margin/orders.rb +57 -0
  40. data/lib/coindcx/rest/public/facade.rb +17 -0
  41. data/lib/coindcx/rest/public/market_data.rb +52 -0
  42. data/lib/coindcx/rest/spot/facade.rb +17 -0
  43. data/lib/coindcx/rest/spot/orders.rb +67 -0
  44. data/lib/coindcx/rest/transfers/facade.rb +17 -0
  45. data/lib/coindcx/rest/transfers/wallets.rb +40 -0
  46. data/lib/coindcx/rest/user/accounts.rb +17 -0
  47. data/lib/coindcx/rest/user/facade.rb +17 -0
  48. data/lib/coindcx/transport/circuit_breaker.rb +65 -0
  49. data/lib/coindcx/transport/http_client.rb +290 -0
  50. data/lib/coindcx/transport/rate_limit_registry.rb +65 -0
  51. data/lib/coindcx/transport/request_policy.rb +152 -0
  52. data/lib/coindcx/transport/response_normalizer.rb +40 -0
  53. data/lib/coindcx/transport/retry_policy.rb +79 -0
  54. data/lib/coindcx/utils/payload.rb +51 -0
  55. data/lib/coindcx/version.rb +5 -0
  56. data/lib/coindcx/ws/connection_manager.rb +423 -0
  57. data/lib/coindcx/ws/connection_state.rb +75 -0
  58. data/lib/coindcx/ws/parsers/order_book_snapshot.rb +42 -0
  59. data/lib/coindcx/ws/private_channels.rb +38 -0
  60. data/lib/coindcx/ws/public_channels.rb +92 -0
  61. data/lib/coindcx/ws/socket_io_client.rb +89 -0
  62. data/lib/coindcx/ws/socket_io_simple_backend.rb +63 -0
  63. data/lib/coindcx/ws/subscription_registry.rb +80 -0
  64. data/lib/coindcx/ws/uri_ruby3_compat.rb +13 -0
  65. data/lib/coindcx.rb +63 -0
  66. data/spec/auth_signer_spec.rb +22 -0
  67. data/spec/client_spec.rb +19 -0
  68. data/spec/contracts/order_request_spec.rb +136 -0
  69. data/spec/contracts/wallet_transfer_request_spec.rb +45 -0
  70. data/spec/models/base_model_spec.rb +18 -0
  71. data/spec/rest/funding/orders_spec.rb +43 -0
  72. data/spec/rest/futures/market_data_spec.rb +49 -0
  73. data/spec/rest/futures/orders_spec.rb +107 -0
  74. data/spec/rest/futures/positions_spec.rb +57 -0
  75. data/spec/rest/futures/wallets_spec.rb +44 -0
  76. data/spec/rest/margin/orders_spec.rb +87 -0
  77. data/spec/rest/public/market_data_spec.rb +31 -0
  78. data/spec/rest/spot/orders_spec.rb +152 -0
  79. data/spec/rest/transfers/wallets_spec.rb +33 -0
  80. data/spec/rest/user/accounts_spec.rb +21 -0
  81. data/spec/spec_helper.rb +11 -0
  82. data/spec/transport/http_client_spec.rb +232 -0
  83. data/spec/transport/rate_limit_registry_spec.rb +28 -0
  84. data/spec/transport/request_policy_spec.rb +67 -0
  85. data/spec/transport/response_normalizer_spec.rb +63 -0
  86. data/spec/ws/connection_manager_spec.rb +339 -0
  87. data/spec/ws/order_book_snapshot_spec.rb +25 -0
  88. data/spec/ws/private_channels_spec.rb +28 -0
  89. data/spec/ws/public_channels_spec.rb +89 -0
  90. data/spec/ws/socket_io_client_spec.rb +229 -0
  91. data/spec/ws/socket_io_simple_backend_spec.rb +41 -0
  92. data/spec/ws/uri_ruby3_compat_spec.rb +12 -0
  93. 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.
@@ -0,0 +1,3 @@
1
+ # CoinDCX Docs Coverage Gaps (REST + WebSocket)
2
+
3
+ - None identified on audit date **2026-04-10** against `https://docs.coindcx.com/?javascript`.
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