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,298 @@
1
+ """Execution command group — orders, balance, positions, leverage."""
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
9
+
10
+ VENUES = [
11
+ "binance", "bybit", "okx", "kraken", "coinbase", "kucoin",
12
+ "hyperliquid", "ibkr", "alpaca", "paper", "universal", "gmx",
13
+ "oanda", "oanda_paper", "saxo", "saxo_paper",
14
+ ]
15
+
16
+ ORDER_TYPES = ["market", "limit", "stop", "stop_limit", "trailing_stop"]
17
+ SIDES = ["buy", "sell"]
18
+
19
+
20
+ @click.group()
21
+ def execute():
22
+ """Order execution: place, cancel, list orders, balance, positions.
23
+
24
+ Supported venues: binance, bybit, okx, kraken, coinbase, kucoin,
25
+ hyperliquid, ibkr, alpaca, universal (internal OB).
26
+ """
27
+ pass
28
+
29
+
30
+ # ── venues ───────────────────────────────────────────────────────────────
31
+
32
+ @execute.command()
33
+ @click.pass_context
34
+ def venues(ctx):
35
+ """List all available venues and their capabilities.
36
+
37
+ Shows which operations each venue supports (orders, balance, positions).
38
+ """
39
+ client: FastBooksClient = ctx.obj
40
+ try:
41
+ data = client.venues()
42
+ emit(ctx, data, lambda d: _format_venues(d))
43
+ except Exception as e:
44
+ emit_error(ctx, str(e))
45
+ ctx.exit(1)
46
+
47
+
48
+ def _format_venues(data: dict) -> str:
49
+ items = data if isinstance(data, list) else data.get("venues", data.get("data", []))
50
+ if not isinstance(items, list):
51
+ return str(data)
52
+ return format_table(
53
+ items,
54
+ ["venue", "placeOrder", "cancelOrder", "getBalance", "getPositions"],
55
+ ["Venue", "Place", "Cancel", "Balance", "Positions"],
56
+ )
57
+
58
+
59
+ # ── place order ──────────────────────────────────────────────────────────
60
+
61
+ @execute.command()
62
+ @click.argument("venue", type=click.Choice(VENUES, case_sensitive=False))
63
+ @click.argument("symbol")
64
+ @click.argument("side", type=click.Choice(SIDES, case_sensitive=False))
65
+ @click.argument("order_type", type=click.Choice(ORDER_TYPES, case_sensitive=False))
66
+ @click.argument("amount", type=float)
67
+ @click.option("--price", "-p", type=float, default=None, help="Limit price (required for limit orders).")
68
+ @click.option("--client-id", type=str, default=None, help="Client order ID for tracking.")
69
+ @click.option("--confirm/--no-confirm", default=True, help="Require confirmation before placing.")
70
+ @click.pass_context
71
+ def order(ctx, venue, symbol, side, order_type, amount, price, client_id, confirm):
72
+ """Place an order on a venue.
73
+
74
+ VENUE: Exchange or broker to execute on.
75
+ SYMBOL: Trading pair (e.g. BTC/USDT, AAPL, ETH/USDC).
76
+ SIDE: buy or sell.
77
+ ORDER_TYPE: market, limit, stop, stop_limit, trailing_stop.
78
+ AMOUNT: Quantity to trade.
79
+
80
+ Examples:
81
+ fastbooks execute order binance BTC/USDT buy market 0.01
82
+ fastbooks execute order alpaca AAPL buy limit 10 --price 150.00
83
+ fastbooks execute order hyperliquid ETH/USDC sell market 1.5
84
+ fastbooks execute order ibkr MSFT buy market 100
85
+ """
86
+ client: FastBooksClient = ctx.obj
87
+
88
+ if order_type == "limit" and price is None:
89
+ emit_error(ctx, "Limit orders require --price")
90
+ ctx.exit(1)
91
+ return
92
+
93
+ if confirm and not ctx.find_root().params.get("json_output", False):
94
+ click.echo(f"\n Order: {side.upper()} {amount} {symbol} @ {order_type} on {venue}")
95
+ if price:
96
+ click.echo(f" Price: {price}")
97
+ if not click.confirm("\n Confirm?"):
98
+ click.echo(" Cancelled.")
99
+ return
100
+
101
+ try:
102
+ data = client.place_order(venue, symbol, side, order_type, amount, price, client_id)
103
+ emit(ctx, data, lambda d: f" Order placed: {d}")
104
+ except Exception as e:
105
+ emit_error(ctx, str(e))
106
+ ctx.exit(1)
107
+
108
+
109
+ # ── cancel order ─────────────────────────────────────────────────────────
110
+
111
+ @execute.command()
112
+ @click.argument("venue", type=click.Choice(VENUES, case_sensitive=False))
113
+ @click.argument("order_id")
114
+ @click.option("--symbol", "-s", type=str, default=None, help="Symbol (required for some venues).")
115
+ @click.pass_context
116
+ def cancel(ctx, venue, order_id, symbol):
117
+ """Cancel an open order.
118
+
119
+ Examples:
120
+ fastbooks execute cancel binance abc123 --symbol BTC/USDT
121
+ fastbooks execute cancel hyperliquid def456
122
+ """
123
+ client: FastBooksClient = ctx.obj
124
+ try:
125
+ data = client.cancel_order(venue, order_id, symbol)
126
+ emit(ctx, data, lambda d: f" Order {order_id} cancelled.")
127
+ except Exception as e:
128
+ emit_error(ctx, str(e))
129
+ ctx.exit(1)
130
+
131
+
132
+ # ── list orders ──────────────────────────────────────────────────────────
133
+
134
+ @execute.command("orders")
135
+ @click.argument("venue", type=click.Choice(VENUES, case_sensitive=False))
136
+ @click.option("--symbol", "-s", type=str, default=None, help="Filter by symbol.")
137
+ @click.pass_context
138
+ def list_orders(ctx, venue, symbol):
139
+ """List open orders on a venue.
140
+
141
+ Examples:
142
+ fastbooks execute orders binance
143
+ fastbooks execute orders ibkr --symbol AAPL
144
+ """
145
+ client: FastBooksClient = ctx.obj
146
+ try:
147
+ data = client.list_orders(venue, symbol)
148
+ emit(ctx, data, lambda d: _format_orders(d))
149
+ except Exception as e:
150
+ emit_error(ctx, str(e))
151
+ ctx.exit(1)
152
+
153
+
154
+ def _format_orders(data) -> str:
155
+ items = data if isinstance(data, list) else data.get("orders", data.get("data", []))
156
+ if not isinstance(items, list):
157
+ return str(data)
158
+ if not items:
159
+ return " No open orders."
160
+ return format_table(
161
+ items,
162
+ ["orderId", "symbol", "side", "type", "amount", "price", "status"],
163
+ ["Order ID", "Symbol", "Side", "Type", "Amount", "Price", "Status"],
164
+ )
165
+
166
+
167
+ # ── get order ────────────────────────────────────────────────────────────
168
+
169
+ @execute.command("order-status")
170
+ @click.argument("venue", type=click.Choice(VENUES, case_sensitive=False))
171
+ @click.argument("order_id")
172
+ @click.option("--symbol", "-s", type=str, default=None)
173
+ @click.pass_context
174
+ def get_order(ctx, venue, order_id, symbol):
175
+ """Get status of a specific order.
176
+
177
+ Examples:
178
+ fastbooks execute order-status binance abc123
179
+ """
180
+ client: FastBooksClient = ctx.obj
181
+ try:
182
+ data = client.get_order(venue, order_id, symbol)
183
+ emit(ctx, data)
184
+ except Exception as e:
185
+ emit_error(ctx, str(e))
186
+ ctx.exit(1)
187
+
188
+
189
+ # ── balance ──────────────────────────────────────────────────────────────
190
+
191
+ @execute.command()
192
+ @click.argument("venue", type=click.Choice(VENUES, case_sensitive=False))
193
+ @click.option("--asset", "-a", type=str, default=None, help="Filter by asset (e.g. BTC, USD).")
194
+ @click.pass_context
195
+ def balance(ctx, venue, asset):
196
+ """Get account balance / holdings.
197
+
198
+ Examples:
199
+ fastbooks execute balance binance
200
+ fastbooks execute balance ibkr --asset USD
201
+ fastbooks execute balance alpaca
202
+ """
203
+ client: FastBooksClient = ctx.obj
204
+ try:
205
+ data = client.balance(venue, asset)
206
+ emit(ctx, data, lambda d: _format_balance(d))
207
+ except Exception as e:
208
+ emit_error(ctx, str(e))
209
+ ctx.exit(1)
210
+
211
+
212
+ def _format_balance(data) -> str:
213
+ items = data if isinstance(data, list) else data.get("balances", data.get("data", []))
214
+ if not isinstance(items, list):
215
+ return str(data)
216
+ return format_table(
217
+ items,
218
+ ["asset", "free", "locked", "total"],
219
+ ["Asset", "Free", "Locked", "Total"],
220
+ )
221
+
222
+
223
+ # ── positions ────────────────────────────────────────────────────────────
224
+
225
+ @execute.command()
226
+ @click.argument("venue", type=click.Choice(VENUES, case_sensitive=False))
227
+ @click.pass_context
228
+ def positions(ctx, venue):
229
+ """Get open positions (perpetuals / margin).
230
+
231
+ Examples:
232
+ fastbooks execute positions hyperliquid
233
+ fastbooks execute positions binance
234
+ """
235
+ client: FastBooksClient = ctx.obj
236
+ try:
237
+ data = client.positions(venue)
238
+ emit(ctx, data, lambda d: _format_positions(d))
239
+ except Exception as e:
240
+ emit_error(ctx, str(e))
241
+ ctx.exit(1)
242
+
243
+
244
+ def _format_positions(data) -> str:
245
+ items = data if isinstance(data, list) else data.get("positions", data.get("data", []))
246
+ if not isinstance(items, list):
247
+ return str(data)
248
+ if not items:
249
+ return " No open positions."
250
+ return format_table(
251
+ items,
252
+ ["symbol", "side", "size", "entryPrice", "unrealizedPnl", "leverage"],
253
+ ["Symbol", "Side", "Size", "Entry", "Unrealized PnL", "Leverage"],
254
+ )
255
+
256
+
257
+ # ── leverage ─────────────────────────────────────────────────────────────
258
+
259
+ @execute.command()
260
+ @click.argument("venue", type=click.Choice(VENUES, case_sensitive=False))
261
+ @click.argument("symbol")
262
+ @click.argument("leverage_value", type=int)
263
+ @click.option("--cross/--isolated", default=False, help="Cross or isolated margin mode.")
264
+ @click.pass_context
265
+ def leverage(ctx, venue, symbol, leverage_value, cross):
266
+ """Update leverage for a perpetual position.
267
+
268
+ Examples:
269
+ fastbooks execute leverage hyperliquid ETH/USDC 10
270
+ fastbooks execute leverage binance BTC/USDT 5 --cross
271
+ """
272
+ client: FastBooksClient = ctx.obj
273
+ try:
274
+ data = client.update_leverage(venue, symbol, leverage_value, cross)
275
+ emit(ctx, data, lambda d: f" Leverage set to {leverage_value}x for {symbol} on {venue}")
276
+ except Exception as e:
277
+ emit_error(ctx, str(e))
278
+ ctx.exit(1)
279
+
280
+
281
+ # ── order history ────────────────────────────────────────────────────────
282
+
283
+ @execute.command()
284
+ @click.option("--venue", "-v", type=click.Choice(VENUES, case_sensitive=False), default="ibkr")
285
+ @click.pass_context
286
+ def history(ctx, venue):
287
+ """Get order history (filled, cancelled, rejected).
288
+
289
+ Examples:
290
+ fastbooks execute history --venue ibkr
291
+ """
292
+ client: FastBooksClient = ctx.obj
293
+ try:
294
+ data = client.order_history(venue)
295
+ emit(ctx, data, lambda d: _format_orders(d))
296
+ except Exception as e:
297
+ emit_error(ctx, str(e))
298
+ ctx.exit(1)
@@ -0,0 +1,149 @@
1
+ """`init` — one-command onboarding for controlling FastBooks with an AI agent.
2
+
3
+ Sets up, in ~/.fastbooks/config.json (0600): the FastBooks data key (so the bot is
4
+ throttled), which exchanges are activated, per-exchange trading keys, and an MCP host
5
+ config. Interactive by default; fully non-interactive for agents/scripts.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+
12
+ import click
13
+
14
+ from ..client import FastBooksClient
15
+ from ..output import emit, emit_error
16
+ from .. import config_store
17
+
18
+ # Curated activation catalog: (title, exchange ids, backend kind)
19
+ CATALOG = [
20
+ ("Crypto CEX — spot + perps (ccxt: apiKey + secret [+ password])",
21
+ ["binance", "bybit", "okx", "kraken", "coinbase", "kucoin", "bitget", "gate", "mexc", "htx"], "ccxt"),
22
+ ("On-chain perps / DEX — via ccxt (hyperliquid/dydx/… use a wallet key as 'secret')",
23
+ ["hyperliquid", "dydx", "paradex", "apex", "woofipro"], "ccxt"),
24
+ ("Stocks — FastBooks platform (connect: `fastbooks --token <JWT> wallet add ...`)",
25
+ ["alpaca", "ibkr"], "platform"),
26
+ ("Forex — FastBooks platform",
27
+ ["oanda", "saxo"], "platform"),
28
+ ("Prediction markets — FastBooks platform (Polymarket tradable via `execute`)",
29
+ ["polymarket", "limitless"], "platform"),
30
+ ]
31
+
32
+
33
+ def _kind_of(ex: str) -> str:
34
+ for _title, ids, kind in CATALOG:
35
+ if ex in ids:
36
+ return kind
37
+ return "ccxt" # default: treat unknown as a ccxt exchange id
38
+
39
+
40
+ @click.command("init")
41
+ @click.option("--activate", "activate_csv", default=None, help="Comma list of exchanges to activate (non-interactive).")
42
+ @click.option("--mint-key", is_flag=True, help="Mint a free FastBooks data key (x-api-key throttle).")
43
+ @click.option("--api-key", "api_key", default=None, help="Store this existing FastBooks data key instead of minting.")
44
+ @click.option("--mcp-config/--no-mcp-config", default=True, help="Write an MCP host config for agents (~/.fastbooks/mcp.json).")
45
+ @click.option("--yes", "-y", is_flag=True, help="Non-interactive: apply flags with no prompts.")
46
+ @click.pass_context
47
+ def init_cmd(ctx, activate_csv, mint_key, api_key, mcp_config, yes):
48
+ """Set up FastBooks for your AI agent — data key, exchange activation, MCP config.
49
+
50
+ Interactive: fastbooks init
51
+ Agent/scripted: fastbooks --json init -y --mint-key --activate binance,hyperliquid,polymarket
52
+ """
53
+ client: FastBooksClient = ctx.obj
54
+ json_mode = bool(ctx.find_root().params.get("json_output"))
55
+ interactive = not (yes or json_mode or activate_csv or mint_key or api_key)
56
+
57
+ summary = {
58
+ "config_path": str(config_store.CONFIG_PATH),
59
+ "data_key": None,
60
+ "activated": [],
61
+ "credentials_set": [],
62
+ "mcp_config": None,
63
+ "next_steps": [],
64
+ }
65
+
66
+ # ── Step 1: FastBooks data key (per-bot throttle) ──
67
+ key = api_key
68
+ if not key and (mint_key or interactive and click.confirm(
69
+ "Mint a free FastBooks data key now (lets the platform rate-throttle your bot)?", default=True)):
70
+ try:
71
+ res = client.create_fastbooks_api_key("cli-agent")
72
+ key = res.get("apiKey") or res.get("api_key")
73
+ if interactive and key:
74
+ click.echo(f" minted {key}")
75
+ except Exception as e: # noqa: BLE001
76
+ if interactive:
77
+ click.echo(f" (could not mint — {e}; is the backend up?)")
78
+ if interactive and not key:
79
+ entered = click.prompt(" Or paste an existing fb_live_ key (blank to skip)", default="", show_default=False)
80
+ key = entered.strip() or None
81
+ if key:
82
+ config_store.set_api_key(key)
83
+ summary["data_key"] = "set"
84
+
85
+ # ── Step 2: choose exchanges to activate ──
86
+ chosen: list[str] = []
87
+ if activate_csv:
88
+ chosen = [x.strip().lower() for x in activate_csv.split(",") if x.strip()]
89
+ elif interactive:
90
+ click.echo("\n Available venues:")
91
+ for title, ids, _kind in CATALOG:
92
+ click.echo(f" • {title}\n {', '.join(ids)}")
93
+ raw = click.prompt("\n Activate which? (comma list, 'all-cex', or blank)", default="", show_default=False)
94
+ if raw.strip().lower() == "all-cex":
95
+ chosen = list(CATALOG[0][1])
96
+ else:
97
+ chosen = [x.strip().lower() for x in raw.split(",") if x.strip()]
98
+
99
+ for ex in chosen:
100
+ config_store.activate(ex)
101
+ summary["activated"].append(ex)
102
+
103
+ # ── Step 3: trading keys for activated ccxt venues ──
104
+ for ex in chosen:
105
+ kind = _kind_of(ex)
106
+ if kind == "platform":
107
+ summary["next_steps"].append(
108
+ f"Connect {ex} (platform venue): fastbooks --token <JWT> wallet add {ex} --api-key ... --secret ..."
109
+ )
110
+ continue
111
+ if interactive and click.confirm(f" Add trading keys for '{ex}' now? (skip = data-only)", default=False):
112
+ ak = click.prompt(f" {ex} apiKey", default="", show_default=False).strip()
113
+ sk = click.prompt(f" {ex} secret/wallet-key", default="", show_default=False, hide_input=True).strip()
114
+ pw = click.prompt(f" {ex} password/passphrase (blank if none)", default="", show_default=False, hide_input=True).strip()
115
+ config_store.set_credentials(ex, ak or None, sk or None, pw or None)
116
+ summary["credentials_set"].append(ex)
117
+
118
+ # ── Step 4: MCP host config ──
119
+ if mcp_config:
120
+ env = {"FASTBOOKS_API_KEY": key} if key else {}
121
+ host_cfg = {"mcpServers": {"fastbooks": {"command": "fastbooks-mcp", **({"env": env} if env else {})}}}
122
+ config_store.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
123
+ path = config_store.CONFIG_DIR / "mcp.json"
124
+ path.write_text(json.dumps(host_cfg, indent=2))
125
+ summary["mcp_config"] = str(path)
126
+
127
+ summary["next_steps"] += [
128
+ "Data (keyless): fastbooks --json direct ticker hyperliquid BTC/USDC:USDC",
129
+ "Trade (activated): fastbooks direct order binance BTC/USDT buy 0.001 --type limit --price 60000",
130
+ "Agent (MCP): add ~/.fastbooks/mcp.json to your MCP host — or run `fastbooks-mcp`",
131
+ ]
132
+ emit(ctx, summary, _format)
133
+
134
+
135
+ def _format(s: dict) -> str:
136
+ lines = [
137
+ "",
138
+ " FastBooks onboarding complete",
139
+ " =============================",
140
+ f" Config: {s['config_path']}",
141
+ f" Data key: {s['data_key'] or '(none — mint later with `fastbooks apikey create`)'}",
142
+ f" Activated: {', '.join(s['activated']) or '(none)'}",
143
+ f" Keys set: {', '.join(s['credentials_set']) or '(none — data-only)'}",
144
+ f" MCP config: {s['mcp_config'] or '(skipped)'}",
145
+ "",
146
+ " Next:",
147
+ ]
148
+ lines += [f" • {n}" for n in s["next_steps"]]
149
+ return "\n".join(lines)