From e6274d3a000542f2dbb992de7e40f3348b7b7a5b Mon Sep 17 00:00:00 2001 From: Tacit Lab Date: Wed, 15 Apr 2026 20:28:24 +0800 Subject: [PATCH] feat: polish exec cli ergonomics and output --- README.md | 65 ++++++---- src/coinhunter/cli.py | 78 ++++++++++-- .../services/smart_executor_parser.py | 118 +++++++++++++----- src/coinhunter/services/trade_execution.py | 11 +- 4 files changed, 207 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 1922dce..2e6985f 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,8 @@ coinhunter --help coinhunter --version ``` +Short aliases are available for the most common commands. The original long-form command names remain supported for backward compatibility. + ## Quickstart Initialize user state: @@ -90,13 +92,13 @@ Inspect runtime wiring: ```bash coinhunter paths -coinhunter doctor +coinhunter diag ``` Validate exchange credentials: ```bash -coinhunter check-api +coinhunter api-check ``` Run precheck / gate plumbing: @@ -105,30 +107,48 @@ Run precheck / gate plumbing: coinhunter precheck coinhunter precheck --mark-run-requested "external-gate queued cron run" coinhunter precheck --ack "analysis finished" +coinhunter gate ``` Inspect balances or execute trading actions: ```bash -coinhunter smart-executor balances -coinhunter smart-executor status -coinhunter smart-executor hold -coinhunter smart-executor buy ENJUSDT 50 -coinhunter smart-executor sell-all ENJUSDT +coinhunter exec bal +coinhunter exec overview +coinhunter exec hold +coinhunter exec buy ENJUSDT 50 +coinhunter exec flat ENJUSDT +coinhunter exec rotate PEPEUSDT ETHUSDT ``` +Preferred `exec` verbs are `bal`, `overview`, `hold`, `buy`, `flat`, and `rotate`. +Legacy forms remain supported for backward compatibility: `balances`, `balance`, `acct`, `status`, `sell-all`, `sell_all`, and `rebalance`. + Generate review data: ```bash -coinhunter review-context 12 -coinhunter review-engine 12 +coinhunter review 12 +coinhunter recap 12 ``` Probe external market data: ```bash +coinhunter probe bybit-ticker BTCUSDT +coinhunter probe bybit-klines BTCUSDT 60 20 +``` + +Long-form equivalents still work: + +```bash +coinhunter doctor +coinhunter check-api +coinhunter smart-executor bal +coinhunter review-context 12 +coinhunter review-engine 12 coinhunter market-probe bybit-ticker BTCUSDT -coinhunter market-probe bybit-klines BTCUSDT 60 20 +coinhunter external-gate +coinhunter rotate-external-gate-log ``` ## Runtime model @@ -159,34 +179,37 @@ Credential loading: ### Diagnostics ```bash -coinhunter doctor +coinhunter diag coinhunter paths -coinhunter check-api +coinhunter api-check ``` ### Trading and execution ```bash -coinhunter smart-executor balances -coinhunter smart-executor status -coinhunter smart-executor hold -coinhunter smart-executor rebalance FROMUSDT TOUSDT +coinhunter exec bal +coinhunter exec overview +coinhunter exec hold +coinhunter exec buy ENJUSDT 50 +coinhunter exec flat ENJUSDT +coinhunter exec rotate FROMUSDT TOUSDT ``` ### Precheck and orchestration ```bash coinhunter precheck -coinhunter external-gate -coinhunter rotate-external-gate-log +coinhunter gate +coinhunter rotate-gate-log +coinhunter rotate-log ``` ### Review and market research ```bash -coinhunter review-context 12 -coinhunter review-engine 12 -coinhunter market-probe bybit-ticker BTCUSDT +coinhunter review 12 +coinhunter recap 12 +coinhunter probe bybit-ticker BTCUSDT ``` ## Development notes diff --git a/src/coinhunter/cli.py b/src/coinhunter/cli.py index 451ba03..9c995fa 100755 --- a/src/coinhunter/cli.py +++ b/src/coinhunter/cli.py @@ -23,6 +23,41 @@ MODULE_MAP = { "auto-trader": "auto_trader", } +ALIASES = { + "api-check": "check-api", + "diag": "doctor", + "gate": "external-gate", + "probe": "market-probe", + "review": "review-context", + "recap": "review-engine", + "rotate-gate-log": "rotate-external-gate-log", + "rotate-log": "rotate-external-gate-log", + "exec": "smart-executor", +} + +COMMAND_HELP = [ + ("api-check", "check-api", "Validate exchange/API connectivity"), + ("diag", "doctor", "Inspect runtime wiring and diagnostics"), + ("gate", "external-gate", "Run external gate orchestration"), + ("init", None, "Initialize user runtime state"), + ("paths", None, "Print runtime path resolution"), + ("precheck", None, "Run precheck workflow"), + ("probe", "market-probe", "Query external market data"), + ("review", "review-context", "Generate review context"), + ("recap", "review-engine", "Generate review recap/engine output"), + ("rotate-gate-log, rotate-log", "rotate-external-gate-log", "Rotate external gate logs"), + ("exec", "smart-executor", "Trading and execution actions"), + ("auto-trader", None, "Auto trader entrypoint"), +] + + +def _command_listing() -> str: + lines = [] + for names, canonical, summary in COMMAND_HELP: + label = names if canonical is None else f"{names} (alias for {canonical})" + lines.append(f" {label:<45} {summary}") + return "\n".join(lines) + class VersionAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): @@ -36,34 +71,46 @@ def build_parser() -> argparse.ArgumentParser: description="CoinHunter trading operations CLI", formatter_class=argparse.RawTextHelpFormatter, epilog=( + "Commands:\n" + f"{_command_listing()}\n\n" "Examples:\n" - " coinhunter doctor\n" + " coinhunter diag\n" " coinhunter paths\n" - " coinhunter check-api\n" - " coinhunter smart-executor balances\n" - " coinhunter smart-executor hold\n" - " coinhunter smart-executor --analysis '...' --reasoning '...' buy ENJUSDT 50\n" + " coinhunter api-check\n" + " coinhunter exec bal\n" + " coinhunter exec overview\n" + " coinhunter exec hold\n" + " coinhunter exec --analysis '...' --reasoning '...' buy ENJUSDT 50\n" " coinhunter precheck\n" " coinhunter precheck --ack '分析完成:HOLD'\n" - " coinhunter external-gate\n" - " coinhunter review-context 12\n" - " coinhunter market-probe bybit-ticker BTCUSDT\n" + " coinhunter gate\n" + " coinhunter review 12\n" + " coinhunter recap 12\n" + " coinhunter probe bybit-ticker BTCUSDT\n" " coinhunter init\n" + "\n" + "Preferred exec verbs are bal, overview, hold, buy, flat, and rotate.\n" + "Legacy command names remain supported for backward compatibility.\n" ), ) parser.add_argument("--version", nargs=0, action=VersionAction, help="Print installed version and exit") - parser.add_argument("command", nargs="?", choices=sorted(MODULE_MAP.keys()), help="CoinHunter command to run") + parser.add_argument( + "command", + nargs="?", + metavar="COMMAND", + help="Command to run. Use --help to see canonical names and short aliases.", + ) parser.add_argument("args", nargs=argparse.REMAINDER) return parser -def run_python_module(module_name: str, argv: list[str]) -> int: +def run_python_module(module_name: str, argv: list[str], display_name: str) -> int: module = importlib.import_module(f".{module_name}", package="coinhunter") if not hasattr(module, "main"): raise RuntimeError(f"Module {module_name} has no main()") old_argv = sys.argv[:] try: - sys.argv = [f"coinhunter {module_name}", *argv] + sys.argv = [display_name, *argv] result = module.main() return int(result) if isinstance(result, int) else 0 except SystemExit as exc: @@ -78,11 +125,16 @@ def main() -> int: if not parsed.command: parser.print_help() return 0 - module_name = MODULE_MAP[parsed.command] + command = ALIASES.get(parsed.command, parsed.command) + if command not in MODULE_MAP: + parser.error( + f"invalid command: {parsed.command!r}. Use `coinhunter --help` to see supported commands and aliases." + ) + module_name = MODULE_MAP[command] argv = list(parsed.args) if argv and argv[0] == "--": argv = argv[1:] - return run_python_module(module_name, argv) + return run_python_module(module_name, argv, f"coinhunter {parsed.command}") if __name__ == "__main__": diff --git a/src/coinhunter/services/smart_executor_parser.py b/src/coinhunter/services/smart_executor_parser.py index 9821751..4d44f66 100644 --- a/src/coinhunter/services/smart_executor_parser.py +++ b/src/coinhunter/services/smart_executor_parser.py @@ -2,47 +2,101 @@ import argparse +COMMAND_CANONICAL = { + "bal": "balances", + "balances": "balances", + "balance": "balances", + "acct": "status", + "overview": "status", + "status": "status", + "hold": "hold", + "buy": "buy", + "flat": "sell-all", + "sell-all": "sell-all", + "sell_all": "sell-all", + "rotate": "rebalance", + "rebalance": "rebalance", +} + + +def add_shared_options(parser: argparse.ArgumentParser) -> None: + parser.add_argument("--decision-id", help="Override the decision ID; otherwise one is derived automatically") + parser.add_argument("--analysis", help="Persist analysis text with the execution record") + parser.add_argument("--reasoning", help="Persist reasoning text with the execution record") + parser.add_argument("--dry-run", action="store_true", help="Simulate the command without placing live orders") + + def build_parser() -> argparse.ArgumentParser: + shared = argparse.ArgumentParser(add_help=False) + add_shared_options(shared) parser = argparse.ArgumentParser( - description="Coin Hunter Smart Executor", + prog="coinhunter exec", + description="Professional execution console for account inspection and spot trading workflows", formatter_class=argparse.RawTextHelpFormatter, + parents=[shared], epilog=( - "示例:\n" - " python smart_executor.py hold\n" - " python smart_executor.py sell-all ETHUSDT\n" - " python smart_executor.py buy ENJUSDT 100\n" - " python smart_executor.py rebalance PEPEUSDT ETHUSDT\n" - " python smart_executor.py balances\n\n" - "兼容旧调用:\n" - " python smart_executor.py HOLD\n" - " python smart_executor.py --decision HOLD --dry-run\n" + "Preferred verbs:\n" + " bal Print live balances as stable JSON\n" + " overview Print balances, positions, and market snapshot as stable JSON\n" + " hold Record a hold decision without trading\n" + " buy SYMBOL USDT Buy a symbol using a USDT notional amount\n" + " flat SYMBOL Exit an entire symbol position\n" + " rotate FROM TO Rotate exposure from one symbol into another\n\n" + "Examples:\n" + " coinhunter exec bal\n" + " coinhunter exec overview\n" + " coinhunter exec hold\n" + " coinhunter exec buy ENJUSDT 100\n" + " coinhunter exec flat ENJUSDT --dry-run\n" + " coinhunter exec rotate PEPEUSDT ETHUSDT\n\n" + "Legacy forms remain supported for backward compatibility:\n" + " balances, balance -> bal\n" + " acct, status -> overview\n" + " sell-all, sell_all -> flat\n" + " rebalance -> rotate\n" + " HOLD / BUY / SELL_ALL / REBALANCE via --decision are still accepted\n" ), ) - parser.add_argument("--decision-id", help="Override decision id (otherwise derived automatically)") - parser.add_argument("--analysis", help="Decision analysis text to persist into logs") - parser.add_argument("--reasoning", help="Decision reasoning text to persist into logs") - parser.add_argument("--dry-run", action="store_true", help="Force dry-run mode for this invocation") + subparsers = parser.add_subparsers( + dest="command", + metavar="{bal,overview,hold,buy,flat,rotate,...}", + ) - subparsers = parser.add_subparsers(dest="command") + subparsers.add_parser("bal", parents=[shared], help="Preferred: print live balances as stable JSON") + subparsers.add_parser("overview", parents=[shared], help="Preferred: print the account overview as stable JSON") + subparsers.add_parser("hold", parents=[shared], help="Preferred: record a hold decision without trading") - subparsers.add_parser("hold", help="Log a HOLD decision without trading") - subparsers.add_parser("balances", help="Print live balances as JSON") - subparsers.add_parser("balance", help="Alias of balances") - subparsers.add_parser("status", help="Print balances + positions + snapshot as JSON") - - sell_all = subparsers.add_parser("sell-all", help="Sell all of one symbol") - sell_all.add_argument("symbol") - sell_all_legacy = subparsers.add_parser("sell_all", help=argparse.SUPPRESS) - sell_all_legacy.add_argument("symbol") - - buy = subparsers.add_parser("buy", help="Buy symbol with USDT amount") + buy = subparsers.add_parser("buy", parents=[shared], help="Preferred: buy a symbol with a USDT notional amount") buy.add_argument("symbol") buy.add_argument("amount_usdt", type=float) - rebalance = subparsers.add_parser("rebalance", help="Sell one symbol and rotate to another") + flat = subparsers.add_parser("flat", parents=[shared], help="Preferred: exit an entire symbol position") + flat.add_argument("symbol") + + rebalance = subparsers.add_parser("rotate", parents=[shared], help="Preferred: rotate exposure from one symbol into another") rebalance.add_argument("from_symbol") rebalance.add_argument("to_symbol") + subparsers.add_parser("balances", parents=[shared], help=argparse.SUPPRESS) + subparsers.add_parser("balance", parents=[shared], help=argparse.SUPPRESS) + subparsers.add_parser("acct", parents=[shared], help=argparse.SUPPRESS) + subparsers.add_parser("status", parents=[shared], help=argparse.SUPPRESS) + + sell_all = subparsers.add_parser("sell-all", parents=[shared], help=argparse.SUPPRESS) + sell_all.add_argument("symbol") + sell_all_legacy = subparsers.add_parser("sell_all", parents=[shared], help=argparse.SUPPRESS) + sell_all_legacy.add_argument("symbol") + + rebalance_legacy = subparsers.add_parser("rebalance", parents=[shared], help=argparse.SUPPRESS) + rebalance_legacy.add_argument("from_symbol") + rebalance_legacy.add_argument("to_symbol") + + subparsers._choices_actions = [ + action + for action in subparsers._choices_actions + if action.help != argparse.SUPPRESS + ] + return parser @@ -53,6 +107,11 @@ def normalize_legacy_argv(argv: list[str]) -> list[str]: action_aliases = { "HOLD": ["hold"], "hold": ["hold"], + "bal": ["balances"], + "acct": ["status"], + "overview": ["status"], + "flat": ["sell-all"], + "rotate": ["rebalance"], "SELL_ALL": ["sell-all"], "sell_all": ["sell-all"], "sell-all": ["sell-all"], @@ -66,6 +125,8 @@ def normalize_legacy_argv(argv: list[str]) -> list[str]: "balances": ["balances"], "STATUS": ["status"], "status": ["status"], + "OVERVIEW": ["status"], + "overview": ["status"], } has_legacy_flag = any(t.startswith("--decision") for t in argv) @@ -130,8 +191,7 @@ def parse_cli_args(argv: list[str]): if not args.command: parser.print_help() raise SystemExit(1) - if args.command == "sell_all": - args.command = "sell-all" + args.command = COMMAND_CANONICAL.get(args.command, args.command) return args, normalized diff --git a/src/coinhunter/services/trade_execution.py b/src/coinhunter/services/trade_execution.py index 778fa88..482c71b 100644 --- a/src/coinhunter/services/trade_execution.py +++ b/src/coinhunter/services/trade_execution.py @@ -1,4 +1,6 @@ """Trade execution actions (buy, sell, rebalance, hold, status).""" +import json + from ..logger import log_decision, log_trade from .exchange_service import ( fetch_balances, @@ -12,6 +14,10 @@ from .portfolio_service import load_positions, save_positions, upsert_position, from .trade_common import is_dry_run, USDT_BUFFER_PCT, log, bj_now_iso +def print_json(payload: dict) -> None: + print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True)) + + def build_decision_context(ex, action: str, argv_tail: list[str], decision_id: str): balances = fetch_balances(ex) positions = load_positions() @@ -168,11 +174,12 @@ def command_status(ex): "positions": positions, "market_snapshot": market_snapshot, } - print(payload) + print_json(payload) return payload def command_balances(ex): balances = fetch_balances(ex) - print({"balances": balances}) + payload = {"balances": balances} + print_json(payload) return balances