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:
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user