修复bug
This commit is contained in:
@@ -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, c(ts,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(
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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]
|
||||
Reference in New Issue
Block a user