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,479 @@
|
|
|
1
|
+
"""Bots command group — create, manage, monitor trading bots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from ..client import FastBooksClient
|
|
10
|
+
from ..output import emit, emit_error, format_table
|
|
11
|
+
|
|
12
|
+
VENUES = [
|
|
13
|
+
"binance", "bybit", "okx", "kraken", "coinbase", "kucoin",
|
|
14
|
+
"hyperliquid", "ibkr", "alpaca",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
STRATEGIES = [
|
|
18
|
+
"conviction_swing", "dca", "momentum", "mean_reversion",
|
|
19
|
+
"grid", "tech_momentum", "morning_scalper",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
ASSET_TYPES = ["stock", "crypto", "forex"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@click.group()
|
|
26
|
+
def bots():
|
|
27
|
+
"""Bot management: create, start, stop, monitor trading bots.
|
|
28
|
+
|
|
29
|
+
Bots execute automated strategies on supported venues. They can be
|
|
30
|
+
configured for paper or live trading with conviction-based entries.
|
|
31
|
+
"""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── create ───────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
@bots.command()
|
|
38
|
+
@click.option("--name", "-n", required=True, help="Bot display name.")
|
|
39
|
+
@click.option("--venue", "-v", type=click.Choice(VENUES, case_sensitive=False), required=True)
|
|
40
|
+
@click.option("--strategy", "-s", type=click.Choice(STRATEGIES, case_sensitive=False), default="conviction_swing")
|
|
41
|
+
@click.option("--asset-type", type=click.Choice(ASSET_TYPES, case_sensitive=False), default="stock")
|
|
42
|
+
@click.option("--capital", "-c", type=float, required=True, help="Starting capital (in quote currency).")
|
|
43
|
+
@click.option("--quote-currency", "-q", type=str, default="USD", help="Quote currency (USD, USDT, USDC).")
|
|
44
|
+
@click.option("--paper/--live", default=True, help="Paper or live trading (default: paper).")
|
|
45
|
+
@click.option("--conviction-threshold", type=float, default=65.0, help="Min conviction score to enter (0-100).")
|
|
46
|
+
@click.option("--market-hours-only", is_flag=True, help="Only trade during market hours.")
|
|
47
|
+
@click.option("--auto-rebalance", is_flag=True, help="Enable automatic portfolio rebalancing.")
|
|
48
|
+
@click.option("--config-json", type=str, default=None, help="Full bot config as JSON string (overrides other options).")
|
|
49
|
+
@click.pass_context
|
|
50
|
+
def create(ctx, name, venue, strategy, asset_type, capital, quote_currency, paper, conviction_threshold, market_hours_only, auto_rebalance, config_json):
|
|
51
|
+
"""Create a new trading bot.
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
fastbooks bots create --name "BTC DCA" --venue binance --strategy dca --capital 1000 --paper
|
|
55
|
+
fastbooks bots create --name "Perp Grid" --venue hyperliquid --strategy grid --capital 5000 -q USDC
|
|
56
|
+
fastbooks bots create --name "IBKR Swing" --venue ibkr --strategy conviction_swing --capital 50000 --live
|
|
57
|
+
fastbooks bots create --config-json '{"name":"Custom","venue":"alpaca","capital":10000}'
|
|
58
|
+
"""
|
|
59
|
+
client: FastBooksClient = ctx.obj
|
|
60
|
+
|
|
61
|
+
if config_json:
|
|
62
|
+
try:
|
|
63
|
+
bot_config = json.loads(config_json)
|
|
64
|
+
except json.JSONDecodeError as e:
|
|
65
|
+
emit_error(ctx, f"Invalid JSON: {e}")
|
|
66
|
+
ctx.exit(1)
|
|
67
|
+
return
|
|
68
|
+
else:
|
|
69
|
+
bot_config = {
|
|
70
|
+
"name": name,
|
|
71
|
+
"venue": venue,
|
|
72
|
+
"strategy": strategy,
|
|
73
|
+
"asset_type": asset_type,
|
|
74
|
+
"capital": capital,
|
|
75
|
+
"quote_currency": quote_currency,
|
|
76
|
+
"is_paper_trading": paper,
|
|
77
|
+
"conviction_threshold": conviction_threshold,
|
|
78
|
+
"market_hours_only": market_hours_only,
|
|
79
|
+
"auto_rebalance": auto_rebalance,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
data = client.create_bot(bot_config)
|
|
84
|
+
emit(ctx, data, lambda d: f" Bot created: {d.get('id', d.get('bot_id', '?'))} ({name})")
|
|
85
|
+
except Exception as e:
|
|
86
|
+
emit_error(ctx, str(e))
|
|
87
|
+
ctx.exit(1)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ── list ─────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
@bots.command("list")
|
|
93
|
+
@click.option("--investor-id", type=str, default=None, help="Filter by investor ID.")
|
|
94
|
+
@click.pass_context
|
|
95
|
+
def list_bots(ctx, investor_id):
|
|
96
|
+
"""List all bots.
|
|
97
|
+
|
|
98
|
+
Examples:
|
|
99
|
+
fastbooks bots list
|
|
100
|
+
fastbooks --json bots list
|
|
101
|
+
"""
|
|
102
|
+
client: FastBooksClient = ctx.obj
|
|
103
|
+
try:
|
|
104
|
+
data = client.list_bots(investor_id)
|
|
105
|
+
emit(ctx, data, lambda d: _format_bots(d))
|
|
106
|
+
except Exception as e:
|
|
107
|
+
emit_error(ctx, str(e))
|
|
108
|
+
ctx.exit(1)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _format_bots(data) -> str:
|
|
112
|
+
items = data if isinstance(data, list) else data.get("bots", data.get("data", []))
|
|
113
|
+
if not isinstance(items, list):
|
|
114
|
+
return str(data)
|
|
115
|
+
if not items:
|
|
116
|
+
return " No bots found."
|
|
117
|
+
return format_table(
|
|
118
|
+
items,
|
|
119
|
+
["id", "name", "venue", "status", "capital", "is_paper_trading"],
|
|
120
|
+
["ID", "Name", "Venue", "Status", "Capital", "Paper?"],
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ── info ─────────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
@bots.command()
|
|
127
|
+
@click.argument("bot_id")
|
|
128
|
+
@click.pass_context
|
|
129
|
+
def info(ctx, bot_id):
|
|
130
|
+
"""Get detailed bot information.
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
fastbooks bots info abc-123
|
|
134
|
+
"""
|
|
135
|
+
client: FastBooksClient = ctx.obj
|
|
136
|
+
try:
|
|
137
|
+
data = client.get_bot(bot_id)
|
|
138
|
+
emit(ctx, data)
|
|
139
|
+
except Exception as e:
|
|
140
|
+
emit_error(ctx, str(e))
|
|
141
|
+
ctx.exit(1)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ── lifecycle commands ───────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
@bots.command()
|
|
147
|
+
@click.argument("bot_id")
|
|
148
|
+
@click.pass_context
|
|
149
|
+
def start(ctx, bot_id):
|
|
150
|
+
"""Start a bot.
|
|
151
|
+
|
|
152
|
+
Example: fastbooks bots start abc-123
|
|
153
|
+
"""
|
|
154
|
+
client: FastBooksClient = ctx.obj
|
|
155
|
+
try:
|
|
156
|
+
data = client.start_bot(bot_id)
|
|
157
|
+
emit(ctx, data, lambda d: f" Bot {bot_id} started.")
|
|
158
|
+
except Exception as e:
|
|
159
|
+
emit_error(ctx, str(e))
|
|
160
|
+
ctx.exit(1)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@bots.command()
|
|
164
|
+
@click.argument("bot_id")
|
|
165
|
+
@click.pass_context
|
|
166
|
+
def stop(ctx, bot_id):
|
|
167
|
+
"""Stop a running bot.
|
|
168
|
+
|
|
169
|
+
Example: fastbooks bots stop abc-123
|
|
170
|
+
"""
|
|
171
|
+
client: FastBooksClient = ctx.obj
|
|
172
|
+
try:
|
|
173
|
+
data = client.stop_bot(bot_id)
|
|
174
|
+
emit(ctx, data, lambda d: f" Bot {bot_id} stopped.")
|
|
175
|
+
except Exception as e:
|
|
176
|
+
emit_error(ctx, str(e))
|
|
177
|
+
ctx.exit(1)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@bots.command()
|
|
181
|
+
@click.argument("bot_id")
|
|
182
|
+
@click.pass_context
|
|
183
|
+
def pause(ctx, bot_id):
|
|
184
|
+
"""Pause a running bot (keeps state).
|
|
185
|
+
|
|
186
|
+
Example: fastbooks bots pause abc-123
|
|
187
|
+
"""
|
|
188
|
+
client: FastBooksClient = ctx.obj
|
|
189
|
+
try:
|
|
190
|
+
data = client.pause_bot(bot_id)
|
|
191
|
+
emit(ctx, data, lambda d: f" Bot {bot_id} paused.")
|
|
192
|
+
except Exception as e:
|
|
193
|
+
emit_error(ctx, str(e))
|
|
194
|
+
ctx.exit(1)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@bots.command()
|
|
198
|
+
@click.argument("bot_id")
|
|
199
|
+
@click.pass_context
|
|
200
|
+
def resume(ctx, bot_id):
|
|
201
|
+
"""Resume a paused bot.
|
|
202
|
+
|
|
203
|
+
Example: fastbooks bots resume abc-123
|
|
204
|
+
"""
|
|
205
|
+
client: FastBooksClient = ctx.obj
|
|
206
|
+
try:
|
|
207
|
+
data = client.resume_bot(bot_id)
|
|
208
|
+
emit(ctx, data, lambda d: f" Bot {bot_id} resumed.")
|
|
209
|
+
except Exception as e:
|
|
210
|
+
emit_error(ctx, str(e))
|
|
211
|
+
ctx.exit(1)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ── update ───────────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
@bots.command()
|
|
217
|
+
@click.argument("bot_id")
|
|
218
|
+
@click.option("--capital", type=float, default=None, help="Update capital.")
|
|
219
|
+
@click.option("--conviction-threshold", type=float, default=None, help="Update conviction threshold.")
|
|
220
|
+
@click.option("--paper/--live", default=None, help="Switch paper/live mode.")
|
|
221
|
+
@click.option("--config-json", type=str, default=None, help="Partial config as JSON.")
|
|
222
|
+
@click.pass_context
|
|
223
|
+
def update(ctx, bot_id, capital, conviction_threshold, paper, config_json):
|
|
224
|
+
"""Update bot configuration.
|
|
225
|
+
|
|
226
|
+
Examples:
|
|
227
|
+
fastbooks bots update abc-123 --capital 20000
|
|
228
|
+
fastbooks bots update abc-123 --conviction-threshold 70
|
|
229
|
+
fastbooks bots update abc-123 --config-json '{"auto_rebalance": true}'
|
|
230
|
+
"""
|
|
231
|
+
client: FastBooksClient = ctx.obj
|
|
232
|
+
|
|
233
|
+
if config_json:
|
|
234
|
+
try:
|
|
235
|
+
updates = json.loads(config_json)
|
|
236
|
+
except json.JSONDecodeError as e:
|
|
237
|
+
emit_error(ctx, f"Invalid JSON: {e}")
|
|
238
|
+
ctx.exit(1)
|
|
239
|
+
return
|
|
240
|
+
else:
|
|
241
|
+
updates = {}
|
|
242
|
+
if capital is not None:
|
|
243
|
+
updates["capital"] = capital
|
|
244
|
+
if conviction_threshold is not None:
|
|
245
|
+
updates["conviction_threshold"] = conviction_threshold
|
|
246
|
+
if paper is not None:
|
|
247
|
+
updates["is_paper_trading"] = paper
|
|
248
|
+
|
|
249
|
+
if not updates:
|
|
250
|
+
emit_error(ctx, "No updates provided.")
|
|
251
|
+
ctx.exit(1)
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
data = client.update_bot(bot_id, updates)
|
|
256
|
+
emit(ctx, data, lambda d: f" Bot {bot_id} updated.")
|
|
257
|
+
except Exception as e:
|
|
258
|
+
emit_error(ctx, str(e))
|
|
259
|
+
ctx.exit(1)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ── delete ───────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
@bots.command()
|
|
265
|
+
@click.argument("bot_id")
|
|
266
|
+
@click.option("--confirm/--no-confirm", default=True, help="Require confirmation.")
|
|
267
|
+
@click.pass_context
|
|
268
|
+
def delete(ctx, bot_id, confirm):
|
|
269
|
+
"""Delete a bot permanently.
|
|
270
|
+
|
|
271
|
+
Example: fastbooks bots delete abc-123
|
|
272
|
+
"""
|
|
273
|
+
client: FastBooksClient = ctx.obj
|
|
274
|
+
|
|
275
|
+
if confirm and not ctx.find_root().params.get("json_output", False):
|
|
276
|
+
if not click.confirm(f" Delete bot {bot_id}? This cannot be undone."):
|
|
277
|
+
click.echo(" Cancelled.")
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
data = client.delete_bot(bot_id)
|
|
282
|
+
emit(ctx, data, lambda d: f" Bot {bot_id} deleted.")
|
|
283
|
+
except Exception as e:
|
|
284
|
+
emit_error(ctx, str(e))
|
|
285
|
+
ctx.exit(1)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# ── monitoring ───────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
@bots.command()
|
|
291
|
+
@click.argument("bot_id")
|
|
292
|
+
@click.option("--limit", "-n", type=int, default=50, help="Max events to return.")
|
|
293
|
+
@click.pass_context
|
|
294
|
+
def events(ctx, bot_id, limit):
|
|
295
|
+
"""Get bot events / decision log.
|
|
296
|
+
|
|
297
|
+
Example: fastbooks bots events abc-123 --limit 20
|
|
298
|
+
"""
|
|
299
|
+
client: FastBooksClient = ctx.obj
|
|
300
|
+
try:
|
|
301
|
+
data = client.bot_events(bot_id, limit)
|
|
302
|
+
emit(ctx, data)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
emit_error(ctx, str(e))
|
|
305
|
+
ctx.exit(1)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@bots.command()
|
|
309
|
+
@click.argument("bot_id")
|
|
310
|
+
@click.option("--since", type=int, default=0, help="Show logs since timestamp.")
|
|
311
|
+
@click.pass_context
|
|
312
|
+
def logs(ctx, bot_id, since):
|
|
313
|
+
"""Get bot subprocess logs (stdout/stderr).
|
|
314
|
+
|
|
315
|
+
Example: fastbooks bots logs abc-123
|
|
316
|
+
"""
|
|
317
|
+
client: FastBooksClient = ctx.obj
|
|
318
|
+
try:
|
|
319
|
+
data = client.bot_logs(bot_id, since)
|
|
320
|
+
emit(ctx, data, lambda d: d.get("logs", str(d)) if isinstance(d, dict) else str(d))
|
|
321
|
+
except Exception as e:
|
|
322
|
+
emit_error(ctx, str(e))
|
|
323
|
+
ctx.exit(1)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@bots.command()
|
|
327
|
+
@click.argument("bot_id")
|
|
328
|
+
@click.pass_context
|
|
329
|
+
def report(ctx, bot_id):
|
|
330
|
+
"""Generate a performance report for a bot.
|
|
331
|
+
|
|
332
|
+
Example: fastbooks bots report abc-123
|
|
333
|
+
"""
|
|
334
|
+
client: FastBooksClient = ctx.obj
|
|
335
|
+
try:
|
|
336
|
+
data = client.bot_report(bot_id)
|
|
337
|
+
emit(ctx, data)
|
|
338
|
+
except Exception as e:
|
|
339
|
+
emit_error(ctx, str(e))
|
|
340
|
+
ctx.exit(1)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# ── rebalance / trade ────────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
@bots.command()
|
|
346
|
+
@click.argument("bot_id")
|
|
347
|
+
@click.pass_context
|
|
348
|
+
def rebalance(ctx, bot_id):
|
|
349
|
+
"""Trigger a portfolio rebalance for a bot.
|
|
350
|
+
|
|
351
|
+
Example: fastbooks bots rebalance abc-123
|
|
352
|
+
"""
|
|
353
|
+
client: FastBooksClient = ctx.obj
|
|
354
|
+
try:
|
|
355
|
+
data = client.rebalance_bot(bot_id)
|
|
356
|
+
emit(ctx, data, lambda d: f" Rebalance triggered for bot {bot_id}.")
|
|
357
|
+
except Exception as e:
|
|
358
|
+
emit_error(ctx, str(e))
|
|
359
|
+
ctx.exit(1)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@bots.command()
|
|
363
|
+
@click.argument("bot_id")
|
|
364
|
+
@click.argument("symbol")
|
|
365
|
+
@click.argument("side", type=click.Choice(["buy", "sell"], case_sensitive=False))
|
|
366
|
+
@click.argument("amount", type=float)
|
|
367
|
+
@click.option("--reason", type=str, default=None, help="Trade reason.")
|
|
368
|
+
@click.pass_context
|
|
369
|
+
def trade(ctx, bot_id, symbol, side, amount, reason):
|
|
370
|
+
"""Submit a manual trade intent to a bot.
|
|
371
|
+
|
|
372
|
+
Example: fastbooks bots trade abc-123 AAPL buy 10 --reason "earnings play"
|
|
373
|
+
"""
|
|
374
|
+
client: FastBooksClient = ctx.obj
|
|
375
|
+
intent = {"symbol": symbol, "side": side, "amount": amount}
|
|
376
|
+
if reason:
|
|
377
|
+
intent["reason"] = reason
|
|
378
|
+
try:
|
|
379
|
+
data = client.bot_trade(bot_id, intent)
|
|
380
|
+
emit(ctx, data, lambda d: f" Trade intent submitted to bot {bot_id}.")
|
|
381
|
+
except Exception as e:
|
|
382
|
+
emit_error(ctx, str(e))
|
|
383
|
+
ctx.exit(1)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# ── deploy (template → running bot) ───────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
@bots.command()
|
|
389
|
+
@click.option("--name", "-n", required=True, help="Bot display name.")
|
|
390
|
+
@click.option("--venue", "-v", type=click.Choice(VENUES, case_sensitive=False), required=True)
|
|
391
|
+
@click.option("--strategy", "-s", type=click.Choice(STRATEGIES, case_sensitive=False), default="conviction_swing")
|
|
392
|
+
@click.option("--asset-type", type=click.Choice(ASSET_TYPES, case_sensitive=False), default="stock")
|
|
393
|
+
@click.option("--capital", "-c", type=float, required=True, help="Starting capital (in quote currency).")
|
|
394
|
+
@click.option("--quote-currency", "-q", type=str, default="USD", help="Quote currency (USD, USDT, USDC).")
|
|
395
|
+
@click.option("--paper/--live", default=True, help="Paper or live trading (default: paper).")
|
|
396
|
+
@click.option("--start/--no-start", default=True, help="Start the bot immediately after deploy.")
|
|
397
|
+
@click.option("--config-json", type=str, default=None, help="Full deploy config as JSON (overrides options).")
|
|
398
|
+
@click.pass_context
|
|
399
|
+
def deploy(ctx, name, venue, strategy, asset_type, capital, quote_currency, paper, start, config_json):
|
|
400
|
+
"""Deploy a bot from a template and (by default) start it on paper.
|
|
401
|
+
|
|
402
|
+
Examples:
|
|
403
|
+
fastbooks bots deploy -n "Paper Momentum" -v alpaca -s momentum -c 50000 --paper
|
|
404
|
+
fastbooks bots deploy -n "HL Grid" -v hyperliquid -s grid -c 5000 -q USDC --live
|
|
405
|
+
"""
|
|
406
|
+
client: FastBooksClient = ctx.obj
|
|
407
|
+
if config_json:
|
|
408
|
+
try:
|
|
409
|
+
cfg = json.loads(config_json)
|
|
410
|
+
except json.JSONDecodeError as e:
|
|
411
|
+
emit_error(ctx, f"Invalid JSON: {e}")
|
|
412
|
+
ctx.exit(1)
|
|
413
|
+
return
|
|
414
|
+
else:
|
|
415
|
+
cfg = {
|
|
416
|
+
"name": name,
|
|
417
|
+
"venue": venue,
|
|
418
|
+
"strategy": strategy,
|
|
419
|
+
"asset_type": asset_type,
|
|
420
|
+
"capital": capital,
|
|
421
|
+
"quote_currency": quote_currency,
|
|
422
|
+
"is_paper_trading": paper,
|
|
423
|
+
"auto_start": start,
|
|
424
|
+
}
|
|
425
|
+
try:
|
|
426
|
+
data = client.deploy_bot(cfg)
|
|
427
|
+
bot_id = data.get("id") or data.get("bot_id") or "?"
|
|
428
|
+
mode = "paper" if paper else "live"
|
|
429
|
+
emit(ctx, data, lambda d: f" Bot deployed ({mode}): {bot_id} ({name})")
|
|
430
|
+
except Exception as e:
|
|
431
|
+
emit_error(ctx, str(e))
|
|
432
|
+
ctx.exit(1)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
# ── command (control channel) ─────────────────────────────────────────────
|
|
436
|
+
|
|
437
|
+
@bots.command("command")
|
|
438
|
+
@click.argument("bot_id")
|
|
439
|
+
@click.argument("command")
|
|
440
|
+
@click.option("--args-json", type=str, default=None, help="Command args as JSON object.")
|
|
441
|
+
@click.pass_context
|
|
442
|
+
def send_command(ctx, bot_id, command, args_json):
|
|
443
|
+
"""Send a control command to a running bot.
|
|
444
|
+
|
|
445
|
+
Example: fastbooks bots command abc-123 flatten
|
|
446
|
+
"""
|
|
447
|
+
client: FastBooksClient = ctx.obj
|
|
448
|
+
args = None
|
|
449
|
+
if args_json:
|
|
450
|
+
try:
|
|
451
|
+
args = json.loads(args_json)
|
|
452
|
+
except json.JSONDecodeError as e:
|
|
453
|
+
emit_error(ctx, f"Invalid JSON: {e}")
|
|
454
|
+
ctx.exit(1)
|
|
455
|
+
return
|
|
456
|
+
try:
|
|
457
|
+
data = client.bot_command(bot_id, command, args)
|
|
458
|
+
emit(ctx, data, lambda d: f" Command '{command}' sent to bot {bot_id}.")
|
|
459
|
+
except Exception as e:
|
|
460
|
+
emit_error(ctx, str(e))
|
|
461
|
+
ctx.exit(1)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# ── emergency stop-all ─────────────────────────────────────────────────────
|
|
465
|
+
|
|
466
|
+
@bots.command("emergency-stop")
|
|
467
|
+
@click.option("--confirm", is_flag=True, help="Skip the confirmation prompt.")
|
|
468
|
+
@click.pass_context
|
|
469
|
+
def emergency_stop(ctx, confirm):
|
|
470
|
+
"""Emergency-stop ALL running bots."""
|
|
471
|
+
if not confirm and not click.confirm("Stop ALL running bots?"):
|
|
472
|
+
ctx.exit(0)
|
|
473
|
+
client: FastBooksClient = ctx.obj
|
|
474
|
+
try:
|
|
475
|
+
data = client.emergency_stop_all()
|
|
476
|
+
emit(ctx, data, lambda d: f" Emergency stop issued. {d.get('stopped', '?')} bots stopped.")
|
|
477
|
+
except Exception as e:
|
|
478
|
+
emit_error(ctx, str(e))
|
|
479
|
+
ctx.exit(1)
|