melaya 0.1.2 → 0.2.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,342 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Pipelines API — overview dashboard, pipeline run listing, traces, and
5
+ # cron-based scheduling.
6
+ #
7
+ # Maps to:
8
+ # /api/v1/private/overview/* — dashboard + run listing
9
+ # /api/v1/private/runs/:id/traces/* — distributed traces
10
+ # /api/v1/private/pipeline-schedule — cron scheduling
11
+ # /api/v1/version — server version (public)
12
+ #
13
+ # @example
14
+ # runs = melaya.pipelines.list(project: "my-project", limit: 20)
15
+ # melaya.pipelines.upsert_schedule("my-project", "nightly-report",
16
+ # cron: "0 2 * * *")
17
+ class PipelinesAPI
18
+ def initialize(http)
19
+ @http = http
20
+ end
21
+
22
+ # ── Overview ───────────────────────────────────────────────────────────────
23
+
24
+ # GET /api/v1/private/overview
25
+ # Dashboard overview: usage stats, active strategies, recent runs.
26
+ def overview
27
+ @http.get("/api/v1/private/overview")
28
+ end
29
+
30
+ # GET /api/v1/private/overview/model-prices
31
+ # Get pricing data for available AI models.
32
+ def model_prices
33
+ @http.get("/api/v1/private/overview/model-prices")
34
+ end
35
+
36
+ # GET /api/v1/private/overview/chart
37
+ # Get chart data for overview dashboard (cost/usage over time).
38
+ def chart_data(params = {})
39
+ @http.get("/api/v1/private/overview/chart", params)
40
+ end
41
+
42
+ # GET /api/v1/private/overview/cost-breakdown
43
+ # Get cost breakdown by model/provider.
44
+ def cost_breakdown(params = {})
45
+ @http.get("/api/v1/private/overview/cost-breakdown", params)
46
+ end
47
+
48
+ # GET /api/v1/private/overview/pipeline-count
49
+ # Count of pipeline runs grouped by status.
50
+ def count
51
+ @http.get("/api/v1/private/overview/pipeline-count")
52
+ end
53
+
54
+ # GET /api/v1/private/overview/pipelines
55
+ # Paginated list of pipeline runs.
56
+ # @param project [String, nil]
57
+ # @param pipeline_name [String, nil]
58
+ # @param status [String, nil]
59
+ # @param limit [Integer, nil]
60
+ # @param offset [Integer, nil]
61
+ def list(project: nil, pipeline_name: nil, status: nil, limit: nil, offset: nil)
62
+ @http.get("/api/v1/private/overview/pipelines",
63
+ compact("project" => project, "pipelineName" => pipeline_name,
64
+ "status" => status, "limit" => limit,
65
+ "offset" => offset))
66
+ end
67
+
68
+ # GET /api/v1/private/overview/pipelines/recent
69
+ # Most recent pipeline runs for a dashboard widget.
70
+ def recent
71
+ @http.get("/api/v1/private/overview/pipelines/recent")
72
+ end
73
+
74
+ # ── Traces ─────────────────────────────────────────────────────────────────
75
+
76
+ # GET /api/v1/private/runs/:runId/traces
77
+ # List traces for a run (paginated).
78
+ #
79
+ # Returns a paginated envelope, NOT a flat array:
80
+ # {
81
+ # "data" => {
82
+ # "list" => Array<Hash>, # trace summaries
83
+ # "total" => Integer, # total matching traces
84
+ # "page" => Integer,
85
+ # "pageSize" => Integer
86
+ # }
87
+ # }
88
+ #
89
+ # @param run_id [String]
90
+ # @param params [Hash] optional pagination / filter params (page:, pageSize:, ...)
91
+ # @return [Hash] paginated envelope as described above
92
+ def traces(run_id, params = {})
93
+ @http.get("/api/v1/private/runs/#{enc(run_id)}/traces", params)
94
+ end
95
+
96
+ # GET /api/v1/private/runs/:runId/traces/:traceId
97
+ # Get a single trace by ID.
98
+ def trace(run_id, trace_id)
99
+ @http.get("/api/v1/private/runs/#{enc(run_id)}/traces/#{enc(trace_id)}")
100
+ end
101
+
102
+ # GET /api/v1/private/runs/:runId/traces/:traceId/stats
103
+ # Get statistics for a specific trace.
104
+ def trace_stats(run_id, trace_id)
105
+ @http.get("/api/v1/private/runs/#{enc(run_id)}/traces/#{enc(trace_id)}/stats")
106
+ end
107
+
108
+ # DELETE /api/v1/private/runs/:runId/traces
109
+ # Delete all traces (and their spans) for a run by runId.
110
+ # No request body required — the runId in the path identifies the target.
111
+ #
112
+ # @param run_id [String]
113
+ # @return [Hash] { "deletedSpans" => Integer, "requestedTraces" => Integer (optional) }
114
+ def delete_traces(run_id)
115
+ @http.delete("/api/v1/private/runs/#{enc(run_id)}/traces")
116
+ end
117
+
118
+ # ── Schedule ───────────────────────────────────────────────────────────────
119
+
120
+ # GET /api/v1/private/pipeline-schedule
121
+ # List all pipeline schedules accessible to the caller.
122
+ def list_schedules
123
+ @http.get("/api/v1/private/pipeline-schedule")
124
+ end
125
+
126
+ # GET /api/v1/private/pipeline-schedule/:project/:pipelineName
127
+ # Get schedule status for a pipeline.
128
+ # @param project [String]
129
+ # @param pipeline_name [String]
130
+ def get_schedule(project, pipeline_name)
131
+ @http.get("/api/v1/private/pipeline-schedule/#{enc(project)}/#{enc(pipeline_name)}")
132
+ end
133
+
134
+ # PUT /api/v1/private/pipeline-schedule/:project/:pipelineName
135
+ # Create or update a pipeline schedule (cron expression + optional config).
136
+ # @param project [String]
137
+ # @param pipeline_name [String]
138
+ # @param cron [String] cron expression e.g. "0 2 * * *"
139
+ # @param config [Hash, nil] optional pipeline config overrides
140
+ def upsert_schedule(project, pipeline_name, cron:, config: nil)
141
+ body = compact("cron" => cron, "config" => config)
142
+ @http.put("/api/v1/private/pipeline-schedule/#{enc(project)}/#{enc(pipeline_name)}", body)
143
+ end
144
+
145
+ # POST /api/v1/private/pipeline-schedule/:project/:pipelineName/pause
146
+ # Pause a pipeline schedule.
147
+ def pause_schedule(project, pipeline_name)
148
+ @http.post("/api/v1/private/pipeline-schedule/#{enc(project)}/#{enc(pipeline_name)}/pause")
149
+ end
150
+
151
+ # POST /api/v1/private/pipeline-schedule/:project/:pipelineName/resume
152
+ # Resume a paused pipeline schedule.
153
+ def resume_schedule(project, pipeline_name)
154
+ @http.post("/api/v1/private/pipeline-schedule/#{enc(project)}/#{enc(pipeline_name)}/resume")
155
+ end
156
+
157
+ # ── Pipeline lifecycle (CRUD + run + outputs + AI build) ──────────────────
158
+
159
+ # GET /api/v1/private/pipelines
160
+ # List all pipeline configs accessible to the caller.
161
+ # Returns { "pipelines" => [...] }.
162
+ def list_pipelines
163
+ @http.get("/api/v1/private/pipelines")
164
+ end
165
+
166
+ # POST /api/v1/private/pipelines
167
+ # Create a new pipeline config.
168
+ # @param name [String] pipeline name
169
+ # @param project [String] owning project
170
+ # @param description [String, nil]
171
+ # @param config [Hash] additional config keys merged into the request body
172
+ # @return [Hash] created pipeline config
173
+ def create(name:, project:, description: nil, **config)
174
+ body = compact(
175
+ "name" => name,
176
+ "project" => project,
177
+ "description" => description
178
+ ).merge(stringify_keys(config))
179
+ @http.post("/api/v1/private/pipelines", body)
180
+ end
181
+
182
+ # GET /api/v1/private/pipelines/:name
183
+ # Fetch a single pipeline config by name.
184
+ # @param name [String] pipeline name
185
+ # @param project [String, nil] owning project (disambiguates when multiple projects share a name)
186
+ # @return [Hash] pipeline config
187
+ def get(name, project: nil)
188
+ params = compact("project" => project)
189
+ @http.get("/api/v1/private/pipelines/#{enc(name)}", params)
190
+ end
191
+
192
+ # PUT /api/v1/private/pipelines/:name
193
+ # Replace a pipeline's config.
194
+ # @param name [String] pipeline name
195
+ # @param config [Hash] full pipeline config payload
196
+ # @param project [String, nil] owning project
197
+ # @return [Hash] updated pipeline config
198
+ def update(name, config:, project: nil)
199
+ body = compact("config" => config, "project" => project)
200
+ @http.put("/api/v1/private/pipelines/#{enc(name)}", body)
201
+ end
202
+
203
+ # DELETE /api/v1/private/pipelines/:name
204
+ # Delete a pipeline config.
205
+ # @param name [String] pipeline name
206
+ # @param project [String, nil] owning project
207
+ # @return [Hash] empty hash on success
208
+ def delete_pipeline(name, project: nil)
209
+ params = compact("project" => project)
210
+ @http.delete("/api/v1/private/pipelines/#{enc(name)}", params)
211
+ end
212
+
213
+ # POST /api/v1/private/pipelines/:name/run
214
+ # Enqueue a pipeline run.
215
+ # @param name [String] pipeline name
216
+ # @param project [String, nil]
217
+ # @param execution_target [String, nil] runner target identifier
218
+ # @param studio_url [String, nil] override studio URL
219
+ # @param env_overrides [Hash, nil] environment variable overrides
220
+ # @return [Hash] { "run_id" => String, "queued" => Boolean }
221
+ def run(name, project: nil, execution_target: nil, studio_url: nil, env_overrides: nil)
222
+ body = compact(
223
+ "project" => project,
224
+ "executionTarget" => execution_target,
225
+ "studio_url" => studio_url,
226
+ "env_overrides" => env_overrides
227
+ )
228
+ @http.post("/api/v1/private/pipelines/#{enc(name)}/run", body.empty? ? nil : body)
229
+ end
230
+
231
+ # GET /api/v1/private/pipelines/:name/runs
232
+ # List all run IDs for a pipeline.
233
+ # @param name [String] pipeline name
234
+ # @return [Hash] { "run_ids" => Array<String> }
235
+ def run_ids(name)
236
+ @http.get("/api/v1/private/pipelines/#{enc(name)}/runs")
237
+ end
238
+
239
+ # GET /api/v1/private/pipelines/:name/runs/:run_id
240
+ # Get the status of a specific pipeline run.
241
+ # @param name [String] pipeline name
242
+ # @param run_id [String] run identifier
243
+ # @return [Hash] { "runId", "status", "createdAt", "executionTarget", "cost" }
244
+ def run_status(name, run_id)
245
+ @http.get("/api/v1/private/pipelines/#{enc(name)}/runs/#{enc(run_id)}")
246
+ end
247
+
248
+ # DELETE /api/v1/private/pipelines/:name/runs/:run_id
249
+ # Cancel an in-progress pipeline run.
250
+ # @param name [String] pipeline name
251
+ # @param run_id [String] run identifier
252
+ # @return [Hash] empty hash on success
253
+ def cancel_run(name, run_id)
254
+ @http.delete("/api/v1/private/pipelines/#{enc(name)}/runs/#{enc(run_id)}")
255
+ end
256
+
257
+ # GET /api/v1/private/pipelines/:name/outputs
258
+ # List all output artifacts produced by a pipeline.
259
+ # @param name [String] pipeline name
260
+ # @return artifacts listing
261
+ def outputs(name)
262
+ @http.get("/api/v1/private/pipelines/#{enc(name)}/outputs")
263
+ end
264
+
265
+ # GET /api/v1/private/pipelines/:name/outputs/:path
266
+ # Fetch a specific output artifact. Each segment of +path+ is individually
267
+ # URL-encoded so slashes in segment values are preserved as separators.
268
+ # @param name [String] pipeline name
269
+ # @param path [String] artifact path, e.g. "reports/2024-01/summary.json"
270
+ # @param download [Boolean] if true, adds +?download=1+ to trigger a download response
271
+ # @return artifact content or download redirect
272
+ def output(name, path, download: false)
273
+ encoded_path = path.to_s.split("/").map { |seg| enc(seg) }.join("/")
274
+ params = download ? { "download" => 1 } : {}
275
+ @http.get("/api/v1/private/pipelines/#{enc(name)}/outputs/#{encoded_path}", params)
276
+ end
277
+
278
+ # POST /api/v1/private/pipelines/preview-code
279
+ # Generate a code preview for a pipeline config without saving it.
280
+ # @param config [Hash] pipeline config to preview
281
+ # @return preview payload
282
+ def preview_code(config)
283
+ @http.post("/api/v1/private/pipelines/preview-code", config)
284
+ end
285
+
286
+ # GET /api/v1/private/pipelines/tools
287
+ # Fetch the registry of tools available to pipeline steps.
288
+ # @return [Hash] tool registry
289
+ def tools
290
+ @http.get("/api/v1/private/pipelines/tools")
291
+ end
292
+
293
+ # GET /api/v1/private/pipelines/subagents
294
+ # Fetch the registry of sub-agent definitions available to pipelines.
295
+ # @return [Hash] sub-agent registry
296
+ def subagents
297
+ @http.get("/api/v1/private/pipelines/subagents")
298
+ end
299
+
300
+ # POST /api/v1/private/templates/:template_id/instantiate
301
+ # Instantiate a platform template into a new pipeline config.
302
+ # @param template_id [String] the template to instantiate
303
+ # @param name [String] name for the resulting pipeline
304
+ # @param project [String] target project
305
+ # @param overrides [Hash, nil] optional config overrides applied on top of the template defaults
306
+ # @return [Hash] { "pipeline" => config }
307
+ def instantiate_template(template_id, name:, project:, overrides: nil)
308
+ body = compact("name" => name, "project" => project, "overrides" => overrides)
309
+ @http.post("/api/v1/private/templates/#{enc(template_id)}/instantiate", body)
310
+ end
311
+
312
+ # POST /api/v1/private/ai/build-pipeline/sync
313
+ # Generate a pipeline config from a natural-language brief using AI.
314
+ # @param brief [Hash] free-form brief payload sent to the AI builder
315
+ # @return [Hash] generated pipeline config
316
+ def build_with_ai(brief)
317
+ @http.post("/api/v1/private/ai/build-pipeline/sync", brief)
318
+ end
319
+
320
+ # ── Misc ───────────────────────────────────────────────────────────────────
321
+
322
+ # GET /api/v1/version (public)
323
+ # Get current server version string.
324
+ def server_version
325
+ @http.get("/api/v1/version")
326
+ end
327
+
328
+ private
329
+
330
+ def enc(s)
331
+ URI.encode_www_form_component(s.to_s)
332
+ end
333
+
334
+ def compact(hash)
335
+ hash.reject { |_, v| v.nil? }
336
+ end
337
+
338
+ def stringify_keys(hash)
339
+ hash.transform_keys(&:to_s)
340
+ end
341
+ end
342
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Projects API — create and list agent projects.
5
+ #
6
+ # Projects are the top-level namespace for pipelines, connectors, and team
7
+ # membership in the Melaya platform.
8
+ #
9
+ # Maps to /api/v1/private/projects/*.
10
+ #
11
+ # @example
12
+ # projects = melaya.projects.list
13
+ # project = melaya.projects.create(name: "my-project", description: "...")
14
+ # melaya.projects.rename(old_name: "my-project", new_name: "renamed-project")
15
+ class ProjectsAPI
16
+ def initialize(http)
17
+ @http = http
18
+ end
19
+
20
+ # GET /api/v1/private/projects
21
+ # List all projects the authenticated user can access
22
+ # (owned projects + projects they are a member of).
23
+ def list
24
+ @http.get("/api/v1/private/projects")
25
+ end
26
+
27
+ # POST /api/v1/private/projects
28
+ # Create a new agent project.
29
+ # @param name [String]
30
+ # @param description [String, nil]
31
+ def create(name:, description: nil)
32
+ body = compact("name" => name, "description" => description)
33
+ @http.post("/api/v1/private/projects", body)
34
+ end
35
+
36
+ # PATCH /api/v1/private/projects/rename
37
+ # Rename a project (body: oldName, newName).
38
+ # @param old_name [String]
39
+ # @param new_name [String]
40
+ def rename(old_name:, new_name:)
41
+ @http.patch("/api/v1/private/projects/rename",
42
+ "oldName" => old_name,
43
+ "newName" => new_name)
44
+ end
45
+
46
+ # GET /api/v1/private/projects/runner
47
+ # Get projects list (runner-facing, authed).
48
+ def runner_projects
49
+ @http.get("/api/v1/private/projects/runner")
50
+ end
51
+
52
+ private
53
+
54
+ def compact(hash)
55
+ hash.reject { |_, v| v.nil? }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Runner API — mint, list, and revoke runner tokens.
5
+ #
6
+ # Runner tokens (mel_run_ prefix) authenticate the Melaya runner CLI process
7
+ # that executes agent pipelines on your infrastructure.
8
+ # The plaintext token is returned only once on creation — store it securely.
9
+ #
10
+ # Maps to /api/v1/private/runner/tokens/*.
11
+ #
12
+ # @example
13
+ # result = melaya.runner.create_token(label: "prod-server-1")
14
+ # token = result["token"] # store securely — shown only once
15
+ #
16
+ # tokens = melaya.runner.list_tokens
17
+ # melaya.runner.revoke_token(tokens.first["id"])
18
+ class RunnerAPI
19
+ def initialize(http)
20
+ @http = http
21
+ end
22
+
23
+ # POST /api/v1/private/runner/tokens
24
+ # Mint a new mel_run_ runner token.
25
+ # @param label [String, nil] human-readable label for the token
26
+ def create_token(label: nil)
27
+ body = label ? { "label" => label } : nil
28
+ @http.post("/api/v1/private/runner/tokens", body)
29
+ end
30
+
31
+ # GET /api/v1/private/runner/tokens
32
+ # List all runner tokens for the caller (masked, with last_seen).
33
+ def list_tokens
34
+ @http.get("/api/v1/private/runner/tokens")
35
+ end
36
+
37
+ # DELETE /api/v1/private/runner/tokens/:tokenId
38
+ # Revoke a runner token by ID.
39
+ # @param token_id [String]
40
+ def revoke_token(token_id)
41
+ @http.delete("/api/v1/private/runner/tokens/#{URI.encode_www_form_component(token_id.to_s)}")
42
+ end
43
+ end
44
+ end
data/lib/melaya/sim.rb CHANGED
@@ -1,110 +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
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