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,294 @@
1
+ """Config command group — service URLs, defaults, diagnostics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ import click
8
+
9
+ from ..client import FastBooksClient, DEFAULT_ROUTING_URL, DEFAULT_BOTS_URL
10
+ from ..output import emit, emit_error
11
+ from .. import config_store
12
+
13
+
14
+ @click.group()
15
+ def config():
16
+ """Configuration and diagnostics.
17
+
18
+ Environment variables:
19
+ FASTBOOKS_ROUTING_URL — Routing/API service (default: http://localhost:3002)
20
+ FASTBOOKS_BOTS_URL — Bots service (default: http://localhost:8003)
21
+ ALPACA_API_KEY — Alpaca API key
22
+ ALPACA_API_SECRET — Alpaca API secret
23
+ EODHD_API_TOKEN — EODHD market data token
24
+ IBKR_ENCRYPTION_KEY — AES-256 encryption key for IBKR credentials
25
+ SOLANA_RPC_URL — Solana RPC endpoint
26
+ """
27
+ pass
28
+
29
+
30
+ @config.command()
31
+ @click.pass_context
32
+ def show(ctx):
33
+ """Show current configuration and service URLs.
34
+
35
+ Example: fastbooks config show
36
+ """
37
+ client: FastBooksClient = ctx.obj
38
+ data = {
39
+ "routing_url": client.routing_url,
40
+ "bots_url": client.bots_url,
41
+ "timeout": client.timeout,
42
+ "env": {
43
+ "FASTBOOKS_ROUTING_URL": os.environ.get("FASTBOOKS_ROUTING_URL", f"(default: {DEFAULT_ROUTING_URL})"),
44
+ "FASTBOOKS_BOTS_URL": os.environ.get("FASTBOOKS_BOTS_URL", f"(default: {DEFAULT_BOTS_URL})"),
45
+ "ALPACA_API_KEY": "***" if os.environ.get("ALPACA_API_KEY") else "(not set)",
46
+ "ALPACA_API_SECRET": "***" if os.environ.get("ALPACA_API_SECRET") else "(not set)",
47
+ "EODHD_API_TOKEN": "***" if os.environ.get("EODHD_API_TOKEN") else "(not set)",
48
+ "IBKR_ENCRYPTION_KEY": "***" if os.environ.get("IBKR_ENCRYPTION_KEY") else "(not set)",
49
+ "SOLANA_RPC_URL": os.environ.get("SOLANA_RPC_URL", "(not set)"),
50
+ },
51
+ }
52
+ data["stored"] = config_store.redacted()
53
+ emit(ctx, data, lambda d: _format_config(d))
54
+
55
+
56
+ @config.command("set-key")
57
+ @click.argument("exchange")
58
+ @click.option("--api-key", "api_key", default=None, help="Exchange API key.")
59
+ @click.option("--secret", default=None, help="Exchange API secret / wallet key.")
60
+ @click.option("--password", default=None, help="Passphrase (okx/kucoin/etc.), if required.")
61
+ @click.pass_context
62
+ def set_key(ctx, exchange, api_key, secret, password):
63
+ """Store trading keys for a ccxt exchange in the local config (agent-friendly, non-interactive).
64
+
65
+ e.g. fastbooks config set-key binance --api-key AK --secret SK
66
+ """
67
+ if not any([api_key, secret, password]):
68
+ emit_error(ctx, "Provide at least one of --api-key / --secret / --password.")
69
+ ctx.exit(1)
70
+ config_store.set_credentials(exchange, api_key, secret, password)
71
+ config_store.activate(exchange)
72
+ emit(ctx, {"exchange": exchange.lower(), "stored": True, "activated": True},
73
+ lambda d: f" Stored keys for {d['exchange']} and activated it.")
74
+
75
+
76
+ @config.command("activate")
77
+ @click.argument("exchange")
78
+ @click.pass_context
79
+ def activate_cmd(ctx, exchange):
80
+ """Activate an exchange (data + trading if keys are set)."""
81
+ config_store.activate(exchange)
82
+ emit(ctx, {"activated": config_store.activated()}, lambda d: f" Activated. Now: {', '.join(d['activated'])}")
83
+
84
+
85
+ @config.command("deactivate")
86
+ @click.argument("exchange")
87
+ @click.pass_context
88
+ def deactivate_cmd(ctx, exchange):
89
+ """Deactivate an exchange."""
90
+ config_store.deactivate(exchange)
91
+ emit(ctx, {"activated": config_store.activated()}, lambda d: f" Deactivated. Now: {', '.join(d['activated']) or '(none)'}")
92
+
93
+
94
+ def _format_config(data: dict) -> str:
95
+ lines = [
96
+ "",
97
+ " FastBooks CLI Configuration",
98
+ " ===========================",
99
+ f" Routing URL: {data['routing_url']}",
100
+ f" Bots URL: {data['bots_url']}",
101
+ f" Timeout: {data['timeout']}s",
102
+ "",
103
+ " Environment Variables:",
104
+ ]
105
+ for key, val in data.get("env", {}).items():
106
+ lines.append(f" {key}: {val}")
107
+ return "\n".join(lines)
108
+
109
+
110
+ @config.command()
111
+ @click.pass_context
112
+ def health(ctx):
113
+ """Check connectivity to FastBooks services.
114
+
115
+ Tests connection to the routing API and bots service.
116
+
117
+ Example: fastbooks config health
118
+ """
119
+ client: FastBooksClient = ctx.obj
120
+ results = {}
121
+
122
+ # Test routing service
123
+ try:
124
+ client._get(client.routing_url, "/api/execution/venues")
125
+ results["routing"] = {"status": "ok", "url": client.routing_url}
126
+ except Exception as e:
127
+ results["routing"] = {"status": "error", "url": client.routing_url, "error": str(e)}
128
+
129
+ # Test bots service
130
+ try:
131
+ client._get(client.bots_url, "/bots")
132
+ results["bots"] = {"status": "ok", "url": client.bots_url}
133
+ except Exception as e:
134
+ results["bots"] = {"status": "error", "url": client.bots_url, "error": str(e)}
135
+
136
+ emit(ctx, results, lambda d: _format_health(d))
137
+
138
+
139
+ def _format_health(data: dict) -> str:
140
+ lines = ["", " Service Health", " =============="]
141
+ for service, info in data.items():
142
+ status = info.get("status", "unknown")
143
+ color = "green" if status == "ok" else "red"
144
+ symbol = "+" if status == "ok" else "x"
145
+ line = f" [{symbol}] {service}: {info.get('url', '?')}"
146
+ if info.get("error"):
147
+ line += f" — {info['error']}"
148
+ lines.append(click.style(line, fg=color))
149
+ return "\n".join(lines)
150
+
151
+
152
+ @config.command()
153
+ @click.pass_context
154
+ def exchanges(ctx):
155
+ """List all supported exchanges with their capabilities and requirements.
156
+
157
+ Example: fastbooks config exchanges
158
+ """
159
+ data = {
160
+ "exchanges": [
161
+ {
162
+ "name": "binance",
163
+ "type": "CEX",
164
+ "markets": ["spot", "perpetual"],
165
+ "auth": "API key + secret",
166
+ "features": ["orders", "balance", "positions", "leverage"],
167
+ "notes": "Testnet available. Most liquid crypto exchange.",
168
+ },
169
+ {
170
+ "name": "bybit",
171
+ "type": "CEX",
172
+ "markets": ["spot", "perpetual"],
173
+ "auth": "API key + secret",
174
+ "features": ["orders", "balance", "positions", "leverage"],
175
+ "notes": "Unified trading account.",
176
+ },
177
+ {
178
+ "name": "okx",
179
+ "type": "CEX",
180
+ "markets": ["spot", "perpetual"],
181
+ "auth": "API key + secret + passphrase",
182
+ "features": ["orders", "balance", "positions", "leverage"],
183
+ "notes": "Passphrase required.",
184
+ },
185
+ {
186
+ "name": "kraken",
187
+ "type": "CEX",
188
+ "markets": ["spot", "perpetual"],
189
+ "auth": "API key + secret",
190
+ "features": ["orders", "balance", "positions"],
191
+ "notes": "Pro API for perps.",
192
+ },
193
+ {
194
+ "name": "coinbase",
195
+ "type": "CEX",
196
+ "markets": ["spot"],
197
+ "auth": "API key + secret",
198
+ "features": ["orders", "balance"],
199
+ "notes": "Advanced Trade API. Spot only.",
200
+ },
201
+ {
202
+ "name": "kucoin",
203
+ "type": "CEX",
204
+ "markets": ["spot"],
205
+ "auth": "API key + secret + passphrase",
206
+ "features": ["orders", "balance"],
207
+ "notes": "Passphrase required. Spot only.",
208
+ },
209
+ {
210
+ "name": "hyperliquid",
211
+ "type": "DEX (on-chain perps)",
212
+ "markets": ["perpetual"],
213
+ "auth": "EVM private key (64 hex chars)",
214
+ "features": ["orders", "positions", "leverage"],
215
+ "notes": "On-chain perpetuals on Arbitrum. No API secret needed, uses wallet signing.",
216
+ },
217
+ {
218
+ "name": "ibkr",
219
+ "type": "Traditional broker",
220
+ "markets": ["stock", "options", "forex", "futures"],
221
+ "auth": "Gateway connection + encrypted credentials",
222
+ "features": ["orders", "balance", "positions", "order_history"],
223
+ "notes": "Requires IB Gateway running (port 4001/4002). Paper/live modes.",
224
+ },
225
+ {
226
+ "name": "alpaca",
227
+ "type": "Traditional broker",
228
+ "markets": ["stock", "crypto"],
229
+ "auth": "API key + secret",
230
+ "features": ["orders", "balance"],
231
+ "notes": "Paper mode via sandbox flag. Commission-free stocks.",
232
+ },
233
+ {
234
+ "name": "deribit",
235
+ "type": "CEX (options)",
236
+ "markets": ["options", "perpetual"],
237
+ "auth": "API key + secret",
238
+ "features": ["orderbooks", "options_chain"],
239
+ "notes": "Options and perps. Testnet available.",
240
+ },
241
+ {
242
+ "name": "polymarket",
243
+ "type": "Prediction market (Polygon)",
244
+ "markets": ["prediction"],
245
+ "auth": "EVM private key (POLYMARKET_PRIVATE_KEY) for trading; read-only without",
246
+ "features": [
247
+ "markets", "events", "search", "tags",
248
+ "orderbooks", "prices", "price-history",
249
+ "limit-orders", "market-orders", "cancel",
250
+ "positions", "balance", "trades",
251
+ "ctf-split", "ctf-merge", "ctf-redeem",
252
+ "bridge", "approvals", "leaderboard",
253
+ ],
254
+ "notes": "Full Polymarket integration: browse, trade, CTF ops, bridging. Use 'fastbooks polymarket' subcommands.",
255
+ },
256
+ {
257
+ "name": "limitless",
258
+ "type": "Prediction market",
259
+ "markets": ["prediction"],
260
+ "auth": "None (read-only)",
261
+ "features": ["orderbooks"],
262
+ "notes": "Alternative prediction market.",
263
+ },
264
+ {
265
+ "name": "jupiter",
266
+ "type": "DEX aggregator (Solana)",
267
+ "markets": ["spot"],
268
+ "auth": "Solana wallet",
269
+ "features": ["tickers", "swap"],
270
+ "notes": "5000+ Solana tokens. Best price routing.",
271
+ },
272
+ {
273
+ "name": "uniswap",
274
+ "type": "DEX (Ethereum)",
275
+ "markets": ["spot"],
276
+ "auth": "EVM wallet",
277
+ "features": ["tickers", "pools"],
278
+ "notes": "Uniswap V3. Ethereum mainnet.",
279
+ },
280
+ ]
281
+ }
282
+ emit(ctx, data, lambda d: _format_exchanges(d))
283
+
284
+
285
+ def _format_exchanges(data: dict) -> str:
286
+ lines = ["", " Supported Exchanges", " ===================", ""]
287
+ for ex in data["exchanges"]:
288
+ lines.append(f" {ex['name'].upper()} ({ex['type']})")
289
+ lines.append(f" Markets: {', '.join(ex['markets'])}")
290
+ lines.append(f" Auth: {ex['auth']}")
291
+ lines.append(f" Features: {', '.join(ex['features'])}")
292
+ lines.append(f" Notes: {ex['notes']}")
293
+ lines.append("")
294
+ return "\n".join(lines)