-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.py
More file actions
307 lines (256 loc) · 13.5 KB
/
Copy pathcli.py
File metadata and controls
307 lines (256 loc) · 13.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
import sys
import argparse
import logging
from typing import Optional
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.prompt import Prompt, Confirm
from bot import config
from bot.logging_config import setup_logging
from bot.enums import OrderSide, OrderType, ExecutionMode
from bot.models import OrderRequest, OrderResponse
from bot.client import BinanceClient
from bot.trading_service import TradingService
from bot.exceptions import TradingBotError, ValidationError
# Initialize Rich Console
console = Console()
def run_health_check(service: TradingService, verbose: bool):
"""Executes a diagnostic health check of the Binance Futures Testnet environment."""
console.print(Panel("[bold cyan]Executing Binance Futures Testnet Health Check[/bold cyan]", expand=False))
start_time = logging.getLogger("bot").handlers[0].stream if logging.getLogger("bot").handlers else None
try:
# 1. Ping the server
console.print("[yellow]Pinging Binance Futures Testnet server...[/yellow]")
service.client.request("GET", "/fapi/v1/ping")
console.print("[green][SUCCESS] Connectivity Ping successful.[/green]")
# 2. Get server time
server_time_resp = service.client.request("GET", "/fapi/v1/time")
server_time = server_time_resp.get("serverTime")
console.print(f"[green][SUCCESS] Server Time retrieved: {server_time}[/green]")
# 3. Test symbol caching and filters
console.print("[yellow]Fetching exchange filters...[/yellow]")
info = service.fetch_exchange_info(force_refresh=True)
symbols_count = len(info.get("symbols", []))
console.print(f"[green][SUCCESS] Local Exchange Cache generated. Cached {symbols_count} symbols successfully.[/green]")
# 4. Verify API credentials status
if service.client.mode == ExecutionMode.LIVE:
console.print("[yellow]Validating API Key permissions configuration...[/yellow]")
# Minimal signature testing: perform a query that requires signatures (e.g. exchange info does not, but getting account information does)
# For simplicity, we verify that config values are non-empty
if not config.API_KEY or not config.API_SECRET:
console.print("[red][ERROR] Credentials Missing: Live mode requires keys in .env[/red]")
else:
console.print(f"[green][SUCCESS] Credentials configured. Key: {config.mask_credential(config.API_KEY)}[/green]")
else:
console.print(f"[cyan][INFO] Health check completed in {service.client.mode.value} mode. (Keys not required)[/cyan]")
console.print(Panel("[bold green]System Health Check: PASS[/bold green]", expand=False))
except Exception as e:
console.print(Panel(f"[bold red]System Health Check: FAIL[/bold red]\n[red]Reason: {e}[/red]", expand=False))
def run_interactive_wizard(mode: ExecutionMode) -> OrderRequest:
"""Launches an interactive guided CLI wizard to configure order parameters."""
console.print(Panel(
f"[bold cyan]Interactive Guided Wizard[/bold cyan]\n"
f"[dim]Configure your order details. Execution Mode: {mode.value}[/dim]",
expand=False
))
# 1. Select Symbol
symbol = Prompt.ask("[bold yellow]Enter Trading Symbol[/bold yellow] (e.g., BTCUSDT, ETHUSDT)", default="BTCUSDT").upper().strip()
# 2. Select Side
side_str = Prompt.ask(
"[bold yellow]Select Order Side[/bold yellow]",
choices=[s.value for s in OrderSide],
default=OrderSide.BUY.value
)
side = OrderSide(side_str)
# 3. Select Type
type_str = Prompt.ask(
"[bold yellow]Select Order Type[/bold yellow]",
choices=[t.value for t in OrderType],
default=OrderType.MARKET.value
)
order_type = OrderType(type_str)
# 4. Quantity input validation
quantity = 0.0
while True:
qty_str = Prompt.ask("[bold yellow]Enter Quantity[/bold yellow]")
try:
quantity = float(qty_str)
if quantity <= 0:
console.print("[red]Quantity must be positive.[/red]")
continue
break
except ValueError:
console.print("[red]Invalid decimal number.[/red]")
# 5. Price input (LIMIT, STOP_LIMIT)
price: Optional[float] = None
if order_type in (OrderType.LIMIT, OrderType.STOP_LIMIT):
while True:
price_str = Prompt.ask("[bold yellow]Enter Limit Price[/bold yellow]")
try:
price = float(price_str)
if price <= 0:
console.print("[red]Price must be positive.[/red]")
continue
break
except ValueError:
console.print("[red]Invalid decimal number.[/red]")
# 6. Stop Price input (STOP_MARKET, STOP_LIMIT)
stop_price: Optional[float] = None
if order_type in (OrderType.STOP_MARKET, OrderType.STOP_LIMIT):
while True:
stop_str = Prompt.ask("[bold yellow]Enter Stop Trigger Price[/bold yellow]")
try:
stop_price = float(stop_str)
if stop_price <= 0:
console.print("[red]Stop Price must be positive.[/red]")
continue
break
except ValueError:
console.print("[red]Invalid decimal number.[/red]")
return OrderRequest(
symbol=symbol,
side=side,
order_type=order_type,
quantity=quantity,
price=price,
stop_price=stop_price
)
def print_order_summary(request: OrderRequest, mode: ExecutionMode):
"""Prints a structured aesthetic table summarizing the parsed OrderRequest parameters."""
table = Table(title="Order Parameter Summary", title_style="bold cyan", header_style="bold magenta")
table.add_column("Parameter", justify="left")
table.add_column("Value", justify="left")
table.add_row("Symbol", request.symbol)
table.add_row("Side", f"[bold {'green' if request.side == OrderSide.BUY else 'red'}]{request.side.value}[/]")
table.add_row("Type", request.order_type.value)
table.add_row("Quantity", f"{request.quantity}")
table.add_row("Limit Price", f"{request.price}" if request.price is not None else "[dim]N/A[/dim]")
table.add_row("Stop Trigger Price", f"{request.stop_price}" if request.stop_price is not None else "[dim]N/A[/dim]")
table.add_row("Execution Mode", f"[bold yellow]{mode.value}[/]")
console.print(table)
def print_order_result(response: OrderResponse, latency_ms: Optional[int] = None):
"""Prints a beautiful, color-coded tabular summary of the execution outcome."""
# Determine color indicator based on status
status = response.status.upper()
status_color = "green" if status in ("FILLED", "NEW", "DRY_RUN_VALIDATED") else "red"
table = Table(title="Order Placement Outcome", title_style=f"bold {status_color}", header_style="bold blue")
table.add_column("Field", justify="left")
table.add_column("Details", justify="left")
table.add_row("Order ID", str(response.order_id))
table.add_row("Symbol", response.symbol)
table.add_row("Status", f"[bold {status_color}]{response.status}[/]")
table.add_row("Side", response.side)
table.add_row("Execution Type", response.order_type)
table.add_row("Executed Qty", f"{response.executed_qty}")
table.add_row("Average Price", f"{response.avg_price:.2f}")
table.add_row("Client Order ID", response.client_order_id)
if latency_ms is not None:
table.add_row("Client RTT Latency", f"{latency_ms}ms")
console.print(table)
def main():
# Setup Rotating Logging
setup_logging()
# 1. Define Argument Parser
parser = argparse.ArgumentParser(
description="Resilient & Observable Binance Futures Trading Bot (USDT-M Testnet)",
formatter_class=argparse.RawTextHelpFormatter
)
parser.add_argument("command", nargs="?", choices=["place", "ping"], default="place",
help="Action to execute: 'place' orders (default) or 'ping' health check diagnostics.")
parser.add_argument("--symbol", type=str, help="Trading symbol (e.g. BTCUSDT)")
parser.add_argument("--side", type=str, choices=[s.value for s in OrderSide], help="Order Side (BUY/SELL)")
parser.add_argument("--type", type=str, choices=[t.value for t in OrderType], help="Order Type (MARKET/LIMIT/STOP_MARKET/STOP_LIMIT)")
parser.add_argument("--quantity", type=float, help="Order Quantity (e.g. 0.05)")
parser.add_argument("--price", type=float, help="Limit Price (required for LIMIT & STOP_LIMIT)")
parser.add_argument("--stop-price", type=float, help="Stop Trigger Price (required for STOP_MARKET & STOP_LIMIT)")
parser.add_argument("--mode", type=str, choices=[m.value for m in ExecutionMode],
help="Execution mode (LIVE, MOCK, DRY_RUN).\nIf not provided, falls back to DEFAULT_EXECUTION_MODE in config/env.")
parser.add_argument("--mock-failure", type=str, choices=["timeout", "rate_limit", "insufficient_balance", "bad_auth"],
help="Inject failure simulation inside MOCK execution mode.")
parser.add_argument("--verbose", action="store_true", help="Surfaces detailed request query strings, raw responses, and telemetries.")
# 2. Parse Arguments
args = parser.parse_args()
# 3. Resolve execution mode
mode_str = args.mode or config.DEFAULT_EXECUTION_MODE
try:
exec_mode = ExecutionMode(mode_str.upper())
except ValueError:
console.print(f"[red]Error: Invalid default execution mode: {mode_str}. Must be LIVE, MOCK, or DRY_RUN[/red]")
sys.exit(1)
# 4. Initialize Core Components
client = BinanceClient(mode=exec_mode, mock_failure=args.mock_failure)
service = TradingService(client=client)
# 5. Handle KeyboardInterrupt Gracefully
try:
# Route subcommands
if args.command == "ping":
run_health_check(service, args.verbose)
sys.exit(0)
# Place orders workflow
# Check if argument-based order parameters are present
has_cli_args = any([args.symbol, args.side, args.type, args.quantity is not None])
if has_cli_args:
# Reconstruct Order Request from CLI arguments with strict structure validation
if not args.symbol or not args.side or not args.type or args.quantity is None:
console.print(
"[bold red]Validation Error:[/bold red] "
"When passing parameters via CLI, you must provide all core requirements: "
"--symbol, --side, --type, and --quantity."
)
sys.exit(1)
req = OrderRequest(
symbol=args.symbol.upper().strip(),
side=OrderSide(args.side.upper()),
order_type=OrderType(args.type.upper()),
quantity=args.quantity,
price=args.price,
stop_price=args.stop_price
)
else:
# Launch interactive gui-style prompting wizard
req = run_interactive_wizard(exec_mode)
# Print structured parameters overview
print_order_summary(req, exec_mode)
# Safety Confirmation Prompts
# Ask for manual confirmation before dispatch, especially for LIVE mode.
is_confirmed = True
if exec_mode == ExecutionMode.LIVE:
is_confirmed = Confirm.ask(
"[bold red]WARNING: You are in LIVE execution mode. Are you sure you want to execute this real order?[/bold red]"
)
else:
is_confirmed = Confirm.ask(
"[bold yellow]Are you sure you want to proceed with order validation and execution?[/bold yellow]"
)
if not is_confirmed:
console.print("[yellow]Order placement cancelled gracefully.[/yellow]")
sys.exit(0)
# Dispatch through Trading Service
console.print("[yellow]Processing, validating, and placing order...[/yellow]")
import time as tm
start_rtt = tm.perf_counter()
response = service.place_order(req)
latency_ms = int((tm.perf_counter() - start_rtt) * 1000)
# Render stunning formatted outcome
console.print("\n[green]Order Placement Transaction Complete![/green]")
print_order_result(response, latency_ms)
# If --verbose, surface the full details
if args.verbose:
console.print("\n[bold cyan]Verbose API Payload Telemetry:[/bold cyan]")
import json as js
console.print_json(js.dumps(response.raw_payload))
except KeyboardInterrupt:
console.print("\n[bold yellow]KeyboardInterrupt received. Exiting bot gracefully without data corruption.[/bold yellow]")
sys.exit(0)
except ValidationError as e:
console.print(Panel(f"[bold red]Local Validation Reject[/bold red]\n[red]{e.message}[/red]", expand=False))
sys.exit(1)
except TradingBotError as e:
console.print(Panel(f"[bold red]API Execution Failure[/bold red]\n[red]{e.message}[/red]", expand=False))
sys.exit(1)
except Exception as e:
console.print(Panel(f"[bold red]Fatal Operational Crash[/bold red]\n[red]{e}[/red]", expand=False))
sys.exit(1)
if __name__ == "__main__":
main()