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
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`
|