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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.cursor/.gitignore +1 -0
  3. data/CHANGELOG.md +11 -0
  4. data/CODE_OF_CONDUCT.md +10 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +253 -0
  7. data/Rakefile +12 -0
  8. data/docs/AUTHENTICATION.md +49 -0
  9. data/docs/GETTING_STARTED.md +67 -0
  10. data/docs/RAILS_INTEGRATION.md +135 -0
  11. data/docs/REST_API_GUIDE.md +150 -0
  12. data/docs/STANDALONE_RUBY_GUIDE.md +73 -0
  13. data/docs/WEBSOCKET_GUIDE.md +160 -0
  14. data/exe/delta_exchange +4 -0
  15. data/lib/delta_exchange/auth.rb +12 -0
  16. data/lib/delta_exchange/client.rb +196 -0
  17. data/lib/delta_exchange/configuration.rb +40 -0
  18. data/lib/delta_exchange/constants.rb +72 -0
  19. data/lib/delta_exchange/contracts/order_contract.rb +24 -0
  20. data/lib/delta_exchange/contracts/position_contract.rb +21 -0
  21. data/lib/delta_exchange/contracts/wallet_transfer_contract.rb +16 -0
  22. data/lib/delta_exchange/core/base_model.rb +54 -0
  23. data/lib/delta_exchange/core/error_handler.rb +16 -0
  24. data/lib/delta_exchange/error.rb +37 -0
  25. data/lib/delta_exchange/helpers/attribute_helper.rb +22 -0
  26. data/lib/delta_exchange/helpers/validation_helper.rb +34 -0
  27. data/lib/delta_exchange/models/asset.rb +23 -0
  28. data/lib/delta_exchange/models/fee_tier.rb +19 -0
  29. data/lib/delta_exchange/models/fill.rb +20 -0
  30. data/lib/delta_exchange/models/funding_rate.rb +19 -0
  31. data/lib/delta_exchange/models/index.rb +23 -0
  32. data/lib/delta_exchange/models/open_interest.rb +19 -0
  33. data/lib/delta_exchange/models/order.rb +34 -0
  34. data/lib/delta_exchange/models/position.rb +43 -0
  35. data/lib/delta_exchange/models/product.rb +43 -0
  36. data/lib/delta_exchange/models/profile.rb +20 -0
  37. data/lib/delta_exchange/models/ticker.rb +26 -0
  38. data/lib/delta_exchange/models/trading_preferences.rb +27 -0
  39. data/lib/delta_exchange/models/wallet_balance.rb +23 -0
  40. data/lib/delta_exchange/models/wallet_transaction.rb +20 -0
  41. data/lib/delta_exchange/resources/account.rb +53 -0
  42. data/lib/delta_exchange/resources/assets.rb +11 -0
  43. data/lib/delta_exchange/resources/base.rb +37 -0
  44. data/lib/delta_exchange/resources/fills.rb +15 -0
  45. data/lib/delta_exchange/resources/heartbeat.rb +20 -0
  46. data/lib/delta_exchange/resources/indices.rb +11 -0
  47. data/lib/delta_exchange/resources/market_data.rb +56 -0
  48. data/lib/delta_exchange/resources/orders.rb +76 -0
  49. data/lib/delta_exchange/resources/positions.rb +47 -0
  50. data/lib/delta_exchange/resources/products.rb +39 -0
  51. data/lib/delta_exchange/resources/wallet.rb +45 -0
  52. data/lib/delta_exchange/version.rb +5 -0
  53. data/lib/delta_exchange/websocket/client.rb +55 -0
  54. data/lib/delta_exchange/websocket/connection.rb +114 -0
  55. data/lib/delta_exchange.rb +39 -0
  56. data/sig/delta_exchange.rbs +4 -0
  57. metadata +231 -0
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeltaExchange
4
+ module Resources
5
+ class Base
6
+ attr_reader :client
7
+
8
+ def initialize(client = nil)
9
+ raise ArgumentError, "client must be provided" unless client
10
+
11
+ @client = client
12
+ end
13
+
14
+ protected
15
+
16
+ def get(path, params = {}, authenticate: true)
17
+ client.get(path, params, authenticate: authenticate)
18
+ end
19
+
20
+ def post(path, payload = {}, authenticate: true)
21
+ client.post(path, payload, authenticate: authenticate)
22
+ end
23
+
24
+ def put(path, payload = {}, authenticate: true)
25
+ client.put(path, payload, authenticate: authenticate)
26
+ end
27
+
28
+ def patch(path, payload = {}, authenticate: true)
29
+ client.patch(path, payload, authenticate: authenticate)
30
+ end
31
+
32
+ def delete(path, payload = {}, params = {}, authenticate: true)
33
+ client.delete(path, payload, params, authenticate: authenticate)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeltaExchange
4
+ module Resources
5
+ class Fills < Base
6
+ def all(params = {})
7
+ get("/v2/fills", params)
8
+ end
9
+
10
+ def history_csv(params = {})
11
+ get("/v2/fills/history/download/csv", params)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeltaExchange
4
+ module Resources
5
+ # Session keepalive endpoints; confirm paths against your Delta environment (not in public REST slate list).
6
+ class Heartbeat < Base
7
+ def create(payload)
8
+ post("/v2/heartbeats", payload)
9
+ end
10
+
11
+ def ack(payload)
12
+ put("/v2/heartbeats/ack", payload)
13
+ end
14
+
15
+ def all(params = {})
16
+ get("/v2/heartbeats", params)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeltaExchange
4
+ module Resources
5
+ class Indices < Base
6
+ def all
7
+ get("/v2/indices", {}, authenticate: false)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module DeltaExchange
6
+ module Resources
7
+ class MarketData < Base
8
+ def l2_orderbook(symbol, params = {})
9
+ sym = CGI.escape(symbol.to_s)
10
+ get("/v2/l2orderbook/#{sym}", params, authenticate: false)
11
+ end
12
+
13
+ def trades(symbol, params = {})
14
+ sym = CGI.escape(symbol.to_s)
15
+ get("/v2/trades/#{sym}", params, authenticate: false)
16
+ end
17
+
18
+ def candles(params = {})
19
+ get("/v2/history/candles", params, authenticate: false)
20
+ end
21
+
22
+ def sparklines(params = {})
23
+ get("/v2/history/sparklines", params, authenticate: false)
24
+ end
25
+
26
+ def stats(params = {})
27
+ get("/v2/stats", params, authenticate: false)
28
+ end
29
+
30
+ def mark_price(symbol)
31
+ sym = CGI.escape(symbol.to_s)
32
+ get("/v2/mark_price/#{sym}", {}, authenticate: false)
33
+ end
34
+
35
+ def insurance_fund
36
+ get("/v2/insurance_fund", {}, authenticate: false)
37
+ end
38
+
39
+ def option_greeks(params = {})
40
+ get("/v2/option_greeks", params, authenticate: false)
41
+ end
42
+
43
+ def funding_rates(params = {})
44
+ get("/v2/history/funding_rates", params, authenticate: false)
45
+ end
46
+
47
+ def open_interest(params = {})
48
+ get("/v2/history/open_interest", params, authenticate: false)
49
+ end
50
+
51
+ def volume_stats(params = {})
52
+ get("/v2/stats/volume", params, authenticate: false)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module DeltaExchange
6
+ module Resources
7
+ class Orders < Base
8
+ def all(params = {})
9
+ get("/v2/orders", params)
10
+ end
11
+
12
+ def find(id)
13
+ get("/v2/orders/#{id}")
14
+ end
15
+
16
+ def find_by_client_order_id(client_order_id)
17
+ oid = CGI.escape(client_order_id.to_s)
18
+ get("/v2/orders/client_order_id/#{oid}")
19
+ end
20
+
21
+ def history(params = {})
22
+ get("/v2/orders/history", params)
23
+ end
24
+
25
+ def create(payload)
26
+ validate_order!(payload)
27
+ post("/v2/orders", payload)
28
+ end
29
+
30
+ def create_bracket(payload)
31
+ # Bracket orders have similar but slightly different fields
32
+ post("/v2/orders/bracket", payload)
33
+ end
34
+
35
+ def create_batch(payload)
36
+ post("/v2/orders/batch", payload)
37
+ end
38
+
39
+ def update(payload)
40
+ validate_order!(payload)
41
+ put("/v2/orders", payload)
42
+ end
43
+
44
+ private
45
+
46
+ def validate_order!(payload)
47
+ result = Contracts::OrderContract.new.call(payload)
48
+ return if result.success?
49
+
50
+ raise ValidationError, "Invalid order parameters: #{result.errors.to_h}"
51
+ end
52
+
53
+ # @param payload [Hash] payload containing product_id and id (or client_order_id)
54
+ # @example client.orders.cancel(product_id: 1, id: 123)
55
+ def cancel(payload)
56
+ delete("/v2/orders", payload)
57
+ end
58
+
59
+ def cancel_all(params = {})
60
+ delete("/v2/orders/all", {}, params)
61
+ end
62
+
63
+ def cancel_batch(payload)
64
+ delete("/v2/orders/batch", payload)
65
+ end
66
+
67
+ def cancel_after(payload)
68
+ post("/v2/orders/cancel_after", payload)
69
+ end
70
+
71
+ def set_leverage(payload)
72
+ post("/v2/orders/leverage", payload)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeltaExchange
4
+ module Resources
5
+ class Positions < Base
6
+ def all(params = {})
7
+ get("/v2/positions", params)
8
+ end
9
+
10
+ def margined(params = {})
11
+ get("/v2/positions/margined", params)
12
+ end
13
+
14
+ def find(product_id)
15
+ get("/v2/positions/#{product_id}")
16
+ end
17
+
18
+ def adjust_margin(payload)
19
+ validate_position!(payload)
20
+ post("/v2/positions/change_margin", payload)
21
+ end
22
+
23
+ def change_leverage(payload)
24
+ validate_position!(payload)
25
+ post("/v2/positions/change_leverage", payload)
26
+ end
27
+
28
+ private
29
+
30
+ def validate_position!(payload)
31
+ result = Contracts::PositionContract.new.call(payload)
32
+ return if result.success?
33
+
34
+ raise ValidationError, "Invalid position parameters: #{result.errors.to_h}"
35
+ end
36
+
37
+ def auto_topup(payload)
38
+ put("/v2/positions/auto_topup", payload)
39
+ end
40
+
41
+ def close_all(payload = {})
42
+ post("/v2/positions/close_all", payload)
43
+ end
44
+ public :auto_topup, :close_all
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module DeltaExchange
6
+ module Resources
7
+ class Products < Base
8
+ def all(params = {})
9
+ get("/v2/products", params, authenticate: false)
10
+ end
11
+
12
+ def find(symbol)
13
+ sym = CGI.escape(symbol.to_s)
14
+ get("/v2/products/#{sym}", {}, authenticate: false)
15
+ end
16
+
17
+ def tickers(params = {})
18
+ get("/v2/tickers", params, authenticate: false)
19
+ end
20
+
21
+ def ticker(symbol)
22
+ sym = CGI.escape(symbol.to_s)
23
+ get("/v2/tickers/#{sym}", {}, authenticate: false)
24
+ end
25
+
26
+ def settlement_prices(params = {})
27
+ get("/v2/settlement_prices", params, authenticate: false)
28
+ end
29
+
30
+ def leverage(product_id)
31
+ get("/v2/products/#{product_id}/orders/leverage")
32
+ end
33
+
34
+ def set_leverage(product_id, payload)
35
+ post("/v2/products/#{product_id}/orders/leverage", payload)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeltaExchange
4
+ module Resources
5
+ class Wallet < Base
6
+ def balances
7
+ get("/v2/wallet/balances")
8
+ end
9
+
10
+ def transactions(params = {})
11
+ get("/v2/wallet/transactions", params)
12
+ end
13
+
14
+ def transactions_download(params = {})
15
+ get("/v2/wallet/transactions/download", params)
16
+ end
17
+
18
+ def subaccount_transfer_history(params = {})
19
+ get("/v2/wallets/sub_accounts_transfer_history", params)
20
+ end
21
+
22
+ def subaccount_transfer(payload)
23
+ validate_transfer!(payload)
24
+ post("/v2/wallets/sub_account_balance_transfer", payload)
25
+ end
26
+
27
+ private
28
+
29
+ def validate_transfer!(payload)
30
+ result = Contracts::WalletTransferContract.new.call(payload)
31
+ return if result.success?
32
+
33
+ raise ValidationError, "Invalid transfer parameters: #{result.errors.to_h}"
34
+ end
35
+
36
+ def withdrawals(params = {})
37
+ get("/v2/wallet/withdrawals", params)
38
+ end
39
+
40
+ def deposits(params = {})
41
+ get("/v2/wallet/deposits", params)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeltaExchange
4
+ VERSION = "0.1.2"
5
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeltaExchange
4
+ module Websocket
5
+ class Client
6
+ def initialize(api_key: nil, api_secret: nil, testnet: nil)
7
+ @api_key = api_key || DeltaExchange.configuration.api_key
8
+ @api_secret = api_secret || DeltaExchange.configuration.api_secret
9
+ use_testnet = testnet.nil? ? DeltaExchange.configuration.testnet : testnet
10
+ @url = use_testnet ? Constants::Urls::WEBSOCKET_TESTNET : Constants::Urls::WEBSOCKET_PRODUCTION
11
+ @callbacks = Hash.new { |h, k| h[k] = [] }
12
+
13
+ @connection = Connection.new(@url, api_key: @api_key, api_secret: @api_secret)
14
+ @connection.on_open = ->(event) { emit(:open, event) }
15
+ @connection.on_message = ->(msg) { emit(:message, msg) }
16
+ @connection.on_close = ->(event) { emit(:close, event) }
17
+ @connection.on_error = ->(event) { emit(:error, event) }
18
+ end
19
+
20
+ def on(event, &block)
21
+ @callbacks[event] << block
22
+ self
23
+ end
24
+
25
+ def connect!
26
+ @connection.start
27
+ self
28
+ end
29
+
30
+ def subscribe(channels)
31
+ @connection.send_json({
32
+ type: "subscribe",
33
+ payload: { channels: channels }
34
+ })
35
+ end
36
+
37
+ def unsubscribe(channels)
38
+ @connection.send_json({
39
+ type: "unsubscribe",
40
+ payload: { channels: channels }
41
+ })
42
+ end
43
+
44
+ def close
45
+ @connection.stop
46
+ end
47
+
48
+ private
49
+
50
+ def emit(event, data)
51
+ @callbacks[event].each { |cb| cb.call(data) }
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faye/websocket"
4
+ require "eventmachine"
5
+ require "json"
6
+
7
+ module DeltaExchange
8
+ module Websocket
9
+ class Connection
10
+ RECONNECT_DELAY = 5
11
+ attr_accessor :on_open, :on_message, :on_close, :on_error
12
+
13
+ def initialize(url, api_key: nil, api_secret: nil)
14
+ @url = url
15
+ @api_key = api_key
16
+ @api_secret = api_secret
17
+ @ws = nil
18
+ @stop = false
19
+ end
20
+
21
+ def start
22
+ @thr = Thread.new { loop_run }
23
+ end
24
+
25
+ def stop
26
+ @stop = true
27
+ @ws&.close
28
+ EM.stop if EM.reactor_running?
29
+ end
30
+
31
+ def send_json(data)
32
+ @ws&.send(data.to_json)
33
+ end
34
+
35
+ def authenticate!
36
+ timestamp = Time.now.utc.to_i
37
+ path = "/v2/websocket"
38
+ method = "GET"
39
+ signature = Auth.sign(method, timestamp.to_s, path, "", "", @api_secret)
40
+
41
+ send_json({
42
+ type: "auth",
43
+ api_key: @api_key,
44
+ timestamp: timestamp,
45
+ signature: signature
46
+ })
47
+ end
48
+
49
+ private
50
+
51
+ def loop_run
52
+ until @stop
53
+ begin
54
+ if EM.reactor_running?
55
+ setup_ws
56
+ wait_for_connection_close
57
+ else
58
+ EM.run { setup_ws }
59
+ end
60
+ rescue StandardError => e
61
+ DeltaExchange.logger.error("[DeltaExchange::WS] Loop Error: #{e.message}")
62
+ end
63
+ handle_reconnect_delay
64
+ end
65
+ end
66
+
67
+ def wait_for_connection_close
68
+ # Instead of a tight loop, we sleep for longer intervals while connected
69
+ # or we could use a ConditionVariable if we wanted to be more reactive.
70
+ # But even a longer sleep is better than the original "busy-wait".
71
+ # The WebSocket's on :close will resume the loop if EM stops.
72
+ sleep 1 while @ws&.ready_state == 1 && !@stop
73
+ end
74
+
75
+ def handle_reconnect_delay
76
+ return if @stop
77
+
78
+ delay = DeltaExchange.configuration.websocket_reconnect_delay
79
+ sleep delay
80
+ end
81
+
82
+ def setup_ws
83
+ headers = { "User-Agent" => DeltaExchange.configuration.user_agent }
84
+ @ws = Faye::WebSocket::Client.new(@url, nil, { headers: headers })
85
+
86
+ @ws.on :open do |event|
87
+ DeltaExchange.logger.info("[DeltaExchange::WS] Connected")
88
+ authenticate! if @api_key && @api_secret
89
+ @on_open&.call(event)
90
+ end
91
+
92
+ @ws.on :message do |event|
93
+ data = begin
94
+ JSON.parse(event.data)
95
+ rescue StandardError
96
+ event.data
97
+ end
98
+ @on_message&.call(data)
99
+ end
100
+
101
+ @ws.on :close do |event|
102
+ DeltaExchange.logger.warn("[DeltaExchange::WS] Closed: #{event.code} #{event.reason}")
103
+ @on_close&.call(event)
104
+ EM.stop unless EM.reactor_running? # Only stop if we started it
105
+ end
106
+
107
+ @ws.on :error do |event|
108
+ DeltaExchange.logger.error("[DeltaExchange::WS] Error: #{event.message}")
109
+ @on_error&.call(event)
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+ require "active_support/core_ext/hash/indifferent_access"
5
+ require "active_support/core_ext/object/blank"
6
+ require "logger"
7
+
8
+ module DeltaExchange
9
+ loader = Zeitwerk::Loader.for_gem
10
+ loader.setup
11
+
12
+ require_relative "delta_exchange/error"
13
+
14
+ class << self
15
+ attr_writer :configuration, :logger
16
+
17
+ def configuration
18
+ @configuration ||= Configuration.new
19
+ end
20
+
21
+ def configure
22
+ self.configuration ||= Configuration.new
23
+ yield(configuration)
24
+ self.logger ||= Logger.new($stdout, level: Logger::INFO)
25
+ end
26
+
27
+ def logger
28
+ @logger ||= Logger.new($stdout, level: Logger::INFO)
29
+ end
30
+
31
+ def ensure_configuration!
32
+ self.configuration ||= Configuration.new
33
+ end
34
+
35
+ def reset!
36
+ @configuration = Configuration.new
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,4 @@
1
+ module DeltaExchange
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end