melaya 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b379d2fb9b62ca5b6420c3fd1b62eddb2fdd1a4912b6ee13b467199352ffb1a8
4
+ data.tar.gz: 77644a810237149040ff1f7557885ac049017b00ae0aef6c5221050b78c744de
5
+ SHA512:
6
+ metadata.gz: 9a802bbcc841b6a2a5ff36ee0364f81d0f2a0d645611da784fa267b70a9092bbc1277ebd7f834646eb99200b5e73357ed928d2473efdf27cae70295a0b95f245
7
+ data.tar.gz: a0b627edbfa5dd8e770ebd1c36fd9c3fd9a8ee953cc8cb38dc66847451c616b7589691b70a93332eab0a425d93f96905547d3a5286439e955c16650c89d259fa
data/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # melaya
2
+
3
+ Official Ruby SDK for the **[Melaya](https://melaya.org)** trading platform — normalized market data, paper + live trading, backtesting, and an AI agentic trading crew across **70+ venues**, powered by an in-house Rust engine.
4
+
5
+ - Zero runtime gem dependencies (stdlib `net/http`, `openssl`, `json` only).
6
+ - Pure Ruby WebSocket client (RFC 6455) — no external gem required for streaming.
7
+ - Full market data, strategies, sim trading, backtesting, and streaming from one client.
8
+
9
+ ## Install
10
+
11
+ Add to your `Gemfile`:
12
+
13
+ ```ruby
14
+ gem "melaya", path: "path/to/sdk-ruby" # local checkout
15
+ ```
16
+
17
+ Or once published to RubyGems:
18
+
19
+ ```bash
20
+ gem install melaya
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ ```ruby
26
+ require "melaya"
27
+
28
+ melaya = Melaya::Client.new(api_key: ENV["MELAYA_API_KEY"]) # keys are prefixed mk_
29
+
30
+ # REST — normalized ticker from any of 70+ venues
31
+ t = melaya.market.ticker(exchange: "binance", symbol: "BTC/USDT", market: "spot")
32
+ puts t["last"], t["bid"], t["ask"]
33
+
34
+ # Order book
35
+ ob = melaya.market.orderbook(exchange: "bybit", symbol: "BTC/USDT", market: "spot", limit: 20)
36
+
37
+ # Candles
38
+ candles = melaya.market.ohlcv(exchange: "okx", symbol: "ETH/USDT", timeframe: "1h", limit: 200)
39
+ ```
40
+
41
+ ## Streaming
42
+
43
+ ```ruby
44
+ # Live ticker (block form — closes when block returns)
45
+ melaya.stream.ticker(exchange: "binance", symbol: "BTC/USDT", market: "spot") do |frame|
46
+ puts frame["last"]
47
+ break # close after first frame
48
+ end
49
+
50
+ # Liquidation firehose
51
+ melaya.stream.liquidations(exchange: "binance") do |ev|
52
+ puts ev["side"], ev["notional"]
53
+ break
54
+ end
55
+ ```
56
+
57
+ ## Trading
58
+
59
+ ```ruby
60
+ # Account: connected exchange keys and usage
61
+ keys = melaya.account.keys # [{ "apiKeyId" => "BINANCEUSDM_0", "exchange" => "binanceusdm", ... }]
62
+ usage = melaya.account.usage
63
+
64
+ # Strategies — launch immediately. Paper (dry_run: true) needs no exchange key.
65
+ # SDK-launchable strategies are `custom` Rhai definitions.
66
+ result = melaya.strategies.create(
67
+ name: "my-bot",
68
+ strategy_type: "custom",
69
+ exchange: "binanceusdm",
70
+ symbol: "BTC/USDT:USDT",
71
+ market: "FUTURES",
72
+ dry_run: true,
73
+ params: {
74
+ "language" => "rhai",
75
+ "definition" => 'fn evaluate() { emit_long(param("qty")); }',
76
+ "qty" => 0.001,
77
+ }
78
+ )
79
+ sid = result["strategyId"]
80
+ melaya.strategies.pause(sid)
81
+ melaya.strategies.resume(sid)
82
+ trades = melaya.strategies.trades(sid)
83
+
84
+ # Paper trading (sim broker) — synthetic fills, no venue state
85
+ bal = melaya.sim.balance(strategy_id: sid)
86
+ fill = melaya.sim.create_order(
87
+ strategy_id: sid,
88
+ exchange: "binanceusdm",
89
+ symbol: "BTC/USDT:USDT",
90
+ side: "buy",
91
+ type: "market",
92
+ amount: 0.001,
93
+ market: "FUTURES"
94
+ )
95
+
96
+ # Backtest on the Rust engine
97
+ r = melaya.backtest.start(
98
+ "strategyType" => "custom",
99
+ "exchange" => "binance",
100
+ "symbol" => "BTC/USDT",
101
+ "timeframe" => "1h",
102
+ "since_ms" => (Time.now.to_i - 90 * 86400) * 1000,
103
+ "until_ms" => Time.now.to_i * 1000,
104
+ "language" => "rhai",
105
+ "definition" => 'fn evaluate() { emit_long(param("qty")); }',
106
+ "params" => { "qty" => 0.001 }
107
+ )
108
+ job_id = r["job_id"]
109
+ loop do
110
+ j = melaya.backtest.job(job_id)
111
+ break if %w[done error].include?(j["status"])
112
+ sleep 2
113
+ end
114
+ result = melaya.backtest.results(job_id)
115
+
116
+ # Private streaming (ticket minted automatically)
117
+ melaya.stream.strategies do |ev|
118
+ puts ev["type"], ev["strategyId"]
119
+ break
120
+ end
121
+
122
+ # Always clean up
123
+ melaya.strategies.stop(sid)
124
+ melaya.strategies.delete(sid)
125
+ ```
126
+
127
+ ## Authentication
128
+
129
+ Create an API key in the dashboard (**melaya.org → Settings → API Keys**). Keys are prefixed `mk_`; the SDK sends it automatically on every REST call and WebSocket connection.
130
+
131
+ Public market-data and account/strategy reads work with the `mk_` key alone. **Live** order placement and live strategy launches additionally require a connected exchange key — connect one in **Settings → Connectors**, then reference it by `apiKeyId`. Paper trading and backtesting never touch a venue and need no exchange credentials.
132
+
133
+ ## TLS verification
134
+
135
+ The SDK verifies TLS certificates by default. To disable on a dev box with TLS interception:
136
+
137
+ ```bash
138
+ MELAYA_INSECURE_TLS=1 ruby your_script.rb
139
+ ```
140
+
141
+ Or pass `verify_ssl: false` to the constructor. **Never disable TLS in production.**
142
+
143
+ ## API surface
144
+
145
+ | Area | Methods |
146
+ |---|---|
147
+ | Reference | `market.list_exchanges`, `catalog_counts` |
148
+ | Market data | `market.ticker`, `orderbook`, `ohlcv`, `ohlcv_multi`, `trades`, `markets`, `currencies`, `market_constraints`, `status`, `time` |
149
+ | Batch / derivatives | `market.tickers`, `funding_rates`, `funding_rate_history`, `funding_rate_history_multi`, `open_interest`, `open_interest_history`, `open_interest_history_multi`, `instruments`, `liquidation_events` |
150
+ | Prediction markets | `market.prediction_markets` (polymarket, kalshi, drift_pm, sxbet, azuro, overtime) |
151
+ | Account | `account.keys`, `usage`, `api_key_status` |
152
+ | Strategies | `strategies.create`, `list`, `get`, `pause`, `resume`, `stop`, `delete`, `update_params`, `status`, `performance`, `executions`, `trades`, `logs` |
153
+ | AI optimizer | `strategies.ai_opt_start`, `ai_opt_status`, `ai_opt_approve`, `ai_opt_stop`, `ai_opt_runs` |
154
+ | Paper trading | `sim.balance`, `positions`, `open_orders`, `my_trades`, `create_order`, `cancel_order`, `list_accounts` |
155
+ | Backtesting | `backtest.start`, `job`, `results`, `trades`, `sweep`, `list`, `favorites`, `funding_range`, `cancel`, `delete`, `delete_all` |
156
+ | Public streaming | `stream.ticker`, `orderbook`, `ohlcv`, `trades`, `liquidations` |
157
+ | Private streaming | `stream.strategies`, `stream.private` |
158
+
159
+ Full docs: **[melaya.org/docs](https://melaya.org/docs)**.
160
+
161
+ ## License
162
+
163
+ [Apache-2.0](../../LICENSE)
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Account API — authenticated reads about your Melaya account.
5
+ #
6
+ # Connected-exchange key references (masked), tier limits, and live usage
7
+ # counters. Requires an mk_ key on the private plane.
8
+ class AccountAPI
9
+ def initialize(http)
10
+ @http = http
11
+ end
12
+
13
+ # The exchange API keys connected to your account. +api_key+ is masked
14
+ # (display-only); use +api_key_id+ (e.g. BINANCEUSDM_0) when launching
15
+ # strategies or minting a private stream ticket.
16
+ def keys
17
+ @http.get("/api/v1/private/keys")["keys"]
18
+ end
19
+
20
+ # Tier, plan limits, and live usage counters (mirrors the dashboard's usage page).
21
+ def usage
22
+ @http.get("/api/v1/private/usage")
23
+ end
24
+
25
+ # Status of your platform API key (tier, max concurrent connections).
26
+ def api_key_status
27
+ @http.get("/api/v1/private/api-key")
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Backtest API — run strategies against historical data on the Rust engine.
5
+ #
6
+ # Start a single run or a parameter sweep (grid / random), poll the job,
7
+ # then pull metrics, the equity curve, and the trade list. All backtests run
8
+ # natively on Melaya's in-house engine — no per-venue SDK in the loop.
9
+ #
10
+ # Maps to https://api.melaya.org/api/v1/private/backtest/* on the private plane.
11
+ class BacktestAPI
12
+ def initialize(http)
13
+ @http = http
14
+ end
15
+
16
+ # Start a backtest. +mode+ defaults to a single run; pass "grid_sweep" /
17
+ # "random_sweep" with +param_ranges+ to fan out a parameter search.
18
+ # Returns a hash with "job_id" (and optionally "count").
19
+ #
20
+ # @param body [Hash] see BacktestStart type in the reference SDKs
21
+ def start(body)
22
+ @http.post("/api/v1/private/backtest/start", body)
23
+ end
24
+
25
+ # Job status + progress (+status+, +progress_pct+, ...).
26
+ # @param job_id [String]
27
+ def job(job_id)
28
+ @http.get("/api/v1/private/backtest/jobs/#{job_id}")
29
+ end
30
+
31
+ # Metrics, equity curve, and OHLCV for a completed job.
32
+ # @param job_id [String]
33
+ def results(job_id)
34
+ @http.get("/api/v1/private/backtest/results/#{job_id}")["result"]
35
+ end
36
+
37
+ # The trade list for a completed job (default 500, max 5000 per call).
38
+ # @param job_id [String]
39
+ # @param limit [Integer, nil]
40
+ # @param offset [Integer, nil]
41
+ def trades(job_id, limit: nil, offset: nil)
42
+ @http.get("/api/v1/private/backtest/trades/#{job_id}",
43
+ "limit" => limit, "offset" => offset
44
+ )["trades"]
45
+ end
46
+
47
+ # Ranked children of a sweep parent (default objective: sharpe DESC).
48
+ # @param parent_id [String]
49
+ # @param objective [String, nil]
50
+ # @param limit [Integer, nil]
51
+ def sweep(parent_id, objective: nil, limit: nil)
52
+ @http.get("/api/v1/private/backtest/sweep/#{parent_id}",
53
+ "objective" => objective, "limit" => limit
54
+ )
55
+ end
56
+
57
+ # Your backtest jobs, newest first.
58
+ # @param limit [Integer, nil]
59
+ # @param offset [Integer, nil]
60
+ def list(limit: nil, offset: nil)
61
+ @http.get("/api/v1/private/backtest",
62
+ "limit" => limit, "offset" => offset
63
+ ).dig("data", "jobs")
64
+ end
65
+
66
+ # Your favorited backtest jobs (Forge tier and above).
67
+ # @param limit [Integer, nil]
68
+ # @param offset [Integer, nil]
69
+ def favorites(limit: nil, offset: nil)
70
+ @http.get("/api/v1/private/backtest/favorites",
71
+ "limit" => limit, "offset" => offset
72
+ ).dig("data", "jobs")
73
+ end
74
+
75
+ # Earliest funding-rate timestamp available for an exchange+symbol (ms, or nil).
76
+ # @param exchange [String]
77
+ # @param symbol [String]
78
+ def funding_range(exchange:, symbol:)
79
+ @http.get("/api/v1/private/backtest/funding-range",
80
+ "exchange" => exchange, "symbol" => symbol
81
+ )["earliest_ms"]
82
+ end
83
+
84
+ # Cancel an in-flight job.
85
+ # @param job_id [String]
86
+ def cancel(job_id)
87
+ @http.post("/api/v1/private/backtest/#{job_id}/cancel")
88
+ end
89
+
90
+ # Soft-delete a single job.
91
+ # @param job_id [String]
92
+ def delete(job_id)
93
+ @http.delete("/api/v1/private/backtest/#{job_id}")
94
+ end
95
+
96
+ # Soft-delete every non-favorited job. Returns a hash with "deleted" count.
97
+ def delete_all
98
+ @http.delete("/api/v1/private/backtest")
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Raised for non-2xx REST responses or ok:false envelopes.
5
+ class MelayaError < StandardError
6
+ attr_reader :status, :code, :body
7
+
8
+ def initialize(message, status: 0, code: nil, body: nil)
9
+ super(message)
10
+ @status = status
11
+ @code = code
12
+ @body = body
13
+ end
14
+ end
15
+ end
@@ -0,0 +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
@@ -0,0 +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
data/lib/melaya/sim.rb ADDED
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Paper-trading (sim broker) API.
5
+ #
6
+ # The sim broker synthesises fills from Melaya's live ticker tape and keeps a
7
+ # virtual wallet per strategy — no venue-side state changes, no exchange
8
+ # credentials needed. Every call is scoped to a +strategy_id+.
9
+ #
10
+ # Create a paper strategy first:
11
+ # result = melaya.strategies.create(name: "test", strategy_type: "custom", ...)
12
+ # sid = result["strategyId"]
13
+ class SimAPI
14
+ def initialize(http)
15
+ @http = http
16
+ end
17
+
18
+ # Paper accounts (one virtual wallet per paper strategy).
19
+ def list_accounts
20
+ r = @http.get("/api/v1/private/sim/list-accounts")
21
+ r.is_a?(Array) ? r : (r.is_a?(Hash) ? (r["accounts"] || []) : [])
22
+ end
23
+
24
+ # Virtual balance for a paper strategy (equity, realized/unrealized PnL, free/used).
25
+ # @param strategy_id [String]
26
+ # @param asset [String, nil]
27
+ def balance(strategy_id:, asset: nil)
28
+ @http.get("/api/v1/private/sim/balance",
29
+ "strategy_id" => strategy_id, "asset" => asset
30
+ )
31
+ end
32
+
33
+ # Open paper positions for a strategy.
34
+ def positions(strategy_id:)
35
+ r = @http.get("/api/v1/private/sim/positions", "strategy_id" => strategy_id)
36
+ r.is_a?(Array) ? r : (r.is_a?(Hash) ? (r["positions"] || []) : [])
37
+ end
38
+
39
+ # Resting paper orders for a strategy.
40
+ def open_orders(strategy_id:)
41
+ r = @http.get("/api/v1/private/sim/open-orders", "strategy_id" => strategy_id)
42
+ r.is_a?(Array) ? r : (r.is_a?(Hash) ? (r["orders"] || []) : [])
43
+ end
44
+
45
+ # Filled paper trades for a strategy.
46
+ def my_trades(strategy_id:)
47
+ r = @http.get("/api/v1/private/sim/my-trades", "strategy_id" => strategy_id)
48
+ r.is_a?(Array) ? r : (r.is_a?(Hash) ? (r["trades"] || []) : [])
49
+ end
50
+
51
+ # Place a paper order. Fills synthesise from the live ticker; nothing hits the venue.
52
+ #
53
+ # @param strategy_id [String]
54
+ # @param exchange [String]
55
+ # @param symbol [String]
56
+ # @param side [String] "buy" or "sell"
57
+ # @param amount [Numeric]
58
+ # @param type [String] "market" or "limit" (default "market")
59
+ # @param price [Numeric, nil] required for limit orders
60
+ # @param market [String, nil]
61
+ # @param leverage [Numeric, nil]
62
+ # @param reduce_only [Boolean, nil]
63
+ # @param sl_price [Numeric, nil]
64
+ # @param tp_price [Numeric, nil]
65
+ # @param client_order_id [String, nil]
66
+ # @param params [Hash, nil]
67
+ def create_order(strategy_id:, exchange:, symbol:, side:, amount:,
68
+ type: "market", price: nil, market: nil, leverage: nil,
69
+ reduce_only: nil, sl_price: nil, tp_price: nil,
70
+ client_order_id: nil, params: nil)
71
+ body = {
72
+ "strategy_id" => strategy_id,
73
+ "exchange" => exchange,
74
+ "symbol" => symbol,
75
+ "side" => side,
76
+ "amount" => amount,
77
+ "order_type" => type,
78
+ "orderType" => type,
79
+ "price" => price,
80
+ "market" => market,
81
+ "market_type" => market,
82
+ "leverage" => leverage,
83
+ "reduceOnly" => reduce_only,
84
+ "slPrice" => sl_price,
85
+ "tpPrice" => tp_price,
86
+ "client_order_id" => client_order_id,
87
+ "clientOrderId" => client_order_id,
88
+ "params" => params,
89
+ }.reject { |_, v| v.nil? }
90
+ @http.post("/api/v1/private/sim/create-order", body)
91
+ end
92
+
93
+ # Cancel a resting paper order.
94
+ #
95
+ # @param strategy_id [String]
96
+ # @param order_id [String]
97
+ # @param symbol [String, nil]
98
+ # @param exchange [String, nil]
99
+ def cancel_order(strategy_id:, order_id:, symbol: nil, exchange: nil)
100
+ body = {
101
+ "strategy_id" => strategy_id,
102
+ "order_id" => order_id,
103
+ "orderId" => order_id,
104
+ "symbol" => symbol,
105
+ "exchange" => exchange,
106
+ }.reject { |_, v| v.nil? }
107
+ @http.post("/api/v1/private/sim/cancel-order", body)
108
+ end
109
+ end
110
+ end