feat: add catlog command, agent flag reorder, and TUI polish

- Add `coinhunter catlog` with limit/offset pagination for audit logs
- Optimize audit log reading with deque to avoid loading all history
- Allow `-a/--agent` flag after subcommands
- Fix upgrade spinner artifact and empty line issues
- Render audit log TUI as timeline with low-saturation event colors
- Convert audit timestamps to local timezone in TUI
- Remove futures-related capabilities
- Add conda environment.yml for development
- Bump version to 2.0.9 and update README

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 16:42:47 +08:00
parent 6923013694
commit f528575aa8
15 changed files with 339 additions and 92 deletions

View File

@@ -7,6 +7,7 @@ import sys
from typing import Any
from . import __version__
from .audit import read_audit_log
from .binance.spot_client import SpotBinanceClient
from .config import ensure_init_files, get_binance_credentials, load_config
from .runtime import get_runtime_paths, install_shell_completion, print_output, self_upgrade, with_spinner
@@ -78,7 +79,9 @@ def build_parser() -> argparse.ArgumentParser:
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(
"-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")
@@ -90,6 +93,12 @@ def build_parser() -> argparse.ArgumentParser:
subparsers.add_parser("upgrade", help="Upgrade coinhunter to the latest version")
catlog_parser = subparsers.add_parser("catlog", help="Read recent audit log entries")
catlog_parser.add_argument("-n", "--limit", type=int, default=10, help="Number of entries (default: 10)")
catlog_parser.add_argument(
"-o", "--offset", type=int, default=0, help="Skip the most recent N entries (default: 0)"
)
completion_parser = subparsers.add_parser("completion", help="Generate shell completion script")
completion_parser.add_argument("shell", choices=["bash", "zsh"], help="Target shell")
@@ -145,24 +154,18 @@ def main(argv: list[str] | None = None) -> int:
spot_client = _load_spot_client(config)
if args.account_command == "overview":
with with_spinner("Fetching account overview...", enabled=not args.agent):
print_output(
account_service.get_overview(config, spot_client=spot_client),
agent=args.agent,
)
result = account_service.get_overview(config, spot_client=spot_client)
print_output(result, agent=args.agent)
return 0
if args.account_command == "balances":
with with_spinner("Fetching balances...", enabled=not args.agent):
print_output(
account_service.get_balances(config, spot_client=spot_client),
agent=args.agent,
)
result = account_service.get_balances(config, spot_client=spot_client)
print_output(result, agent=args.agent)
return 0
if args.account_command == "positions":
with with_spinner("Fetching positions...", enabled=not args.agent):
print_output(
account_service.get_positions(config, spot_client=spot_client),
agent=args.agent,
)
result = account_service.get_positions(config, spot_client=spot_client)
print_output(result, agent=args.agent)
return 0
parser.error("account requires one of: overview, balances, positions")
@@ -170,56 +173,68 @@ def main(argv: list[str] | None = None) -> int:
spot_client = _load_spot_client(config)
if args.market_command == "tickers":
with with_spinner("Fetching tickers...", enabled=not args.agent):
print_output(market_service.get_tickers(config, args.symbols, spot_client=spot_client), agent=args.agent)
result = market_service.get_tickers(config, args.symbols, spot_client=spot_client)
print_output(result, agent=args.agent)
return 0
if args.market_command == "klines":
with with_spinner("Fetching klines...", enabled=not args.agent):
print_output(
market_service.get_klines(
config,
args.symbols,
interval=args.interval,
limit=args.limit,
spot_client=spot_client,
),
agent=args.agent,
result = market_service.get_klines(
config,
args.symbols,
interval=args.interval,
limit=args.limit,
spot_client=spot_client,
)
print_output(result, agent=args.agent)
return 0
parser.error("market requires one of: tickers, klines")
if args.command == "trade":
spot_client = _load_spot_client(config)
with with_spinner("Placing order...", enabled=not args.agent):
print_output(
trade_service.execute_spot_trade(
config,
side=args.trade_action,
symbol=args.symbol,
qty=args.qty,
quote=args.quote,
order_type=args.type,
price=args.price,
dry_run=True if args.dry_run else None,
spot_client=spot_client,
),
agent=args.agent,
result = trade_service.execute_spot_trade(
config,
side=args.trade_action,
symbol=args.symbol,
qty=args.qty,
quote=args.quote,
order_type=args.type,
price=args.price,
dry_run=True if args.dry_run else None,
spot_client=spot_client,
)
print_output(result, agent=args.agent)
return 0
if args.command == "opportunity":
spot_client = _load_spot_client(config)
if args.opportunity_command == "portfolio":
with with_spinner("Analyzing portfolio...", enabled=not args.agent):
print_output(opportunity_service.analyze_portfolio(config, spot_client=spot_client), agent=args.agent)
result = opportunity_service.analyze_portfolio(config, spot_client=spot_client)
print_output(result, agent=args.agent)
return 0
if args.opportunity_command == "scan":
with with_spinner("Scanning opportunities...", enabled=not args.agent):
print_output(opportunity_service.scan_opportunities(config, spot_client=spot_client, symbols=args.symbols), agent=args.agent)
result = opportunity_service.scan_opportunities(
config, spot_client=spot_client, symbols=args.symbols
)
print_output(result, agent=args.agent)
return 0
parser.error("opportunity requires `portfolio` or `scan`")
if args.command == "upgrade":
print_output(self_upgrade(), agent=args.agent)
with with_spinner("Upgrading coinhunter...", enabled=not args.agent):
result = self_upgrade()
print_output(result, agent=args.agent)
return 0
if args.command == "catlog":
with with_spinner("Reading audit logs...", enabled=not args.agent):
entries = read_audit_log(limit=args.limit, offset=args.offset)
print_output(
{"entries": entries, "limit": args.limit, "offset": args.offset, "total": len(entries)},
agent=args.agent,
)
return 0
parser.error(f"Unsupported command {args.command}")