首次上传
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
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
|
||||
Reference in New Issue
Block a user