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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: aff1bffd5369c08e4ec429c808b0ec33d15ed8caad1cc2f6a92c31ba9c4c2ded
4
+ data.tar.gz: a1c49884264af908b3e24f0f2a4d2e3dd871d64cd0fee4cfb35e6813e0129fff
5
+ SHA512:
6
+ metadata.gz: 727949f960ce763d670a4687361aa8a379028d9828f8ebd250cd8113ba5c34aa7fb135a96428f24badc006139b4f2cbe792b6d6f308e460972b2d9c8bc142d82
7
+ data.tar.gz: 84c1dceb9aba5448f127c2fd42168ed4b500594285860dac9d60395db8180d287e65739aea945c6cc570419d34b9fef79c747c8f654ab2b5b46eda865050fad4
@@ -0,0 +1,55 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [ main ]
7
+ tags:
8
+ - 'v*.*.*'
9
+
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+
14
+ strategy:
15
+ matrix:
16
+ ruby: ['3.2', '3.3']
17
+
18
+ steps:
19
+ - name: Checkout
20
+ uses: actions/checkout@v4
21
+
22
+ - name: Setup Ruby
23
+ uses: ruby/setup-ruby@v1
24
+ with:
25
+ ruby-version: ${{ matrix.ruby }}
26
+ bundler-cache: true
27
+
28
+ - name: Install dependencies
29
+ run: bundle install
30
+
31
+ - name: Verify version matches tag
32
+ if: startsWith(github.ref, 'refs/tags/v')
33
+ run: |
34
+ VERSION=$(ruby -e "require './lib/coindcx/version'; puts CoinDCX::VERSION")
35
+ TAG=${GITHUB_REF#refs/tags/v}
36
+
37
+ if [ "$VERSION" != "$TAG" ]; then
38
+ echo "Version mismatch: $VERSION != $TAG"
39
+ exit 1
40
+ fi
41
+
42
+ - name: Run RuboCop
43
+ run: bundle exec rubocop
44
+
45
+ - name: Run RSpec
46
+ run: bundle exec rspec
47
+
48
+ - name: Verify coverage
49
+ run: |
50
+ if [ -f coverage/.last_run.json ]; then
51
+ cat coverage/.last_run.json
52
+ else
53
+ echo "coverage/.last_run.json not found"
54
+ exit 1
55
+ fi
@@ -0,0 +1,138 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*.*.*'
7
+ workflow_dispatch:
8
+ inputs:
9
+ version:
10
+ description: 'Gem version to publish (must match lib/coindcx/version.rb)'
11
+ required: true
12
+ publish:
13
+ description: 'Publish to RubyGems'
14
+ required: true
15
+ type: boolean
16
+
17
+ jobs:
18
+ release:
19
+ runs-on: ubuntu-latest
20
+ environment: rubygems-release
21
+ permissions:
22
+ contents: read
23
+
24
+ steps:
25
+ - name: Checkout
26
+ uses: actions/checkout@v4
27
+ with:
28
+ fetch-depth: 0
29
+ persist-credentials: false
30
+
31
+ - name: Setup Ruby
32
+ uses: ruby/setup-ruby@v1
33
+ with:
34
+ ruby-version: '3.2'
35
+ bundler-cache: true
36
+
37
+ - name: Run compatibility tests on minimum Ruby
38
+ run: bundle exec rspec
39
+
40
+ - name: Setup release Ruby
41
+ uses: ruby/setup-ruby@v1
42
+ with:
43
+ ruby-version: '3.3'
44
+ bundler-cache: true
45
+
46
+ - name: Install dependencies
47
+ run: bundle install
48
+
49
+ - name: Resolve release version
50
+ id: release_version
51
+ run: |
52
+ if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
53
+ echo "value=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT"
54
+ else
55
+ echo "value=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT"
56
+ fi
57
+
58
+ - name: Verify version matches release trigger
59
+ run: |
60
+ VERSION=$(ruby -e "require './lib/coindcx/version'; puts CoinDCX::VERSION")
61
+ TARGET_VERSION='${{ steps.release_version.outputs.value }}'
62
+
63
+ if [ "$VERSION" != "$TARGET_VERSION" ]; then
64
+ echo "Version mismatch: $VERSION != $TARGET_VERSION"
65
+ exit 1
66
+ fi
67
+
68
+ - name: Verify changelog entry exists
69
+ run: |
70
+ if [ ! -f CHANGELOG.md ]; then
71
+ echo "CHANGELOG.md is required for releases"
72
+ exit 1
73
+ fi
74
+
75
+ TARGET_VERSION='${{ steps.release_version.outputs.value }}'
76
+ if ! ruby -e "
77
+ version = ARGV[0]
78
+ found = File.read('CHANGELOG.md').lines.any? { |line| line.match?(/\A##\s+#{Regexp.escape(version)}\s*\z/) }
79
+ exit(found ? 0 : 1)
80
+ " "$TARGET_VERSION"; then
81
+ echo "Missing CHANGELOG entry for ${TARGET_VERSION}"
82
+ exit 1
83
+ fi
84
+
85
+ - name: Verify tag is on the current commit
86
+ if: startsWith(github.ref, 'refs/tags/v')
87
+ run: |
88
+ TAG_COMMIT=$(git rev-list -n 1 "${GITHUB_REF}")
89
+ HEAD_COMMIT=$(git rev-parse HEAD)
90
+
91
+ if [ "$TAG_COMMIT" != "$HEAD_COMMIT" ]; then
92
+ echo "Release tag does not point to the checked out commit"
93
+ exit 1
94
+ fi
95
+
96
+ - name: Run RuboCop
97
+ run: bundle exec rubocop
98
+
99
+ - name: Run tests (gate)
100
+ run: bundle exec rspec
101
+
102
+ - name: Build gem
103
+ run: gem build coindcx-client.gemspec
104
+
105
+ - name: Verify built artifact
106
+ run: |
107
+ ls -1 coindcx-client-*.gem
108
+ gem specification coindcx-client-*.gem version > /tmp/gem-version.txt
109
+ gem specification coindcx-client-*.gem name platform > /tmp/gem-spec.txt
110
+ cat /tmp/gem-version.txt
111
+ cat /tmp/gem-spec.txt
112
+ if ! grep -qF "${{ steps.release_version.outputs.value }}" /tmp/gem-version.txt; then
113
+ echo "Built gem version does not match release version"
114
+ exit 1
115
+ fi
116
+
117
+ # Same pattern as dhanhq-client: API key via GEM_HOST_API_KEY + TOTP for RubyGems MFA.
118
+ # Add RUBYGEMS_OTP_SECRET (base32) to the rubygems-release environment secrets.
119
+ - name: Install OTP generator
120
+ if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.publish == 'true'
121
+ run: gem install rotp
122
+
123
+ - name: Publish to RubyGems
124
+ if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.publish == 'true'
125
+ env:
126
+ GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
127
+ RUBYGEMS_OTP_SECRET: ${{ secrets.RUBYGEMS_OTP_SECRET }}
128
+ run: |
129
+ set -euo pipefail
130
+ otp_code=$(ruby -r rotp -e "puts ROTP::TOTP.new(ENV['RUBYGEMS_OTP_SECRET']).now")
131
+ gem_version=$(ruby -e "require './lib/coindcx/version'; puts CoinDCX::VERSION")
132
+ gem_file="coindcx-client-${gem_version}.gem"
133
+ if [ ! -f "$gem_file" ]; then
134
+ echo "ERROR: Gem file not found: $gem_file"
135
+ ls -la ./*.gem 2>/dev/null || echo "No gem files in working directory"
136
+ exit 1
137
+ fi
138
+ gem push "$gem_file" --otp "$otp_code"
data/.rubocop.yml ADDED
@@ -0,0 +1,56 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.2
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+ Exclude:
6
+ - 'vendor/**/*'
7
+
8
+ Layout/LineLength:
9
+ Max: 140
10
+ Exclude:
11
+ - 'spec/**/*'
12
+
13
+ Metrics/AbcSize:
14
+ Max: 30
15
+
16
+ Metrics/BlockLength:
17
+ Exclude:
18
+ - 'spec/**/*'
19
+ - '.github/workflows/*.yml'
20
+
21
+ Metrics/ClassLength:
22
+ Max: 120
23
+ Exclude:
24
+ - 'scripts/**/*'
25
+
26
+ Metrics/CyclomaticComplexity:
27
+ Exclude:
28
+ - 'scripts/**/*'
29
+
30
+ Metrics/MethodLength:
31
+ Max: 15
32
+ Exclude:
33
+ - 'scripts/**/*'
34
+
35
+ Metrics/PerceivedComplexity:
36
+ Exclude:
37
+ - 'scripts/**/*'
38
+
39
+ Metrics/ParameterLists:
40
+ Max: 7
41
+
42
+ Lint/ScriptPermission:
43
+ Exclude:
44
+ - 'scripts/**/*'
45
+
46
+ Naming/BlockForwarding:
47
+ Enabled: false
48
+
49
+ Style/ArgumentsForwarding:
50
+ Enabled: false
51
+
52
+ Style/Documentation:
53
+ Enabled: false
54
+
55
+ Style/StringLiterals:
56
+ Enabled: false
data/AGENT.md ADDED
@@ -0,0 +1,352 @@
1
+ # AGENT.md — CoinDCX Client (Pattern-Enforced Architecture)
2
+
3
+ ## 1. Design Philosophy
4
+
5
+ - patterns are tools, not goals
6
+ - each pattern must solve a real constraint
7
+ - zero speculative abstraction
8
+
9
+ ## 2. Approved Design Patterns (STRICT)
10
+
11
+ Only these patterns are allowed.
12
+
13
+ | Pattern | Mandatory | Reason |
14
+ | --- | --- | --- |
15
+ | Factory | yes | resource/client creation |
16
+ | Strategy | yes | auth + retry + rate limit |
17
+ | Adapter | yes | HTTP + Socket.io isolation |
18
+ | Observer | yes | WebSocket event system |
19
+ | Command | yes | order execution encapsulation |
20
+ | Template Method | yes | REST execution pipeline |
21
+ | Builder | yes | request construction |
22
+ | Decorator | yes | logging + retry wrapping |
23
+ | Singleton | limited | config only |
24
+ | Facade | yes | client interface |
25
+ | State | limited | WS only, connection lifecycle |
26
+
27
+ Everything else -> reject.
28
+
29
+ ## 3. Pattern Mapping to System
30
+
31
+ ### 3.1 Facade Pattern -> Client
32
+
33
+ Purpose:
34
+
35
+ - expose clean interface
36
+ - `client.public.markets`
37
+ - `client.spot.place_order(...)`
38
+ - `client.ws.subscribe(...)`
39
+
40
+ Implementation:
41
+
42
+ ```ruby
43
+ module Coindcx
44
+ class Client
45
+ def public
46
+ @public ||= Rest::Public::Facade.new(http_client)
47
+ end
48
+
49
+ def spot
50
+ @spot ||= Rest::Spot::Facade.new(http_client)
51
+ end
52
+
53
+ def ws
54
+ @ws ||= Ws::Facade.new(ws_client)
55
+ end
56
+ end
57
+ end
58
+ ```
59
+
60
+ ### 3.2 Factory Pattern -> Resource Creation
61
+
62
+ Purpose:
63
+
64
+ - decouple instantiation
65
+
66
+ ```ruby
67
+ module Coindcx
68
+ class ResourceFactory
69
+ def self.build(type, client)
70
+ case type
71
+ when :markets then Rest::Public::Markets.new(client)
72
+ when :orders then Rest::Spot::Orders.new(client)
73
+ else raise "Unknown resource"
74
+ end
75
+ end
76
+ end
77
+ end
78
+ ```
79
+
80
+ ### 3.3 Strategy Pattern -> Auth / Retry / RateLimit
81
+
82
+ Purpose:
83
+
84
+ - swap runtime behavior without branching chaos
85
+
86
+ Auth Strategy:
87
+
88
+ ```ruby
89
+ class HmacAuthStrategy
90
+ def sign(payload, secret)
91
+ OpenSSL::HMAC.hexdigest('SHA256', secret, payload.to_json)
92
+ end
93
+ end
94
+ ```
95
+
96
+ Retry Strategy:
97
+
98
+ ```ruby
99
+ class ExponentialBackoffStrategy
100
+ def execute
101
+ retries = 0
102
+ begin
103
+ yield
104
+ rescue => e
105
+ raise if retries >= 3
106
+ sleep(2 ** retries)
107
+ retries += 1
108
+ retry
109
+ end
110
+ end
111
+ end
112
+ ```
113
+
114
+ ### 3.4 Adapter Pattern -> HTTP + Socket.io
115
+
116
+ Purpose:
117
+
118
+ - shield external libraries
119
+
120
+ HTTP Adapter:
121
+
122
+ ```ruby
123
+ class FaradayAdapter
124
+ def call(method, url, payload, headers)
125
+ Faraday.public_send(method, url) do |req|
126
+ req.headers = headers
127
+ req.body = payload.to_json if method == :post
128
+ end
129
+ end
130
+ end
131
+ ```
132
+
133
+ Socket Adapter:
134
+
135
+ ```ruby
136
+ class SocketIoAdapter
137
+ def initialize(url)
138
+ @socket = SocketIO::Client::Simple.connect(url)
139
+ end
140
+
141
+ def emit(event, payload)
142
+ @socket.emit(event, payload)
143
+ end
144
+
145
+ def on(event, &block)
146
+ @socket.on(event, &block)
147
+ end
148
+ end
149
+ ```
150
+
151
+ ### 3.5 Observer Pattern -> WebSocket Events (CRITICAL)
152
+
153
+ Reference: Observer Pattern Ruby example
154
+
155
+ Purpose:
156
+
157
+ - event-driven system (mandatory for trading)
158
+
159
+ Subject:
160
+
161
+ ```ruby
162
+ module Coindcx
163
+ module Ws
164
+ class EventBus
165
+ def initialize
166
+ @listeners = {}
167
+ end
168
+
169
+ def subscribe(event, listener)
170
+ @listeners[event] ||= []
171
+ @listeners[event] << listener
172
+ end
173
+
174
+ def publish(event, data)
175
+ (@listeners[event] || []).each do |listener|
176
+ listener.call(data)
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+ ```
183
+
184
+ Usage:
185
+
186
+ ```ruby
187
+ event_bus.subscribe(:ltp_update, ->(data) {
188
+ puts data
189
+ })
190
+
191
+ event_bus.publish(:ltp_update, payload)
192
+ ```
193
+
194
+ ### 3.6 Command Pattern -> Order Execution
195
+
196
+ Purpose:
197
+
198
+ - encapsulate actions (important for retries + audit)
199
+
200
+ ```ruby
201
+ class PlaceOrderCommand
202
+ def initialize(client, params)
203
+ @client = client
204
+ @params = params
205
+ end
206
+
207
+ def execute
208
+ @client.post("/orders", @params)
209
+ end
210
+ end
211
+ ```
212
+
213
+ ### 3.7 Template Method -> HTTP Pipeline
214
+
215
+ Purpose:
216
+
217
+ - standardize request flow
218
+
219
+ ```ruby
220
+ class BaseRequest
221
+ def execute
222
+ validate
223
+ payload = build_payload
224
+ signed = sign(payload)
225
+ response = send_request(signed)
226
+ parse(response)
227
+ end
228
+
229
+ def validate; end
230
+ def build_payload; end
231
+ def sign(payload); payload; end
232
+ def send_request(payload); end
233
+ def parse(response); end
234
+ end
235
+ ```
236
+
237
+ ### 3.8 Builder Pattern -> Request Construction
238
+
239
+ Purpose:
240
+
241
+ - avoid hash chaos
242
+
243
+ ```ruby
244
+ class OrderBuilder
245
+ def initialize
246
+ @params = {}
247
+ end
248
+
249
+ def symbol(val); @params[:symbol] = val; self; end
250
+ def side(val); @params[:side] = val; self; end
251
+ def quantity(val); @params[:quantity] = val; self; end
252
+
253
+ def build
254
+ raise "Missing fields" unless @params[:symbol]
255
+ @params
256
+ end
257
+ end
258
+ ```
259
+
260
+ ### 3.9 Decorator Pattern -> Logging / Retry
261
+
262
+ Purpose:
263
+
264
+ - add behavior without modifying core
265
+
266
+ ```ruby
267
+ class LoggingDecorator
268
+ def initialize(client, logger)
269
+ @client = client
270
+ @logger = logger
271
+ end
272
+
273
+ def call(*args)
274
+ start = Time.now
275
+ result = @client.call(*args)
276
+ @logger.info("Latency: #{Time.now - start}")
277
+ result
278
+ end
279
+ end
280
+ ```
281
+
282
+ ### 3.10 State Pattern -> WebSocket Lifecycle
283
+
284
+ Purpose:
285
+
286
+ - handle connection transitions cleanly
287
+
288
+ ```ruby
289
+ class ConnectedState
290
+ def handle(context)
291
+ # active
292
+ end
293
+ end
294
+
295
+ class ReconnectingState
296
+ def handle(context)
297
+ context.reconnect
298
+ end
299
+ end
300
+ ```
301
+
302
+ ## 4. Pattern Anti-Abuse Rules
303
+
304
+ Reject if:
305
+
306
+ - pattern used without clear constraint
307
+ - multiple patterns solving same problem
308
+ - inheritance chains > 2 levels
309
+ - over-engineered builders for simple calls
310
+
311
+ ## 5. Critical Integration Insight
312
+
313
+ Where Observer connects to your system:
314
+
315
+ - `CoinDCX WS -> EventBus -> AlgoTradingApi -> Exit Engine`
316
+
317
+ This is how you replicate:
318
+
319
+ - Dhan WebSocket feed
320
+ - ActiveCache updates
321
+ - real-time exit logic
322
+
323
+ ## 6. What NOT to implement (explicit)
324
+
325
+ - Repository pattern (no DB)
326
+ - Service layer (anti-pattern for this gem)
327
+ - CQRS (overkill)
328
+ - Event sourcing (belongs to trading system)
329
+ - ActiveRecord-style models
330
+
331
+ ## 7. Final Enforcement Rule
332
+
333
+ > Every pattern must map to a production failure mode.
334
+
335
+ If it doesn't: -> remove it.
336
+
337
+ ## 8. Next Step
338
+
339
+ If proceeding correctly, the next move is:
340
+
341
+ - bootstrap with patterns wired from day 1
342
+
343
+ I can generate:
344
+
345
+ - full gem scaffold
346
+ - all patterns pre-wired
347
+ - working REST + WS base
348
+ - RSpec coverage
349
+
350
+ Say:
351
+
352
+ `bootstrap pattern gem`