from __future__ import annotations from dataclasses import dataclass, field from statistics import mean @dataclass class BtcDailyGateResult: """BTC 日线辅助门控(非产品主线):下跌不扫山寨,其它仍扫 —— 仅 downtrend 时关闭本轮 alt K 线请求。""" allow_alt_scan: bool regime: str # sideways | downtrend | neutral_or_up | unknown reason: str metrics: dict = field(default_factory=dict) def _rows_to_hlc(rows: list[list[str]]) -> tuple[list[float], list[float], list[float]]: """与行情 K 线行对齐:h, l, c(ts,o,h,l,c,...)。""" h, l_, c = [], [], [] for item in rows: if len(item) < 6: continue h.append(float(item[2])) l_.append(float(item[3])) c.append(float(item[4])) return h, l_, c def evaluate_btc_daily_gate( btc_1d_rows: list[list[str]], *, sideways_lookback_days: int = 14, sideways_max_range_pct: float = 10.0, min_bars: int = 30, ) -> BtcDailyGateResult: """ 原则:下跌不扫,其它都扫。 - 下跌(唯一不扫):非横盘,且收盘低于近 20 日收盘均线,且该均线相对前一段走低。 - 其余(横盘、上涨、宽幅震荡、数据不足 unknown 等):一律允许扫山寨。 """ ah, al, ac = _rows_to_hlc(btc_1d_rows) if len(ac) < min_bars: return BtcDailyGateResult( allow_alt_scan=True, regime="unknown", reason=f"insufficient_1d_bars have={len(ac)} need>={min_bars}, gate skipped", metrics={"have": len(ac), "min_bars": min_bars}, ) lb = max(5, min(sideways_lookback_days, len(ah) - 1)) window_h = ah[-lb:] window_l = al[-lb:] range_high = max(window_h) range_low = min(window_l) mid = (range_high + range_low) / 2 if range_high > range_low else 0.0 range_pct = ((range_high - range_low) / mid) * 100 if mid > 0 else 999.0 sma_curr = mean(ac[-20:]) sma_prev = mean(ac[-26:-6]) if len(ac) >= 26 else sma_curr last_close = ac[-1] is_sideways = range_pct <= sideways_max_range_pct if is_sideways: return BtcDailyGateResult( allow_alt_scan=True, regime="sideways", reason="btc_daily_sideways", metrics={ "range_lookback_days": lb, "range_pct": round(range_pct, 4), "sideways_max_range_pct": sideways_max_range_pct, "last_close": last_close, "sma20": round(sma_curr, 6), "sma20_prev_block": round(sma_prev, 6), }, ) is_downtrend = last_close < sma_curr and sma_curr < sma_prev if is_downtrend: return BtcDailyGateResult( allow_alt_scan=False, regime="downtrend", reason="btc_daily_downtrend_below_falling_sma20", metrics={ "range_lookback_days": lb, "range_pct": round(range_pct, 4), "sideways_max_range_pct": sideways_max_range_pct, "last_close": last_close, "sma20": round(sma_curr, 6), "sma20_prev_block": round(sma_prev, 6), }, ) return BtcDailyGateResult( allow_alt_scan=True, regime="neutral_or_up", reason="btc_not_sideways_not_downtrend_gate_open", metrics={ "range_lookback_days": lb, "range_pct": round(range_pct, 4), "last_close": last_close, "sma20": round(sma_curr, 6), "sma20_prev_block": round(sma_prev, 6), }, )