from __future__ import annotations from dataclasses import dataclass, field from statistics import mean # 以下换算仅针对 5m K(与是否单独拉 4h 图无关): # 每小时 60/5 = 12 根;一根「4 小时」大周期对应 4×12 = 48 根 5m。 BARS_5M_PER_HOUR = 12 BARS_5M_PER_4H = BARS_5M_PER_HOUR * 4 # 48 # 箱体回看最短不少于一根 4h 等价的 5m 长度,避免用不足一个 4h 的窗去定义箱体 MIN_BOX_LOOKBACK_BARS_5M = BARS_5M_PER_4H @dataclass class IntradayRuleParams: range_hours: float = 8.0 range_max_pct: float = 1.5 volume_spike_mult: float = 1.6 volume_lookback_bars: int = 20 breakout_buffer_pct: float = 0.05 @dataclass class ExchangeRuleResult: signal_level: str = "NONE" # NONE | WATCH | TRIGGER signal_side: str = "NONE" # NONE | LONG | SHORT trigger_types: list[str] = field(default_factory=list) score: float = 0.0 metrics: dict = field(default_factory=dict) def _rows_to_ohlcv(rows: list[list[str]]) -> tuple[list[float], list[float], list[float], list[float], list[float]]: o, h, l, c, v = [], [], [], [], [] for item in rows: if len(item) < 6: continue o.append(float(item[1])) h.append(float(item[2])) l.append(float(item[3])) c.append(float(item[4])) v.append(float(item[5])) return o, h, l, c, v def evaluate_exchange( symbol: str, alt_rows: list[list[str]], btc_rows: list[list[str]], params: IntradayRuleParams, ) -> ExchangeRuleResult: """ 5m 日内结构规则(中文分级): - range_hours 按「墙钟小时」换成 5m 根数:×12(根/小时);48 根 5m = 4 墙钟小时。 - 观察:箱体回看窗口内(不含突破/确认 K)用最高/最低算振幅,不超过 range_max_pct - 触发:突破 K 在有效带内,确认 K 收在箱体外,并满足放量等条件 """ breakout_max_pct = 0.5 result = ExchangeRuleResult() _, ah, al, ac, av = _rows_to_ohlcv(alt_rows) bars_for_range = max( MIN_BOX_LOOKBACK_BARS_5M, int(params.range_hours * BARS_5M_PER_HOUR), ) vol_lb = max(5, int(params.volume_lookback_bars)) min_need = bars_for_range + vol_lb + 3 if len(ac) < min_need: result.metrics = {"error": "insufficient_candles", "need": min_need, "have": len(ac)} return result # 区间边界:前 N 根(不含倒数第 1 确认 K、倒数第 2 突破 K),用区间内的 highest/lowest seg_h = ah[-bars_for_range - 2 : -2] seg_l = al[-bars_for_range - 2 : -2] range_high = max(seg_h) range_low = min(seg_l) mid = (range_high + range_low) / 2 if range_high > range_low else 0 range_pct = ((range_high - range_low) / mid) * 100 if mid > 0 else 999.0 breakout_close = ac[-2] confirm_close = ac[-1] breakout_high = ah[-2] breakout_low = al[-2] confirm_high = ah[-1] confirm_low = al[-1] last_volume = av[-1] vol_base = mean(av[-vol_lb - 1 : -1]) if len(av) > vol_lb else mean(av) vol_ratio = (last_volume / vol_base) if vol_base > 0 else 0.0 breakout_min_line = range_high * (1 + params.breakout_buffer_pct / 100) breakout_max_line = range_high * (1 + breakout_max_pct / 100) breakdown_min_line = range_low * (1 - params.breakout_buffer_pct / 100) breakdown_max_line = range_low * (1 - breakout_max_pct / 100) is_sideways = range_pct <= params.range_max_pct is_volume_spike = vol_ratio >= params.volume_spike_mult breakout_long_ok = breakout_close > breakout_min_line and breakout_close < breakout_max_line breakout_short_ok = breakout_close < breakdown_min_line and breakout_close > breakdown_max_line confirm_long_ok = confirm_close > range_high confirm_short_ok = confirm_close < range_low if is_sideways: result.signal_level = "WATCH" result.trigger_types = ["横盘结构成立"] result.score = 1.0 if is_sideways and breakout_long_ok and confirm_long_ok and is_volume_spike: result.signal_level = "TRIGGER" result.signal_side = "LONG" result.trigger_types = ["横盘结构成立", "突破K在有效区间", "第二根K确认未回箱体", "放量突破"] result.score = 3.4 elif is_sideways and breakout_short_ok and confirm_short_ok and is_volume_spike: result.signal_level = "TRIGGER" result.signal_side = "SHORT" result.trigger_types = ["横盘结构成立", "突破K在有效区间", "第二根K确认未回箱体", "放量破位"] result.score = 3.4 result.metrics = { "symbol": symbol.upper(), "bar": "5m", "range_hours": params.range_hours, "range_bars": bars_for_range, "range_max_pct": params.range_max_pct, "range_pct": round(range_pct, 4), "range_high": range_high, "range_low": range_low, "breakout_min_pct": params.breakout_buffer_pct, "breakout_max_pct": breakout_max_pct, "breakout_min_line": breakout_min_line, "breakout_max_line": breakout_max_line, "breakdown_min_line": breakdown_min_line, "breakdown_max_line": breakdown_max_line, "breakout_close": breakout_close, "confirm_close": confirm_close, "breakout_high": breakout_high, "breakout_low": breakout_low, "confirm_high": confirm_high, "confirm_low": confirm_low, "volume_lookback_bars": vol_lb, "volume_spike_mult": params.volume_spike_mult, "last_volume": last_volume, "volume_base": round(vol_base, 8), "volume_ratio": round(vol_ratio, 4), "signal_side": result.signal_side, } return result