From 4312b16288d3f2102708b2d564fa24157e0b18b8 Mon Sep 17 00:00:00 2001 From: Tacit Lab Date: Mon, 20 Apr 2026 11:11:11 +0800 Subject: [PATCH] feat: configurable ticker window for market stats (1h, 4h, 1d) - Replace hardcoded ticker_24h with ticker_stats supporting configurable window - Add -w/--window flag to `market tickers` (choices: 1h, 4h, 1d, default 1d) - Update TUI title and JSON output to include window field - Keep opportunity/pf service on 1d default - Sync tests and doc comments Co-Authored-By: Claude Opus 4.7 --- src/coinhunter/binance/spot_client.py | 15 +++++----- src/coinhunter/cli.py | 29 +++++++++++++------ src/coinhunter/runtime.py | 2 +- src/coinhunter/services/market_service.py | 9 +++--- .../services/opportunity_service.py | 2 +- tests/test_account_market_services.py | 2 +- tests/test_opportunity_service.py | 2 +- 7 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/coinhunter/binance/spot_client.py b/src/coinhunter/binance/spot_client.py index f4d07af..10b686a 100644 --- a/src/coinhunter/binance/spot_client.py +++ b/src/coinhunter/binance/spot_client.py @@ -52,13 +52,14 @@ class SpotBinanceClient: 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) + def ticker_stats(self, symbols: list[str] | None = None, *, window: str = "1d") -> list[dict[str, Any]]: + kwargs: dict[str, Any] = {"windowSize": window} + if symbols: + if len(symbols) == 1: + kwargs["symbol"] = symbols[0] + else: + kwargs["symbols"] = symbols + response = self._call("ticker stats", self._client.ticker, **kwargs) return response if isinstance(response, list) else [response] def ticker_price(self, symbols: list[str] | None = None) -> list[dict[str, Any]]: diff --git a/src/coinhunter/cli.py b/src/coinhunter/cli.py index 5d6a41d..7d760eb 100644 --- a/src/coinhunter/cli.py +++ b/src/coinhunter/cli.py @@ -136,6 +136,7 @@ Fields: "market/tickers": { "tui": """\ TUI Output: + TICKERS window=1d ┌─────────┬────────────┬──────────┬──────────────┐ │ Symbol │ Last Price │ Change % │ Quote Volume │ ├─────────┼────────────┼──────────┼──────────────┤ @@ -147,26 +148,30 @@ JSON Output: { "tickers": [ {"symbol": "BTCUSDT", "last_price": 70000.0, "price_change_pct": 2.5, "quote_volume": 123456.0} - ] + ], + "window": "1d" } Fields: symbol – normalized trading pair (e.g. "BTCUSDT") last_price – latest traded price (float) - price_change_pct – 24h change % (float, e.g. 2.5 = +2.5%) - quote_volume – 24h quote volume (float) + price_change_pct – change % over the selected window (float, e.g. 2.5 = +2.5%) + quote_volume – quote volume over the selected window (float) + window – statistics window (enum: 1h, 4h, 1d) """, "json": """\ JSON Output: { "tickers": [ {"symbol": "BTCUSDT", "last_price": 70000.0, "price_change_pct": 2.5, "quote_volume": 123456.0} - ] + ], + "window": "1d" } Fields: symbol – normalized trading pair (e.g. "BTCUSDT") last_price – latest traded price (float) - price_change_pct – 24h change % (float, e.g. 2.5 = +2.5%) - quote_volume – 24h quote volume (float) + price_change_pct – change % over the selected window (float, e.g. 2.5 = +2.5%) + quote_volume – quote volume over the selected window (float) + window – statistics window (enum: 1h, 4h, 1d) """, }, "market/klines": { @@ -720,10 +725,14 @@ def build_parser() -> argparse.ArgumentParser: ) market_subparsers = market_parser.add_subparsers(dest="market_command") tickers_parser = market_subparsers.add_parser( - "tickers", aliases=["tk", "t"], help="Fetch 24h ticker data", - description="Fetch 24h ticker statistics (last price, change %, volume) for one or more symbols.", + "tickers", aliases=["tk", "t"], help="Fetch ticker statistics", + description="Fetch ticker statistics (last price, change %, volume) for one or more symbols with optional window.", ) tickers_parser.add_argument("symbols", nargs="+", metavar="SYM", help="Symbols to query (e.g. BTCUSDT ETH/USDT)") + tickers_parser.add_argument( + "-w", "--window", choices=["1h", "4h", "1d"], default="1d", + help="Statistics window: 1h, 4h, 1d (default: 1d)", + ) _add_global_flags(tickers_parser) klines_parser = market_subparsers.add_parser( "klines", aliases=["k"], help="Fetch OHLCV klines", @@ -987,7 +996,9 @@ 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): - result = market_service.get_tickers(config, args.symbols, spot_client=spot_client) + result = market_service.get_tickers( + config, args.symbols, spot_client=spot_client, window=args.window + ) print_output(result, agent=args.agent) return 0 if args.market_command == "klines": diff --git a/src/coinhunter/runtime.py b/src/coinhunter/runtime.py index d740363..dcc093d 100644 --- a/src/coinhunter/runtime.py +++ b/src/coinhunter/runtime.py @@ -278,7 +278,7 @@ def _render_tui(payload: Any) -> None: ] ) _print_box_table( - "24H TICKERS", + f"TICKERS window={payload.get('window', '1d')}", ["Symbol", "Last Price", "Change %", "Quote Volume"], table_rows, aligns=["left", "right", "right", "right"], diff --git a/src/coinhunter/services/market_service.py b/src/coinhunter/services/market_service.py index 2485ead..128d67b 100644 --- a/src/coinhunter/services/market_service.py +++ b/src/coinhunter/services/market_service.py @@ -48,10 +48,10 @@ class KlineView: quote_volume: float -def get_tickers(config: dict[str, Any], symbols: list[str], *, spot_client: Any) -> dict[str, Any]: +def get_tickers(config: dict[str, Any], symbols: list[str], *, spot_client: Any, window: str = "1d") -> dict[str, Any]: normalized = normalize_symbols(symbols) rows = [] - for ticker in spot_client.ticker_24h(normalized): + for ticker in spot_client.ticker_stats(normalized, window=window): rows.append( asdict( TickerView( @@ -64,7 +64,7 @@ def get_tickers(config: dict[str, Any], symbols: list[str], *, spot_client: Any) ) ) ) - return {"tickers": rows} + return {"tickers": rows, "window": window} def get_klines( @@ -103,6 +103,7 @@ def get_scan_universe( *, spot_client: Any, symbols: list[str] | None = None, + window: str = "1d", ) -> list[dict[str, Any]]: market_config = config.get("market", {}) opportunity_config = config.get("opportunity", {}) @@ -116,7 +117,7 @@ def get_scan_universe( status_map = {normalize_symbol(item["symbol"]): item.get("status", "") for item in exchange_info.get("symbols", [])} rows: list[dict[str, Any]] = [] - for ticker in spot_client.ticker_24h(list(requested) if requested else None): + for ticker in spot_client.ticker_stats(list(requested) if requested else None, window=window): symbol = normalize_symbol(ticker["symbol"]) if not symbol.endswith(quote): continue diff --git a/src/coinhunter/services/opportunity_service.py b/src/coinhunter/services/opportunity_service.py index 9511aa5..7d64c68 100644 --- a/src/coinhunter/services/opportunity_service.py +++ b/src/coinhunter/services/opportunity_service.py @@ -104,7 +104,7 @@ def analyze_portfolio(config: dict[str, Any], *, spot_client: Any) -> dict[str, klines = spot_client.klines(symbol=symbol, interval="1h", limit=24) closes = [float(item[4]) for item in klines] volumes = [float(item[5]) for item in klines] - tickers = spot_client.ticker_24h([symbol]) + tickers = spot_client.ticker_stats([symbol], window="1d") ticker = tickers[0] if tickers else {"priceChangePercent": "0"} concentration = position["notional_usdt"] / total_notional score, metrics = _score_candidate( diff --git a/tests/test_account_market_services.py b/tests/test_account_market_services.py index e01022f..0cf8fd2 100644 --- a/tests/test_account_market_services.py +++ b/tests/test_account_market_services.py @@ -26,7 +26,7 @@ class FakeSpotClient: return list(prices.values()) return [prices[symbol] for symbol in symbols] - def ticker_24h(self, symbols=None): + def ticker_stats(self, symbols=None, *, window="1d"): rows = [ { "symbol": "BTCUSDT", diff --git a/tests/test_opportunity_service.py b/tests/test_opportunity_service.py index 32dc8bf..b01da33 100644 --- a/tests/test_opportunity_service.py +++ b/tests/test_opportunity_service.py @@ -27,7 +27,7 @@ class FakeSpotClient: } return [mapping[symbol] for symbol in symbols] - def ticker_24h(self, symbols=None): + def ticker_stats(self, symbols=None, *, window="1d"): rows = { "BTCUSDT": { "symbol": "BTCUSDT",