Files
gate_scout_order/onchain_scout_gate/app/exchange_rules.py
T
2026-05-18 08:10:28 +08:00

135 lines
5.2 KiB
Python

from __future__ import annotations
from dataclasses import dataclass, field
from statistics import mean
from .candle_rows import rows_to_ohlcv
# 以下换算仅针对 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 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