feat: polish exec cli ergonomics and output

This commit is contained in:
2026-04-15 20:28:24 +08:00
parent f69facde0c
commit e6274d3a00
4 changed files with 207 additions and 65 deletions

View File

@@ -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

View File

@@ -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