feat: human-friendly TUI output with --agent flag for JSON/compact
- Replace default JSON output with styled TUI tables and ANSI colors. - Add -a/--agent global flag: small payloads → JSON, large → pipe-delimited compact. - Update README to reflect new output behavior and remove JSON-first references. - Bump version to 2.0.2. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -67,3 +70,282 @@ def self_update() -> dict[str, Any]:
|
||||
"stdout": result.stdout.strip(),
|
||||
"stderr": result.stderr.strip(),
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TUI / Agent output helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_ANSI_RE = re.compile(r"\033\[[0-9;]*m")
|
||||
_BOLD = "\033[1m"
|
||||
_RESET = "\033[0m"
|
||||
_CYAN = "\033[36m"
|
||||
_GREEN = "\033[32m"
|
||||
_YELLOW = "\033[33m"
|
||||
_RED = "\033[31m"
|
||||
_DIM = "\033[2m"
|
||||
|
||||
|
||||
def _strip_ansi(text: str) -> str:
|
||||
return _ANSI_RE.sub("", text)
|
||||
|
||||
|
||||
def _color(text: str, color: str) -> str:
|
||||
return f"{color}{text}{_RESET}"
|
||||
|
||||
|
||||
def _cell_width(text: str) -> int:
|
||||
return len(_strip_ansi(text))
|
||||
|
||||
|
||||
def _pad(text: str, width: int, align: str = "left") -> str:
|
||||
pad = width - _cell_width(text)
|
||||
if align == "right":
|
||||
return " " * pad + text
|
||||
return text + " " * pad
|
||||
|
||||
|
||||
def _fmt_number(value: Any) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
if isinstance(value, (int, float)):
|
||||
s = f"{value:,.4f}"
|
||||
s = s.rstrip("0").rstrip(".")
|
||||
return s
|
||||
return str(value)
|
||||
|
||||
|
||||
def _is_large_dataset(payload: Any, threshold: int = 8) -> bool:
|
||||
if isinstance(payload, dict):
|
||||
for value in payload.values():
|
||||
if isinstance(value, list) and len(value) > threshold:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _print_compact(payload: dict[str, Any]) -> None:
|
||||
target_key = None
|
||||
target_rows: list[Any] = []
|
||||
for key, value in payload.items():
|
||||
if isinstance(value, list) and len(value) > len(target_rows):
|
||||
target_key = key
|
||||
target_rows = value
|
||||
|
||||
if target_rows and isinstance(target_rows[0], dict):
|
||||
headers = list(target_rows[0].keys())
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output, delimiter="|", lineterminator="\n")
|
||||
writer.writerow(headers)
|
||||
for row in target_rows:
|
||||
writer.writerow([str(row.get(h, "")) for h in headers])
|
||||
print(f"mode=compact|source={target_key}")
|
||||
print(output.getvalue().strip())
|
||||
else:
|
||||
for key, value in payload.items():
|
||||
print(f"{key}={value}")
|
||||
|
||||
|
||||
def _h_line(widths: list[int], left: str, mid: str, right: str) -> str:
|
||||
parts = ["─" * (w + 2) for w in widths]
|
||||
return left + mid.join(parts) + right
|
||||
|
||||
|
||||
def _print_box_table(
|
||||
title: str,
|
||||
headers: list[str],
|
||||
rows: list[list[str]],
|
||||
aligns: list[str] | None = None,
|
||||
) -> None:
|
||||
if not rows:
|
||||
print(f"{_BOLD}{_CYAN}{title}{_RESET}")
|
||||
print(" (empty)")
|
||||
return
|
||||
|
||||
aligns = aligns or ["left"] * len(headers)
|
||||
col_widths = [_cell_width(h) for h in headers]
|
||||
for row in rows:
|
||||
for i, cell in enumerate(row):
|
||||
col_widths[i] = max(col_widths[i], _cell_width(cell))
|
||||
|
||||
if title:
|
||||
print(f"{_BOLD}{_CYAN}{title}{_RESET}")
|
||||
print(_h_line(col_widths, "┌", "┬", "┐"))
|
||||
header_cells = [_pad(headers[i], col_widths[i], aligns[i]) for i in range(len(headers))]
|
||||
print("│ " + " │ ".join(header_cells) + " │")
|
||||
print(_h_line(col_widths, "├", "┼", "┤"))
|
||||
for row in rows:
|
||||
cells = [_pad(row[i], col_widths[i], aligns[i]) for i in range(len(row))]
|
||||
print("│ " + " │ ".join(cells) + " │")
|
||||
print(_h_line(col_widths, "└", "┴", "┘"))
|
||||
|
||||
|
||||
def _render_tui(payload: Any) -> None:
|
||||
if not isinstance(payload, dict):
|
||||
print(str(payload))
|
||||
return
|
||||
|
||||
if "overview" in payload:
|
||||
overview = payload.get("overview", {})
|
||||
print(f"\n{_BOLD}{_CYAN} ACCOUNT OVERVIEW {_RESET}")
|
||||
print(f" Total Equity: {_GREEN}{_fmt_number(overview.get('total_equity_usdt', 0))} USDT{_RESET}")
|
||||
print(f" Spot Equity: {_fmt_number(overview.get('spot_equity_usdt', 0))} USDT")
|
||||
print(f" Futures Equity: {_fmt_number(overview.get('futures_equity_usdt', 0))} USDT")
|
||||
print(f" Spot Assets: {_fmt_number(overview.get('spot_asset_count', 0))}")
|
||||
print(f" Futures Positions: {_fmt_number(overview.get('futures_position_count', 0))}")
|
||||
if payload.get("balances"):
|
||||
print()
|
||||
_render_tui({"balances": payload["balances"]})
|
||||
if payload.get("positions"):
|
||||
print()
|
||||
_render_tui({"positions": payload["positions"]})
|
||||
return
|
||||
|
||||
if "balances" in payload:
|
||||
rows = payload["balances"]
|
||||
table_rows: list[list[str]] = []
|
||||
for r in rows:
|
||||
table_rows.append(
|
||||
[
|
||||
r.get("market_type", ""),
|
||||
r.get("asset", ""),
|
||||
_fmt_number(r.get("free", 0)),
|
||||
_fmt_number(r.get("locked", 0)),
|
||||
_fmt_number(r.get("total", 0)),
|
||||
_fmt_number(r.get("notional_usdt", 0)),
|
||||
]
|
||||
)
|
||||
_print_box_table(
|
||||
"BALANCES",
|
||||
["Market", "Asset", "Free", "Locked", "Total", "Notional (USDT)"],
|
||||
table_rows,
|
||||
aligns=["left", "left", "right", "right", "right", "right"],
|
||||
)
|
||||
return
|
||||
|
||||
if "positions" in payload:
|
||||
rows = payload["positions"]
|
||||
table_rows = []
|
||||
for r in rows:
|
||||
entry = _fmt_number(r.get("entry_price")) if r.get("entry_price") is not None else "—"
|
||||
pnl = _fmt_number(r.get("unrealized_pnl")) if r.get("unrealized_pnl") is not None else "—"
|
||||
table_rows.append(
|
||||
[
|
||||
r.get("market_type", ""),
|
||||
r.get("symbol", ""),
|
||||
r.get("side", ""),
|
||||
_fmt_number(r.get("quantity", 0)),
|
||||
entry,
|
||||
_fmt_number(r.get("mark_price", 0)),
|
||||
_fmt_number(r.get("notional_usdt", 0)),
|
||||
pnl,
|
||||
]
|
||||
)
|
||||
_print_box_table(
|
||||
"POSITIONS",
|
||||
["Market", "Symbol", "Side", "Qty", "Entry", "Mark", "Notional", "PnL"],
|
||||
table_rows,
|
||||
aligns=["left", "left", "left", "right", "right", "right", "right", "right"],
|
||||
)
|
||||
return
|
||||
|
||||
if "tickers" in payload:
|
||||
rows = payload["tickers"]
|
||||
table_rows = []
|
||||
for r in rows:
|
||||
pct = r.get("price_change_pct", 0)
|
||||
pct_str = _color(f"{pct:+.2f}%", _GREEN if pct >= 0 else _RED)
|
||||
table_rows.append(
|
||||
[
|
||||
r.get("symbol", ""),
|
||||
_fmt_number(r.get("last_price", 0)),
|
||||
pct_str,
|
||||
_fmt_number(r.get("quote_volume", 0)),
|
||||
]
|
||||
)
|
||||
_print_box_table(
|
||||
"24H TICKERS",
|
||||
["Symbol", "Last Price", "Change %", "Quote Volume"],
|
||||
table_rows,
|
||||
aligns=["left", "right", "right", "right"],
|
||||
)
|
||||
return
|
||||
|
||||
if "klines" in payload:
|
||||
rows = payload["klines"]
|
||||
print(f"\n{_BOLD}{_CYAN} KLINES {_RESET} interval={payload.get('interval')} limit={payload.get('limit')} count={len(rows)}")
|
||||
display_rows = rows[:10]
|
||||
table_rows = []
|
||||
for r in display_rows:
|
||||
table_rows.append(
|
||||
[
|
||||
r.get("symbol", ""),
|
||||
str(r.get("open_time", ""))[:10],
|
||||
_fmt_number(r.get("open", 0)),
|
||||
_fmt_number(r.get("high", 0)),
|
||||
_fmt_number(r.get("low", 0)),
|
||||
_fmt_number(r.get("close", 0)),
|
||||
_fmt_number(r.get("volume", 0)),
|
||||
]
|
||||
)
|
||||
_print_box_table(
|
||||
"",
|
||||
["Symbol", "Time", "Open", "High", "Low", "Close", "Vol"],
|
||||
table_rows,
|
||||
aligns=["left", "left", "right", "right", "right", "right", "right"],
|
||||
)
|
||||
if len(rows) > 10:
|
||||
print(f" {_DIM}... and {len(rows) - 10} more rows{_RESET}")
|
||||
return
|
||||
|
||||
if "trade" in payload:
|
||||
t = payload["trade"]
|
||||
status = t.get("status", "UNKNOWN")
|
||||
status_color = _GREEN if status == "FILLED" else _YELLOW if status == "DRY_RUN" else _CYAN
|
||||
print(f"\n{_BOLD}{_CYAN} TRADE RESULT {_RESET}")
|
||||
print(f" Market: {t.get('market_type', '').upper()}")
|
||||
print(f" Symbol: {t.get('symbol', '')}")
|
||||
print(f" Side: {t.get('side', '')}")
|
||||
print(f" Type: {t.get('order_type', '')}")
|
||||
print(f" Status: {_color(status, status_color)}")
|
||||
print(f" Dry Run: {_fmt_number(t.get('dry_run', False))}")
|
||||
return
|
||||
|
||||
if "recommendations" in payload:
|
||||
rows = payload["recommendations"]
|
||||
print(f"\n{_BOLD}{_CYAN} RECOMMENDATIONS {_RESET} count={len(rows)}")
|
||||
for i, r in enumerate(rows, 1):
|
||||
score = r.get("score", 0)
|
||||
action = r.get("action", "")
|
||||
action_color = _GREEN if action == "add" else _YELLOW if action == "hold" else _RED if action == "exit" else _CYAN
|
||||
print(f" {i}. {_BOLD}{r.get('symbol', '')}{_RESET} action={_color(action, action_color)} score={score:.4f}")
|
||||
for reason in r.get("reasons", []):
|
||||
print(f" · {reason}")
|
||||
metrics = r.get("metrics", {})
|
||||
if metrics:
|
||||
metric_str = " ".join(f"{k}={v}" for k, v in metrics.items())
|
||||
print(f" {_DIM}{metric_str}{_RESET}")
|
||||
return
|
||||
|
||||
# Generic fallback for single-list payloads
|
||||
if len(payload) == 1:
|
||||
key, value = next(iter(payload.items()))
|
||||
if isinstance(value, list) and value and isinstance(value[0], dict):
|
||||
_render_tui({key: value})
|
||||
return
|
||||
|
||||
# Simple key-value fallback
|
||||
print(f"\n{_BOLD}{_CYAN} RESULT {_RESET}")
|
||||
for key, value in payload.items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
|
||||
def print_output(payload: Any, *, agent: bool = False) -> None:
|
||||
if agent:
|
||||
if _is_large_dataset(payload):
|
||||
_print_compact(payload)
|
||||
else:
|
||||
print_json(payload)
|
||||
else:
|
||||
_render_tui(payload)
|
||||
|
||||
Reference in New Issue
Block a user