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.
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Strategies API — launch, control, and inspect trading strategies.
5
+ #
6
+ # A strategy is a server-managed runner (the Trading Engine, or an Agentic
7
+ # Trading Crew) that trades a universe on a cadence with server-side SL/TP
8
+ # and safety rails. Launch in paper mode (dry_run: true) or live
9
+ # (dry_run: false, which requires a connected exchange key).
10
+ #
11
+ # Maps to https://api.melaya.org/api/v1/strategies/* on the private plane.
12
+ class StrategiesAPI
13
+ def initialize(http)
14
+ @http = http
15
+ end
16
+
17
+ # Every strategy you own (running, paused, paper, and live).
18
+ def list
19
+ @http.get("/api/v1/strategies/list")["strategies"]
20
+ end
21
+
22
+ # A single strategy by id.
23
+ # @param strategy_id [String]
24
+ def get(strategy_id)
25
+ @http.get("/api/v1/strategies/#{strategy_id}")["strategy"]
26
+ end
27
+
28
+ # Launch a strategy. Pass dry_run: true for paper; live needs api_key_id.
29
+ # Returns the full response hash (includes "strategyId").
30
+ #
31
+ # @param name [String]
32
+ # @param strategy_type [String] e.g. "custom"
33
+ # @param exchange [String]
34
+ # @param market [String, nil]
35
+ # @param symbol [String, nil]
36
+ # @param api_key_id [String, nil]
37
+ # @param params [Hash, nil]
38
+ # @param runtime_mode [String, nil]
39
+ # @param dry_run [Boolean]
40
+ # @param key_bindings [Hash, nil]
41
+ def create(name:, strategy_type:, exchange:, market: nil, symbol: nil,
42
+ api_key_id: nil, params: nil, runtime_mode: nil, dry_run: true,
43
+ key_bindings: nil)
44
+ body = {
45
+ "name" => name,
46
+ "strategyType" => strategy_type,
47
+ "exchange" => exchange,
48
+ "market" => market,
49
+ "symbol" => symbol,
50
+ "apiKeyId" => api_key_id,
51
+ "params" => params,
52
+ "runtimeMode" => runtime_mode,
53
+ "dryRun" => dry_run,
54
+ "keyBindings" => key_bindings,
55
+ }.reject { |_, v| v.nil? }
56
+ @http.post("/api/v1/strategies", body)
57
+ end
58
+
59
+ # Pause a running strategy (stops entering new cycles until resumed).
60
+ def pause(strategy_id)
61
+ @http.post("/api/v1/strategies/#{strategy_id}/pause")
62
+ end
63
+
64
+ # Resume a paused strategy.
65
+ def resume(strategy_id)
66
+ @http.post("/api/v1/strategies/#{strategy_id}/resume")
67
+ end
68
+
69
+ # Stop a strategy and tear down its runner. Cancels any in-flight approvals.
70
+ def stop(strategy_id)
71
+ @http.post("/api/v1/strategies/#{strategy_id}/stop")
72
+ end
73
+
74
+ # Soft-delete a strategy.
75
+ def delete(strategy_id)
76
+ @http.delete("/api/v1/strategies/#{strategy_id}")
77
+ end
78
+
79
+ # Update a running strategy's params (e.g. universe, cadence, risk caps).
80
+ # @param strategy_id [String]
81
+ # @param params [Hash]
82
+ def update_params(strategy_id, params)
83
+ @http.post("/api/v1/strategies/#{strategy_id}/update-params", params)
84
+ end
85
+
86
+ # Live runtime status of a strategy's runner (container health, tick count).
87
+ def status(strategy_id)
88
+ @http.get("/api/v1/strategies/#{strategy_id}/status")
89
+ end
90
+
91
+ # Performance series for a strategy (equity, PnL over time).
92
+ def performance(strategy_id)
93
+ @http.get("/api/v1/strategies/#{strategy_id}/performance")["rows"]
94
+ end
95
+
96
+ # Execution (order) rows for a strategy.
97
+ def executions(strategy_id)
98
+ @http.get("/api/v1/strategies/#{strategy_id}/executions")["rows"]
99
+ end
100
+
101
+ # Trade (fill) rows for a strategy.
102
+ def trades(strategy_id)
103
+ @http.get("/api/v1/strategies/#{strategy_id}/trades")["rows"]
104
+ end
105
+
106
+ # Log rows for a strategy (cycle markers, persona messages, errors).
107
+ def logs(strategy_id)
108
+ @http.get("/api/v1/strategies/#{strategy_id}/logs")["rows"]
109
+ end
110
+
111
+ # ── AI parameter optimizer ────────────────────────────────────────────────
112
+
113
+ # Kick off an AI-driven parameter optimization.
114
+ # +param_bounds+ maps each param name to a [min, max] array.
115
+ # +objective+ defaults to "sharpe"; +max_iterations+ is clamped to 1-20.
116
+ # Returns a hash including "runId".
117
+ #
118
+ # @param strategy_id [String]
119
+ # @param param_bounds [Hash] e.g. { "qty" => [0.001, 0.1] }
120
+ # @param objective [String]
121
+ # @param max_iterations [Integer]
122
+ # @param require_approval [Boolean, nil]
123
+ def ai_opt_start(strategy_id, param_bounds:, objective: "sharpe",
124
+ max_iterations: 3, require_approval: nil)
125
+ body = {
126
+ "paramBounds" => param_bounds,
127
+ "objective" => objective,
128
+ "maxIterations" => max_iterations,
129
+ }
130
+ body["requireApproval"] = require_approval unless require_approval.nil?
131
+ @http.post("/api/v1/strategies/#{strategy_id}/ai-opt/start", body)
132
+ end
133
+
134
+ # Current optimization status for a strategy.
135
+ def ai_opt_status(strategy_id)
136
+ @http.get("/api/v1/strategies/#{strategy_id}/ai-opt/status")
137
+ end
138
+
139
+ # Approve and apply the optimizer's proposed params to the running strategy.
140
+ # @param body [Hash] optional extra params
141
+ def ai_opt_approve(strategy_id, body = {})
142
+ @http.post("/api/v1/strategies/#{strategy_id}/ai-opt/approve", body)
143
+ end
144
+
145
+ # Stop an in-progress optimization.
146
+ def ai_opt_stop(strategy_id)
147
+ @http.post("/api/v1/strategies/#{strategy_id}/ai-opt/stop")
148
+ end
149
+
150
+ # Past optimization runs for a strategy.
151
+ def ai_opt_runs(strategy_id)
152
+ @http.get("/api/v1/strategies/#{strategy_id}/ai-opt/runs")
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,336 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "openssl"
5
+ require "uri"
6
+ require "base64"
7
+ require "digest"
8
+ require "json"
9
+ require "securerandom"
10
+
11
+ require_relative "errors"
12
+
13
+ module Melaya
14
+ # A minimal RFC 6455 WebSocket client built on stdlib TCP+TLS.
15
+ # No external gem required. Supports text frames; yields parsed JSON frames.
16
+ #
17
+ # Usage (block form — closes automatically):
18
+ # MelayaWebSocket.connect(url, verify_ssl: true) do |ws|
19
+ # ws.each_frame { |frame| puts frame.inspect; break }
20
+ # end
21
+ #
22
+ # Usage (manual):
23
+ # ws = MelayaWebSocket.new(url, verify_ssl: true)
24
+ # ws.connect
25
+ # ws.each_frame { |f| ... }
26
+ # ws.close
27
+ class MelayaWebSocket
28
+ GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
29
+
30
+ def self.connect(url, verify_ssl: true, &block)
31
+ ws = new(url, verify_ssl: verify_ssl)
32
+ ws.connect
33
+ if block_given?
34
+ begin
35
+ block.call(ws)
36
+ ensure
37
+ ws.close
38
+ end
39
+ else
40
+ ws
41
+ end
42
+ end
43
+
44
+ def initialize(url, verify_ssl: true)
45
+ @uri = URI.parse(url)
46
+ @verify_ssl = verify_ssl
47
+ @socket = nil
48
+ @closed = false
49
+ @buf = String.new("", encoding: "BINARY")
50
+ end
51
+
52
+ def connect
53
+ tcp = TCPSocket.new(@uri.host, @uri.port)
54
+
55
+ @socket = if @uri.scheme == "wss"
56
+ ctx = OpenSSL::SSL::SSLContext.new
57
+ ctx.verify_mode = @verify_ssl ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
58
+ ssl = OpenSSL::SSL::SSLSocket.new(tcp, ctx)
59
+ ssl.hostname = @uri.host
60
+ ssl.connect
61
+ ssl
62
+ else
63
+ tcp
64
+ end
65
+
66
+ handshake
67
+ self
68
+ end
69
+
70
+ # Iterate over incoming JSON frames. Yields each parsed frame Hash.
71
+ # Blocks until the socket closes or the block calls +break+.
72
+ def each_frame
73
+ loop do
74
+ frame = read_frame
75
+ break if frame.nil? # connection closed
76
+
77
+ next if frame.empty? # ping/pong or non-text
78
+
79
+ begin
80
+ yield JSON.parse(frame)
81
+ rescue JSON::ParserError
82
+ next # ignore non-JSON keep-alive text
83
+ end
84
+ end
85
+ rescue IOError, Errno::ECONNRESET, EOFError
86
+ # socket closed by remote
87
+ ensure
88
+ close
89
+ end
90
+
91
+ def close
92
+ return if @closed
93
+ @closed = true
94
+ begin
95
+ # Send a close frame (opcode 0x8) with no body, masked
96
+ send_frame(0x8, "")
97
+ rescue StandardError
98
+ # best-effort
99
+ ensure
100
+ @socket&.close rescue nil
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def handshake
107
+ key = Base64.strict_encode64(SecureRandom.bytes(16))
108
+ path = @uri.request_uri
109
+ host = @uri.host + (@uri.port && ![80, 443].include?(@uri.port) ? ":#{@uri.port}" : "")
110
+
111
+ request = [
112
+ "GET #{path} HTTP/1.1",
113
+ "Host: #{host}",
114
+ "Upgrade: websocket",
115
+ "Connection: Upgrade",
116
+ "Sec-WebSocket-Key: #{key}",
117
+ "Sec-WebSocket-Version: 13",
118
+ "\r\n",
119
+ ].join("\r\n")
120
+
121
+ @socket.write(request)
122
+
123
+ # Read response headers
124
+ response = String.new("", encoding: "BINARY")
125
+ loop do
126
+ line = @socket.readline
127
+ response << line
128
+ break if line == "\r\n"
129
+ end
130
+
131
+ unless response.include?("101")
132
+ raise MelayaError.new("WebSocket handshake failed: #{response.lines.first.strip}", status: 0)
133
+ end
134
+
135
+ expected = Base64.strict_encode64(Digest::SHA1.digest("#{key}#{GUID}"))
136
+ unless response.include?(expected)
137
+ raise MelayaError.new("WebSocket Sec-WebSocket-Accept mismatch", status: 0)
138
+ end
139
+ end
140
+
141
+ # Read one complete WebSocket frame. Returns the payload string (text) or nil on close.
142
+ # Skips ping frames (sends pong) and continuation frames (not used by Melaya).
143
+ def read_frame
144
+ loop do
145
+ # Need at least 2 bytes for header
146
+ fill_buf(2)
147
+ b0 = @buf.getbyte(0)
148
+ b1 = @buf.getbyte(1)
149
+ @buf = @buf[2..]
150
+
151
+ # fin = (b0 & 0x80) != 0 # we accept single-frame messages
152
+ opcode = b0 & 0x0F
153
+ masked = (b1 & 0x80) != 0
154
+ len = b1 & 0x7F
155
+
156
+ len = case len
157
+ when 126
158
+ fill_buf(2)
159
+ v = @buf[0, 2].unpack1("n")
160
+ @buf = @buf[2..]
161
+ v
162
+ when 127
163
+ fill_buf(8)
164
+ v = @buf[0, 8].unpack1("Q>")
165
+ @buf = @buf[8..]
166
+ v
167
+ else
168
+ len
169
+ end
170
+
171
+ mask_key = nil
172
+ if masked
173
+ fill_buf(4)
174
+ mask_key = @buf[0, 4]
175
+ @buf = @buf[4..]
176
+ end
177
+
178
+ fill_buf(len)
179
+ payload = @buf[0, len].dup
180
+ @buf = @buf[len..]
181
+
182
+ if masked && mask_key
183
+ payload.bytes.each_with_index { |b, i| payload.setbyte(i, b ^ mask_key.getbyte(i % 4)) }
184
+ end
185
+
186
+ case opcode
187
+ when 0x1 # text
188
+ return payload.force_encoding("UTF-8")
189
+ when 0x2 # binary — treat as text (Melaya sends JSON)
190
+ return payload.force_encoding("UTF-8")
191
+ when 0x8 # close
192
+ return nil
193
+ when 0x9 # ping — send pong
194
+ send_frame(0xA, payload)
195
+ next
196
+ when 0xA # pong
197
+ next
198
+ else
199
+ next # ignore unknown opcodes
200
+ end
201
+ end
202
+ end
203
+
204
+ # Ensure @buf has at least +n+ bytes.
205
+ def fill_buf(n)
206
+ while @buf.bytesize < n
207
+ chunk = @socket.read(n - @buf.bytesize)
208
+ raise EOFError, "connection closed" if chunk.nil? || chunk.empty?
209
+ @buf = @buf + chunk
210
+ end
211
+ end
212
+
213
+ # Send a WebSocket frame with masking (client->server MUST be masked).
214
+ def send_frame(opcode, payload)
215
+ payload = payload.dup.force_encoding("BINARY")
216
+ len = payload.bytesize
217
+
218
+ header = String.new("", encoding: "BINARY")
219
+ header << (0x80 | opcode).chr(Encoding::BINARY)
220
+
221
+ mask_flag = 0x80 # clients must mask
222
+ if len <= 125
223
+ header << (mask_flag | len).chr(Encoding::BINARY)
224
+ elsif len <= 65535
225
+ header << (mask_flag | 126).chr(Encoding::BINARY)
226
+ header << [len].pack("n")
227
+ else
228
+ header << (mask_flag | 127).chr(Encoding::BINARY)
229
+ header << [len].pack("Q>")
230
+ end
231
+
232
+ mask_key = SecureRandom.bytes(4)
233
+ header << mask_key
234
+
235
+ masked_payload = payload.bytes.each_with_index.map { |b, i| b ^ mask_key.getbyte(i % 4) }.pack("C*")
236
+
237
+ @socket.write(header + masked_payload)
238
+ end
239
+ end
240
+
241
+ # WebSocket streaming API.
242
+ #
243
+ # Each method yields parsed JSON frame hashes to a block, or returns an
244
+ # Enumerator if no block is given. The connection is closed when the block
245
+ # returns or the Enumerator is exhausted.
246
+ #
247
+ # Public stream example:
248
+ # melaya.stream.ticker(exchange: "binance", symbol: "BTC/USDT", market: "spot") do |frame|
249
+ # puts frame["last"]
250
+ # break # close after first frame
251
+ # end
252
+ #
253
+ # Private stream example (ticket minted automatically):
254
+ # melaya.stream.strategies do |ev|
255
+ # puts ev["type"], ev["strategyId"]
256
+ # break
257
+ # end
258
+ class StreamAPI
259
+ DEFAULT_WS_URL = "wss://wss.melaya.org"
260
+
261
+ def initialize(api_key, ws_url, http, verify_ssl: true)
262
+ @api_key = api_key
263
+ @ws_url = ws_url.to_s.chomp("/")
264
+ @http = http
265
+ @verify_ssl = verify_ssl
266
+ end
267
+
268
+ # Live ticker frames.
269
+ def ticker(exchange:, symbol:, market: nil, &block)
270
+ open_public("/ws/ticker", compact(exchange: exchange, symbol: symbol, market: market), &block)
271
+ end
272
+
273
+ # Live order-book frames.
274
+ def orderbook(exchange:, symbol:, limit: nil, market: nil, &block)
275
+ open_public("/ws/orderbook", compact(exchange: exchange, symbol: symbol, limit: limit, market: market), &block)
276
+ end
277
+
278
+ # Live OHLCV candle frames.
279
+ def ohlcv(exchange:, symbol:, timeframe:, market: nil, &block)
280
+ open_public("/ws/ohlcv", compact(exchange: exchange, symbol: symbol, timeframe: timeframe, market: market), &block)
281
+ end
282
+
283
+ # Live public-trade frames.
284
+ def trades(exchange:, symbol:, market: nil, &block)
285
+ open_public("/ws/public-trades", compact(exchange: exchange, symbol: symbol, market: market), &block)
286
+ end
287
+
288
+ # Cross-exchange liquidation firehose. Omit +exchange+ for all venues.
289
+ def liquidations(exchange: nil, &block)
290
+ open_public("/ws/liquidations", compact(exchange: exchange), &block)
291
+ end
292
+
293
+ # Live strategy events for your account (cycle markers, agent messages,
294
+ # approval requests, executions, status). Mints a ticket, opens /ws/strategies.
295
+ def strategies(&block)
296
+ open_private("/ws/strategies", "strategies", {}, &block)
297
+ end
298
+
299
+ # Live private account feed for one connected exchange key (balance,
300
+ # positions, your orders/fills). Pass +api_key_id+ from +account.keys()+.
301
+ def private(exchange:, market: nil, api_key_id: nil, key_id: nil, symbol: nil, &block)
302
+ params = compact(
303
+ exchange: exchange, market: market,
304
+ apiKeyId: api_key_id, keyId: key_id, symbol: symbol
305
+ )
306
+ open_private("/ws/private", "private", params, &block)
307
+ end
308
+
309
+ private
310
+
311
+ def compact(hash)
312
+ hash.reject { |_, v| v.nil? }
313
+ end
314
+
315
+ def build_url(path, params)
316
+ q = params.merge("apiKey" => @api_key)
317
+ "#{@ws_url}#{path}?#{URI.encode_www_form(q)}"
318
+ end
319
+
320
+ def open_public(path, params, &block)
321
+ url = build_url(path, params.transform_keys(&:to_s))
322
+ MelayaWebSocket.connect(url, verify_ssl: @verify_ssl) do |ws|
323
+ ws.each_frame(&block)
324
+ end
325
+ end
326
+
327
+ def open_private(path, stream, params, &block)
328
+ body = { "stream" => stream }.merge(params.transform_keys(&:to_s))
329
+ ticket = @http.post("/api/v1/private/private-ticket", body)["wsTicket"]
330
+ url = "#{@ws_url}#{path}?#{URI.encode_www_form('wsTicket' => ticket)}"
331
+ MelayaWebSocket.connect(url, verify_ssl: @verify_ssl) do |ws|
332
+ ws.each_frame(&block)
333
+ end
334
+ end
335
+ end
336
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Live trading API — credentialed order placement, account state, and
5
+ # position management on a CONNECTED exchange.
6
+ #
7
+ # Every method POSTs to +https://api.melaya.org/api/v1/private/<op>+; the server
8
+ # resolves your connected exchange credential (referenced by +api_key_id+ — see
9
+ # AccountAPI#keys) and forwards the call to the venue through Melaya's in-house
10
+ # Rust engine. Responses share an envelope:
11
+ # +{ ok, exchange, operation, orderId, clientOrderId, payload, data, ... }+.
12
+ #
13
+ # WARNING: these hit the REAL venue with REAL funds. The write methods
14
+ # (create_order, cancel_order, amend_order, cancel_all_orders, cancel_plan_orders,
15
+ # close_position, set_leverage, set_margin_mode, set_position_mode) move money or
16
+ # change account state. For risk-free testing use the sim (paper) broker or a
17
+ # paper strategy instead.
18
+ class TradeAPI
19
+ def initialize(http)
20
+ @http = http
21
+ end
22
+
23
+ # ── Account state (reads) ──────────────────────────────────────────────────
24
+
25
+ # Live account balance on a connected venue.
26
+ def balance(exchange:, api_key_id: nil, key_id: nil, market_type: nil, params: nil)
27
+ op("balance", exchange: exchange, apiKeyId: api_key_id, keyId: key_id,
28
+ marketType: market_type, params: params)
29
+ end
30
+
31
+ # Live open positions.
32
+ def positions(exchange:, api_key_id: nil, market_type: nil, symbol: nil, params: nil)
33
+ op("positions", exchange: exchange, apiKeyId: api_key_id,
34
+ marketType: market_type, symbol: symbol, params: params)
35
+ end
36
+
37
+ # Historical positions (venue-dependent).
38
+ def positions_history(exchange:, api_key_id: nil, market_type: nil, symbol: nil)
39
+ op("positions-history", exchange: exchange, apiKeyId: api_key_id,
40
+ marketType: market_type, symbol: symbol)
41
+ end
42
+
43
+ # Resting (open) orders.
44
+ def open_orders(exchange:, api_key_id: nil, market_type: nil, symbol: nil)
45
+ op("open-orders", exchange: exchange, apiKeyId: api_key_id,
46
+ marketType: market_type, symbol: symbol)
47
+ end
48
+
49
+ # All orders (open + recent).
50
+ def orders(exchange:, api_key_id: nil, market_type: nil, symbol: nil)
51
+ op("orders", exchange: exchange, apiKeyId: api_key_id,
52
+ marketType: market_type, symbol: symbol)
53
+ end
54
+
55
+ # Closed/filled orders.
56
+ def closed_orders(exchange:, api_key_id: nil, market_type: nil, symbol: nil)
57
+ op("closed-orders", exchange: exchange, apiKeyId: api_key_id,
58
+ marketType: market_type, symbol: symbol)
59
+ end
60
+
61
+ # Your trade (fill) history.
62
+ def my_trades(exchange:, api_key_id: nil, market_type: nil, symbol: nil)
63
+ op("my-trades", exchange: exchange, apiKeyId: api_key_id,
64
+ marketType: market_type, symbol: symbol)
65
+ end
66
+
67
+ # Extended trade history (venue-dependent).
68
+ def my_trades_history(exchange:, api_key_id: nil, market_type: nil, symbol: nil)
69
+ op("my-trades-history", exchange: exchange, apiKeyId: api_key_id,
70
+ marketType: market_type, symbol: symbol)
71
+ end
72
+
73
+ # Resting conditional/plan (trigger) orders.
74
+ def plan_orders(exchange:, api_key_id: nil, market_type: nil, symbol: nil)
75
+ op("plan-orders", exchange: exchange, apiKeyId: api_key_id,
76
+ marketType: market_type, symbol: symbol)
77
+ end
78
+
79
+ # Current leverage for a symbol.
80
+ def leverage(exchange:, api_key_id: nil, symbol: nil, market_type: nil)
81
+ op("leverage", exchange: exchange, apiKeyId: api_key_id,
82
+ symbol: symbol, marketType: market_type)
83
+ end
84
+
85
+ # Leverage tiers / brackets for a symbol.
86
+ def leverage_tiers(exchange:, api_key_id: nil, symbol: nil, market_type: nil)
87
+ op("leverage-tiers", exchange: exchange, apiKeyId: api_key_id,
88
+ symbol: symbol, marketType: market_type)
89
+ end
90
+
91
+ # ── Order placement & management (LIVE writes — real funds) ───────────────
92
+
93
+ # Place a live order on the venue. WARNING: real money.
94
+ # stop_price, take_profit_price, and reduce_only are folded into +params+.
95
+ def create_order(exchange:, symbol:, side:, amount:,
96
+ api_key_id: nil, type: "market", price: nil,
97
+ market_type: nil, stop_price: nil, take_profit_price: nil,
98
+ reduce_only: nil, leverage: nil, client_order_id: nil, params: nil)
99
+ p = (params || {}).dup
100
+ p["stopPrice"] = stop_price unless stop_price.nil?
101
+ p["takeProfitPrice"] = take_profit_price unless take_profit_price.nil?
102
+ p["reduceOnly"] = reduce_only unless reduce_only.nil?
103
+ op("create-order",
104
+ exchange: exchange, apiKeyId: api_key_id, symbol: symbol,
105
+ side: side, amount: amount, type: type, price: price,
106
+ marketType: market_type, leverage: leverage,
107
+ clientOrderId: client_order_id, params: p.empty? ? nil : p)
108
+ end
109
+
110
+ # Cancel a live order by id. WARNING.
111
+ def cancel_order(exchange:, api_key_id: nil, order_id: nil, client_order_id: nil,
112
+ symbol: nil, market_type: nil)
113
+ op("cancel-order", exchange: exchange, apiKeyId: api_key_id,
114
+ orderId: order_id, clientOrderId: client_order_id,
115
+ symbol: symbol, marketType: market_type)
116
+ end
117
+
118
+ # Amend (modify) a live order. WARNING.
119
+ def amend_order(exchange:, api_key_id: nil, order_id: nil, symbol: nil,
120
+ amount: nil, price: nil)
121
+ op("amend-order", exchange: exchange, apiKeyId: api_key_id,
122
+ orderId: order_id, symbol: symbol, amount: amount, price: price)
123
+ end
124
+
125
+ # Cancel every open order (optionally scoped to a symbol). WARNING.
126
+ def cancel_all_orders(exchange:, api_key_id: nil, symbol: nil, market_type: nil)
127
+ op("cancel-all-orders", exchange: exchange, apiKeyId: api_key_id,
128
+ symbol: symbol, marketType: market_type)
129
+ end
130
+
131
+ # Cancel resting plan/trigger orders. WARNING.
132
+ def cancel_plan_orders(exchange:, api_key_id: nil, symbol: nil, market_type: nil)
133
+ op("cancel-plan-orders", exchange: exchange, apiKeyId: api_key_id,
134
+ symbol: symbol, marketType: market_type)
135
+ end
136
+
137
+ # Close an open position (market reduce-only). WARNING.
138
+ def close_position(exchange:, symbol:, api_key_id: nil, market_type: nil)
139
+ op("close-position", exchange: exchange, apiKeyId: api_key_id,
140
+ symbol: symbol, marketType: market_type)
141
+ end
142
+
143
+ # Set leverage for a symbol. WARNING.
144
+ def set_leverage(exchange:, symbol:, leverage:, api_key_id: nil, market_type: nil)
145
+ op("set-leverage", exchange: exchange, apiKeyId: api_key_id,
146
+ symbol: symbol, leverage: leverage, marketType: market_type)
147
+ end
148
+
149
+ # Set margin mode (cross/isolated). WARNING.
150
+ def set_margin_mode(exchange:, margin_mode:, api_key_id: nil, symbol: nil, market_type: nil)
151
+ op("set-margin-mode", exchange: exchange, apiKeyId: api_key_id,
152
+ marginMode: margin_mode, symbol: symbol, marketType: market_type)
153
+ end
154
+
155
+ # Set position mode (one-way / hedge). WARNING.
156
+ def set_position_mode(exchange:, api_key_id: nil, hedged: nil, mode: nil, market_type: nil)
157
+ op("set-position-mode", exchange: exchange, apiKeyId: api_key_id,
158
+ hedged: hedged, mode: mode, marketType: market_type)
159
+ end
160
+
161
+ private
162
+
163
+ def op(path_op, **kwargs)
164
+ body = kwargs.reject { |_, v| v.nil? }
165
+ @http.post("/api/v1/private/#{path_op}", body)
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ VERSION = "0.1.0"
5
+ end