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,270 @@
1
+ """Markets command group — orderbooks, tickers, OHLCV, symbols, options."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ import json
7
+
8
+ import click
9
+
10
+ from ..client import FastBooksClient
11
+ from ..output import emit, emit_error, format_table
12
+
13
+ # Safety bounds for --all auto-pagination so a bad backend can't loop forever.
14
+ _ALL_PAGE_SIZE = 1000
15
+ _ALL_MAX_PAGES = 500
16
+ _ALL_MAX_ROWS = 500_000
17
+
18
+ SUPPORTED_EXCHANGES = [
19
+ "binance", "bybit", "okx", "kraken", "coinbase", "kucoin",
20
+ "hyperliquid", "deribit", "polymarket", "limitless",
21
+ ]
22
+
23
+ TIMEFRAMES = ["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w"]
24
+
25
+ REGIONS = ["us", "ca", "uk", "au", "eu", "jp", "asia"]
26
+
27
+
28
+ @click.group()
29
+ def markets():
30
+ """Market data: orderbooks, tickers, OHLCV, symbols, options."""
31
+ pass
32
+
33
+
34
+ # ── orderbook ────────────────────────────────────────────────────────────
35
+
36
+ @markets.command()
37
+ @click.argument("exchange", type=click.Choice(SUPPORTED_EXCHANGES, case_sensitive=False))
38
+ @click.argument("symbol")
39
+ @click.pass_context
40
+ def orderbook(ctx, exchange: str, symbol: str):
41
+ """Fetch orderbook for SYMBOL on EXCHANGE.
42
+
43
+ Examples:
44
+ fastbooks markets orderbook binance BTC/USDT
45
+ fastbooks markets orderbook hyperliquid ETH/USDC
46
+ fastbooks markets orderbook polymarket "Will Trump win?"
47
+ """
48
+ client: FastBooksClient = ctx.obj
49
+ try:
50
+ data = client.orderbook(exchange, symbol)
51
+ emit(ctx, data, lambda d: _format_orderbook(d))
52
+ except Exception as e:
53
+ emit_error(ctx, str(e))
54
+ ctx.exit(1)
55
+
56
+
57
+ def _format_orderbook(data: dict) -> str:
58
+ lines = []
59
+ ob = data if "exchange" in data else data.get("data", data)
60
+ lines.append(f" {ob.get('exchange', '?')} | {ob.get('symbol', '?')}")
61
+ lines.append(f" Mid: {ob.get('midPrice', '?')} Spread: {ob.get('spreadPercent', '?')}%")
62
+ lines.append("")
63
+
64
+ asks = ob.get("asks", [])[:5]
65
+ bids = ob.get("bids", [])[:5]
66
+ lines.append(" ASKS (top 5):")
67
+ for price, size in reversed(asks):
68
+ lines.append(f" {price:>12} {size:>12}")
69
+ lines.append(" ─────────────────────────")
70
+ lines.append(" BIDS (top 5):")
71
+ for price, size in bids:
72
+ lines.append(f" {price:>12} {size:>12}")
73
+ return "\n".join(lines)
74
+
75
+
76
+ # ── aggregated orderbooks ────────────────────────────────────────────────
77
+
78
+ @markets.command()
79
+ @click.option("--category", type=click.Choice(["all", "spot", "perpetuals"]), default="all")
80
+ @click.pass_context
81
+ def orderbooks(ctx, category: str):
82
+ """Fetch aggregated orderbooks across all exchanges.
83
+
84
+ Categories: all, spot, perpetuals.
85
+ """
86
+ client: FastBooksClient = ctx.obj
87
+ try:
88
+ data = client.orderbooks_aggregated(category)
89
+ emit(ctx, data)
90
+ except Exception as e:
91
+ emit_error(ctx, str(e))
92
+ ctx.exit(1)
93
+
94
+
95
+ # ── tickers ──────────────────────────────────────────────────────────────
96
+
97
+ @markets.command()
98
+ @click.option("--category", type=str, default=None, help="Filter: crypto, stocks, forex, dex.")
99
+ @click.option("--exchange", type=str, default=None, help="Filter by exchange name.")
100
+ @click.option("--search", type=str, default=None, help="Search by symbol or name.")
101
+ @click.option("--page", type=int, default=1, help="Page number.")
102
+ @click.option("--limit", type=int, default=50, help="Results per page.")
103
+ @click.option("--all", "fetch_all", is_flag=True, help="Fetch the FULL universe (auto-paginate). Ignores --page.")
104
+ @click.option("--export", "export_path", type=click.Path(dir_okay=False, writable=True), default=None,
105
+ help="Write results to a file (.json or .csv inferred from extension).")
106
+ @click.pass_context
107
+ def tickers(ctx, category, exchange, search, page, limit, fetch_all, export_path):
108
+ """List tickers across all supported markets.
109
+
110
+ Examples:
111
+ fastbooks markets tickers --category crypto --limit 20
112
+ fastbooks markets tickers --search AAPL
113
+ fastbooks markets tickers --all # entire universe
114
+ fastbooks markets tickers --all --category stocks --export stocks.csv
115
+ fastbooks --json markets tickers --all > universe.json
116
+ """
117
+ client: FastBooksClient = ctx.obj
118
+ try:
119
+ if fetch_all:
120
+ data = _fetch_all_tickers(client, category=category, exchange=exchange, search=search)
121
+ else:
122
+ data = client.tickers(page=page, limit=limit, category=category, exchange=exchange, search=search)
123
+
124
+ if export_path:
125
+ items = _extract_tickers(data)
126
+ _write_export(items, export_path)
127
+ emit(ctx, {"exported": len(items), "path": export_path},
128
+ lambda d: f" Exported {d['exported']} tickers to {d['path']}")
129
+ return
130
+
131
+ emit(ctx, data, lambda d: _format_tickers(d))
132
+ except Exception as e:
133
+ emit_error(ctx, str(e))
134
+ ctx.exit(1)
135
+
136
+
137
+ def _extract_tickers(data) -> list:
138
+ if isinstance(data, list):
139
+ return data
140
+ if isinstance(data, dict):
141
+ items = data.get("tickers", data.get("data", []))
142
+ return items if isinstance(items, list) else []
143
+ return []
144
+
145
+
146
+ def _fetch_all_tickers(client: FastBooksClient, category=None, exchange=None, search=None) -> dict:
147
+ """Auto-paginate /tickers/all into a single combined result (bounded)."""
148
+ all_items: list = []
149
+ page = 1
150
+ while page <= _ALL_MAX_PAGES and len(all_items) < _ALL_MAX_ROWS:
151
+ resp = client.tickers(page=page, limit=_ALL_PAGE_SIZE, category=category, exchange=exchange, search=search)
152
+ batch = _extract_tickers(resp)
153
+ if not batch:
154
+ break
155
+ all_items.extend(batch)
156
+ if len(batch) < _ALL_PAGE_SIZE:
157
+ break # last page
158
+ page += 1
159
+ return {"tickers": all_items, "total": len(all_items), "pages": page}
160
+
161
+
162
+ _EXPORT_COLUMNS = ["symbol", "name", "exchange", "category", "price", "change24h", "volume24h"]
163
+
164
+
165
+ def _write_export(items: list, path: str) -> None:
166
+ if path.lower().endswith(".csv"):
167
+ with open(path, "w", newline="", encoding="utf-8") as f:
168
+ writer = csv.DictWriter(f, fieldnames=_EXPORT_COLUMNS, extrasaction="ignore")
169
+ writer.writeheader()
170
+ for it in items:
171
+ if isinstance(it, dict):
172
+ writer.writerow({k: it.get(k, "") for k in _EXPORT_COLUMNS})
173
+ else:
174
+ with open(path, "w", encoding="utf-8") as f:
175
+ json.dump(items, f, indent=2, default=str)
176
+
177
+
178
+ def _format_tickers(data: dict) -> str:
179
+ items = _extract_tickers(data)
180
+ if not isinstance(items, list):
181
+ return str(data)
182
+ total = data.get("total", len(items)) if isinstance(data, dict) else len(items)
183
+ table = format_table(
184
+ items[:50],
185
+ ["symbol", "exchange", "price", "change24h", "volume24h"],
186
+ ["Symbol", "Exchange", "Price", "24h Change", "24h Volume"],
187
+ )
188
+ if len(items) > 50:
189
+ table += f"\n … showing 50 of {total} (use --export to save all, or --json for full output)"
190
+ return table
191
+
192
+
193
+ # ── OHLCV ────────────────────────────────────────────────────────────────
194
+
195
+ @markets.command()
196
+ @click.argument("exchange", type=str)
197
+ @click.argument("symbol", type=str)
198
+ @click.option("--timeframe", "-t", type=click.Choice(TIMEFRAMES), default="1h", help="Candle timeframe.")
199
+ @click.option("--limit", "-n", type=int, default=50, help="Number of candles.")
200
+ @click.pass_context
201
+ def ohlcv(ctx, exchange: str, symbol: str, timeframe: str, limit: int):
202
+ """Fetch OHLCV candle data.
203
+
204
+ Examples:
205
+ fastbooks markets ohlcv binance BTC/USDT --timeframe 1d --limit 30
206
+ fastbooks markets ohlcv alpaca AAPL -t 1h -n 100
207
+ """
208
+ client: FastBooksClient = ctx.obj
209
+ try:
210
+ data = client.ohlcv(exchange, symbol, timeframe, limit)
211
+ emit(ctx, data, lambda d: _format_ohlcv(d))
212
+ except Exception as e:
213
+ emit_error(ctx, str(e))
214
+ ctx.exit(1)
215
+
216
+
217
+ def _format_ohlcv(data: dict) -> str:
218
+ bars = data if isinstance(data, list) else data.get("data", data.get("bars", []))
219
+ if not isinstance(bars, list) or not bars:
220
+ return str(data)
221
+ return format_table(
222
+ bars[:20],
223
+ ["timestamp", "open", "high", "low", "close", "volume"],
224
+ ["Time", "Open", "High", "Low", "Close", "Volume"],
225
+ )
226
+
227
+
228
+ # ── symbols ──────────────────────────────────────────────────────────────
229
+
230
+ @markets.command()
231
+ @click.option("--region", "-r", type=click.Choice(REGIONS), default="us", help="Market region.")
232
+ @click.option("--sector", "-s", type=str, default=None, help="Filter by sector.")
233
+ @click.option("--limit", "-n", type=int, default=100, help="Max symbols.")
234
+ @click.pass_context
235
+ def symbols(ctx, region: str, sector: str | None, limit: int):
236
+ """List symbols from the universe.
237
+
238
+ Regions: us, ca, uk, au, eu, jp, asia.
239
+
240
+ Examples:
241
+ fastbooks markets symbols --region us --sector Technology --limit 50
242
+ fastbooks markets symbols -r ca -n 200
243
+ """
244
+ client: FastBooksClient = ctx.obj
245
+ try:
246
+ data = client.symbols_universe(region, sector, limit)
247
+ emit(ctx, data)
248
+ except Exception as e:
249
+ emit_error(ctx, str(e))
250
+ ctx.exit(1)
251
+
252
+
253
+ # ── options chain ────────────────────────────────────────────────────────
254
+
255
+ @markets.command()
256
+ @click.argument("underlying")
257
+ @click.pass_context
258
+ def options(ctx, underlying: str):
259
+ """Fetch options chain for UNDERLYING (Deribit).
260
+
261
+ Example:
262
+ fastbooks markets options BTC
263
+ """
264
+ client: FastBooksClient = ctx.obj
265
+ try:
266
+ data = client.options_chain(underlying)
267
+ emit(ctx, data)
268
+ except Exception as e:
269
+ emit_error(ctx, str(e))
270
+ ctx.exit(1)
@@ -0,0 +1,27 @@
1
+ """`mcp` — launch the FastBooks MCP server (stdio) for AI agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from ..output import emit_error
8
+
9
+
10
+ @click.command("mcp")
11
+ @click.pass_context
12
+ def mcp_cmd(ctx: click.Context):
13
+ """Start the FastBooks MCP server over stdio (for Claude / MCP-compatible agents).
14
+
15
+ Exposes STANDALONE ccxt tools (100+ exchanges, keyless market data + direct trading)
16
+ plus FastBooks PLATFORM tools (aggregated multi-asset data, the 15-venue execution
17
+ layer, managed bots) when a backend is reachable. Point your agent at this command:
18
+
19
+ {"command": "fastbooks", "args": ["mcp"]} # or the `fastbooks-mcp` script
20
+ """
21
+ try:
22
+ from ..mcp_server import build_server
23
+ except RuntimeError as e: # MCP SDK missing
24
+ emit_error(ctx, str(e))
25
+ ctx.exit(2)
26
+ return
27
+ build_server().run()
@@ -0,0 +1,123 @@
1
+ """`onchain` — FastBots on-chain engine (Solana/EVM: Jupiter / Pump.fun / Hyperliquid).
2
+
3
+ Proxies the isolated solana-trading-bots engine via the backend's `/api/onchain/*` route.
4
+ That proxy is auth + email-gated, so these commands need `--token` / FASTBOOKS_API_TOKEN
5
+ (a JWT whose email is on the engine's allowlist).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import click
11
+
12
+ from ..client import FastBooksClient
13
+ from ..output import emit, emit_error
14
+
15
+
16
+ @click.group()
17
+ def onchain():
18
+ """FastBots on-chain engine — status, strategies, positions, and live bot control.
19
+
20
+ Real-money on-chain execution on Solana/EVM. Requires --token (JWT on the engine allowlist).
21
+ """
22
+
23
+
24
+ @onchain.command("status")
25
+ @click.pass_context
26
+ def status_cmd(ctx: click.Context):
27
+ """Live runner status — which on-chain bots are running."""
28
+ client: FastBooksClient = ctx.obj
29
+ try:
30
+ emit(ctx, client.onchain_live_status())
31
+ except Exception as e: # noqa: BLE001
32
+ emit_error(ctx, str(e))
33
+ ctx.exit(1)
34
+
35
+
36
+ @onchain.command("stats")
37
+ @click.option("--by-bot", is_flag=True, help="Per-bot realized PnL breakdown.")
38
+ @click.pass_context
39
+ def stats_cmd(ctx: click.Context, by_bot: bool):
40
+ """Realized PnL / win-rate stats (global or per bot)."""
41
+ client: FastBooksClient = ctx.obj
42
+ try:
43
+ emit(ctx, client.onchain_stats(by_bot=by_bot))
44
+ except Exception as e: # noqa: BLE001
45
+ emit_error(ctx, str(e))
46
+ ctx.exit(1)
47
+
48
+
49
+ @onchain.command("strategies")
50
+ @click.pass_context
51
+ def strategies_cmd(ctx: click.Context):
52
+ """List deployable on-chain strategies + their params."""
53
+ client: FastBooksClient = ctx.obj
54
+ try:
55
+ emit(ctx, client.onchain_strategies())
56
+ except Exception as e: # noqa: BLE001
57
+ emit_error(ctx, str(e))
58
+ ctx.exit(1)
59
+
60
+
61
+ @onchain.command("positions")
62
+ @click.option("--status", default=None, help="open | closed | failed")
63
+ @click.pass_context
64
+ def positions_cmd(ctx: click.Context, status: str | None):
65
+ """Open/closed on-chain positions."""
66
+ client: FastBooksClient = ctx.obj
67
+ try:
68
+ emit(ctx, client.onchain_positions(status))
69
+ except Exception as e: # noqa: BLE001
70
+ emit_error(ctx, str(e))
71
+ ctx.exit(1)
72
+
73
+
74
+ @onchain.command("trades")
75
+ @click.option("--limit", "-n", default=40, type=int)
76
+ @click.pass_context
77
+ def trades_cmd(ctx: click.Context, limit: int):
78
+ """Recent on-chain trades."""
79
+ client: FastBooksClient = ctx.obj
80
+ try:
81
+ emit(ctx, client.onchain_trades(limit))
82
+ except Exception as e: # noqa: BLE001
83
+ emit_error(ctx, str(e))
84
+ ctx.exit(1)
85
+
86
+
87
+ @onchain.command("start")
88
+ @click.argument("strategy_id")
89
+ @click.option("--alloc", type=float, default=None, help="Max SOL allocation for this bot (maxAllocationSol).")
90
+ @click.option("--slippage-bps", type=int, default=None, help="Real swap slippage tolerance (bps).")
91
+ @click.option("--max-safety-risk", type=int, default=None, help="Block entries at/above this risk score (e.g. 40 to allow caution).")
92
+ @click.option("--yes", is_flag=True, help="Skip the real-funds confirmation.")
93
+ @click.pass_context
94
+ def start_cmd(ctx, strategy_id, alloc, slippage_bps, max_safety_risk, yes):
95
+ """Start a live on-chain bot — REAL FUNDS. e.g. onchain start high_winrate_v2 --alloc 0.5 --max-safety-risk 40"""
96
+ client: FastBooksClient = ctx.obj
97
+ if not yes and not ctx.find_root().params.get("json_output"):
98
+ click.confirm(f"Start LIVE on-chain bot '{strategy_id}' with REAL funds?", abort=True)
99
+ opts: dict = {}
100
+ if alloc is not None:
101
+ opts["maxAllocationSol"] = alloc
102
+ if slippage_bps is not None:
103
+ opts["realSlippageBps"] = slippage_bps
104
+ if max_safety_risk is not None:
105
+ opts["maxSafetyRisk"] = max_safety_risk
106
+ try:
107
+ emit(ctx, client.onchain_live_start(strategy_id, opts))
108
+ except Exception as e: # noqa: BLE001
109
+ emit_error(ctx, str(e))
110
+ ctx.exit(1)
111
+
112
+
113
+ @onchain.command("stop")
114
+ @click.argument("strategy_id")
115
+ @click.pass_context
116
+ def stop_cmd(ctx: click.Context, strategy_id: str):
117
+ """Stop a live on-chain bot."""
118
+ client: FastBooksClient = ctx.obj
119
+ try:
120
+ emit(ctx, client.onchain_live_stop(strategy_id))
121
+ except Exception as e: # noqa: BLE001
122
+ emit_error(ctx, str(e))
123
+ ctx.exit(1)