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 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 11:11:11 +08:00
parent cf26a3dd3a
commit 4312b16288
7 changed files with 37 additions and 24 deletions

View File

@@ -52,13 +52,14 @@ class SpotBinanceClient:
kwargs["symbol"] = symbol kwargs["symbol"] = symbol
return self._call("exchange info", self._client.exchange_info, **kwargs) # type: ignore[no-any-return] 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]]: def ticker_stats(self, symbols: list[str] | None = None, *, window: str = "1d") -> list[dict[str, Any]]:
if not symbols: kwargs: dict[str, Any] = {"windowSize": window}
response = self._call("24h ticker", self._client.ticker_24hr) if symbols:
elif len(symbols) == 1: if len(symbols) == 1:
response = self._call("24h ticker", self._client.ticker_24hr, symbol=symbols[0]) kwargs["symbol"] = symbols[0]
else: else:
response = self._call("24h ticker", self._client.ticker_24hr, symbols=symbols) kwargs["symbols"] = symbols
response = self._call("ticker stats", self._client.ticker, **kwargs)
return response if isinstance(response, list) else [response] return response if isinstance(response, list) else [response]
def ticker_price(self, symbols: list[str] | None = None) -> list[dict[str, Any]]: def ticker_price(self, symbols: list[str] | None = None) -> list[dict[str, Any]]:

View File

@@ -136,6 +136,7 @@ Fields:
"market/tickers": { "market/tickers": {
"tui": """\ "tui": """\
TUI Output: TUI Output:
TICKERS window=1d
┌─────────┬────────────┬──────────┬──────────────┐ ┌─────────┬────────────┬──────────┬──────────────┐
│ Symbol │ Last Price │ Change % │ Quote Volume │ │ Symbol │ Last Price │ Change % │ Quote Volume │
├─────────┼────────────┼──────────┼──────────────┤ ├─────────┼────────────┼──────────┼──────────────┤
@@ -147,26 +148,30 @@ JSON Output:
{ {
"tickers": [ "tickers": [
{"symbol": "BTCUSDT", "last_price": 70000.0, "price_change_pct": 2.5, "quote_volume": 123456.0} {"symbol": "BTCUSDT", "last_price": 70000.0, "price_change_pct": 2.5, "quote_volume": 123456.0}
] ],
"window": "1d"
} }
Fields: Fields:
symbol normalized trading pair (e.g. "BTCUSDT") symbol normalized trading pair (e.g. "BTCUSDT")
last_price latest traded price (float) last_price latest traded price (float)
price_change_pct 24h change % (float, e.g. 2.5 = +2.5%) price_change_pct change % over the selected window (float, e.g. 2.5 = +2.5%)
quote_volume 24h quote volume (float) quote_volume quote volume over the selected window (float)
window statistics window (enum: 1h, 4h, 1d)
""", """,
"json": """\ "json": """\
JSON Output: JSON Output:
{ {
"tickers": [ "tickers": [
{"symbol": "BTCUSDT", "last_price": 70000.0, "price_change_pct": 2.5, "quote_volume": 123456.0} {"symbol": "BTCUSDT", "last_price": 70000.0, "price_change_pct": 2.5, "quote_volume": 123456.0}
] ],
"window": "1d"
} }
Fields: Fields:
symbol normalized trading pair (e.g. "BTCUSDT") symbol normalized trading pair (e.g. "BTCUSDT")
last_price latest traded price (float) last_price latest traded price (float)
price_change_pct 24h change % (float, e.g. 2.5 = +2.5%) price_change_pct change % over the selected window (float, e.g. 2.5 = +2.5%)
quote_volume 24h quote volume (float) quote_volume quote volume over the selected window (float)
window statistics window (enum: 1h, 4h, 1d)
""", """,
}, },
"market/klines": { "market/klines": {
@@ -720,10 +725,14 @@ def build_parser() -> argparse.ArgumentParser:
) )
market_subparsers = market_parser.add_subparsers(dest="market_command") market_subparsers = market_parser.add_subparsers(dest="market_command")
tickers_parser = market_subparsers.add_parser( tickers_parser = market_subparsers.add_parser(
"tickers", aliases=["tk", "t"], help="Fetch 24h ticker data", "tickers", aliases=["tk", "t"], help="Fetch ticker statistics",
description="Fetch 24h ticker statistics (last price, change %, volume) for one or more symbols.", 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("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) _add_global_flags(tickers_parser)
klines_parser = market_subparsers.add_parser( klines_parser = market_subparsers.add_parser(
"klines", aliases=["k"], help="Fetch OHLCV klines", "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) spot_client = _load_spot_client(config)
if args.market_command == "tickers": if args.market_command == "tickers":
with with_spinner("Fetching tickers...", enabled=not args.agent): 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) print_output(result, agent=args.agent)
return 0 return 0
if args.market_command == "klines": if args.market_command == "klines":

View File

@@ -278,7 +278,7 @@ def _render_tui(payload: Any) -> None:
] ]
) )
_print_box_table( _print_box_table(
"24H TICKERS", f"TICKERS window={payload.get('window', '1d')}",
["Symbol", "Last Price", "Change %", "Quote Volume"], ["Symbol", "Last Price", "Change %", "Quote Volume"],
table_rows, table_rows,
aligns=["left", "right", "right", "right"], aligns=["left", "right", "right", "right"],

View File

@@ -48,10 +48,10 @@ class KlineView:
quote_volume: float 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) normalized = normalize_symbols(symbols)
rows = [] rows = []
for ticker in spot_client.ticker_24h(normalized): for ticker in spot_client.ticker_stats(normalized, window=window):
rows.append( rows.append(
asdict( asdict(
TickerView( 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( def get_klines(
@@ -103,6 +103,7 @@ def get_scan_universe(
*, *,
spot_client: Any, spot_client: Any,
symbols: list[str] | None = None, symbols: list[str] | None = None,
window: str = "1d",
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
market_config = config.get("market", {}) market_config = config.get("market", {})
opportunity_config = config.get("opportunity", {}) 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", [])} status_map = {normalize_symbol(item["symbol"]): item.get("status", "") for item in exchange_info.get("symbols", [])}
rows: list[dict[str, Any]] = [] 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"]) symbol = normalize_symbol(ticker["symbol"])
if not symbol.endswith(quote): if not symbol.endswith(quote):
continue continue

View File

@@ -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) klines = spot_client.klines(symbol=symbol, interval="1h", limit=24)
closes = [float(item[4]) for item in klines] closes = [float(item[4]) for item in klines]
volumes = [float(item[5]) 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"} ticker = tickers[0] if tickers else {"priceChangePercent": "0"}
concentration = position["notional_usdt"] / total_notional concentration = position["notional_usdt"] / total_notional
score, metrics = _score_candidate( score, metrics = _score_candidate(

View File

@@ -26,7 +26,7 @@ class FakeSpotClient:
return list(prices.values()) return list(prices.values())
return [prices[symbol] for symbol in symbols] return [prices[symbol] for symbol in symbols]
def ticker_24h(self, symbols=None): def ticker_stats(self, symbols=None, *, window="1d"):
rows = [ rows = [
{ {
"symbol": "BTCUSDT", "symbol": "BTCUSDT",

View File

@@ -27,7 +27,7 @@ class FakeSpotClient:
} }
return [mapping[symbol] for symbol in symbols] return [mapping[symbol] for symbol in symbols]
def ticker_24h(self, symbols=None): def ticker_stats(self, symbols=None, *, window="1d"):
rows = { rows = {
"BTCUSDT": { "BTCUSDT": {
"symbol": "BTCUSDT", "symbol": "BTCUSDT",