delta_exchange 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.cursor/.gitignore +1 -0
- data/CHANGELOG.md +11 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +253 -0
- data/Rakefile +12 -0
- data/docs/AUTHENTICATION.md +49 -0
- data/docs/GETTING_STARTED.md +67 -0
- data/docs/RAILS_INTEGRATION.md +135 -0
- data/docs/REST_API_GUIDE.md +150 -0
- data/docs/STANDALONE_RUBY_GUIDE.md +73 -0
- data/docs/WEBSOCKET_GUIDE.md +160 -0
- data/exe/delta_exchange +4 -0
- data/lib/delta_exchange/auth.rb +12 -0
- data/lib/delta_exchange/client.rb +196 -0
- data/lib/delta_exchange/configuration.rb +40 -0
- data/lib/delta_exchange/constants.rb +72 -0
- data/lib/delta_exchange/contracts/order_contract.rb +24 -0
- data/lib/delta_exchange/contracts/position_contract.rb +21 -0
- data/lib/delta_exchange/contracts/wallet_transfer_contract.rb +16 -0
- data/lib/delta_exchange/core/base_model.rb +54 -0
- data/lib/delta_exchange/core/error_handler.rb +16 -0
- data/lib/delta_exchange/error.rb +37 -0
- data/lib/delta_exchange/helpers/attribute_helper.rb +22 -0
- data/lib/delta_exchange/helpers/validation_helper.rb +34 -0
- data/lib/delta_exchange/models/asset.rb +23 -0
- data/lib/delta_exchange/models/fee_tier.rb +19 -0
- data/lib/delta_exchange/models/fill.rb +20 -0
- data/lib/delta_exchange/models/funding_rate.rb +19 -0
- data/lib/delta_exchange/models/index.rb +23 -0
- data/lib/delta_exchange/models/open_interest.rb +19 -0
- data/lib/delta_exchange/models/order.rb +34 -0
- data/lib/delta_exchange/models/position.rb +43 -0
- data/lib/delta_exchange/models/product.rb +43 -0
- data/lib/delta_exchange/models/profile.rb +20 -0
- data/lib/delta_exchange/models/ticker.rb +26 -0
- data/lib/delta_exchange/models/trading_preferences.rb +27 -0
- data/lib/delta_exchange/models/wallet_balance.rb +23 -0
- data/lib/delta_exchange/models/wallet_transaction.rb +20 -0
- data/lib/delta_exchange/resources/account.rb +53 -0
- data/lib/delta_exchange/resources/assets.rb +11 -0
- data/lib/delta_exchange/resources/base.rb +37 -0
- data/lib/delta_exchange/resources/fills.rb +15 -0
- data/lib/delta_exchange/resources/heartbeat.rb +20 -0
- data/lib/delta_exchange/resources/indices.rb +11 -0
- data/lib/delta_exchange/resources/market_data.rb +56 -0
- data/lib/delta_exchange/resources/orders.rb +76 -0
- data/lib/delta_exchange/resources/positions.rb +47 -0
- data/lib/delta_exchange/resources/products.rb +39 -0
- data/lib/delta_exchange/resources/wallet.rb +45 -0
- data/lib/delta_exchange/version.rb +5 -0
- data/lib/delta_exchange/websocket/client.rb +55 -0
- data/lib/delta_exchange/websocket/connection.rb +114 -0
- data/lib/delta_exchange.rb +39 -0
- data/sig/delta_exchange.rbs +4 -0
- metadata +231 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# REST API Guide
|
|
2
|
+
|
|
3
|
+
DeltaExchange REST API v2 follows standard JSON patterns. The gem exposes resources through the `Client` object.
|
|
4
|
+
|
|
5
|
+
## Initializing the Client
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
client = DeltaExchange::Client.new
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Resources
|
|
12
|
+
|
|
13
|
+
### Products
|
|
14
|
+
|
|
15
|
+
Provides data about tradable instruments.
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
products = client.products.all
|
|
19
|
+
symbol_data = client.products.find('BTCUSD')
|
|
20
|
+
tickers = client.products.tickers
|
|
21
|
+
single_ticker = client.products.ticker('BTCUSD')
|
|
22
|
+
leverage_data = client.products.leverage(1) # For product_id: 1
|
|
23
|
+
client.products.set_leverage(1, leverage: "25")
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Orders
|
|
27
|
+
|
|
28
|
+
Execution management.
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# Create a limit order
|
|
32
|
+
order = client.orders.create(
|
|
33
|
+
product_id: 1,
|
|
34
|
+
size: 10,
|
|
35
|
+
side: 'buy',
|
|
36
|
+
order_type: 'limit_order',
|
|
37
|
+
limit_price: '50000.0'
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Edit an order
|
|
41
|
+
client.orders.update(
|
|
42
|
+
id: order[:id],
|
|
43
|
+
product_id: 1,
|
|
44
|
+
size: 12,
|
|
45
|
+
limit_price: '50100.0'
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Cancel an order
|
|
49
|
+
client.orders.cancel(id: order[:id], product_id: 1)
|
|
50
|
+
|
|
51
|
+
# Cancel all orders
|
|
52
|
+
client.orders.cancel_all(product_id: 1) # Optional product filter
|
|
53
|
+
|
|
54
|
+
# Batch orders (up to 50 in one call)
|
|
55
|
+
client.orders.create_batch(
|
|
56
|
+
product_id: 1,
|
|
57
|
+
orders: [
|
|
58
|
+
{ size: 5, side: 'buy', order_type: 'limit_order', limit_price: '49000.0' },
|
|
59
|
+
{ size: 5, side: 'buy', order_type: 'limit_order', limit_price: '48000.0' }
|
|
60
|
+
]
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Positions
|
|
65
|
+
|
|
66
|
+
Margin and leverage tracking.
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
# Fetch all active open positions (uses /v2/positions/margined internally)
|
|
70
|
+
positions = client.positions.margined
|
|
71
|
+
|
|
72
|
+
# Change leverage for an open position
|
|
73
|
+
client.positions.change_leverage(product_id: 1, leverage: "50")
|
|
74
|
+
|
|
75
|
+
# Adjust margin (add/remove)
|
|
76
|
+
client.positions.adjust_margin(product_id: 1, amount: "100.0", type: "add")
|
|
77
|
+
|
|
78
|
+
# Close all positions
|
|
79
|
+
client.positions.close_all
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Market Data
|
|
83
|
+
|
|
84
|
+
Historical and statistical information.
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
# Get order book (Level 2)
|
|
88
|
+
book = client.market_data.l2_orderbook('BTCUSD')
|
|
89
|
+
|
|
90
|
+
# Get recent trades
|
|
91
|
+
trades = client.market_data.trades('BTCUSD')
|
|
92
|
+
|
|
93
|
+
# Get candles (OHLCV)
|
|
94
|
+
candles = client.market_data.candles(
|
|
95
|
+
product_id: 1,
|
|
96
|
+
resolution: '1m',
|
|
97
|
+
start_time: (Time.now - 3600).to_i,
|
|
98
|
+
end_time: Time.now.to_i
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Get specialized data
|
|
102
|
+
funding = client.market_data.funding_rates(product_id: 1)
|
|
103
|
+
oi = client.market_data.open_interest(product_id: 1)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Wallet
|
|
107
|
+
|
|
108
|
+
Account balances and history.
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
balances = client.wallet.balances
|
|
112
|
+
history = client.wallet.transactions
|
|
113
|
+
deposits = client.wallet.deposits
|
|
114
|
+
withdrawals = client.wallet.withdrawals
|
|
115
|
+
|
|
116
|
+
# Sub-account balance transfer
|
|
117
|
+
client.wallet.subaccount_transfer(
|
|
118
|
+
asset_id: 1, # USDT
|
|
119
|
+
amount: "500.0",
|
|
120
|
+
sub_account_id: 1234,
|
|
121
|
+
method: "deposit" # deposit into sub-account
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Account
|
|
126
|
+
|
|
127
|
+
User-level configuration and statistics.
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
profile = client.account.profile
|
|
131
|
+
fee_tiers = client.account.fee_tiers
|
|
132
|
+
referrals = client.account.referrals
|
|
133
|
+
client.account.update_trading_preferences(cancel_on_disconnect: true)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Error Handling
|
|
137
|
+
|
|
138
|
+
Standardized API and validation errors.
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
begin
|
|
142
|
+
client.orders.create(size: 0) # This will fail validation
|
|
143
|
+
rescue DeltaExchange::ValidationError => e
|
|
144
|
+
puts "Check your inputs: #{e.message}"
|
|
145
|
+
rescue DeltaExchange::RateLimitError => e
|
|
146
|
+
puts "Slowing down! Retry after #{e.retry_after_seconds}s"
|
|
147
|
+
rescue DeltaExchange::ApiError => e
|
|
148
|
+
puts "API Error: #{e.message} (Code: #{e.code})"
|
|
149
|
+
end
|
|
150
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Standalone Ruby Script Guide
|
|
2
|
+
|
|
3
|
+
The DeltaExchange gem can be used in any Ruby environment, from simple CLI scripts to powerful trading bots.
|
|
4
|
+
|
|
5
|
+
## Script Structure
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
require 'bundler/inline'
|
|
11
|
+
|
|
12
|
+
# 1. Inline gem dependency (no need for a Gemfile)
|
|
13
|
+
gemfile do
|
|
14
|
+
source 'https://rubygems.org'
|
|
15
|
+
gem 'delta_exchange'
|
|
16
|
+
gem 'dotenv' # for loading credentials from .env
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require 'delta_exchange'
|
|
20
|
+
require 'dotenv/load'
|
|
21
|
+
|
|
22
|
+
# 2. Configure the client
|
|
23
|
+
DeltaExchange.configure do |config|
|
|
24
|
+
config.api_key = ENV['DELTA_API_KEY']
|
|
25
|
+
config.api_secret = ENV['DELTA_API_SECRET']
|
|
26
|
+
config.testnet = true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# 3. Business logic
|
|
30
|
+
def run_trading_loop
|
|
31
|
+
client = DeltaExchange::Client.new
|
|
32
|
+
|
|
33
|
+
loop do
|
|
34
|
+
ticker = client.products.ticker('BTCUSD')
|
|
35
|
+
puts "BTC Price: #{ticker[:last_price]}"
|
|
36
|
+
|
|
37
|
+
# Place your automated trading strategy here...
|
|
38
|
+
|
|
39
|
+
sleep 10 # Wait 10 seconds before next check
|
|
40
|
+
end
|
|
41
|
+
rescue Interrupt
|
|
42
|
+
puts "\nStopping bot..."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
run_trading_loop
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Running with `.env` file
|
|
49
|
+
|
|
50
|
+
Create a file named `.env` in the same directory as your script:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
DELTA_API_KEY=your_actual_key
|
|
54
|
+
DELTA_API_SECRET=your_actual_secret
|
|
55
|
+
DELTA_TESTNET=true
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Using as a CLI Tool
|
|
59
|
+
|
|
60
|
+
The gem includes a console for interactive use:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# From the project root
|
|
64
|
+
bin/console
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Once inside, you can interact with the API directly:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
irb(main):001> client = DeltaExchange::Client.new
|
|
71
|
+
irb(main):002> client.products.all.first[:symbol]
|
|
72
|
+
=> "BTCUSD"
|
|
73
|
+
```
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# WebSocket Guide
|
|
2
|
+
|
|
3
|
+
DeltaExchange WebSocket API v2 provides real-time streaming for public market data and private user updates.
|
|
4
|
+
|
|
5
|
+
## Basic Usage
|
|
6
|
+
|
|
7
|
+
The WebSocket client runs in a background thread using `EventMachine`.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
ws = DeltaExchange::Websocket::Client.new
|
|
11
|
+
|
|
12
|
+
# Handle public updates
|
|
13
|
+
ws.on :message do |data|
|
|
14
|
+
case data['type']
|
|
15
|
+
when 'v2/ticker'
|
|
16
|
+
puts "Price update: #{data['symbol']} is now #{data['last_price']}"
|
|
17
|
+
when 'l2_updates'
|
|
18
|
+
puts "Orderbook update for #{data['symbol']}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Handle specific events
|
|
23
|
+
ws.on :open do
|
|
24
|
+
puts "WebSocket Connected!"
|
|
25
|
+
|
|
26
|
+
# Subscribe to channels
|
|
27
|
+
ws.subscribe([
|
|
28
|
+
{ name: "v2/ticker", symbols: ["BTCUSD", "ETHUSD"] },
|
|
29
|
+
{ name: "l2_updates", symbols: ["BTCUSD"] }
|
|
30
|
+
])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
ws.connect!
|
|
34
|
+
|
|
35
|
+
# Keep the main process alive (only needed for standalone scripts)
|
|
36
|
+
loop { sleep 1 }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Private Channels
|
|
40
|
+
|
|
41
|
+
If you provide an `api_key` and `api_secret`, the gem automatically handles authentication upon connection.
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
# The gem will use global configuration if not passed explicitly
|
|
45
|
+
ws = DeltaExchange::Websocket::Client.new
|
|
46
|
+
|
|
47
|
+
ws.on :open do
|
|
48
|
+
# Authenticated! Now subscribe to private data
|
|
49
|
+
ws.subscribe([
|
|
50
|
+
{ name: "orders" },
|
|
51
|
+
{ name: "positions" },
|
|
52
|
+
{ name: "margins" }
|
|
53
|
+
])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
ws.on :message do |data|
|
|
57
|
+
if data['type'] == 'orders'
|
|
58
|
+
puts "Order status changed! New status: #{data['status']}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
ws.connect!
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Available Channels
|
|
66
|
+
|
|
67
|
+
### Public
|
|
68
|
+
- `v2/ticker`: Live price and 24h stats.
|
|
69
|
+
- `l1_orderbook`: Top of the book.
|
|
70
|
+
- `l2_orderbook`: Full order book (snapshot).
|
|
71
|
+
- `l2_updates`: Incremental order book updates.
|
|
72
|
+
- `all_trades`: Live public trade stream.
|
|
73
|
+
- `mark_price`: Current mark price for perpetuals.
|
|
74
|
+
- `candlesticks`: Live OHLCV candle updates.
|
|
75
|
+
- `spot_price`: Current index spot price.
|
|
76
|
+
|
|
77
|
+
### Private (Auth Required)
|
|
78
|
+
- `orders`: Personal order fill and status updates.
|
|
79
|
+
- `positions`: Personal position change updates.
|
|
80
|
+
- `user_trades`: Personal execution stream.
|
|
81
|
+
- `margins`: Personal margin balance updates.
|
|
82
|
+
|
|
83
|
+
## Performance Notes
|
|
84
|
+
|
|
85
|
+
* **EM Reactor**: The gem starts an EventMachine reactor in a background thread. This means it won't block your main application (like a Rails server).
|
|
86
|
+
* **JSON Parsing**: Every message is parsed into a Ruby Hash automatically.
|
|
87
|
+
* **Reconnection**: The gem includes internal logic to attempt reconnection if the socket drops.
|
|
88
|
+
|
|
89
|
+
## Advanced: Persistence and Caching
|
|
90
|
+
|
|
91
|
+
In a production trading application, you often need to access the "Last Traded Price" (LTP) from different parts of your application (e.g., a Rails controller or a background job) without maintaining a constant WebSocket connection in every process.
|
|
92
|
+
|
|
93
|
+
The best practice is to use a centralized store like **Redis** or **Rails Cache**.
|
|
94
|
+
|
|
95
|
+
### 1. Storing Ticks in Redis
|
|
96
|
+
|
|
97
|
+
Using Redis allows multiple processes to read the latest price instantly.
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
require 'redis'
|
|
101
|
+
redis = Redis.new
|
|
102
|
+
|
|
103
|
+
ws = DeltaExchange::Websocket::Client.new
|
|
104
|
+
|
|
105
|
+
ws.on :message do |data|
|
|
106
|
+
if data['type'] == 'v2/ticker'
|
|
107
|
+
symbol = data['symbol']
|
|
108
|
+
price = data['last_price']
|
|
109
|
+
|
|
110
|
+
# Store the latest price in Redis with an optional TTL
|
|
111
|
+
redis.set("delta:ltp:#{symbol}", price)
|
|
112
|
+
|
|
113
|
+
# Optionally store the full tick as JSON
|
|
114
|
+
redis.set("delta:tick:#{symbol}", data.to_json)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
ws.connect!
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 2. Storing Ticks in Rails Cache
|
|
122
|
+
|
|
123
|
+
If you are using Rails, you can utilize the built-in cache (which often uses Redis as a backend).
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# Inside your WebSocket worker/task
|
|
127
|
+
ws.on :message do |data|
|
|
128
|
+
if data['type'] == 'v2/ticker'
|
|
129
|
+
Rails.cache.write("delta_ltp_#{data['symbol']}", data['last_price'])
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 3. Accessing LTP from a Controller
|
|
135
|
+
|
|
136
|
+
Once stored, you can access the data from anywhere in your Rails app:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
class TradesController < ApplicationController
|
|
140
|
+
def current_price
|
|
141
|
+
# Fast read from cache/redis
|
|
142
|
+
price = Rails.cache.read("delta_ltp_#{params[:symbol]}")
|
|
143
|
+
|
|
144
|
+
render json: { symbol: params[:symbol], price: price }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 4. Background Job Pattern
|
|
150
|
+
|
|
151
|
+
For complex bots, use the WebSocket to trigger background jobs for execution:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
ws.on :message do |data|
|
|
155
|
+
if data['type'] == 'v2/ticker' && data['last_price'].to_f > 60000.0
|
|
156
|
+
# Trigger a trade job if price threshold met
|
|
157
|
+
TradeExecutionJob.perform_later('BTCUSD', 'sell', data['last_price'])
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
```
|
data/exe/delta_exchange
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module DeltaExchange
|
|
6
|
+
class Auth
|
|
7
|
+
def self.sign(method, timestamp, path, query_string, payload, secret)
|
|
8
|
+
prehash = "#{method.upcase}#{timestamp}#{path}#{query_string}#{payload}"
|
|
9
|
+
OpenSSL::HMAC.hexdigest("SHA256", secret, prehash)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
require "uri"
|
|
7
|
+
require "active_support/core_ext/hash/indifferent_access"
|
|
8
|
+
require "active_support/core_ext/object/blank"
|
|
9
|
+
require "active_support/core_ext/time"
|
|
10
|
+
|
|
11
|
+
module DeltaExchange
|
|
12
|
+
class Client
|
|
13
|
+
attr_reader :root_url, :connection
|
|
14
|
+
|
|
15
|
+
def initialize(api_key: nil, api_secret: nil, testnet: nil, base_url: nil)
|
|
16
|
+
DeltaExchange.ensure_configuration!
|
|
17
|
+
config = DeltaExchange.configuration
|
|
18
|
+
@api_key = api_key || config.api_key
|
|
19
|
+
@api_secret = api_secret || config.api_secret
|
|
20
|
+
@testnet = testnet.nil? ? config.testnet : testnet
|
|
21
|
+
@root_url = resolve_root_url(config, base_url)
|
|
22
|
+
@connection = build_connection(@root_url, config)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def rebuild_connection!
|
|
26
|
+
config = DeltaExchange.configuration
|
|
27
|
+
@connection = build_connection(@root_url, config)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def request(method, path, payload = {}, params = {}, authenticate: true)
|
|
31
|
+
ensure_credentials!(authenticate)
|
|
32
|
+
timestamp = Time.now.utc.to_i.to_s
|
|
33
|
+
query_string = params.empty? ? "" : "?#{URI.encode_www_form(params)}"
|
|
34
|
+
body = payload.empty? ? "" : payload.to_json
|
|
35
|
+
|
|
36
|
+
headers = {
|
|
37
|
+
"Content-Type" => "application/json",
|
|
38
|
+
"Accept" => "application/json",
|
|
39
|
+
"User-Agent" => DeltaExchange.configuration.user_agent
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if authenticate
|
|
43
|
+
headers["api-key"] = @api_key
|
|
44
|
+
headers["timestamp"] = timestamp
|
|
45
|
+
headers["signature"] = Auth.sign(method, timestamp, path, query_string, body, @api_secret)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
response = @connection.send(method) do |req|
|
|
49
|
+
req.url "#{path}#{query_string}"
|
|
50
|
+
req.headers.merge!(headers)
|
|
51
|
+
req.body = body unless body.empty?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
handle_response(response)
|
|
55
|
+
rescue Faraday::TimeoutError, Faraday::ConnectionFailed, Faraday::SSLError => e
|
|
56
|
+
raise(NetworkError.new(e.message).tap { |err| err.set_backtrace(e.backtrace) })
|
|
57
|
+
rescue Faraday::Error => e
|
|
58
|
+
raise(NetworkError.new("Faraday error: #{e.message}").tap { |err| err.set_backtrace(e.backtrace) })
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def get(path, params = {}, authenticate: true)
|
|
62
|
+
request(:get, path, {}, params, authenticate: authenticate)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def post(path, payload = {}, authenticate: true)
|
|
66
|
+
request(:post, path, payload, {}, authenticate: authenticate)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def put(path, payload = {}, authenticate: true)
|
|
70
|
+
request(:put, path, payload, {}, authenticate: authenticate)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def patch(path, payload = {}, authenticate: true)
|
|
74
|
+
request(:patch, path, payload, {}, authenticate: authenticate)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def delete(path, payload = {}, params = {}, authenticate: true)
|
|
78
|
+
request(:delete, path, payload, params, authenticate: authenticate)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def products
|
|
82
|
+
@products ||= Resources::Products.new(self)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def orders
|
|
86
|
+
@orders ||= Resources::Orders.new(self)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def positions
|
|
90
|
+
@positions ||= Resources::Positions.new(self)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def account
|
|
94
|
+
@account ||= Resources::Account.new(self)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def wallet
|
|
98
|
+
@wallet ||= Resources::Wallet.new(self)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def assets
|
|
102
|
+
@assets ||= Resources::Assets.new(self)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def indices
|
|
106
|
+
@indices ||= Resources::Indices.new(self)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def fills
|
|
110
|
+
@fills ||= Resources::Fills.new(self)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def market_data
|
|
114
|
+
@market_data ||= Resources::MarketData.new(self)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def heartbeat
|
|
118
|
+
@heartbeat ||= Resources::Heartbeat.new(self)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def ensure_credentials!(authenticate)
|
|
124
|
+
return unless authenticate
|
|
125
|
+
|
|
126
|
+
return if @api_key.present? && @api_secret.present?
|
|
127
|
+
|
|
128
|
+
raise DeltaExchange::AuthenticationError, "api_key and api_secret are required for authenticated requests"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def resolve_root_url(config, explicit_base_url)
|
|
132
|
+
return explicit_base_url if explicit_base_url.present?
|
|
133
|
+
|
|
134
|
+
base = config.instance_variable_get(:@base_url)
|
|
135
|
+
return base if base.present?
|
|
136
|
+
|
|
137
|
+
@testnet ? DeltaExchange::Configuration::TESTNET_URL : DeltaExchange::Configuration::PRODUCTION_URL
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def build_connection(url, config)
|
|
141
|
+
Faraday.new(url: url) do |f|
|
|
142
|
+
f.request :url_encoded
|
|
143
|
+
if ENV["DELTA_DEBUG"] == "true"
|
|
144
|
+
f.response :logger, DeltaExchange.logger, headers: { redact: ["api-key", "signature"] }, bodies: false
|
|
145
|
+
end
|
|
146
|
+
f.options.timeout = config.read_timeout
|
|
147
|
+
f.options.open_timeout = config.connect_timeout
|
|
148
|
+
f.options.write_timeout = config.write_timeout if f.options.respond_to?(:write_timeout=)
|
|
149
|
+
f.adapter Faraday.default_adapter
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def handle_response(response)
|
|
154
|
+
body_str = response.body.to_s
|
|
155
|
+
|
|
156
|
+
if response.status == 429
|
|
157
|
+
retry_after = response.headers["X-RATE-LIMIT-RESET"]&.to_i
|
|
158
|
+
raise DeltaExchange::RateLimitError.new(
|
|
159
|
+
"Rate limited",
|
|
160
|
+
retry_after_seconds: retry_after,
|
|
161
|
+
response_body: parse_json_optional(body_str)
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
if body_str.blank?
|
|
166
|
+
raise DeltaExchange::Error, "API returned empty body (HTTP #{response.status})" unless response.success?
|
|
167
|
+
|
|
168
|
+
return {}.with_indifferent_access
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
parsed_response = JSON.parse(body_str).with_indifferent_access
|
|
172
|
+
|
|
173
|
+
return parsed_response if http_ok_without_api_failure?(response, parsed_response)
|
|
174
|
+
|
|
175
|
+
raise DeltaExchange::ApiError.from_hash(parsed_response, status: response.status)
|
|
176
|
+
rescue JSON::ParserError
|
|
177
|
+
raise DeltaExchange::Error, "API returned non-JSON response (HTTP #{response.status}): #{body_str[0, 500]}"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def parse_json_optional(raw)
|
|
181
|
+
return {}.with_indifferent_access if raw.blank?
|
|
182
|
+
|
|
183
|
+
JSON.parse(raw).with_indifferent_access
|
|
184
|
+
rescue JSON::ParserError
|
|
185
|
+
{ "raw" => raw }.with_indifferent_access
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def http_ok_without_api_failure?(response, parsed_response)
|
|
189
|
+
return false unless response.success?
|
|
190
|
+
|
|
191
|
+
return parsed_response[:success] != false if parsed_response.key?(:success)
|
|
192
|
+
|
|
193
|
+
true
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DeltaExchange
|
|
4
|
+
class Configuration
|
|
5
|
+
PRODUCTION_URL = Constants::Urls::REST_PRODUCTION
|
|
6
|
+
TESTNET_URL = Constants::Urls::REST_TESTNET
|
|
7
|
+
DEFAULT_CONNECT_TIMEOUT = 10
|
|
8
|
+
DEFAULT_READ_TIMEOUT = 30
|
|
9
|
+
DEFAULT_WRITE_TIMEOUT = 30
|
|
10
|
+
|
|
11
|
+
attr_accessor :api_key, :api_secret, :testnet, :connect_timeout, :read_timeout, :write_timeout, :user_agent, :websocket_reconnect_delay, :time_zone, :auto_retry_rate_limit
|
|
12
|
+
|
|
13
|
+
# When set, used as the REST base URL instead of {PRODUCTION_URL} / {TESTNET_URL}.
|
|
14
|
+
attr_writer :base_url
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@api_key = ENV.fetch("DELTA_API_KEY", nil)
|
|
18
|
+
@api_secret = ENV.fetch("DELTA_API_SECRET", nil)
|
|
19
|
+
@testnet = ENV.fetch("DELTA_TESTNET", "false").to_s.casecmp("true").zero?
|
|
20
|
+
@base_url = nil
|
|
21
|
+
@connect_timeout = ENV.fetch("DELTA_CONNECT_TIMEOUT", DEFAULT_CONNECT_TIMEOUT).to_i
|
|
22
|
+
@read_timeout = ENV.fetch("DELTA_READ_TIMEOUT", DEFAULT_READ_TIMEOUT).to_i
|
|
23
|
+
@write_timeout = ENV.fetch("DELTA_WRITE_TIMEOUT", DEFAULT_WRITE_TIMEOUT).to_i
|
|
24
|
+
@user_agent = ENV.fetch("DELTA_USER_AGENT", "delta-exchange-ruby")
|
|
25
|
+
@websocket_reconnect_delay = ENV.fetch("DELTA_WS_RECONNECT_DELAY", 5).to_i
|
|
26
|
+
@time_zone = ENV.fetch("DELTA_TIME_ZONE", "UTC")
|
|
27
|
+
@auto_retry_rate_limit = ENV.fetch("DELTA_AUTO_RETRY_RATE_LIMIT", "false").to_s.casecmp("true").zero?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def base_url
|
|
31
|
+
return @base_url if @base_url
|
|
32
|
+
|
|
33
|
+
testnet? ? TESTNET_URL : PRODUCTION_URL
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def testnet?
|
|
37
|
+
@testnet == true
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DeltaExchange
|
|
4
|
+
module Constants
|
|
5
|
+
module Urls
|
|
6
|
+
REST_PRODUCTION = "https://api.india.delta.exchange"
|
|
7
|
+
REST_TESTNET = "https://cdn-ind.testnet.deltaex.org"
|
|
8
|
+
WEBSOCKET_PRODUCTION = "wss://socket.india.delta.exchange"
|
|
9
|
+
WEBSOCKET_TESTNET = "wss://socket.testnet.deltaex.org"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module ContractType
|
|
13
|
+
PERPETUAL_FUTURES = "perpetual_futures"
|
|
14
|
+
CALL_OPTIONS = "call_options"
|
|
15
|
+
PUT_OPTIONS = "put_options"
|
|
16
|
+
MOVE_OPTIONS = "move_options"
|
|
17
|
+
SPREADS = "spreads"
|
|
18
|
+
FUTURES = "futures"
|
|
19
|
+
|
|
20
|
+
ALL = [PERPETUAL_FUTURES, CALL_OPTIONS, PUT_OPTIONS, MOVE_OPTIONS, SPREADS, FUTURES].freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module OrderType
|
|
24
|
+
LIMIT = "limit_order"
|
|
25
|
+
MARKET = "market_order"
|
|
26
|
+
STOP_LIMIT = "stop_limit_order"
|
|
27
|
+
STOP_MARKET = "stop_market_order"
|
|
28
|
+
|
|
29
|
+
ALL = [LIMIT, MARKET, STOP_LIMIT, STOP_MARKET].freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module Side
|
|
33
|
+
BUY = "buy"
|
|
34
|
+
SELL = "sell"
|
|
35
|
+
|
|
36
|
+
ALL = [BUY, SELL].freeze
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
module TimeInForce
|
|
40
|
+
GTC = "gtc"
|
|
41
|
+
IOC = "ioc"
|
|
42
|
+
FOK = "fok"
|
|
43
|
+
|
|
44
|
+
ALL = [GTC, IOC, FOK].freeze
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
module ProductState
|
|
48
|
+
UPCOMING = "upcoming"
|
|
49
|
+
LIVE = "live"
|
|
50
|
+
EXPIRED = "expired"
|
|
51
|
+
SETTLED = "settled"
|
|
52
|
+
|
|
53
|
+
ALL = [UPCOMING, LIVE, EXPIRED, SETTLED].freeze
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
module OrderState
|
|
57
|
+
OPEN = "open"
|
|
58
|
+
PENDING = "pending"
|
|
59
|
+
CLOSED = "closed"
|
|
60
|
+
CANCELLED = "cancelled"
|
|
61
|
+
|
|
62
|
+
ALL = [OPEN, PENDING, CLOSED, CANCELLED].freeze
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
module MarginMode
|
|
66
|
+
CROSS = "cross"
|
|
67
|
+
ISOLATED = "isolated"
|
|
68
|
+
|
|
69
|
+
ALL = [CROSS, ISOLATED].freeze
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|