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:
2026-04-16 18:36:23 +08:00
parent b78845eb43
commit 9395978440
5 changed files with 366 additions and 56 deletions

View File

@@ -10,9 +10,21 @@ from . import __version__
from .binance.spot_client import SpotBinanceClient
from .binance.um_futures_client import UMFuturesClient
from .config import ensure_init_files, get_binance_credentials, load_config
from .runtime import get_runtime_paths, print_json, self_update
from .runtime import get_runtime_paths, print_output, self_update
from .services import account_service, market_service, opportunity_service, trade_service
EPILOG = """\
examples:
coinhunter init
coinhunter account overview -sf
coinhunter market tickers BTCUSDT ETHUSDT
coinhunter market klines BTCUSDT -i 1h -l 50
coinhunter trade spot buy BTCUSDT -q 100 -d
coinhunter trade futures sell BTCUSDT -q 0.01 -r
coinhunter opportunity scan -s BTCUSDT ETHUSDT
coinhunter update
"""
def _load_spot_client(config: dict[str, Any], *, client: Any | None = None) -> SpotBinanceClient:
credentials = get_binance_credentials()
@@ -45,62 +57,68 @@ def _resolve_market_flags(args: argparse.Namespace) -> tuple[bool, bool]:
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="coinhunter", description="CoinHunter V2 Binance-first trading CLI")
parser.add_argument("--version", action="version", version=__version__)
parser = argparse.ArgumentParser(
prog="coinhunter",
description="CoinHunter V2 Binance-first trading CLI",
epilog=EPILOG,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("-v", "--version", action="version", version=__version__)
parser.add_argument("-a", "--agent", action="store_true", help="Output in agent-friendly format (JSON or compact)")
subparsers = parser.add_subparsers(dest="command")
init_parser = subparsers.add_parser("init", help="Generate config.toml, .env, and log directory")
init_parser.add_argument("--force", action="store_true")
init_parser.add_argument("-f", "--force", action="store_true", help="Overwrite existing files")
account_parser = subparsers.add_parser("account", help="Account overview, balances, and positions")
account_subparsers = account_parser.add_subparsers(dest="account_command")
for name in ("overview", "balances", "positions"):
sub = account_subparsers.add_parser(name)
sub.add_argument("--spot", action="store_true")
sub.add_argument("--futures", action="store_true")
sub.add_argument("-s", "--spot", action="store_true", help="Include spot market")
sub.add_argument("-f", "--futures", action="store_true", help="Include futures market")
market_parser = subparsers.add_parser("market", help="Batch market queries")
market_subparsers = market_parser.add_subparsers(dest="market_command")
tickers_parser = market_subparsers.add_parser("tickers")
tickers_parser.add_argument("symbols", nargs="+")
klines_parser = market_subparsers.add_parser("klines")
klines_parser.add_argument("symbols", nargs="+")
klines_parser.add_argument("--interval", default="1h")
klines_parser.add_argument("--limit", type=int, default=100)
tickers_parser = market_subparsers.add_parser("tickers", help="Fetch 24h ticker data")
tickers_parser.add_argument("symbols", nargs="+", metavar="SYM", help="Symbols to query (e.g. BTCUSDT ETH/USDT)")
klines_parser = market_subparsers.add_parser("klines", help="Fetch OHLCV klines")
klines_parser.add_argument("symbols", nargs="+", metavar="SYM", help="Symbols to query")
klines_parser.add_argument("-i", "--interval", default="1h", help="Kline interval (default: 1h)")
klines_parser.add_argument("-l", "--limit", type=int, default=100, help="Number of candles (default: 100)")
trade_parser = subparsers.add_parser("trade", help="Spot and futures trade execution")
trade_subparsers = trade_parser.add_subparsers(dest="trade_market")
spot_parser = trade_subparsers.add_parser("spot")
spot_parser = trade_subparsers.add_parser("spot", help="Spot market orders")
spot_subparsers = spot_parser.add_subparsers(dest="trade_action")
for side in ("buy", "sell"):
sub = spot_subparsers.add_parser(side)
sub.add_argument("symbol")
sub.add_argument("--qty", type=float)
sub.add_argument("--quote", type=float)
sub.add_argument("--type", choices=["market", "limit"], default="market")
sub.add_argument("--price", type=float)
sub.add_argument("--dry-run", action="store_true")
sub.add_argument("symbol", metavar="SYM", help="Trading pair (e.g. BTCUSDT)")
sub.add_argument("-q", "--qty", type=float, help="Base asset quantity")
sub.add_argument("-Q", "--quote", type=float, help="Quote asset amount (buy market only)")
sub.add_argument("-t", "--type", choices=["market", "limit"], default="market", help="Order type (default: market)")
sub.add_argument("-p", "--price", type=float, help="Limit price")
sub.add_argument("-d", "--dry-run", action="store_true", help="Simulate without sending")
futures_parser = trade_subparsers.add_parser("futures")
futures_parser = trade_subparsers.add_parser("futures", help="USDT-M futures orders")
futures_subparsers = futures_parser.add_subparsers(dest="trade_action")
for side in ("buy", "sell"):
sub = futures_subparsers.add_parser(side)
sub.add_argument("symbol")
sub.add_argument("--qty", type=float, required=True)
sub.add_argument("--type", choices=["market", "limit"], default="market")
sub.add_argument("--price", type=float)
sub.add_argument("--reduce-only", action="store_true")
sub.add_argument("--dry-run", action="store_true")
close_parser = futures_subparsers.add_parser("close")
close_parser.add_argument("symbol")
close_parser.add_argument("--dry-run", action="store_true")
sub.add_argument("symbol", metavar="SYM", help="Trading pair (e.g. BTCUSDT)")
sub.add_argument("-q", "--qty", type=float, required=True, help="Contract quantity")
sub.add_argument("-t", "--type", choices=["market", "limit"], default="market", help="Order type (default: market)")
sub.add_argument("-p", "--price", type=float, help="Limit price")
sub.add_argument("-r", "--reduce-only", action="store_true", help="Only reduce position")
sub.add_argument("-d", "--dry-run", action="store_true", help="Simulate without sending")
close_parser = futures_subparsers.add_parser("close", help="Close position at market price")
close_parser.add_argument("symbol", metavar="SYM", help="Trading pair to close")
close_parser.add_argument("-d", "--dry-run", action="store_true", help="Simulate without sending")
opportunity_parser = subparsers.add_parser("opportunity", help="Portfolio analysis and market scanning")
opportunity_subparsers = opportunity_parser.add_subparsers(dest="opportunity_command")
opportunity_subparsers.add_parser("portfolio")
scan_parser = opportunity_subparsers.add_parser("scan")
scan_parser.add_argument("--symbols", nargs="*")
opportunity_subparsers.add_parser("portfolio", help="Score current holdings")
scan_parser = opportunity_subparsers.add_parser("scan", help="Scan market for opportunities")
scan_parser.add_argument("-s", "--symbols", nargs="*", metavar="SYM", help="Restrict scan to specific symbols")
subparsers.add_parser("update", help="Upgrade coinhunter to the latest version")
@@ -116,7 +134,7 @@ def main(argv: list[str] | None = None) -> int:
return 0
if args.command == "init":
print_json(ensure_init_files(get_runtime_paths(), force=args.force))
print_output(ensure_init_files(get_runtime_paths(), force=args.force), agent=args.agent)
return 0
config = load_config()
@@ -126,36 +144,39 @@ def main(argv: list[str] | None = None) -> int:
spot_client = _load_spot_client(config) if include_spot else None
futures_client = _load_futures_client(config) if include_futures else None
if args.account_command == "overview":
print_json(
print_output(
account_service.get_overview(
config,
include_spot=include_spot,
include_futures=include_futures,
spot_client=spot_client,
futures_client=futures_client,
)
),
agent=args.agent,
)
return 0
if args.account_command == "balances":
print_json(
print_output(
account_service.get_balances(
config,
include_spot=include_spot,
include_futures=include_futures,
spot_client=spot_client,
futures_client=futures_client,
)
),
agent=args.agent,
)
return 0
if args.account_command == "positions":
print_json(
print_output(
account_service.get_positions(
config,
include_spot=include_spot,
include_futures=include_futures,
spot_client=spot_client,
futures_client=futures_client,
)
),
agent=args.agent,
)
return 0
parser.error("account requires one of: overview, balances, positions")
@@ -163,17 +184,18 @@ def main(argv: list[str] | None = None) -> int:
if args.command == "market":
spot_client = _load_spot_client(config)
if args.market_command == "tickers":
print_json(market_service.get_tickers(config, args.symbols, spot_client=spot_client))
print_output(market_service.get_tickers(config, args.symbols, spot_client=spot_client), agent=args.agent)
return 0
if args.market_command == "klines":
print_json(
print_output(
market_service.get_klines(
config,
args.symbols,
interval=args.interval,
limit=args.limit,
spot_client=spot_client,
)
),
agent=args.agent,
)
return 0
parser.error("market requires one of: tickers, klines")
@@ -181,7 +203,7 @@ def main(argv: list[str] | None = None) -> int:
if args.command == "trade":
if args.trade_market == "spot":
spot_client = _load_spot_client(config)
print_json(
print_output(
trade_service.execute_spot_trade(
config,
side=args.trade_action,
@@ -192,22 +214,24 @@ def main(argv: list[str] | None = None) -> int:
price=args.price,
dry_run=True if args.dry_run else None,
spot_client=spot_client,
)
),
agent=args.agent,
)
return 0
if args.trade_market == "futures":
futures_client = _load_futures_client(config)
if args.trade_action == "close":
print_json(
print_output(
trade_service.close_futures_position(
config,
symbol=args.symbol,
dry_run=True if args.dry_run else None,
futures_client=futures_client,
)
),
agent=args.agent,
)
return 0
print_json(
print_output(
trade_service.execute_futures_trade(
config,
side=args.trade_action,
@@ -218,7 +242,8 @@ def main(argv: list[str] | None = None) -> int:
reduce_only=args.reduce_only,
dry_run=True if args.dry_run else None,
futures_client=futures_client,
)
),
agent=args.agent,
)
return 0
parser.error("trade requires `spot` or `futures`")
@@ -226,15 +251,15 @@ def main(argv: list[str] | None = None) -> int:
if args.command == "opportunity":
spot_client = _load_spot_client(config)
if args.opportunity_command == "portfolio":
print_json(opportunity_service.analyze_portfolio(config, spot_client=spot_client))
print_output(opportunity_service.analyze_portfolio(config, spot_client=spot_client), agent=args.agent)
return 0
if args.opportunity_command == "scan":
print_json(opportunity_service.scan_opportunities(config, spot_client=spot_client, symbols=args.symbols))
print_output(opportunity_service.scan_opportunities(config, spot_client=spot_client, symbols=args.symbols), agent=args.agent)
return 0
parser.error("opportunity requires `portfolio` or `scan`")
if args.command == "update":
print_json(self_update())
print_output(self_update(), agent=args.agent)
return 0
parser.error(f"Unsupported command {args.command}")