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,385 @@
|
|
|
1
|
+
"""REPL mode — interactive session with undo/redo and command history.
|
|
2
|
+
|
|
3
|
+
Following CLI-Anything's ReplSkin pattern: persistent session state,
|
|
4
|
+
structured command execution, and undo/redo for reversible operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import copy
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import readline
|
|
13
|
+
import shlex
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
|
|
21
|
+
from ..client import FastBooksClient
|
|
22
|
+
from ..output import emit, emit_error
|
|
23
|
+
from .. import __version__
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class SessionState:
|
|
28
|
+
"""Tracks REPL session state for undo/redo."""
|
|
29
|
+
active_venue: str | None = None
|
|
30
|
+
active_symbol: str | None = None
|
|
31
|
+
active_bot_id: str | None = None
|
|
32
|
+
history: list[dict] = field(default_factory=list)
|
|
33
|
+
undo_stack: list[dict] = field(default_factory=list)
|
|
34
|
+
redo_stack: list[dict] = field(default_factory=list)
|
|
35
|
+
aliases: dict[str, str] = field(default_factory=dict)
|
|
36
|
+
variables: dict[str, str] = field(default_factory=dict)
|
|
37
|
+
|
|
38
|
+
def snapshot(self) -> dict:
|
|
39
|
+
return {
|
|
40
|
+
"active_venue": self.active_venue,
|
|
41
|
+
"active_symbol": self.active_symbol,
|
|
42
|
+
"active_bot_id": self.active_bot_id,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def restore(self, snap: dict) -> None:
|
|
46
|
+
self.active_venue = snap.get("active_venue")
|
|
47
|
+
self.active_symbol = snap.get("active_symbol")
|
|
48
|
+
self.active_bot_id = snap.get("active_bot_id")
|
|
49
|
+
|
|
50
|
+
def push_undo(self, action: dict) -> None:
|
|
51
|
+
self.undo_stack.append({"state": self.snapshot(), "action": action})
|
|
52
|
+
self.redo_stack.clear()
|
|
53
|
+
|
|
54
|
+
def undo(self) -> dict | None:
|
|
55
|
+
if not self.undo_stack:
|
|
56
|
+
return None
|
|
57
|
+
entry = self.undo_stack.pop()
|
|
58
|
+
self.redo_stack.append({"state": self.snapshot(), "action": entry["action"]})
|
|
59
|
+
self.restore(entry["state"])
|
|
60
|
+
return entry["action"]
|
|
61
|
+
|
|
62
|
+
def redo(self) -> dict | None:
|
|
63
|
+
if not self.redo_stack:
|
|
64
|
+
return None
|
|
65
|
+
entry = self.redo_stack.pop()
|
|
66
|
+
self.undo_stack.append({"state": self.snapshot(), "action": entry["action"]})
|
|
67
|
+
self.restore(entry["state"])
|
|
68
|
+
return entry["action"]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ── REPL built-in commands ───────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
BUILTINS_HELP = """
|
|
74
|
+
Built-in Commands:
|
|
75
|
+
==================
|
|
76
|
+
help / ? Show this help
|
|
77
|
+
exit / quit / q Exit REPL
|
|
78
|
+
undo Undo last state change
|
|
79
|
+
redo Redo last undone change
|
|
80
|
+
history Show command history
|
|
81
|
+
status Show current session state
|
|
82
|
+
use <venue> Set active venue (e.g. 'use binance')
|
|
83
|
+
symbol <sym> Set active symbol (e.g. 'symbol BTC/USDT')
|
|
84
|
+
bot <id> Set active bot ID
|
|
85
|
+
alias <name> <cmd> Create command alias
|
|
86
|
+
unalias <name> Remove alias
|
|
87
|
+
aliases List all aliases
|
|
88
|
+
set <key> <val> Set a session variable
|
|
89
|
+
vars List session variables
|
|
90
|
+
clear Clear screen
|
|
91
|
+
json on/off Toggle JSON output mode
|
|
92
|
+
|
|
93
|
+
CLI Commands:
|
|
94
|
+
=============
|
|
95
|
+
Any 'fastbooks' command works without the 'fastbooks' prefix:
|
|
96
|
+
markets orderbook binance BTC/USDT
|
|
97
|
+
execute balance binance
|
|
98
|
+
bots list
|
|
99
|
+
swap jupiter SOL USDC 1.0
|
|
100
|
+
|
|
101
|
+
Context-Aware Shortcuts:
|
|
102
|
+
========================
|
|
103
|
+
After 'use binance' + 'symbol BTC/USDT':
|
|
104
|
+
orderbook -> markets orderbook binance BTC/USDT
|
|
105
|
+
balance -> execute balance binance
|
|
106
|
+
order buy market 1 -> execute order binance BTC/USDT buy market 1
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _banner() -> str:
|
|
111
|
+
return f"""
|
|
112
|
+
╔══════════════════════════════════════════════════╗
|
|
113
|
+
║ FastBooks REPL v{__version__:<36}║
|
|
114
|
+
║ Agent-native interactive trading session ║
|
|
115
|
+
║ Type 'help' for commands, 'exit' to quit ║
|
|
116
|
+
╚══════════════════════════════════════════════════╝
|
|
117
|
+
|
|
118
|
+
SKILL.md: {os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'SKILL.md'))}
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@click.command("repl")
|
|
123
|
+
@click.option("--venue", "-v", default=None, help="Start with an active venue.")
|
|
124
|
+
@click.option("--symbol", "-s", default=None, help="Start with an active symbol.")
|
|
125
|
+
@click.pass_context
|
|
126
|
+
def repl(ctx, venue, symbol):
|
|
127
|
+
"""Start an interactive REPL session.
|
|
128
|
+
|
|
129
|
+
Provides a persistent session with context-aware shortcuts,
|
|
130
|
+
undo/redo, command history, and aliases.
|
|
131
|
+
|
|
132
|
+
Examples:
|
|
133
|
+
fastbooks repl
|
|
134
|
+
fastbooks repl --venue binance --symbol BTC/USDT
|
|
135
|
+
"""
|
|
136
|
+
from ..main import cli as root_cli
|
|
137
|
+
|
|
138
|
+
client: FastBooksClient = ctx.obj
|
|
139
|
+
state = SessionState(active_venue=venue, active_symbol=symbol)
|
|
140
|
+
json_mode = ctx.find_root().params.get("json_output", False)
|
|
141
|
+
|
|
142
|
+
# Setup readline history
|
|
143
|
+
histfile = os.path.expanduser("~/.fastbooks_history")
|
|
144
|
+
try:
|
|
145
|
+
readline.read_history_file(histfile)
|
|
146
|
+
except FileNotFoundError:
|
|
147
|
+
pass
|
|
148
|
+
readline.set_history_length(1000)
|
|
149
|
+
|
|
150
|
+
click.echo(_banner())
|
|
151
|
+
|
|
152
|
+
if state.active_venue:
|
|
153
|
+
click.echo(f" Active venue: {state.active_venue}")
|
|
154
|
+
if state.active_symbol:
|
|
155
|
+
click.echo(f" Active symbol: {state.active_symbol}")
|
|
156
|
+
click.echo()
|
|
157
|
+
|
|
158
|
+
while True:
|
|
159
|
+
# Build prompt
|
|
160
|
+
prompt_parts = ["fastbooks"]
|
|
161
|
+
if state.active_venue:
|
|
162
|
+
prompt_parts.append(state.active_venue)
|
|
163
|
+
if state.active_symbol:
|
|
164
|
+
prompt_parts.append(state.active_symbol)
|
|
165
|
+
prompt = ":".join(prompt_parts) + "> "
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
line = input(prompt).strip()
|
|
169
|
+
except (EOFError, KeyboardInterrupt):
|
|
170
|
+
click.echo("\n Goodbye.")
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
if not line:
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
# Save to readline history
|
|
177
|
+
state.history.append({"command": line, "timestamp": time.time()})
|
|
178
|
+
|
|
179
|
+
# Resolve aliases
|
|
180
|
+
parts = line.split(None, 1)
|
|
181
|
+
cmd = parts[0].lower()
|
|
182
|
+
rest = parts[1] if len(parts) > 1 else ""
|
|
183
|
+
|
|
184
|
+
if cmd in state.aliases:
|
|
185
|
+
line = state.aliases[cmd] + (" " + rest if rest else "")
|
|
186
|
+
parts = line.split(None, 1)
|
|
187
|
+
cmd = parts[0].lower()
|
|
188
|
+
rest = parts[1] if len(parts) > 1 else ""
|
|
189
|
+
|
|
190
|
+
# ── built-ins ────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
if cmd in ("exit", "quit", "q"):
|
|
193
|
+
click.echo(" Goodbye.")
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
elif cmd in ("help", "?"):
|
|
197
|
+
click.echo(BUILTINS_HELP)
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
elif cmd == "undo":
|
|
201
|
+
action = state.undo()
|
|
202
|
+
if action:
|
|
203
|
+
click.echo(f" Undone: {action.get('description', '?')}")
|
|
204
|
+
else:
|
|
205
|
+
click.echo(" Nothing to undo.")
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
elif cmd == "redo":
|
|
209
|
+
action = state.redo()
|
|
210
|
+
if action:
|
|
211
|
+
click.echo(f" Redone: {action.get('description', '?')}")
|
|
212
|
+
else:
|
|
213
|
+
click.echo(" Nothing to redo.")
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
elif cmd == "history":
|
|
217
|
+
for i, h in enumerate(state.history[-20:], 1):
|
|
218
|
+
click.echo(f" {i:3} {h['command']}")
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
elif cmd == "status":
|
|
222
|
+
click.echo(f" Venue: {state.active_venue or '(none)'}")
|
|
223
|
+
click.echo(f" Symbol: {state.active_symbol or '(none)'}")
|
|
224
|
+
click.echo(f" Bot: {state.active_bot_id or '(none)'}")
|
|
225
|
+
click.echo(f" JSON: {'on' if json_mode else 'off'}")
|
|
226
|
+
click.echo(f" Undo: {len(state.undo_stack)} actions")
|
|
227
|
+
click.echo(f" Redo: {len(state.redo_stack)} actions")
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
elif cmd == "use":
|
|
231
|
+
old = state.active_venue
|
|
232
|
+
state.push_undo({"description": f"use {old} -> {rest}", "type": "venue"})
|
|
233
|
+
state.active_venue = rest.strip() if rest.strip() else None
|
|
234
|
+
click.echo(f" Venue set to: {state.active_venue or '(none)'}")
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
elif cmd == "symbol":
|
|
238
|
+
old = state.active_symbol
|
|
239
|
+
state.push_undo({"description": f"symbol {old} -> {rest}", "type": "symbol"})
|
|
240
|
+
state.active_symbol = rest.strip() if rest.strip() else None
|
|
241
|
+
click.echo(f" Symbol set to: {state.active_symbol or '(none)'}")
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
elif cmd == "bot":
|
|
245
|
+
old = state.active_bot_id
|
|
246
|
+
state.push_undo({"description": f"bot {old} -> {rest}", "type": "bot"})
|
|
247
|
+
state.active_bot_id = rest.strip() if rest.strip() else None
|
|
248
|
+
click.echo(f" Bot set to: {state.active_bot_id or '(none)'}")
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
elif cmd == "alias":
|
|
252
|
+
alias_parts = rest.split(None, 1)
|
|
253
|
+
if len(alias_parts) < 2:
|
|
254
|
+
click.echo(" Usage: alias <name> <command>")
|
|
255
|
+
continue
|
|
256
|
+
state.aliases[alias_parts[0]] = alias_parts[1]
|
|
257
|
+
click.echo(f" Alias '{alias_parts[0]}' -> '{alias_parts[1]}'")
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
elif cmd == "unalias":
|
|
261
|
+
name = rest.strip()
|
|
262
|
+
if name in state.aliases:
|
|
263
|
+
del state.aliases[name]
|
|
264
|
+
click.echo(f" Removed alias '{name}'.")
|
|
265
|
+
else:
|
|
266
|
+
click.echo(f" No alias '{name}'.")
|
|
267
|
+
continue
|
|
268
|
+
|
|
269
|
+
elif cmd == "aliases":
|
|
270
|
+
if not state.aliases:
|
|
271
|
+
click.echo(" No aliases defined.")
|
|
272
|
+
else:
|
|
273
|
+
for name, target in state.aliases.items():
|
|
274
|
+
click.echo(f" {name} -> {target}")
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
elif cmd == "set":
|
|
278
|
+
set_parts = rest.split(None, 1)
|
|
279
|
+
if len(set_parts) < 2:
|
|
280
|
+
click.echo(" Usage: set <key> <value>")
|
|
281
|
+
continue
|
|
282
|
+
state.variables[set_parts[0]] = set_parts[1]
|
|
283
|
+
click.echo(f" {set_parts[0]} = {set_parts[1]}")
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
elif cmd == "vars":
|
|
287
|
+
if not state.variables:
|
|
288
|
+
click.echo(" No variables set.")
|
|
289
|
+
else:
|
|
290
|
+
for k, v in state.variables.items():
|
|
291
|
+
click.echo(f" {k} = {v}")
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
elif cmd == "clear":
|
|
295
|
+
click.clear()
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
elif cmd == "json":
|
|
299
|
+
if rest.strip().lower() in ("on", "true", "1"):
|
|
300
|
+
json_mode = True
|
|
301
|
+
click.echo(" JSON output: on")
|
|
302
|
+
elif rest.strip().lower() in ("off", "false", "0"):
|
|
303
|
+
json_mode = False
|
|
304
|
+
click.echo(" JSON output: off")
|
|
305
|
+
else:
|
|
306
|
+
click.echo(f" JSON output: {'on' if json_mode else 'off'}")
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
# ── context-aware shortcuts ──────────────────────────────────
|
|
310
|
+
|
|
311
|
+
expanded = _expand_shortcut(line, state)
|
|
312
|
+
if expanded != line:
|
|
313
|
+
click.secho(f" -> {expanded}", fg="cyan")
|
|
314
|
+
line = expanded
|
|
315
|
+
|
|
316
|
+
# ── execute as CLI command ───────────────────────────────────
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
args = shlex.split(line)
|
|
320
|
+
except ValueError as e:
|
|
321
|
+
click.secho(f" Parse error: {e}", fg="red")
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
if json_mode and "--json" not in args:
|
|
325
|
+
args = ["--json"] + args
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
root_cli(args, standalone_mode=False, obj=client)
|
|
329
|
+
except SystemExit:
|
|
330
|
+
pass
|
|
331
|
+
except click.exceptions.UsageError as e:
|
|
332
|
+
click.secho(f" {e}", fg="red")
|
|
333
|
+
except Exception as e:
|
|
334
|
+
click.secho(f" Error: {e}", fg="red")
|
|
335
|
+
|
|
336
|
+
click.echo()
|
|
337
|
+
|
|
338
|
+
# Save history
|
|
339
|
+
try:
|
|
340
|
+
readline.write_history_file(histfile)
|
|
341
|
+
except OSError:
|
|
342
|
+
pass
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _expand_shortcut(line: str, state: SessionState) -> str:
|
|
346
|
+
"""Expand context-aware shortcuts using active venue/symbol."""
|
|
347
|
+
parts = line.split()
|
|
348
|
+
if not parts:
|
|
349
|
+
return line
|
|
350
|
+
|
|
351
|
+
cmd = parts[0].lower()
|
|
352
|
+
|
|
353
|
+
# Direct command shortcuts when venue/symbol are set
|
|
354
|
+
if cmd == "orderbook" and state.active_venue and state.active_symbol:
|
|
355
|
+
return f"markets orderbook {state.active_venue} {state.active_symbol}"
|
|
356
|
+
|
|
357
|
+
if cmd == "balance" and state.active_venue:
|
|
358
|
+
extra = " ".join(parts[1:])
|
|
359
|
+
return f"execute balance {state.active_venue}" + (f" {extra}" if extra else "")
|
|
360
|
+
|
|
361
|
+
if cmd == "positions" and state.active_venue:
|
|
362
|
+
return f"execute positions {state.active_venue}"
|
|
363
|
+
|
|
364
|
+
if cmd == "orders" and state.active_venue:
|
|
365
|
+
extra = " ".join(parts[1:])
|
|
366
|
+
return f"execute orders {state.active_venue}" + (f" {extra}" if extra else "")
|
|
367
|
+
|
|
368
|
+
if cmd == "order" and state.active_venue and state.active_symbol and len(parts) >= 4:
|
|
369
|
+
# order buy market 0.01 -> execute order <venue> <symbol> buy market 0.01
|
|
370
|
+
extra = " ".join(parts[1:])
|
|
371
|
+
return f"execute order {state.active_venue} {state.active_symbol} {extra}"
|
|
372
|
+
|
|
373
|
+
if cmd == "ohlcv" and state.active_venue and state.active_symbol:
|
|
374
|
+
extra = " ".join(parts[1:])
|
|
375
|
+
return f"markets ohlcv {state.active_venue} {state.active_symbol}" + (f" {extra}" if extra else "")
|
|
376
|
+
|
|
377
|
+
if cmd == "tickers" and state.active_venue:
|
|
378
|
+
extra = " ".join(parts[1:])
|
|
379
|
+
return f"markets tickers --exchange {state.active_venue}" + (f" {extra}" if extra else "")
|
|
380
|
+
|
|
381
|
+
# Bot shortcuts
|
|
382
|
+
if cmd in ("start", "stop", "pause", "resume", "logs", "events", "report", "rebalance") and state.active_bot_id and len(parts) == 1:
|
|
383
|
+
return f"bots {cmd} {state.active_bot_id}"
|
|
384
|
+
|
|
385
|
+
return line
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Strategy command group — run local trading bots from the CLI.
|
|
2
|
+
|
|
3
|
+
Unlike ``bots`` (cloud-managed), strategies are local Python modules
|
|
4
|
+
that run in-process. They are shipped as a separate, optionally-installed
|
|
5
|
+
plugin package (``fastbooks-clistrategies``) and discovered at runtime via
|
|
6
|
+
the ``fastbooks_cli.strategies`` entry point.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
fastbooks strategy list
|
|
10
|
+
fastbooks strategy info hl-momentum-hunter
|
|
11
|
+
fastbooks strategy run hl-momentum-hunter --private-key 0x... --account 0x...
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
|
|
22
|
+
from ..output import emit, emit_error, format_table
|
|
23
|
+
from ..strategies import REGISTRY
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@click.group()
|
|
27
|
+
def strategy():
|
|
28
|
+
"""Local trading strategies — run bots directly from the CLI.
|
|
29
|
+
|
|
30
|
+
Strategies are self-contained trading algorithms that run in-process.
|
|
31
|
+
Keys are passed via CLI flags or environment variables — never hardcoded.
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
fastbooks strategy list
|
|
35
|
+
fastbooks strategy info hl-momentum-hunter
|
|
36
|
+
fastbooks strategy run hl-momentum-hunter --private-key $HL_PRIVATE_KEY --account $HL_ACCOUNT
|
|
37
|
+
"""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── list ─────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
@strategy.command("list")
|
|
44
|
+
@click.pass_context
|
|
45
|
+
def strategy_list(ctx):
|
|
46
|
+
"""List all available strategies.
|
|
47
|
+
|
|
48
|
+
Example: fastbooks strategy list
|
|
49
|
+
"""
|
|
50
|
+
rows = []
|
|
51
|
+
for slug, meta in REGISTRY.items():
|
|
52
|
+
rows.append({
|
|
53
|
+
"slug": slug,
|
|
54
|
+
"name": meta.name,
|
|
55
|
+
"venue": meta.venue,
|
|
56
|
+
"description": meta.description,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
data = {"strategies": rows}
|
|
60
|
+
emit(ctx, data, lambda d: _format_strategy_list(d))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _format_strategy_list(data) -> str:
|
|
64
|
+
items = data.get("strategies", [])
|
|
65
|
+
if not items:
|
|
66
|
+
return " No strategies registered."
|
|
67
|
+
return format_table(
|
|
68
|
+
items,
|
|
69
|
+
["slug", "name", "venue", "description"],
|
|
70
|
+
["Slug", "Name", "Venue", "Description"],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ── info ─────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
@strategy.command("info")
|
|
77
|
+
@click.argument("slug")
|
|
78
|
+
@click.pass_context
|
|
79
|
+
def strategy_info(ctx, slug):
|
|
80
|
+
"""Show details for a strategy.
|
|
81
|
+
|
|
82
|
+
Example: fastbooks strategy info hl-momentum-hunter
|
|
83
|
+
"""
|
|
84
|
+
meta = REGISTRY.get(slug)
|
|
85
|
+
if not meta:
|
|
86
|
+
emit_error(ctx, f"Unknown strategy: {slug}")
|
|
87
|
+
ctx.exit(1)
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
data = {
|
|
91
|
+
"slug": meta.slug,
|
|
92
|
+
"name": meta.name,
|
|
93
|
+
"venue": meta.venue,
|
|
94
|
+
"description": meta.description,
|
|
95
|
+
"required_keys": meta.required_keys,
|
|
96
|
+
"optional_keys": meta.optional_keys,
|
|
97
|
+
"module": meta.module,
|
|
98
|
+
}
|
|
99
|
+
emit(ctx, data, lambda d: _format_strategy_info(d))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _format_strategy_info(data) -> str:
|
|
103
|
+
lines = [
|
|
104
|
+
f"\n {data['name']}",
|
|
105
|
+
f" Slug: {data['slug']}",
|
|
106
|
+
f" Venue: {data['venue']}",
|
|
107
|
+
f" Module: {data['module']}",
|
|
108
|
+
f" {data['description']}",
|
|
109
|
+
"",
|
|
110
|
+
" Required keys (via --private-key/--account or env vars):",
|
|
111
|
+
]
|
|
112
|
+
for k in data["required_keys"]:
|
|
113
|
+
lines.append(f" - {k}")
|
|
114
|
+
if data.get("optional_keys"):
|
|
115
|
+
lines.append(" Optional:")
|
|
116
|
+
for k in data["optional_keys"]:
|
|
117
|
+
lines.append(f" - {k}")
|
|
118
|
+
return "\n".join(lines)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ── run ──────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
@strategy.command("run")
|
|
124
|
+
@click.argument("slug")
|
|
125
|
+
@click.option(
|
|
126
|
+
"--private-key", envvar="HL_PRIVATE_KEY", default=None,
|
|
127
|
+
help="Wallet private key (or set HL_PRIVATE_KEY).",
|
|
128
|
+
)
|
|
129
|
+
@click.option(
|
|
130
|
+
"--account", envvar="HL_ACCOUNT_ADDRESS", default=None,
|
|
131
|
+
help="Main account address (or set HL_ACCOUNT_ADDRESS).",
|
|
132
|
+
)
|
|
133
|
+
@click.option("--testnet", is_flag=True, envvar="HL_TESTNET", help="Use testnet.")
|
|
134
|
+
@click.option(
|
|
135
|
+
"--config-json", type=str, default=None,
|
|
136
|
+
help="Override strategy params as JSON string.",
|
|
137
|
+
)
|
|
138
|
+
@click.option(
|
|
139
|
+
"--config-file", type=click.Path(exists=True), default=None,
|
|
140
|
+
help="Override strategy params from a JSON file.",
|
|
141
|
+
)
|
|
142
|
+
@click.option(
|
|
143
|
+
"--confirm/--no-confirm", default=True,
|
|
144
|
+
help="Require confirmation before live trading.",
|
|
145
|
+
)
|
|
146
|
+
@click.pass_context
|
|
147
|
+
def strategy_run(ctx, slug, private_key, account, testnet, config_json, config_file, confirm):
|
|
148
|
+
"""Run a strategy.
|
|
149
|
+
|
|
150
|
+
Keys are passed via flags or environment variables. Strategy-specific
|
|
151
|
+
parameters can be overridden with --config-json or --config-file.
|
|
152
|
+
|
|
153
|
+
Examples:
|
|
154
|
+
fastbooks strategy run hl-momentum-hunter \\
|
|
155
|
+
--private-key 0x... --account 0x...
|
|
156
|
+
|
|
157
|
+
# With env vars:
|
|
158
|
+
export HL_PRIVATE_KEY=0x...
|
|
159
|
+
export HL_ACCOUNT_ADDRESS=0x...
|
|
160
|
+
fastbooks strategy run hl-momentum-hunter
|
|
161
|
+
|
|
162
|
+
# Testnet + custom params:
|
|
163
|
+
fastbooks strategy run hl-momentum-hunter --testnet \\
|
|
164
|
+
--config-json '{"max_order_usd": 100, "max_leverage": 3}'
|
|
165
|
+
|
|
166
|
+
# From config file:
|
|
167
|
+
fastbooks strategy run hl-momentum-hunter \\
|
|
168
|
+
--config-file my_settings.json
|
|
169
|
+
"""
|
|
170
|
+
meta = REGISTRY.get(slug)
|
|
171
|
+
if not meta:
|
|
172
|
+
emit_error(ctx, f"Unknown strategy: {slug}")
|
|
173
|
+
if REGISTRY:
|
|
174
|
+
emit_error(ctx, f"Available: {', '.join(REGISTRY.keys())}")
|
|
175
|
+
else:
|
|
176
|
+
emit_error(ctx, "No strategies installed. Install the plugin package: pip install fastbooks-clistrategies")
|
|
177
|
+
ctx.exit(1)
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
# Build config dict
|
|
181
|
+
cfg: dict = {}
|
|
182
|
+
|
|
183
|
+
# Load config file first (lowest priority)
|
|
184
|
+
if config_file:
|
|
185
|
+
try:
|
|
186
|
+
with open(config_file) as f:
|
|
187
|
+
cfg.update(json.load(f))
|
|
188
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
189
|
+
emit_error(ctx, f"Config file error: {e}")
|
|
190
|
+
ctx.exit(1)
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
# Overlay JSON string (higher priority)
|
|
194
|
+
if config_json:
|
|
195
|
+
try:
|
|
196
|
+
cfg.update(json.loads(config_json))
|
|
197
|
+
except json.JSONDecodeError as e:
|
|
198
|
+
emit_error(ctx, f"Invalid JSON: {e}")
|
|
199
|
+
ctx.exit(1)
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
# Auth keys (highest priority — CLI flags / env vars)
|
|
203
|
+
if private_key:
|
|
204
|
+
cfg["private_key"] = private_key
|
|
205
|
+
if account:
|
|
206
|
+
cfg["account_address"] = account
|
|
207
|
+
cfg["use_testnet"] = testnet
|
|
208
|
+
|
|
209
|
+
# Validate required keys
|
|
210
|
+
missing = []
|
|
211
|
+
key_map = {
|
|
212
|
+
"HL_PRIVATE_KEY": "private_key",
|
|
213
|
+
"HL_ACCOUNT_ADDRESS": "account_address",
|
|
214
|
+
}
|
|
215
|
+
for env_name in meta.required_keys:
|
|
216
|
+
cfg_key = key_map.get(env_name, env_name.lower())
|
|
217
|
+
if not cfg.get(cfg_key):
|
|
218
|
+
# Try env var as fallback
|
|
219
|
+
env_val = os.environ.get(env_name)
|
|
220
|
+
if env_val:
|
|
221
|
+
cfg[cfg_key] = env_val
|
|
222
|
+
else:
|
|
223
|
+
missing.append(env_name)
|
|
224
|
+
|
|
225
|
+
if missing:
|
|
226
|
+
emit_error(ctx, f"Missing required keys: {', '.join(missing)}")
|
|
227
|
+
emit_error(ctx, "Pass via CLI flags or set as environment variables.")
|
|
228
|
+
ctx.exit(1)
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
# Safety confirmation for mainnet
|
|
232
|
+
if not testnet and confirm:
|
|
233
|
+
click.echo(f"\n Strategy: {meta.name}")
|
|
234
|
+
click.echo(f" Venue: {meta.venue}")
|
|
235
|
+
click.echo(f" Network: MAINNET")
|
|
236
|
+
click.echo(f" Account: {cfg.get('account_address', '?')}")
|
|
237
|
+
if not click.confirm("\n Start live trading?"):
|
|
238
|
+
click.echo(" Cancelled.")
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
# Run
|
|
242
|
+
import importlib
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
mod = importlib.import_module(meta.module)
|
|
246
|
+
except ImportError as e:
|
|
247
|
+
emit_error(ctx, f"Cannot import strategy: {e}")
|
|
248
|
+
emit_error(
|
|
249
|
+
ctx,
|
|
250
|
+
"Install the strategy plugin and its deps: pip install fastbooks-clistrategies",
|
|
251
|
+
)
|
|
252
|
+
ctx.exit(1)
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
click.echo(f"\n Starting {meta.name}...")
|
|
256
|
+
try:
|
|
257
|
+
asyncio.run(mod.run(cfg))
|
|
258
|
+
except KeyboardInterrupt:
|
|
259
|
+
click.echo("\n Strategy stopped.")
|