修复bug
This commit is contained in:
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from statistics import mean
|
from statistics import mean
|
||||||
|
|
||||||
|
from .candle_rows import rows_to_hlc
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BtcDailyGateResult:
|
class BtcDailyGateResult:
|
||||||
@@ -14,18 +16,6 @@ class BtcDailyGateResult:
|
|||||||
metrics: dict = field(default_factory=dict)
|
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(
|
def evaluate_btc_daily_gate(
|
||||||
btc_1d_rows: list[list[str]],
|
btc_1d_rows: list[list[str]],
|
||||||
*,
|
*,
|
||||||
@@ -39,7 +29,7 @@ def evaluate_btc_daily_gate(
|
|||||||
- 下跌(唯一不扫):非横盘,且收盘低于近 20 日收盘均线,且该均线相对前一段走低。
|
- 下跌(唯一不扫):非横盘,且收盘低于近 20 日收盘均线,且该均线相对前一段走低。
|
||||||
- 其余(横盘、上涨、宽幅震荡、数据不足 unknown 等):一律允许扫山寨。
|
- 其余(横盘、上涨、宽幅震荡、数据不足 unknown 等):一律允许扫山寨。
|
||||||
"""
|
"""
|
||||||
ah, al, ac = _rows_to_hlc(btc_1d_rows)
|
ah, al, ac = rows_to_hlc(btc_1d_rows)
|
||||||
|
|
||||||
if len(ac) < min_bars:
|
if len(ac) < min_bars:
|
||||||
return BtcDailyGateResult(
|
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 io
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from .candle_rows import rows_to_ohl
|
||||||
|
|
||||||
LOGGER = logging.getLogger("onchain_scout.chart_candles")
|
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")
|
LOGGER.warning("matplotlib not installed, skip chart image")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
o, h, l, c, _ = [], [], [], [], []
|
o, h, l, c = rows_to_ohl(rows_1d)
|
||||||
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]))
|
|
||||||
n = len(c)
|
n = len(c)
|
||||||
if n < 5:
|
if n < 5:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -3,18 +3,7 @@ from __future__ import annotations
|
|||||||
import math
|
import math
|
||||||
from statistics import mean
|
from statistics import mean
|
||||||
|
|
||||||
|
from .candle_rows import rows_to_ohlcv
|
||||||
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 build_daily_programmatic(rows_1d: list[list[str]], est_quote_vol_24h_usdt: float) -> dict:
|
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 typing import TYPE_CHECKING
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
from .candle_rows import rows_to_close
|
||||||
from .config import Settings
|
from .config import Settings
|
||||||
from .notifier import WeComNotifier
|
from .notifier import WeComNotifier
|
||||||
from .gate import GateClient
|
from .gate import GateClient
|
||||||
@@ -20,15 +21,6 @@ CN_TZ = ZoneInfo("Asia/Shanghai")
|
|||||||
BTC_INST = "BTC_USDT"
|
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:
|
def _sma(values: list[float], n: int) -> float:
|
||||||
if not values:
|
if not values:
|
||||||
return 0.0
|
return 0.0
|
||||||
@@ -118,7 +110,7 @@ class DailyReportService:
|
|||||||
funnel_push_count += 1
|
funnel_push_count += 1
|
||||||
|
|
||||||
btc_rows = await self.gate.get_candles(BTC_INST, "1D", limit=100)
|
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
|
last_close = closes[-1] if closes else 0.0
|
||||||
prev_close = closes[-2] if len(closes) >= 2 else last_close
|
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
|
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 dataclasses import dataclass, field
|
||||||
from statistics import mean
|
from statistics import mean
|
||||||
|
|
||||||
|
from .candle_rows import rows_to_ohlcv
|
||||||
|
|
||||||
# 以下换算仅针对 5m K(与是否单独拉 4h 图无关):
|
# 以下换算仅针对 5m K(与是否单独拉 4h 图无关):
|
||||||
# 每小时 60/5 = 12 根;一根「4 小时」大周期对应 4×12 = 48 根 5m。
|
# 每小时 60/5 = 12 根;一根「4 小时」大周期对应 4×12 = 48 根 5m。
|
||||||
BARS_5M_PER_HOUR = 12
|
BARS_5M_PER_HOUR = 12
|
||||||
@@ -29,19 +31,6 @@ class ExchangeRuleResult:
|
|||||||
metrics: dict = field(default_factory=dict)
|
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(
|
def evaluate_exchange(
|
||||||
symbol: str,
|
symbol: str,
|
||||||
alt_rows: list[list[str]],
|
alt_rows: list[list[str]],
|
||||||
@@ -56,7 +45,7 @@ def evaluate_exchange(
|
|||||||
"""
|
"""
|
||||||
breakout_max_pct = 0.5
|
breakout_max_pct = 0.5
|
||||||
result = ExchangeRuleResult()
|
result = ExchangeRuleResult()
|
||||||
_, ah, al, ac, av = _rows_to_ohlcv(alt_rows)
|
_, ah, al, ac, av = rows_to_ohlcv(alt_rows)
|
||||||
|
|
||||||
bars_for_range = max(
|
bars_for_range = max(
|
||||||
MIN_BOX_LOOKBACK_BARS_5M,
|
MIN_BOX_LOOKBACK_BARS_5M,
|
||||||
|
|||||||
@@ -460,7 +460,12 @@ class MonitorService:
|
|||||||
await self.storage.add_log("WARN", f"{sym} candles_failed: {exc}")
|
await self.storage.add_log("WARN", f"{sym} candles_failed: {exc}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
result = evaluate_exchange(sym, alt_rows, btc_rows, rule_params)
|
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"}:
|
if result.signal_level in {"WATCH", "TRIGGER"}:
|
||||||
est_vol = float(vol_map.get(inst, 0.0)) if vol_map else 0.0
|
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")
|
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