trading-cli 0.3.0__py3-none-any.whl

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,507 @@
1
+ """DEX venues command group — GMX, dYdX, Aevo perpetual trading."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from ..client import FastBooksClient
8
+ from ..output import emit, emit_error, format_table, require_experimental
9
+
10
+ # ── venue configs ────────────────────────────────────────────────────────
11
+
12
+ DEX_PERP_VENUES = {
13
+ "gmx": {
14
+ "name": "GMX",
15
+ "chain": "arbitrum",
16
+ "markets": "perpetual",
17
+ "auth": "EVM private key",
18
+ "api_base": "https://api.gmx.io",
19
+ "notes": "GMX V2 on Arbitrum. Decentralized perps with GLP liquidity.",
20
+ },
21
+ "dydx": {
22
+ "name": "dYdX",
23
+ "chain": "dydx-chain (Cosmos)",
24
+ "markets": "perpetual",
25
+ "auth": "dYdX mnemonic or API key",
26
+ "api_base": "https://indexer.dydx.trade/v4",
27
+ "notes": "dYdX V4 sovereign chain. Fully decentralized orderbook.",
28
+ },
29
+ "aevo": {
30
+ "name": "Aevo",
31
+ "chain": "aevo-rollup (Ethereum L2)",
32
+ "markets": "perpetual, options",
33
+ "auth": "EVM private key + API key",
34
+ "api_base": "https://api.aevo.xyz",
35
+ "notes": "High-performance options and perps on custom L2 rollup.",
36
+ },
37
+ }
38
+
39
+
40
+ @click.group()
41
+ @click.pass_context
42
+ def dex(ctx):
43
+ """[experimental] DEX perpetual venues: GMX, dYdX, Aevo.
44
+
45
+ NOTE: requires a DEX-perps router not enabled on standard FastBooks
46
+ deployments. Gated behind FASTBOOKS_EXPERIMENTAL=1.
47
+
48
+ Trade perpetual futures on decentralized exchanges. Each venue has
49
+ different chain requirements and authentication methods.
50
+
51
+ GMX: Arbitrum-based perps. EVM wallet signing.
52
+ dYdX: Sovereign Cosmos chain. Mnemonic or API key.
53
+ Aevo: Custom L2 rollup. EVM wallet + API key.
54
+ """
55
+ require_experimental(ctx, "dex")
56
+
57
+
58
+ # ── venue info ───────────────────────────────────────────────────────────
59
+
60
+ @dex.command("venues")
61
+ @click.pass_context
62
+ def dex_venues(ctx):
63
+ """List all DEX perpetual venues and their requirements.
64
+
65
+ Example: fastbooks dex venues
66
+ """
67
+ data = {"venues": list(DEX_PERP_VENUES.values())}
68
+ emit(ctx, data, lambda d: _format_dex_venues(d))
69
+
70
+
71
+ def _format_dex_venues(data: dict) -> str:
72
+ lines = ["", " DEX Perpetual Venues", " ====================", ""]
73
+ for v in data["venues"]:
74
+ lines.append(f" {v['name']} ({v['chain']})")
75
+ lines.append(f" Markets: {v['markets']}")
76
+ lines.append(f" Auth: {v['auth']}")
77
+ lines.append(f" Notes: {v['notes']}")
78
+ lines.append("")
79
+ return "\n".join(lines)
80
+
81
+
82
+ # ══════════════════════════════════════════════════════════════════════════
83
+ # GMX V2 (Arbitrum)
84
+ # ══════════════════════════════════════════════════════════════════════════
85
+
86
+ @dex.group()
87
+ def gmx():
88
+ """GMX V2 perpetual trading on Arbitrum.
89
+
90
+ Trade perpetual futures using GMX's decentralized liquidity pools.
91
+ Requires an Arbitrum EVM private key.
92
+
93
+ Supported pairs: ETH/USD, BTC/USD, ARB/USD, SOL/USD, LINK/USD, etc.
94
+ Leverage: up to 50x.
95
+ """
96
+ pass
97
+
98
+
99
+ @gmx.command("markets")
100
+ @click.pass_context
101
+ def gmx_markets(ctx):
102
+ """List available GMX markets.
103
+
104
+ Example: fastbooks dex gmx markets
105
+ """
106
+ client: FastBooksClient = ctx.obj
107
+ try:
108
+ data = client.gmx_markets()
109
+ emit(ctx, data, lambda d: _format_perp_markets(d, "GMX"))
110
+ except Exception as e:
111
+ emit_error(ctx, str(e))
112
+ ctx.exit(1)
113
+
114
+
115
+ @gmx.command("order")
116
+ @click.argument("symbol")
117
+ @click.argument("side", type=click.Choice(["long", "short"], case_sensitive=False))
118
+ @click.argument("size_usd", type=float)
119
+ @click.option("--leverage", "-l", type=float, default=1.0, help="Leverage multiplier.")
120
+ @click.option("--order-type", type=click.Choice(["market", "limit"]), default="market")
121
+ @click.option("--price", "-p", type=float, default=None, help="Limit price.")
122
+ @click.option("--stop-loss", type=float, default=None, help="Stop loss price.")
123
+ @click.option("--take-profit", type=float, default=None, help="Take profit price.")
124
+ @click.option("--wallet-key", envvar="EVM_PRIVATE_KEY", default=None)
125
+ @click.option("--confirm/--no-confirm", default=True)
126
+ @click.pass_context
127
+ def gmx_order(ctx, symbol, side, size_usd, leverage, order_type, price, stop_loss, take_profit, wallet_key, confirm):
128
+ """Open a perpetual position on GMX.
129
+
130
+ SYMBOL: Market pair (e.g. ETH/USD, BTC/USD).
131
+ SIDE: long or short.
132
+ SIZE_USD: Position size in USD.
133
+
134
+ Examples:
135
+ fastbooks dex gmx order ETH/USD long 1000 --leverage 5
136
+ fastbooks dex gmx order BTC/USD short 5000 -l 10 --stop-loss 70000
137
+ fastbooks dex gmx order ARB/USD long 500 --order-type limit --price 1.50
138
+ """
139
+ client: FastBooksClient = ctx.obj
140
+
141
+ if not wallet_key:
142
+ emit_error(ctx, "EVM private key required. Use --wallet-key or set EVM_PRIVATE_KEY.")
143
+ ctx.exit(1)
144
+ return
145
+
146
+ if confirm and not ctx.find_root().params.get("json_output", False):
147
+ click.echo(f"\n GMX {side.upper()} {symbol}: ${size_usd} @ {leverage}x ({order_type})")
148
+ if price:
149
+ click.echo(f" Limit price: {price}")
150
+ if stop_loss:
151
+ click.echo(f" Stop loss: {stop_loss}")
152
+ if take_profit:
153
+ click.echo(f" Take profit: {take_profit}")
154
+ if not click.confirm("\n Execute?"):
155
+ click.echo(" Cancelled.")
156
+ return
157
+
158
+ try:
159
+ data = client.gmx_order(
160
+ symbol=symbol, side=side, size_usd=size_usd,
161
+ leverage=leverage, order_type=order_type, price=price,
162
+ stop_loss=stop_loss, take_profit=take_profit,
163
+ wallet_key=wallet_key,
164
+ )
165
+ emit(ctx, data, lambda d: _format_perp_order(d, "GMX"))
166
+ except Exception as e:
167
+ emit_error(ctx, str(e))
168
+ ctx.exit(1)
169
+
170
+
171
+ @gmx.command("positions")
172
+ @click.option("--wallet-key", envvar="EVM_PRIVATE_KEY", default=None)
173
+ @click.pass_context
174
+ def gmx_positions(ctx, wallet_key):
175
+ """List open GMX positions.
176
+
177
+ Example: fastbooks dex gmx positions
178
+ """
179
+ client: FastBooksClient = ctx.obj
180
+ if not wallet_key:
181
+ emit_error(ctx, "EVM private key required.")
182
+ ctx.exit(1)
183
+ return
184
+ try:
185
+ data = client.gmx_positions(wallet_key)
186
+ emit(ctx, data, lambda d: _format_perp_positions(d, "GMX"))
187
+ except Exception as e:
188
+ emit_error(ctx, str(e))
189
+ ctx.exit(1)
190
+
191
+
192
+ @gmx.command("close")
193
+ @click.argument("position_key")
194
+ @click.option("--wallet-key", envvar="EVM_PRIVATE_KEY", default=None)
195
+ @click.option("--confirm/--no-confirm", default=True)
196
+ @click.pass_context
197
+ def gmx_close(ctx, position_key, wallet_key, confirm):
198
+ """Close an open GMX position.
199
+
200
+ Example: fastbooks dex gmx close 0xabc123...
201
+ """
202
+ client: FastBooksClient = ctx.obj
203
+ if not wallet_key:
204
+ emit_error(ctx, "EVM private key required.")
205
+ ctx.exit(1)
206
+ return
207
+ if confirm and not ctx.find_root().params.get("json_output", False):
208
+ if not click.confirm(f" Close position {position_key[:16]}...?"):
209
+ click.echo(" Cancelled.")
210
+ return
211
+ try:
212
+ data = client.gmx_close_position(position_key, wallet_key)
213
+ emit(ctx, data, lambda d: f" Position closed. Tx: {d.get('txHash', '?')}")
214
+ except Exception as e:
215
+ emit_error(ctx, str(e))
216
+ ctx.exit(1)
217
+
218
+
219
+ # ══════════════════════════════════════════════════════════════════════════
220
+ # dYdX V4
221
+ # ══════════════════════════════════════════════════════════════════════════
222
+
223
+ @dex.group()
224
+ def dydx():
225
+ """dYdX V4 perpetual trading on dYdX Chain.
226
+
227
+ Trade perpetual futures on dYdX's sovereign Cosmos-based chain.
228
+ Requires a dYdX mnemonic or API credentials.
229
+
230
+ Supported pairs: ETH-USD, BTC-USD, SOL-USD, AVAX-USD, etc.
231
+ Leverage: up to 20x.
232
+ """
233
+ pass
234
+
235
+
236
+ @dydx.command("markets")
237
+ @click.pass_context
238
+ def dydx_markets(ctx):
239
+ """List available dYdX markets.
240
+
241
+ Example: fastbooks dex dydx markets
242
+ """
243
+ client: FastBooksClient = ctx.obj
244
+ try:
245
+ data = client.dydx_markets()
246
+ emit(ctx, data, lambda d: _format_perp_markets(d, "dYdX"))
247
+ except Exception as e:
248
+ emit_error(ctx, str(e))
249
+ ctx.exit(1)
250
+
251
+
252
+ @dydx.command("order")
253
+ @click.argument("symbol")
254
+ @click.argument("side", type=click.Choice(["long", "short"], case_sensitive=False))
255
+ @click.argument("size", type=float)
256
+ @click.option("--order-type", type=click.Choice(["market", "limit"]), default="market")
257
+ @click.option("--price", "-p", type=float, default=None, help="Limit price.")
258
+ @click.option("--leverage", "-l", type=float, default=1.0)
259
+ @click.option("--reduce-only", is_flag=True, help="Reduce-only order.")
260
+ @click.option("--time-in-force", type=click.Choice(["GTT", "FOK", "IOC"]), default="GTT")
261
+ @click.option("--mnemonic", envvar="DYDX_MNEMONIC", default=None, help="dYdX mnemonic (or set DYDX_MNEMONIC).")
262
+ @click.option("--confirm/--no-confirm", default=True)
263
+ @click.pass_context
264
+ def dydx_order(ctx, symbol, side, size, order_type, price, leverage, reduce_only, time_in_force, mnemonic, confirm):
265
+ """Place a perpetual order on dYdX.
266
+
267
+ SYMBOL: Market pair (e.g. ETH-USD, BTC-USD).
268
+ SIDE: long or short.
269
+ SIZE: Position size in base asset.
270
+
271
+ Examples:
272
+ fastbooks dex dydx order ETH-USD long 1.0 --leverage 5
273
+ fastbooks dex dydx order BTC-USD short 0.1 -l 10 --order-type limit --price 65000
274
+ fastbooks dex dydx order SOL-USD long 50 --reduce-only
275
+ """
276
+ client: FastBooksClient = ctx.obj
277
+
278
+ if not mnemonic:
279
+ emit_error(ctx, "dYdX mnemonic required. Use --mnemonic or set DYDX_MNEMONIC.")
280
+ ctx.exit(1)
281
+ return
282
+
283
+ if confirm and not ctx.find_root().params.get("json_output", False):
284
+ click.echo(f"\n dYdX {side.upper()} {size} {symbol} @ {leverage}x ({order_type})")
285
+ if price:
286
+ click.echo(f" Limit price: {price}")
287
+ if not click.confirm("\n Execute?"):
288
+ click.echo(" Cancelled.")
289
+ return
290
+
291
+ try:
292
+ data = client.dydx_order(
293
+ symbol=symbol, side=side, size=size,
294
+ order_type=order_type, price=price, leverage=leverage,
295
+ reduce_only=reduce_only, time_in_force=time_in_force,
296
+ mnemonic=mnemonic,
297
+ )
298
+ emit(ctx, data, lambda d: _format_perp_order(d, "dYdX"))
299
+ except Exception as e:
300
+ emit_error(ctx, str(e))
301
+ ctx.exit(1)
302
+
303
+
304
+ @dydx.command("positions")
305
+ @click.option("--mnemonic", envvar="DYDX_MNEMONIC", default=None)
306
+ @click.pass_context
307
+ def dydx_positions(ctx, mnemonic):
308
+ """List open dYdX positions.
309
+
310
+ Example: fastbooks dex dydx positions
311
+ """
312
+ client: FastBooksClient = ctx.obj
313
+ if not mnemonic:
314
+ emit_error(ctx, "dYdX mnemonic required.")
315
+ ctx.exit(1)
316
+ return
317
+ try:
318
+ data = client.dydx_positions(mnemonic)
319
+ emit(ctx, data, lambda d: _format_perp_positions(d, "dYdX"))
320
+ except Exception as e:
321
+ emit_error(ctx, str(e))
322
+ ctx.exit(1)
323
+
324
+
325
+ @dydx.command("orderbook")
326
+ @click.argument("symbol")
327
+ @click.pass_context
328
+ def dydx_orderbook(ctx, symbol):
329
+ """Get dYdX orderbook for a market.
330
+
331
+ Example: fastbooks dex dydx orderbook ETH-USD
332
+ """
333
+ client: FastBooksClient = ctx.obj
334
+ try:
335
+ data = client.dydx_orderbook(symbol)
336
+ emit(ctx, data)
337
+ except Exception as e:
338
+ emit_error(ctx, str(e))
339
+ ctx.exit(1)
340
+
341
+
342
+ # ══════════════════════════════════════════════════════════════════════════
343
+ # Aevo
344
+ # ══════════════════════════════════════════════════════════════════════════
345
+
346
+ @dex.group()
347
+ def aevo():
348
+ """Aevo perpetual and options trading on Aevo L2.
349
+
350
+ Trade perpetual futures and options on Aevo's high-performance rollup.
351
+ Requires EVM private key and Aevo API credentials.
352
+
353
+ Supported pairs: ETH-PERP, BTC-PERP, SOL-PERP, ARB-PERP, etc.
354
+ Also supports options: ETH-C-*, BTC-P-*, etc.
355
+ Leverage: up to 20x.
356
+ """
357
+ pass
358
+
359
+
360
+ @aevo.command("markets")
361
+ @click.option("--type", "market_type", type=click.Choice(["perpetual", "options", "all"]), default="all")
362
+ @click.pass_context
363
+ def aevo_markets(ctx, market_type):
364
+ """List available Aevo markets.
365
+
366
+ Examples:
367
+ fastbooks dex aevo markets
368
+ fastbooks dex aevo markets --type perpetual
369
+ fastbooks dex aevo markets --type options
370
+ """
371
+ client: FastBooksClient = ctx.obj
372
+ try:
373
+ data = client.aevo_markets(market_type)
374
+ emit(ctx, data, lambda d: _format_perp_markets(d, "Aevo"))
375
+ except Exception as e:
376
+ emit_error(ctx, str(e))
377
+ ctx.exit(1)
378
+
379
+
380
+ @aevo.command("order")
381
+ @click.argument("instrument")
382
+ @click.argument("side", type=click.Choice(["buy", "sell"], case_sensitive=False))
383
+ @click.argument("amount", type=float)
384
+ @click.option("--order-type", type=click.Choice(["market", "limit"]), default="market")
385
+ @click.option("--price", "-p", type=float, default=None)
386
+ @click.option("--leverage", "-l", type=float, default=1.0)
387
+ @click.option("--reduce-only", is_flag=True)
388
+ @click.option("--wallet-key", envvar="EVM_PRIVATE_KEY", default=None)
389
+ @click.option("--api-key", envvar="AEVO_API_KEY", default=None)
390
+ @click.option("--api-secret", envvar="AEVO_API_SECRET", default=None)
391
+ @click.option("--confirm/--no-confirm", default=True)
392
+ @click.pass_context
393
+ def aevo_order(ctx, instrument, side, amount, order_type, price, leverage, reduce_only, wallet_key, api_key, api_secret, confirm):
394
+ """Place an order on Aevo (perps or options).
395
+
396
+ INSTRUMENT: e.g. ETH-PERP, BTC-PERP, ETH-20250328-4000-C.
397
+ SIDE: buy or sell.
398
+ AMOUNT: Position size.
399
+
400
+ Examples:
401
+ fastbooks dex aevo order ETH-PERP buy 1.0 --leverage 5
402
+ fastbooks dex aevo order BTC-PERP sell 0.1 --order-type limit --price 65000
403
+ fastbooks dex aevo order ETH-20250328-4000-C buy 5 --price 150
404
+ """
405
+ client: FastBooksClient = ctx.obj
406
+
407
+ if not wallet_key:
408
+ emit_error(ctx, "EVM private key required. Use --wallet-key or set EVM_PRIVATE_KEY.")
409
+ ctx.exit(1)
410
+ return
411
+
412
+ if confirm and not ctx.find_root().params.get("json_output", False):
413
+ click.echo(f"\n Aevo {side.upper()} {amount} {instrument} @ {leverage}x ({order_type})")
414
+ if price:
415
+ click.echo(f" Price: {price}")
416
+ if not click.confirm("\n Execute?"):
417
+ click.echo(" Cancelled.")
418
+ return
419
+
420
+ try:
421
+ data = client.aevo_order(
422
+ instrument=instrument, side=side, amount=amount,
423
+ order_type=order_type, price=price, leverage=leverage,
424
+ reduce_only=reduce_only, wallet_key=wallet_key,
425
+ api_key=api_key, api_secret=api_secret,
426
+ )
427
+ emit(ctx, data, lambda d: _format_perp_order(d, "Aevo"))
428
+ except Exception as e:
429
+ emit_error(ctx, str(e))
430
+ ctx.exit(1)
431
+
432
+
433
+ @aevo.command("positions")
434
+ @click.option("--api-key", envvar="AEVO_API_KEY", default=None)
435
+ @click.option("--api-secret", envvar="AEVO_API_SECRET", default=None)
436
+ @click.pass_context
437
+ def aevo_positions(ctx, api_key, api_secret):
438
+ """List open Aevo positions.
439
+
440
+ Example: fastbooks dex aevo positions
441
+ """
442
+ client: FastBooksClient = ctx.obj
443
+ if not api_key:
444
+ emit_error(ctx, "Aevo API key required. Use --api-key or set AEVO_API_KEY.")
445
+ ctx.exit(1)
446
+ return
447
+ try:
448
+ data = client.aevo_positions(api_key, api_secret)
449
+ emit(ctx, data, lambda d: _format_perp_positions(d, "Aevo"))
450
+ except Exception as e:
451
+ emit_error(ctx, str(e))
452
+ ctx.exit(1)
453
+
454
+
455
+ @aevo.command("orderbook")
456
+ @click.argument("instrument")
457
+ @click.pass_context
458
+ def aevo_orderbook(ctx, instrument):
459
+ """Get Aevo orderbook for an instrument.
460
+
461
+ Example: fastbooks dex aevo orderbook ETH-PERP
462
+ """
463
+ client: FastBooksClient = ctx.obj
464
+ try:
465
+ data = client.aevo_orderbook(instrument)
466
+ emit(ctx, data)
467
+ except Exception as e:
468
+ emit_error(ctx, str(e))
469
+ ctx.exit(1)
470
+
471
+
472
+ # ── shared formatters ────────────────────────────────────────────────────
473
+
474
+ def _format_perp_markets(data, venue_name: str) -> str:
475
+ items = data if isinstance(data, list) else data.get("markets", data.get("data", []))
476
+ if not isinstance(items, list):
477
+ return str(data)
478
+ if not items:
479
+ return f" No {venue_name} markets found."
480
+ return f" {venue_name} Markets\n {'=' * 20}\n" + format_table(
481
+ items[:30],
482
+ ["symbol", "indexPrice", "markPrice", "fundingRate", "openInterest", "maxLeverage"],
483
+ ["Symbol", "Index", "Mark", "Funding", "OI", "Max Lev"],
484
+ )
485
+
486
+
487
+ def _format_perp_order(data: dict, venue_name: str) -> str:
488
+ lines = [f"\n {venue_name} Order"]
489
+ if data.get("orderId") or data.get("id"):
490
+ lines.append(f" ID: {data.get('orderId', data.get('id', '?'))}")
491
+ if data.get("txHash"):
492
+ lines.append(f" Tx: {data['txHash']}")
493
+ lines.append(f" Status: {data.get('status', '?')}")
494
+ return "\n".join(lines)
495
+
496
+
497
+ def _format_perp_positions(data, venue_name: str) -> str:
498
+ items = data if isinstance(data, list) else data.get("positions", data.get("data", []))
499
+ if not isinstance(items, list):
500
+ return str(data)
501
+ if not items:
502
+ return f" No open {venue_name} positions."
503
+ return f" {venue_name} Positions\n" + format_table(
504
+ items,
505
+ ["symbol", "side", "size", "entryPrice", "markPrice", "unrealizedPnl", "leverage"],
506
+ ["Symbol", "Side", "Size", "Entry", "Mark", "PnL", "Leverage"],
507
+ )
@@ -0,0 +1,150 @@
1
+ """`direct` — standalone exchange access via ccxt (NO FastBooks backend required).
2
+
3
+ Public data is keyless. Trading reads keys from the environment:
4
+ CCXT_<EXCHANGE>_API_KEY / _SECRET / _PASSWORD (e.g. CCXT_BINANCE_API_KEY).
5
+ This is the group that makes the CLI a self-contained, backend-free trader for 100+ exchanges.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import click
11
+
12
+ from .. import direct as d
13
+ from ..output import emit, emit_error
14
+
15
+
16
+ @click.group()
17
+ def direct():
18
+ """Trade & read 100+ exchanges DIRECTLY via ccxt — no backend, keyless for data.
19
+
20
+ Public data (ticker/ohlcv/orderbook/markets) needs no key. Trading reads creds from
21
+ env: CCXT_<EXCHANGE>_API_KEY / _SECRET / _PASSWORD. Add --sandbox for testnet where supported.
22
+ """
23
+
24
+
25
+ @direct.command("exchanges")
26
+ @click.option("--search", default=None, help="Filter by substring.")
27
+ @click.pass_context
28
+ def exchanges_cmd(ctx: click.Context, search: str | None):
29
+ """List all exchanges ccxt supports (100+)."""
30
+ try:
31
+ xs = d.list_exchanges()
32
+ if search:
33
+ xs = [x for x in xs if search.lower() in x.lower()]
34
+ emit(ctx, {"count": len(xs), "exchanges": xs},
35
+ lambda r: f"{r['count']} exchanges:\n" + ", ".join(r["exchanges"]))
36
+ except Exception as e: # noqa: BLE001
37
+ emit_error(ctx, str(e))
38
+ ctx.exit(1)
39
+
40
+
41
+ @direct.command("ticker")
42
+ @click.argument("exchange")
43
+ @click.argument("symbol")
44
+ @click.pass_context
45
+ def ticker_cmd(ctx: click.Context, exchange: str, symbol: str):
46
+ """Live ticker (keyless). e.g. direct ticker binance BTC/USDT"""
47
+ try:
48
+ emit(ctx, d.ticker(exchange, symbol))
49
+ except Exception as e: # noqa: BLE001
50
+ emit_error(ctx, str(e))
51
+ ctx.exit(1)
52
+
53
+
54
+ @direct.command("ohlcv")
55
+ @click.argument("exchange")
56
+ @click.argument("symbol")
57
+ @click.option("--timeframe", "-t", default="1h", help="1m,5m,15m,1h,4h,1d,...")
58
+ @click.option("--limit", "-n", default=100, type=int)
59
+ @click.pass_context
60
+ def ohlcv_cmd(ctx: click.Context, exchange: str, symbol: str, timeframe: str, limit: int):
61
+ """OHLCV candles (keyless). e.g. direct ohlcv kraken ETH/USD -t 15m -n 200"""
62
+ try:
63
+ emit(ctx, d.ohlcv(exchange, symbol, timeframe, limit))
64
+ except Exception as e: # noqa: BLE001
65
+ emit_error(ctx, str(e))
66
+ ctx.exit(1)
67
+
68
+
69
+ @direct.command("orderbook")
70
+ @click.argument("exchange")
71
+ @click.argument("symbol")
72
+ @click.option("--depth", "-d", default=10, type=int)
73
+ @click.pass_context
74
+ def orderbook_cmd(ctx: click.Context, exchange: str, symbol: str, depth: int):
75
+ """Order book (keyless). e.g. direct orderbook okx BTC/USDT -d 20"""
76
+ try:
77
+ emit(ctx, d.orderbook(exchange, symbol, depth))
78
+ except Exception as e: # noqa: BLE001
79
+ emit_error(ctx, str(e))
80
+ ctx.exit(1)
81
+
82
+
83
+ @direct.command("markets")
84
+ @click.argument("exchange")
85
+ @click.option("--quote", default=None, help="Filter by quote asset, e.g. USDT.")
86
+ @click.option("--limit", "-n", default=200, type=int)
87
+ @click.pass_context
88
+ def markets_cmd(ctx: click.Context, exchange: str, quote: str | None, limit: int):
89
+ """List tradable symbols on an exchange (keyless)."""
90
+ try:
91
+ emit(ctx, d.markets(exchange, quote, limit))
92
+ except Exception as e: # noqa: BLE001
93
+ emit_error(ctx, str(e))
94
+ ctx.exit(1)
95
+
96
+
97
+ @direct.command("balance")
98
+ @click.argument("exchange")
99
+ @click.pass_context
100
+ def balance_cmd(ctx: click.Context, exchange: str):
101
+ """Account balance (needs CCXT_<EXCHANGE>_API_KEY/_SECRET env)."""
102
+ try:
103
+ emit(ctx, d.balance(exchange))
104
+ except Exception as e: # noqa: BLE001
105
+ emit_error(ctx, str(e))
106
+ ctx.exit(1)
107
+
108
+
109
+ @direct.command("positions")
110
+ @click.argument("exchange")
111
+ @click.pass_context
112
+ def positions_cmd(ctx: click.Context, exchange: str):
113
+ """Open positions (needs creds; derivatives exchanges only)."""
114
+ try:
115
+ emit(ctx, d.positions(exchange))
116
+ except Exception as e: # noqa: BLE001
117
+ emit_error(ctx, str(e))
118
+ ctx.exit(1)
119
+
120
+
121
+ @direct.command("order")
122
+ @click.argument("exchange")
123
+ @click.argument("symbol")
124
+ @click.argument("side", type=click.Choice(["buy", "sell"]))
125
+ @click.argument("amount", type=float)
126
+ @click.option("--type", "order_type", type=click.Choice(["market", "limit"]), default="market")
127
+ @click.option("--price", type=float, default=None, help="Required for limit orders.")
128
+ @click.option("--sandbox", is_flag=True, help="Use the exchange testnet where supported.")
129
+ @click.option("--yes", is_flag=True, help="Skip the live-order confirmation.")
130
+ @click.pass_context
131
+ def order_cmd(ctx, exchange, symbol, side, amount, order_type, price, sandbox, yes):
132
+ """Place an order directly on an exchange (needs creds).
133
+
134
+ e.g. direct order binance BTC/USDT buy 0.001 --type limit --price 60000
135
+ """
136
+ if order_type == "limit" and price is None:
137
+ emit_error(ctx, "Limit orders require --price.")
138
+ ctx.exit(1)
139
+ if not sandbox and not yes:
140
+ root = ctx.find_root()
141
+ if not root.params.get("json_output"):
142
+ click.confirm(
143
+ f"LIVE order: {side} {amount} {symbol} on {exchange} ({order_type}). Proceed?",
144
+ abort=True,
145
+ )
146
+ try:
147
+ emit(ctx, d.place_order(exchange, symbol, side, order_type, amount, price, sandbox=sandbox))
148
+ except Exception as e: # noqa: BLE001
149
+ emit_error(ctx, str(e))
150
+ ctx.exit(1)