refactor: rewrite to CoinHunter V2 flat architecture

Replace the V1 commands/services split with a flat, direct architecture:
- cli.py dispatches directly to service functions
- New services: account, market, trade, opportunity
- Thin Binance wrappers: spot_client, um_futures_client
- Add audit logging, runtime paths, and TOML config
- Remove legacy V1 code: commands/, precheck, review engine, smart executor
- Add ruff + mypy toolchain and fix edge cases in trade params

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 17:22:29 +08:00
parent 3819e35a7b
commit 52cd76a750
78 changed files with 2023 additions and 5407 deletions

View File

@@ -0,0 +1,75 @@
"""Thin wrapper around the official Binance Spot connector."""
from __future__ import annotations
from collections.abc import Callable
from typing import Any
from requests.exceptions import RequestException, SSLError
class SpotBinanceClient:
def __init__(
self,
*,
api_key: str,
api_secret: str,
base_url: str,
recv_window: int,
client: Any | None = None,
) -> None:
self.recv_window = recv_window
if client is not None:
self._client = client
return
try:
from binance.spot import Spot
except ModuleNotFoundError as exc: # pragma: no cover
raise RuntimeError("binance-connector is not installed") from exc
self._client = Spot(api_key=api_key, api_secret=api_secret, base_url=base_url)
def _call(self, operation: str, func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
try:
return func(*args, **kwargs)
except SSLError as exc:
raise RuntimeError(
"Binance Spot request failed because TLS certificate verification failed. "
"This usually means the local Python trust store is incomplete or a proxy is intercepting HTTPS. "
"Update the local CA trust chain or configure the host environment with the correct corporate/root CA."
) from exc
except RequestException as exc:
raise RuntimeError(f"Binance Spot request failed during {operation}: {exc}") from exc
def account_info(self) -> dict[str, Any]:
return self._call("account info", self._client.account, recvWindow=self.recv_window) # type: ignore[no-any-return]
def exchange_info(self, symbol: str | None = None) -> dict[str, Any]:
kwargs: dict[str, Any] = {"recvWindow": self.recv_window}
if symbol:
kwargs["symbol"] = symbol
return self._call("exchange info", self._client.exchange_info, **kwargs) # type: ignore[no-any-return]
def ticker_24h(self, symbols: list[str] | None = None) -> list[dict[str, Any]]:
if not symbols:
response = self._call("24h ticker", self._client.ticker_24hr)
elif len(symbols) == 1:
response = self._call("24h ticker", self._client.ticker_24hr, symbol=symbols[0])
else:
response = self._call("24h ticker", self._client.ticker_24hr, symbols=symbols)
return response if isinstance(response, list) else [response] # type: ignore[no-any-return]
def ticker_price(self, symbols: list[str] | None = None) -> list[dict[str, Any]]:
if not symbols:
response = self._call("ticker price", self._client.ticker_price)
elif len(symbols) == 1:
response = self._call("ticker price", self._client.ticker_price, symbol=symbols[0])
else:
response = self._call("ticker price", self._client.ticker_price, symbols=symbols)
return response if isinstance(response, list) else [response] # type: ignore[no-any-return]
def klines(self, symbol: str, interval: str, limit: int) -> list[list[Any]]:
return self._call("klines", self._client.klines, symbol=symbol, interval=interval, limit=limit) # type: ignore[no-any-return]
def new_order(self, **kwargs: Any) -> dict[str, Any]:
kwargs.setdefault("recvWindow", self.recv_window)
return self._call("new order", self._client.new_order, **kwargs) # type: ignore[no-any-return]