修复突破计算,执行器下单问题

This commit is contained in:
dekun
2026-05-23 17:18:38 +08:00
parent bfde4b60c6
commit ffba2e60e6
9 changed files with 242 additions and 38 deletions
+2
View File
@@ -115,6 +115,8 @@ class MonitorConfig(BaseModel):
symbol_signal_dedupe_hours: float = 4.0
# 企业微信主推送(突破预警):仅对本轮监控池内 24h 成交额排名前 N 的合约推送;0 表示不限制
wecom_push_max_volume_rank: int = 30
# 全市场自动箱体 WATCH/TRIGGER 是否发企微(默认关;仅 GEMMA 漏斗优先推送 + 关键位监控推送)
push_watch_trigger_wecom: bool = False
class GemmaConfig(BaseModel):
+65 -27
View File
@@ -4,7 +4,12 @@ from __future__ import annotations
from statistics import mean
from .candle_rows import rows_to_ohlcv
from .candle_rows import field_float
def _row_vol(item: list[str]) -> float:
v = field_float(item, 5)
return float(v) if v is not None else 0.0
def key_hard_checks_from_rows(
@@ -21,45 +26,67 @@ def key_hard_checks_from_rows(
volume_rank_total: int = 0,
volume_rank_max: int = 30,
) -> dict:
"""rows:Gate K 线行,最后一根应为最近一根已闭合 5m(调用方负责剔除未闭合)。"""
"""
rows:Gate K 线行(时间正序),最后一根应为最近一根已闭合 5m。
突破 K / 确认 K 固定为 rows[-2] / rows[-1](与 crypto_monitor_gate 一致)。
"""
out: dict = {"ok": False}
_, ah, al, ac, av = rows_to_ohlcv(rows)
vol_lb = max(5, int(volume_ma_bars))
min_need = vol_lb + 3
if len(ac) < min_need:
out["reason"] = f"insufficient_candles have={len(ac)} need>={min_need}"
if len(rows) < min_need:
out["reason"] = f"insufficient_candles have={len(rows)} need>={min_need}"
return out
breakout_close = float(ac[-2])
confirm_close = float(ac[-1])
breakout_high = float(ah[-2])
breakout_low = float(al[-2])
try:
open_b = float(rows[-2][1])
except (IndexError, TypeError, ValueError):
open_b = breakout_close
br_row = rows[-2]
cf_row = rows[-1]
open_b = field_float(br_row, 1)
breakout_high = field_float(br_row, 2)
breakout_low = field_float(br_row, 3)
breakout_close = field_float(br_row, 4)
confirm_close = field_float(cf_row, 4)
if None in (open_b, breakout_high, breakout_low, breakout_close, confirm_close):
out["reason"] = "invalid_breakout_or_confirm_ohlc"
return out
prev_vol = av[-vol_lb - 2 : -2]
open_b = float(open_b)
breakout_high = float(breakout_high)
breakout_low = float(breakout_low)
breakout_close = float(breakout_close)
confirm_close = float(confirm_close)
hist = rows[-vol_lb - 2 : -2]
prev_vol = [_row_vol(x) for x in hist if _row_vol(x) > 0]
avg20 = mean(prev_vol) if prev_vol else 0.0
vol_break = float(av[-2])
vol_break = _row_vol(br_row)
vol_ok = vol_break > avg20 * volume_ratio_min if avg20 > 0 else False
amp_pct = abs(breakout_close - open_b) / open_b * 100 if open_b > 0 else 0.0
amp_ok = (amp_pct > breakout_amp_min_pct) and (amp_pct < breakout_amp_max_pct)
direction = (direction or "long").strip().lower()
edge = float(upper) if direction == "long" else float(lower)
breakout_ok = (breakout_close > float(upper)) if direction == "long" else (breakout_close < float(lower))
confirm_ok_raw = (confirm_close > edge) if direction == "long" else (confirm_close < edge)
amp_ok = amp_ok and breakout_ok
confirm_ok = confirm_ok_raw and breakout_ok
edge_ref = edge if edge > 0 else 1.0
if direction == "long":
breach_pct = (breakout_close - edge) / edge_ref * 100.0 if breakout_close > edge else 0.0
breakout_ok = breakout_close > float(upper)
confirm_ok = confirm_close > edge
else:
breach_pct = (edge - breakout_close) / edge_ref * 100.0 if breakout_close < edge else 0.0
breakout_ok = breakout_close < float(lower)
confirm_ok = confirm_close < edge
body_pct = abs(breakout_close - open_b) / open_b * 100.0 if open_b > 0 else 0.0
amp_ok = (
breakout_ok
and breach_pct > breakout_amp_min_pct
and breach_pct < breakout_amp_max_pct
)
confirm_ok = confirm_ok and breakout_ok
rank_ok = (volume_rank is not None) and (int(volume_rank) <= volume_rank_max)
try:
seg = rows[-48:] if len(rows) >= 48 else rows
hh = max(float(x[2]) for x in seg)
ll = min(float(x[3]) for x in seg)
hh = max(float(x[2]) for x in seg if field_float(x, 2) is not None)
ll = min(float(x[3]) for x in seg if field_float(x, 3) is not None)
swing4h_pct = ((hh - ll) / ll * 100.0) if ll > 0 else 0.0
except Exception:
swing4h_pct = 0.0
@@ -71,7 +98,9 @@ def key_hard_checks_from_rows(
"avg20": avg20,
"vol_break": vol_break,
"amp_ok": amp_ok,
"amp_pct": amp_pct,
"amp_pct": breach_pct,
"breach_pct": breach_pct,
"body_pct": body_pct,
"breakout_ok": breakout_ok,
"breakout_close": breakout_close,
"confirm_ok": confirm_ok,
@@ -82,18 +111,27 @@ def key_hard_checks_from_rows(
"rank_ok": rank_ok,
"breakout_high": breakout_high,
"breakout_low": breakout_low,
"swing4h_pct": swing4h_pct,
"breakout_open": open_b,
"direction": direction,
"swing4h_pct": swing4h_pct,
}
)
return out
def key_hard_lines_from_checks(checks: dict, *, volume_ratio_min: float) -> list[str]:
breach = float(checks.get("breach_pct") if checks.get("breach_pct") is not None else checks.get("amp_pct") or 0)
body = float(checks.get("body_pct") or 0)
br_hi = checks.get("breakout_high")
br_lo = checks.get("breakout_low")
return [
f"量能:{'通过' if checks.get('vol_ok') else '不通过'}(突破K量 {round(float(checks.get('vol_break') or 0), 4)} / 前20均量 {round(float(checks.get('avg20') or 0), 4)},阈值{volume_ratio_min:g}x",
f"突破价位:{'通过' if checks.get('breakout_ok') else '不通过'}(突破K收盘 {round(float(checks.get('breakout_close') or 0), 8)},关键位 {checks.get('edge_price')}",
f"突破K幅度:{'通过' if checks.get('amp_ok') else '不通过'}{round(float(checks.get('amp_pct') or 0), 4)}%,要求0.03%~0.5%",
(
f"突破越过关键位:{'通过' if checks.get('amp_ok') else '不通过'}"
f"(越过 {round(breach, 4)}%K线实体 {round(body, 4)}%,要求越过 0.03%~0.5%"
),
f"第二根确认:{'通过' if checks.get('confirm_ok') else '不通过'}(确认收盘 {checks.get('confirm_close')},关键位 {checks.get('edge_price')}",
f"日成交量排名:{'通过' if checks.get('rank_ok') else '不通过'}{checks.get('rank')}/{checks.get('rank_total')},要求前30",
f"突破K极值:高 {br_hi}|低 {br_lo}(止损据此 ± 外扩%",
]
+1 -1
View File
@@ -553,7 +553,7 @@ class MonitorService:
},
)
if result.signal_level == "TRIGGER":
if strict_push_ok:
if strict_push_ok and self.settings.monitor.push_watch_trigger_wecom:
push_metrics = dict(result.metrics)
push_metrics["signal_side"] = signal_side
push_metrics["btc_bias"] = btc_bias_5m
+2
View File
@@ -530,6 +530,7 @@ def create_app(settings: Settings) -> FastAPI:
km = settings.key_monitor
return (
f"周期 5m|突破K/确认K:倒数第2/第1根闭合K|量能:突破K量 > 前{km.volume_ma_bars}均量×{km.volume_ratio_min}"
f"突破越过关键位 {km.breakout_amp_min_pct:g}%~{km.breakout_amp_max_pct:g}%(非K线实体)|"
f"计划RR须 > {km.min_planned_rr:g}|日成交额排名前{km.daily_volume_rank_max}"
f"箱体/收敛方案:标准突破(止损突破K外{km.standard_stop_outside_pct:g}%|止盈1×H)或 "
f"趋势突破(止损突破K外{km.trend_stop_outside_pct:g}%|止盈手填)|"
@@ -721,6 +722,7 @@ def create_app(settings: Settings) -> FastAPI:
"btc_sideways_max_range_pct": settings.monitor.btc_sideways_max_range_pct,
"symbol_signal_dedupe_hours": settings.monitor.symbol_signal_dedupe_hours,
"wecom_push_max_volume_rank": settings.monitor.wecom_push_max_volume_rank,
"push_watch_trigger_wecom": settings.monitor.push_watch_trigger_wecom,
"gemma": {
"enabled": g.enabled,
"ollama_base_url": g.ollama_base_url,
+2
View File
@@ -64,6 +64,8 @@ monitor:
symbol_signal_dedupe_hours: 4
# 企业微信主推送:仅成交量排名前 N;0 表示不限制
wecom_push_max_volume_rank: 30
# 全市场自动箱体 WATCH/TRIGGER 企微(默认 false;只保留 GEMMA 漏斗推送 + 关键位推送)
push_watch_trigger_wecom: false
# 仅在 universe=watchlist 时使用;all_swaps 下可留空列表
watch_symbols: []
+82
View File
@@ -0,0 +1,82 @@
from __future__ import annotations
import unittest
from app.key_gate import key_hard_checks_from_rows
from app.key_sl_tp import plan_key_sl_tp
def _row(ts: int, o: float, h: float, l: float, c: float, v: float = 1000.0) -> list[str]:
return [str(ts), str(o), str(h), str(l), str(c), str(v)]
def _history(n: int, *, base_ts: int = 1_700_000_000_000, vol: float = 1000.0) -> list[list[str]]:
return [_row(base_ts + i * 300_000, 84.0, 84.2, 83.8, 84.0, vol) for i in range(n)]
class TestKeyGate(unittest.TestCase):
def test_short_breach_too_far_fails(self):
"""SOL 类:收盘远低于下沿,越过 >0.5% 应拒绝(不再用 K 线实体误判)。"""
rows = _history(22, vol=40000.0)
rows.append(_row(1, 83.0, 83.55, 82.0, 82.19, 76791.0)) # 突破 K
rows.append(_row(2, 82.1, 82.3, 81.9, 82.02, 50000.0)) # 确认 K
checks = key_hard_checks_from_rows(
rows,
direction="short",
upper=87.8,
lower=83.28,
volume_rank=3,
volume_rank_total=721,
)
breach = (83.28 - 82.19) / 83.28 * 100
self.assertGreater(breach, 0.5)
self.assertFalse(checks["amp_ok"])
self.assertFalse(checks["ok"])
def test_short_breach_in_band_passes_amp(self):
rows = _history(22, vol=40000.0)
# 越过下沿约 0.12%83.28 -> 83.18
rows.append(_row(1, 83.4, 83.5, 83.1, 83.18, 76791.0))
rows.append(_row(2, 83.15, 83.2, 83.0, 83.10, 50000.0))
checks = key_hard_checks_from_rows(
rows,
direction="short",
upper=87.8,
lower=83.28,
volume_rank=3,
volume_rank_total=721,
)
self.assertTrue(checks["amp_ok"])
self.assertAlmostEqual(checks["breach_pct"], (83.28 - 83.18) / 83.28 * 100, places=4)
def test_trend_short_sl_uses_breakout_high(self):
rows = _history(22, vol=40000.0)
br_hi = 83.55
rows.append(_row(1, 83.0, br_hi, 82.0, 82.19, 76791.0))
rows.append(_row(2, 82.1, 82.3, 81.9, 82.02, 50000.0))
checks = key_hard_checks_from_rows(
rows,
direction="short",
upper=87.8,
lower=83.28,
volume_rank=3,
volume_rank_total=721,
)
self.assertEqual(checks["breakout_high"], br_hi)
plan = plan_key_sl_tp(
"trend_manual",
"short",
87.8,
83.28,
checks,
outside_pct=0.3,
trend_outside_pct=1.0,
manual_take_profit=78.7,
)
self.assertIsNotNone(plan)
_, sl, _, _ = plan # type: ignore[misc]
self.assertAlmostEqual(sl, br_hi * 1.01, places=4)
if __name__ == "__main__":
unittest.main()