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.
- fastbooks_cli/__init__.py +3 -0
- fastbooks_cli/__main__.py +6 -0
- fastbooks_cli/client.py +1169 -0
- fastbooks_cli/commands/__init__.py +0 -0
- fastbooks_cli/commands/apikey.py +144 -0
- fastbooks_cli/commands/backtest.py +450 -0
- fastbooks_cli/commands/bots.py +479 -0
- fastbooks_cli/commands/config.py +294 -0
- fastbooks_cli/commands/dex.py +507 -0
- fastbooks_cli/commands/direct.py +150 -0
- fastbooks_cli/commands/execution.py +298 -0
- fastbooks_cli/commands/init_cmd.py +149 -0
- fastbooks_cli/commands/markets.py +270 -0
- fastbooks_cli/commands/mcp_cmd.py +27 -0
- fastbooks_cli/commands/onchain.py +123 -0
- fastbooks_cli/commands/polymarket.py +2215 -0
- fastbooks_cli/commands/repl.py +385 -0
- fastbooks_cli/commands/strategy.py +259 -0
- fastbooks_cli/commands/swap.py +320 -0
- fastbooks_cli/commands/wallet.py +447 -0
- fastbooks_cli/config_store.py +112 -0
- fastbooks_cli/direct.py +209 -0
- fastbooks_cli/main.py +88 -0
- fastbooks_cli/mcp_server.py +171 -0
- fastbooks_cli/output.py +86 -0
- fastbooks_cli/strategies/__init__.py +87 -0
- trading_cli-0.3.0.dist-info/METADATA +309 -0
- trading_cli-0.3.0.dist-info/RECORD +32 -0
- trading_cli-0.3.0.dist-info/WHEEL +5 -0
- trading_cli-0.3.0.dist-info/entry_points.txt +4 -0
- trading_cli-0.3.0.dist-info/licenses/LICENSE +21 -0
- trading_cli-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -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)
|