C12: fix falsy-value display and validate symbol/interval

Two issues in the live_trading example:

- The status line rendered indicator values with `... if snap.rsi
  else "--"`. A genuine reading of 0.0 is falsy, so an RSI / MACD
  histogram / Bollinger value of exactly zero was misreported as "--".
  Switch to explicit `is not None` checks.

- --symbol and --interval were interpolated straight into the stream
  name and WebSocket URL with no checks. Add validate_args: the symbol
  must be strictly alphanumeric and the interval must be one Binance
  recognises. main() validates before connecting and exits with a clear
  error and code 2 otherwise; the strict values keep the URL well-formed
  without escaping.
This commit is contained in:
kingchenc
2026-05-22 12:27:13 +02:00
parent f0471ba824
commit a8fb0b8181
+44 -3
View File
@@ -20,6 +20,7 @@ import argparse
import asyncio import asyncio
import json import json
import logging import logging
import re
import signal import signal
import sys import sys
from dataclasses import dataclass from dataclasses import dataclass
@@ -36,6 +37,41 @@ except ImportError:
BINANCE_WS = "wss://stream.binance.com:9443/stream" BINANCE_WS = "wss://stream.binance.com:9443/stream"
# Kline intervals the public Binance WebSocket API recognises.
VALID_INTERVALS = frozenset(
{
"1s", "1m", "3m", "5m", "15m", "30m",
"1h", "2h", "4h", "6h", "8h", "12h",
"1d", "3d", "1w", "1M",
}
)
# A Binance symbol is strictly alphanumeric (e.g. BTCUSDT).
_SYMBOL_RE = re.compile(r"^[A-Za-z0-9]+$")
def validate_args(symbol: str, interval: str) -> None:
"""Reject a symbol or interval that is not safe to splice into the WS URL.
Both values are interpolated straight into the stream name and URL, so an
unexpected value would otherwise produce a confusing connection failure.
Validating up front keeps the URL well-formed without needing to escape it.
Raises:
ValueError: if ``symbol`` is not strictly alphanumeric, or ``interval``
is not one Binance recognises.
"""
if not _SYMBOL_RE.match(symbol):
raise ValueError(
f"invalid --symbol {symbol!r}: expected only letters and digits, "
"e.g. BTCUSDT"
)
if interval not in VALID_INTERVALS:
raise ValueError(
f"invalid --interval {interval!r}: expected one of "
+ ", ".join(sorted(VALID_INTERVALS))
)
@dataclass @dataclass
class Snapshot: class Snapshot:
@@ -109,10 +145,10 @@ async def run(symbol: str, interval: str) -> None:
"%s close=%.4f rsi=%s hist=%s bb=%s", "%s close=%.4f rsi=%s hist=%s bb=%s",
"BAR" if is_closed else "tick", "BAR" if is_closed else "tick",
close, close,
f"{snap.rsi:.1f}" if snap.rsi else "--", f"{snap.rsi:.1f}" if snap.rsi is not None else "--",
f"{snap.macd_hist:+.4f}" if snap.macd_hist else "--", f"{snap.macd_hist:+.4f}" if snap.macd_hist is not None else "--",
f"{snap.bb_lower:.2f}/{snap.bb_middle:.2f}/{snap.bb_upper:.2f}" f"{snap.bb_lower:.2f}/{snap.bb_middle:.2f}/{snap.bb_upper:.2f}"
if snap.bb_upper if snap.bb_upper is not None
else "--", else "--",
) )
emit_signal(snap, log) emit_signal(snap, log)
@@ -128,6 +164,11 @@ def parse_args() -> argparse.Namespace:
def main() -> int: def main() -> int:
args = parse_args() args = parse_args()
try:
validate_args(args.symbol, args.interval)
except ValueError as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
logging.basicConfig( logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO, level=logging.DEBUG if args.verbose else logging.INFO,
format="%(asctime)s %(levelname)s %(message)s", format="%(asctime)s %(levelname)s %(message)s",