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:
@@ -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]]:
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user