refactor: remove all futures-related capabilities

Delete USDT-M futures support since the user's Binance API key does not
support futures trading. This simplifies the CLI to spot-only:

- Remove futures client wrapper (um_futures_client.py)
- Remove futures trade commands and close position logic
- Simplify account service to spot-only (no market_type field)
- Remove futures references from opportunity service
- Update README and tests to reflect spot-only architecture
- Bump version to 2.0.7

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 20:10:15 +08:00
parent 680bd3d33c
commit 0f862957b0
11 changed files with 112 additions and 549 deletions

View File

@@ -8,7 +8,6 @@ from typing import Any
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, install_shell_completion, print_output, self_upgrade, with_spinner
from .services import account_service, market_service, opportunity_service, trade_service
@@ -16,11 +15,11 @@ from .services import account_service, market_service, opportunity_service, trad
EPILOG = """\
examples:
coinhunter init
coinhunter account overview -sf
coinhunter account overview
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 trade buy BTCUSDT -q 100 -d
coinhunter trade sell BTCUSDT --qty 0.01 --type limit --price 90000
coinhunter opportunity scan -s BTCUSDT ETHUSDT
coinhunter upgrade
"""
@@ -38,24 +37,6 @@ def _load_spot_client(config: dict[str, Any], *, client: Any | None = None) -> S
)
def _load_futures_client(config: dict[str, Any], *, client: Any | None = None) -> UMFuturesClient:
credentials = get_binance_credentials()
binance_config = config["binance"]
return UMFuturesClient(
api_key=credentials["api_key"],
api_secret=credentials["api_secret"],
base_url=binance_config["futures_base_url"],
recv_window=int(binance_config["recv_window"]),
client=client,
)
def _resolve_market_flags(args: argparse.Namespace) -> tuple[bool, bool]:
if getattr(args, "spot", False) or getattr(args, "futures", False):
return bool(getattr(args, "spot", False)), bool(getattr(args, "futures", False))
return True, True
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="coinhunter",
@@ -73,14 +54,12 @@ def build_parser() -> argparse.ArgumentParser:
account_parser = subparsers.add_parser("account", help="Account overview, balances, and positions")
account_subparsers = account_parser.add_subparsers(dest="account_command")
account_commands_help = {
"overview": "Total equity and summary across markets",
"overview": "Total equity and summary",
"balances": "List asset balances",
"positions": "List open positions",
}
for name in ("overview", "balances", "positions"):
sub = account_subparsers.add_parser(name, help=account_commands_help[name])
sub.add_argument("-s", "--spot", action="store_true", help="Include spot market")
sub.add_argument("-f", "--futures", action="store_true", help="Include futures market")
account_subparsers.add_parser(name, help=account_commands_help[name])
market_parser = subparsers.add_parser("market", help="Batch market queries")
market_subparsers = market_parser.add_subparsers(dest="market_command")
@@ -91,14 +70,11 @@ def build_parser() -> argparse.ArgumentParser:
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", help="Spot market orders")
spot_subparsers = spot_parser.add_subparsers(dest="trade_action")
spot_side_help = {"buy": "Buy base asset with quote quantity", "sell": "Sell base asset quantity"}
trade_parser = subparsers.add_parser("trade", help="Spot trade execution")
trade_subparsers = trade_parser.add_subparsers(dest="trade_action")
trade_side_help = {"buy": "Buy base asset with quote quantity", "sell": "Sell base asset quantity"}
for side in ("buy", "sell"):
sub = spot_subparsers.add_parser(side, help=spot_side_help[side])
sub = trade_subparsers.add_parser(side, help=trade_side_help[side])
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)")
@@ -106,21 +82,6 @@ def build_parser() -> argparse.ArgumentParser:
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", help="USDT-M futures orders")
futures_subparsers = futures_parser.add_subparsers(dest="trade_action")
futures_side_help = {"buy": "Open or add to a LONG position", "sell": "Open or add to a SHORT position"}
for side in ("buy", "sell"):
sub = futures_subparsers.add_parser(side, help=futures_side_help[side])
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", help="Score current holdings")
@@ -181,45 +142,25 @@ def main(argv: list[str] | None = None) -> int:
config = load_config()
if args.command == "account":
include_spot, include_futures = _resolve_market_flags(args)
spot_client = _load_spot_client(config) if include_spot else None
futures_client = _load_futures_client(config) if include_futures else None
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,
include_spot=include_spot,
include_futures=include_futures,
spot_client=spot_client,
futures_client=futures_client,
),
account_service.get_overview(config, spot_client=spot_client),
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,
include_spot=include_spot,
include_futures=include_futures,
spot_client=spot_client,
futures_client=futures_client,
),
account_service.get_balances(config, spot_client=spot_client),
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,
include_spot=include_spot,
include_futures=include_futures,
spot_client=spot_client,
futures_client=futures_client,
),
account_service.get_positions(config, spot_client=spot_client),
agent=args.agent,
)
return 0
@@ -247,55 +188,23 @@ def main(argv: list[str] | None = None) -> int:
parser.error("market requires one of: tickers, klines")
if args.command == "trade":
if args.trade_market == "spot":
spot_client = _load_spot_client(config)
with with_spinner("Placing spot 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,
)
return 0
if args.trade_market == "futures":
futures_client = _load_futures_client(config)
if args.trade_action == "close":
with with_spinner("Closing futures position...", enabled=not args.agent):
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
with with_spinner("Placing futures order...", enabled=not args.agent):
print_output(
trade_service.execute_futures_trade(
config,
side=args.trade_action,
symbol=args.symbol,
qty=args.qty,
order_type=args.type,
price=args.price,
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`")
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,
)
return 0
if args.command == "opportunity":
spot_client = _load_spot_client(config)