melaya 0.1.0 → 0.1.4

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.
@@ -1,97 +1,97 @@
1
- # frozen_string_literal: true
2
-
3
- require "net/http"
4
- require "uri"
5
- require "json"
6
- require "openssl"
7
-
8
- require_relative "errors"
9
-
10
- module Melaya
11
- # Internal HTTP client. Injects the API key on every call as both
12
- # a query-param (?apiKey=) and Authorization: Bearer header.
13
- class HttpClient
14
- DEFAULT_BASE_URL = "https://api.melaya.org"
15
-
16
- def initialize(api_key:, base_url: DEFAULT_BASE_URL, verify_ssl: true)
17
- @api_key = api_key
18
- @base_uri = URI.parse(base_url.chomp("/"))
19
- @verify_ssl = verify_ssl
20
- end
21
-
22
- def get(path, params = {})
23
- request(:get, path, params: params)
24
- end
25
-
26
- def post(path, body = nil)
27
- request(:post, path, body: body)
28
- end
29
-
30
- def delete(path, params = {})
31
- request(:delete, path, params: params)
32
- end
33
-
34
- private
35
-
36
- def build_uri(path, params = {})
37
- uri = URI.parse("#{@base_uri}#{path}")
38
- query = { "apiKey" => @api_key }
39
- params.each { |k, v| query[k.to_s] = v.to_s unless v.nil? }
40
- uri.query = URI.encode_www_form(query)
41
- uri
42
- end
43
-
44
- def request(method, path, params: {}, body: nil)
45
- uri = build_uri(path, params)
46
-
47
- http = Net::HTTP.new(uri.host, uri.port)
48
- http.use_ssl = uri.scheme == "https"
49
- http.verify_mode = @verify_ssl ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
50
- http.open_timeout = 15
51
- http.read_timeout = 60
52
-
53
- req = case method
54
- when :get then Net::HTTP::Get.new(uri)
55
- when :post then Net::HTTP::Post.new(uri)
56
- when :delete then Net::HTTP::Delete.new(uri)
57
- else raise ArgumentError, "Unknown HTTP method: #{method}"
58
- end
59
-
60
- req["Authorization"] = "Bearer #{@api_key}"
61
- req["Accept"] = "application/json"
62
-
63
- if body
64
- req["Content-Type"] = "application/json"
65
- req.body = JSON.generate(body)
66
- end
67
-
68
- resp = http.request(req)
69
- parse(resp)
70
- end
71
-
72
- def parse(resp)
73
- text = resp.body.to_s.strip
74
- data = begin
75
- text.empty? ? nil : JSON.parse(text)
76
- rescue JSON::ParserError
77
- text
78
- end
79
-
80
- if resp.code.to_i >= 400
81
- code = data.is_a?(Hash) ? data["error"] : nil
82
- msg = "Melaya API #{resp.code}" + (code ? " (#{code})" : "")
83
- raise MelayaError.new(msg, status: resp.code.to_i, code: code, body: data)
84
- end
85
-
86
- # The API wraps every payload in { "ok": true/false, ... }.
87
- # ok:false is a request-level failure — raise instead of returning silently.
88
- if data.is_a?(Hash) && data["ok"] == false
89
- code = data["error"]
90
- msg = "Melaya API request failed" + (code ? ": #{code}" : "")
91
- raise MelayaError.new(msg, status: resp.code.to_i, code: code, body: data)
92
- end
93
-
94
- data
95
- end
96
- end
97
- end
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+ require "openssl"
7
+
8
+ require_relative "errors"
9
+
10
+ module Melaya
11
+ # Internal HTTP client. Injects the API key on every call as both
12
+ # a query-param (?apiKey=) and Authorization: Bearer header.
13
+ class HttpClient
14
+ DEFAULT_BASE_URL = "https://api.melaya.org"
15
+
16
+ def initialize(api_key:, base_url: DEFAULT_BASE_URL, verify_ssl: true)
17
+ @api_key = api_key
18
+ @base_uri = URI.parse(base_url.chomp("/"))
19
+ @verify_ssl = verify_ssl
20
+ end
21
+
22
+ def get(path, params = {})
23
+ request(:get, path, params: params)
24
+ end
25
+
26
+ def post(path, body = nil)
27
+ request(:post, path, body: body)
28
+ end
29
+
30
+ def delete(path, params = {})
31
+ request(:delete, path, params: params)
32
+ end
33
+
34
+ private
35
+
36
+ def build_uri(path, params = {})
37
+ uri = URI.parse("#{@base_uri}#{path}")
38
+ query = { "apiKey" => @api_key }
39
+ params.each { |k, v| query[k.to_s] = v.to_s unless v.nil? }
40
+ uri.query = URI.encode_www_form(query)
41
+ uri
42
+ end
43
+
44
+ def request(method, path, params: {}, body: nil)
45
+ uri = build_uri(path, params)
46
+
47
+ http = Net::HTTP.new(uri.host, uri.port)
48
+ http.use_ssl = uri.scheme == "https"
49
+ http.verify_mode = @verify_ssl ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
50
+ http.open_timeout = 15
51
+ http.read_timeout = 60
52
+
53
+ req = case method
54
+ when :get then Net::HTTP::Get.new(uri)
55
+ when :post then Net::HTTP::Post.new(uri)
56
+ when :delete then Net::HTTP::Delete.new(uri)
57
+ else raise ArgumentError, "Unknown HTTP method: #{method}"
58
+ end
59
+
60
+ req["Authorization"] = "Bearer #{@api_key}"
61
+ req["Accept"] = "application/json"
62
+
63
+ if body
64
+ req["Content-Type"] = "application/json"
65
+ req.body = JSON.generate(body)
66
+ end
67
+
68
+ resp = http.request(req)
69
+ parse(resp)
70
+ end
71
+
72
+ def parse(resp)
73
+ text = resp.body.to_s.strip
74
+ data = begin
75
+ text.empty? ? nil : JSON.parse(text)
76
+ rescue JSON::ParserError
77
+ text
78
+ end
79
+
80
+ if resp.code.to_i >= 400
81
+ code = data.is_a?(Hash) ? data["error"] : nil
82
+ msg = "Melaya API #{resp.code}" + (code ? " (#{code})" : "")
83
+ raise MelayaError.new(msg, status: resp.code.to_i, code: code, body: data)
84
+ end
85
+
86
+ # The API wraps every payload in { "ok": true/false, ... }.
87
+ # ok:false is a request-level failure — raise instead of returning silently.
88
+ if data.is_a?(Hash) && data["ok"] == false
89
+ code = data["error"]
90
+ msg = "Melaya API request failed" + (code ? ": #{code}" : "")
91
+ raise MelayaError.new(msg, status: resp.code.to_i, code: code, body: data)
92
+ end
93
+
94
+ data
95
+ end
96
+ end
97
+ end
data/lib/melaya/market.rb CHANGED
@@ -1,156 +1,156 @@
1
- # frozen_string_literal: true
2
-
3
- module Melaya
4
- # REST market-data API — normalized across all 70+ venues.
5
- #
6
- # Every method maps to a documented endpoint under
7
- # https://api.melaya.org/api/v1/market/*. Unwraps the inner data key from the
8
- # {ok, <data>} envelope.
9
- class MarketAPI
10
- def initialize(http)
11
- @http = http
12
- end
13
-
14
- # List the exchanges Melaya supports right now (the source of truth).
15
- def list_exchanges
16
- @http.get("/api/v1/market/list-exchanges")["exchanges"]
17
- end
18
-
19
- # Best bid/ask, last price, and 24h aggregates for one symbol.
20
- # @param exchange [String]
21
- # @param symbol [String]
22
- # @param market [String, nil] e.g. "spot", "future", "swap"
23
- def ticker(exchange:, symbol:, market: nil)
24
- @http.get("/api/v1/market/ticker",
25
- "exchange" => exchange, "symbol" => symbol, "market" => market
26
- )["ticker"]
27
- end
28
-
29
- # Order book to a given depth.
30
- def orderbook(exchange:, symbol:, limit: nil, market: nil)
31
- @http.get("/api/v1/market/orderbook",
32
- "exchange" => exchange, "symbol" => symbol, "limit" => limit, "market" => market
33
- )["orderbook"]
34
- end
35
-
36
- # OHLCV candles. Each candle is [timestamp, open, high, low, close, volume].
37
- def ohlcv(exchange:, symbol:, timeframe:, limit: nil, market: nil)
38
- @http.get("/api/v1/market/ohlcv",
39
- "exchange" => exchange, "symbol" => symbol, "timeframe" => timeframe,
40
- "limit" => limit, "market" => market
41
- )["candles"]
42
- end
43
-
44
- # Recent public trades.
45
- def trades(exchange:, symbol:, market: nil)
46
- @http.get("/api/v1/market/trades",
47
- "exchange" => exchange, "symbol" => symbol, "market" => market
48
- )["trades"]
49
- end
50
-
51
- # Tradable markets on a venue.
52
- def markets(exchange:)
53
- @http.get("/api/v1/market/markets", "exchange" => exchange)["markets"]
54
- end
55
-
56
- # Listed currencies on a venue. (Not supported on every venue.)
57
- def currencies(exchange:)
58
- @http.get("/api/v1/market/currencies", "exchange" => exchange)["currencies"]
59
- end
60
-
61
- # Operational status: ok / maintenance / degraded.
62
- def status(exchange:)
63
- @http.get("/api/v1/market/status", "exchange" => exchange)["status"]
64
- end
65
-
66
- # Exchange server time.
67
- def time(exchange:)
68
- @http.get("/api/v1/market/time", "exchange" => exchange)["time"]
69
- end
70
-
71
- # ── Batch / derivatives (POST) ──────────────────────────────────────────
72
-
73
- # Tickers for many symbols on one venue in a single call. Keyed by symbol.
74
- def tickers(exchange:, symbols:, market: nil)
75
- body = compact("exchange" => exchange, "symbols" => symbols, "market" => market)
76
- @http.post("/api/v1/market/tickers", body)["tickers"]
77
- end
78
-
79
- # Latest funding rates for perpetuals. Keyed by symbol.
80
- def funding_rates(exchange:, symbols:, market: nil)
81
- body = compact("exchange" => exchange, "symbols" => symbols, "market" => market)
82
- @http.post("/api/v1/market/funding-rates", body)["rates"]
83
- end
84
-
85
- # Funding-rate history.
86
- def funding_rate_history(exchange:, symbol:, hours: nil, market: nil)
87
- body = compact("exchange" => exchange, "symbol" => symbol, "hours" => hours, "market" => market)
88
- @http.post("/api/v1/market/funding-rate-history", body)["history"]
89
- end
90
-
91
- # Open interest for one or more perpetuals. Keyed by symbol.
92
- def open_interest(exchange:, symbols:, market: nil)
93
- body = compact("exchange" => exchange, "symbols" => symbols, "market" => market)
94
- @http.post("/api/v1/market/open-interest", body)["openInterest"]
95
- end
96
-
97
- # Open-interest history.
98
- def open_interest_history(exchange:, symbol:, hours: nil, market: nil)
99
- body = compact("exchange" => exchange, "symbol" => symbol, "hours" => hours, "market" => market)
100
- @http.post("/api/v1/market/open-interest-history", body)["history"]
101
- end
102
-
103
- # Instrument list + trading constraints (tick size, min notional, qty step).
104
- def instruments(exchange:, market: nil)
105
- body = compact("exchange" => exchange, "market" => market)
106
- @http.post("/api/v1/market/instruments", body)
107
- end
108
-
109
- # Cross-exchange liquidation events (historical query).
110
- def liquidation_events(exchange: nil, symbol: nil, since_ms: nil, limit: nil)
111
- body = compact("exchange" => exchange, "symbol" => symbol, "sinceMs" => since_ms, "limit" => limit)
112
- @http.post("/api/v1/market/liquidation-events", body)["events"]
113
- end
114
-
115
- # Multi-symbol OHLCV in one call. Returns candle arrays keyed by symbol.
116
- def ohlcv_multi(exchange:, symbols:, timeframe:, limit: nil, market: nil)
117
- body = compact("exchange" => exchange, "symbols" => symbols, "timeframe" => timeframe,
118
- "limit" => limit, "market" => market)
119
- @http.post("/api/v1/market/ohlcv-multi", body)["perSymbol"]
120
- end
121
-
122
- # Trading constraints for one symbol (tick size, min notional, qty step, leverage).
123
- def market_constraints(exchange:, symbol:, market: nil)
124
- body = compact("exchange" => exchange, "symbol" => symbol, "market" => market)
125
- @http.post("/api/v1/market/market-constraints", body)["constraints"]
126
- end
127
-
128
- # Funding-rate history for one symbol across several venues. Keyed by exchange.
129
- def funding_rate_history_multi(exchanges:, symbol:, hours: nil)
130
- body = compact("exchanges" => exchanges, "symbol" => symbol, "hours" => hours)
131
- @http.post("/api/v1/market/funding-rate-history-multi", body)["perExchange"]
132
- end
133
-
134
- # Open-interest history for one symbol across several venues. Keyed by exchange.
135
- def open_interest_history_multi(exchanges:, symbol:, hours: nil)
136
- body = compact("exchanges" => exchanges, "symbol" => symbol, "hours" => hours)
137
- @http.post("/api/v1/market/open-interest-history-multi", body)["perExchange"]
138
- end
139
-
140
- # Prediction-market listings for a venue (polymarket, kalshi, drift_pm, sxbet, azuro, overtime).
141
- def prediction_markets(venue: "polymarket")
142
- @http.post("/api/v1/market/pm-markets", { "venue" => venue })["markets"]
143
- end
144
-
145
- # Live platform catalog counts (agentic tools, subagents, by category). Public.
146
- def catalog_counts
147
- @http.get("/api/v1/public/catalog-counts")
148
- end
149
-
150
- private
151
-
152
- def compact(hash)
153
- hash.reject { |_, v| v.nil? }
154
- end
155
- end
156
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # REST market-data API — normalized across all 70+ venues.
5
+ #
6
+ # Every method maps to a documented endpoint under
7
+ # https://api.melaya.org/api/v1/market/*. Unwraps the inner data key from the
8
+ # {ok, <data>} envelope.
9
+ class MarketAPI
10
+ def initialize(http)
11
+ @http = http
12
+ end
13
+
14
+ # List the exchanges Melaya supports right now (the source of truth).
15
+ def list_exchanges
16
+ @http.get("/api/v1/market/list-exchanges")["exchanges"]
17
+ end
18
+
19
+ # Best bid/ask, last price, and 24h aggregates for one symbol.
20
+ # @param exchange [String]
21
+ # @param symbol [String]
22
+ # @param market [String, nil] e.g. "spot", "future", "swap"
23
+ def ticker(exchange:, symbol:, market: nil)
24
+ @http.get("/api/v1/market/ticker",
25
+ "exchange" => exchange, "symbol" => symbol, "market" => market
26
+ )["ticker"]
27
+ end
28
+
29
+ # Order book to a given depth.
30
+ def orderbook(exchange:, symbol:, limit: nil, market: nil)
31
+ @http.get("/api/v1/market/orderbook",
32
+ "exchange" => exchange, "symbol" => symbol, "limit" => limit, "market" => market
33
+ )["orderbook"]
34
+ end
35
+
36
+ # OHLCV candles. Each candle is [timestamp, open, high, low, close, volume].
37
+ def ohlcv(exchange:, symbol:, timeframe:, limit: nil, market: nil)
38
+ @http.get("/api/v1/market/ohlcv",
39
+ "exchange" => exchange, "symbol" => symbol, "timeframe" => timeframe,
40
+ "limit" => limit, "market" => market
41
+ )["candles"]
42
+ end
43
+
44
+ # Recent public trades.
45
+ def trades(exchange:, symbol:, market: nil)
46
+ @http.get("/api/v1/market/trades",
47
+ "exchange" => exchange, "symbol" => symbol, "market" => market
48
+ )["trades"]
49
+ end
50
+
51
+ # Tradable markets on a venue.
52
+ def markets(exchange:)
53
+ @http.get("/api/v1/market/markets", "exchange" => exchange)["markets"]
54
+ end
55
+
56
+ # Listed currencies on a venue. (Not supported on every venue.)
57
+ def currencies(exchange:)
58
+ @http.get("/api/v1/market/currencies", "exchange" => exchange)["currencies"]
59
+ end
60
+
61
+ # Operational status: ok / maintenance / degraded.
62
+ def status(exchange:)
63
+ @http.get("/api/v1/market/status", "exchange" => exchange)["status"]
64
+ end
65
+
66
+ # Exchange server time.
67
+ def time(exchange:)
68
+ @http.get("/api/v1/market/time", "exchange" => exchange)["time"]
69
+ end
70
+
71
+ # ── Batch / derivatives (POST) ──────────────────────────────────────────
72
+
73
+ # Tickers for many symbols on one venue in a single call. Keyed by symbol.
74
+ def tickers(exchange:, symbols:, market: nil)
75
+ body = compact("exchange" => exchange, "symbols" => symbols, "market" => market)
76
+ @http.post("/api/v1/market/tickers", body)["tickers"]
77
+ end
78
+
79
+ # Latest funding rates for perpetuals. Keyed by symbol.
80
+ def funding_rates(exchange:, symbols:, market: nil)
81
+ body = compact("exchange" => exchange, "symbols" => symbols, "market" => market)
82
+ @http.post("/api/v1/market/funding-rates", body)["rates"]
83
+ end
84
+
85
+ # Funding-rate history.
86
+ def funding_rate_history(exchange:, symbol:, hours: nil, market: nil)
87
+ body = compact("exchange" => exchange, "symbol" => symbol, "hours" => hours, "market" => market)
88
+ @http.post("/api/v1/market/funding-rate-history", body)["history"]
89
+ end
90
+
91
+ # Open interest for one or more perpetuals. Keyed by symbol.
92
+ def open_interest(exchange:, symbols:, market: nil)
93
+ body = compact("exchange" => exchange, "symbols" => symbols, "market" => market)
94
+ @http.post("/api/v1/market/open-interest", body)["openInterest"]
95
+ end
96
+
97
+ # Open-interest history.
98
+ def open_interest_history(exchange:, symbol:, hours: nil, market: nil)
99
+ body = compact("exchange" => exchange, "symbol" => symbol, "hours" => hours, "market" => market)
100
+ @http.post("/api/v1/market/open-interest-history", body)["history"]
101
+ end
102
+
103
+ # Instrument list + trading constraints (tick size, min notional, qty step).
104
+ def instruments(exchange:, market: nil)
105
+ body = compact("exchange" => exchange, "market" => market)
106
+ @http.post("/api/v1/market/instruments", body)
107
+ end
108
+
109
+ # Cross-exchange liquidation events (historical query).
110
+ def liquidation_events(exchange: nil, symbol: nil, since_ms: nil, limit: nil)
111
+ body = compact("exchange" => exchange, "symbol" => symbol, "sinceMs" => since_ms, "limit" => limit)
112
+ @http.post("/api/v1/market/liquidation-events", body)["events"]
113
+ end
114
+
115
+ # Multi-symbol OHLCV in one call. Returns candle arrays keyed by symbol.
116
+ def ohlcv_multi(exchange:, symbols:, timeframe:, limit: nil, market: nil)
117
+ body = compact("exchange" => exchange, "symbols" => symbols, "timeframe" => timeframe,
118
+ "limit" => limit, "market" => market)
119
+ @http.post("/api/v1/market/ohlcv-multi", body)["perSymbol"]
120
+ end
121
+
122
+ # Trading constraints for one symbol (tick size, min notional, qty step, leverage).
123
+ def market_constraints(exchange:, symbol:, market: nil)
124
+ body = compact("exchange" => exchange, "symbol" => symbol, "market" => market)
125
+ @http.post("/api/v1/market/market-constraints", body)["constraints"]
126
+ end
127
+
128
+ # Funding-rate history for one symbol across several venues. Keyed by exchange.
129
+ def funding_rate_history_multi(exchanges:, symbol:, hours: nil)
130
+ body = compact("exchanges" => exchanges, "symbol" => symbol, "hours" => hours)
131
+ @http.post("/api/v1/market/funding-rate-history-multi", body)["perExchange"]
132
+ end
133
+
134
+ # Open-interest history for one symbol across several venues. Keyed by exchange.
135
+ def open_interest_history_multi(exchanges:, symbol:, hours: nil)
136
+ body = compact("exchanges" => exchanges, "symbol" => symbol, "hours" => hours)
137
+ @http.post("/api/v1/market/open-interest-history-multi", body)["perExchange"]
138
+ end
139
+
140
+ # Prediction-market listings for a venue (polymarket, kalshi, drift_pm, sxbet, azuro, overtime).
141
+ def prediction_markets(venue: "polymarket")
142
+ @http.post("/api/v1/market/pm-markets", { "venue" => venue })["markets"]
143
+ end
144
+
145
+ # Live platform catalog counts (agentic tools, subagents, by category). Public.
146
+ def catalog_counts
147
+ @http.get("/api/v1/public/catalog-counts")
148
+ end
149
+
150
+ private
151
+
152
+ def compact(hash)
153
+ hash.reject { |_, v| v.nil? }
154
+ end
155
+ end
156
+ end