修复bug

This commit is contained in:
dekun
2026-05-18 08:10:28 +08:00
parent 24550b0ce0
commit 82a7237063
8 changed files with 132 additions and 58 deletions
+3 -13
View File
@@ -3,6 +3,8 @@ from __future__ import annotations
from dataclasses import dataclass, field
from statistics import mean
from .candle_rows import rows_to_hlc
@dataclass
class BtcDailyGateResult:
@@ -14,18 +16,6 @@ class BtcDailyGateResult:
metrics: dict = field(default_factory=dict)
def _rows_to_hlc(rows: list[list[str]]) -> tuple[list[float], list[float], list[float]]:
"""与行情 K 线行对齐:h, l, cts,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]],
*,
@@ -39,7 +29,7 @@ def evaluate_btc_daily_gate(
- 下跌(唯一不扫):非横盘,且收盘低于近 20 日收盘均线,且该均线相对前一段走低。
- 其余(横盘、上涨、宽幅震荡、数据不足 unknown 等):一律允许扫山寨。
"""
ah, al, ac = _rows_to_hlc(btc_1d_rows)
ah, al, ac = rows_to_hlc(btc_1d_rows)
if len(ac) < min_bars:
return BtcDailyGateResult(
+91
View File
@@ -0,0 +1,91 @@
from __future__ import annotations
def field_float(item: list[str], idx: int) -> float | None:
"""Parse one K-line field; empty or invalid strings return None."""
if idx >= len(item):
return None
raw = str(item[idx]).strip()
if not raw:
return None
try:
return float(raw)
except (TypeError, ValueError):
return None
def rows_to_ohlcv(
rows: list[list[str]],
*,
require_volume: bool = True,
) -> tuple[list[float], list[float], list[float], list[float], list[float]]:
"""
Gate candle rows: [ts_ms, o, h, l, c, v, ...].
Skips rows with missing/invalid OHLC; skips rows with missing volume when require_volume=True.
"""
o, h, l, c, v = [], [], [], [], []
for item in rows:
if len(item) < 5:
continue
o_val = field_float(item, 1)
h_val = field_float(item, 2)
l_val = field_float(item, 3)
c_val = field_float(item, 4)
if o_val is None or h_val is None or l_val is None or c_val is None:
continue
vol_val = field_float(item, 5) if len(item) > 5 else None
if require_volume and vol_val is None:
continue
o.append(o_val)
h.append(h_val)
l.append(l_val)
c.append(c_val)
v.append(vol_val if vol_val is not None else 0.0)
return o, h, l, c, v
def rows_to_hlc(rows: list[list[str]]) -> tuple[list[float], list[float], list[float]]:
"""High / low / close series aligned to valid OHLC rows."""
h, l_, c = [], [], []
for item in rows:
if len(item) < 5:
continue
h_val = field_float(item, 2)
l_val = field_float(item, 3)
c_val = field_float(item, 4)
if h_val is None or l_val is None or c_val is None:
continue
h.append(h_val)
l_.append(l_val)
c.append(c_val)
return h, l_, c
def rows_to_ohl(rows: list[list[str]]) -> tuple[list[float], list[float], list[float], list[float]]:
"""Open / high / low / close for chart rendering."""
o, h, l, c = [], [], [], []
for item in rows:
if len(item) < 5:
continue
o_val = field_float(item, 1)
h_val = field_float(item, 2)
l_val = field_float(item, 3)
c_val = field_float(item, 4)
if o_val is None or h_val is None or l_val is None or c_val is None:
continue
o.append(o_val)
h.append(h_val)
l.append(l_val)
c.append(c_val)
return o, h, l, c
def rows_to_close(rows: list[list[str]]) -> list[float]:
out: list[float] = []
for item in rows:
if len(item) < 5:
continue
c_val = field_float(item, 4)
if c_val is not None:
out.append(c_val)
return out
+3 -8
View File
@@ -4,6 +4,8 @@ import base64
import io
import logging
from .candle_rows import rows_to_ohl
LOGGER = logging.getLogger("onchain_scout.chart_candles")
@@ -22,14 +24,7 @@ def daily_candles_png_base64(rows_1d: list[list[str]], symbol: str, max_bars: in
LOGGER.warning("matplotlib not installed, skip chart image")
return None
o, h, l, c, _ = [], [], [], [], []
for item in rows_1d:
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]))
o, h, l, c = rows_to_ohl(rows_1d)
n = len(c)
if n < 5:
return None
+1 -12
View File
@@ -3,18 +3,7 @@ from __future__ import annotations
import math
from statistics import mean
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
from .candle_rows import rows_to_ohlcv
def build_daily_programmatic(rows_1d: list[list[str]], est_quote_vol_24h_usdt: float) -> dict:
+2 -10
View File
@@ -7,6 +7,7 @@ from statistics import mean
from typing import TYPE_CHECKING
from zoneinfo import ZoneInfo
from .candle_rows import rows_to_close
from .config import Settings
from .notifier import WeComNotifier
from .gate import GateClient
@@ -20,15 +21,6 @@ CN_TZ = ZoneInfo("Asia/Shanghai")
BTC_INST = "BTC_USDT"
def _rows_to_close(rows: list[list[str]]) -> list[float]:
out: list[float] = []
for r in rows:
if len(r) < 5:
continue
out.append(float(r[4]))
return out
def _sma(values: list[float], n: int) -> float:
if not values:
return 0.0
@@ -118,7 +110,7 @@ class DailyReportService:
funnel_push_count += 1
btc_rows = await self.gate.get_candles(BTC_INST, "1D", limit=100)
closes = _rows_to_close(btc_rows)
closes = rows_to_close(btc_rows)
last_close = closes[-1] if closes else 0.0
prev_close = closes[-2] if len(closes) >= 2 else last_close
day_change_pct = ((last_close - prev_close) / prev_close * 100.0) if prev_close else 0.0
+3 -14
View File
@@ -3,6 +3,8 @@ 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
@@ -29,19 +31,6 @@ class ExchangeRuleResult:
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]],
@@ -56,7 +45,7 @@ def evaluate_exchange(
"""
breakout_max_pct = 0.5
result = ExchangeRuleResult()
_, ah, al, ac, av = _rows_to_ohlcv(alt_rows)
_, ah, al, ac, av = rows_to_ohlcv(alt_rows)
bars_for_range = max(
MIN_BOX_LOOKBACK_BARS_5M,
+6 -1
View File
@@ -460,7 +460,12 @@ class MonitorService:
await self.storage.add_log("WARN", f"{sym} candles_failed: {exc}")
continue
result = evaluate_exchange(sym, alt_rows, btc_rows, rule_params)
try:
result = evaluate_exchange(sym, alt_rows, btc_rows, rule_params)
except Exception as exc: # noqa: BLE001
await self.storage.add_log("WARN", f"{sym} evaluate_failed: {exc}")
continue
if result.signal_level in {"WATCH", "TRIGGER"}:
est_vol = float(vol_map.get(inst, 0.0)) if vol_map else 0.0
signal_side = str((result.metrics or {}).get("signal_side") or result.signal_side or "NONE")
@@ -0,0 +1,23 @@
from app.candle_rows import rows_to_ohlcv
def test_rows_to_ohlcv_skips_empty_volume() -> None:
rows = [
["1", "100", "101", "99", "100.5", "123"],
["2", "100.5", "102", "100", "101", ""],
["3", "101", "103", "100.5", "102", "50"],
]
o, h, l, c, v = rows_to_ohlcv(rows)
assert len(c) == 2
assert c == [100.5, 102.0]
assert v == [123.0, 50.0]
def test_rows_to_ohlcv_skips_invalid_ohlc() -> None:
rows = [
["1", "", "101", "99", "100.5", "10"],
["2", "100", "101", "99", "100.5", "20"],
]
_, _, _, c, v = rows_to_ohlcv(rows)
assert c == [100.5]
assert v == [20.0]