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,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)