feat(web): 增量自动刷新、结构区修复与默认指标/周期

自动刷新常态只拉 recent 尾部 K,每 1 分钟全量重算缠论;修复结构区缓存导入;默认指标/4h·1h·15m/近30天;同步 ECR-009 screener 相关改动。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-08 15:45:40 +08:00
co-authored by Cursor
parent 0f6eb92a1f
commit 18a7f485e6
23 changed files with 2133 additions and 310 deletions
+92 -30
View File
@@ -28,10 +28,22 @@ DATA_SERVICE_URL = os.environ.get(
).rstrip("/")
# Continuous crypto: bar counts (not A-share weekend-padded calendar multipliers)
# Provider has 1d/1w but no 1M — monthly is resampled locally from daily UTC months.
LOOKBACK = {"1d": 250, "1w": 104, "1M": 60}
TF_PROVIDER = ("1d", "1w")
# Provider has many TFs; 1M is resampled locally from daily UTC months.
LOOKBACK = {
"1h": 500,
"2h": 400,
"4h": 300,
"6h": 280,
"8h": 250,
"12h": 220,
"1d": 250,
"1w": 104,
"1M": 60,
}
# Default D/W/M stack (kept for compat); combos may request more TFs from provider.
TF_PROVIDER = ("1h", "4h", "8h", "1d", "1w")
TF_LIST = ("1d", "1w", "1M")
LOCAL_ONLY_TFS = frozenset({"1M"})
def ensure_dirs() -> None:
@@ -125,7 +137,29 @@ def upsert_bars(symbol: str, tf: str, rows: list[dict]) -> int:
conn.close()
def load_frame(symbol: str, tf: str, lookback: int | None = None) -> OHLCVFrame | None:
def is_intraday_tf(tf: str) -> bool:
"""True for minute/hour TFs that need clock time on charts."""
t = (tf or "").strip()
return t.endswith("m") or t.endswith("h")
def load_bars_with_ts(
symbol: str, tf: str, lookback: int | None = None
) -> list[dict]:
"""Return OHLCV rows with UTC ms ts (for chart labels).
``datetime`` is wall-clock in Asia/Shanghai (UTC+8) for display.
"""
from zoneinfo import ZoneInfo
tz_cn = ZoneInfo("Asia/Shanghai")
if lookback is None:
try:
from crypto_wyckoff.combos import lookback_for
lookback = lookback_for(tf)
except Exception:
lookback = LOOKBACK.get(tf, 100)
lookback = lookback or LOOKBACK.get(tf, 100)
conn = _bars_conn()
try:
@@ -140,20 +174,40 @@ def load_frame(symbol: str, tf: str, lookback: int | None = None) -> OHLCVFrame
rows = list(reversed(cur.fetchall()))
finally:
conn.close()
out = []
for ts, o, h, l, c, v in rows:
dt_utc = datetime.fromtimestamp(ts / 1000.0, tz=timezone.utc)
dt_cn = dt_utc.astimezone(tz_cn)
out.append(
{
"ts": int(ts),
"datetime": dt_cn.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
"date": dt_cn.strftime("%Y-%m-%d"),
"open": o,
"high": h,
"low": l,
"close": c,
"volume": v,
}
)
return out
def load_frame(symbol: str, tf: str, lookback: int | None = None) -> OHLCVFrame | None:
rows = load_bars_with_ts(symbol, tf, lookback)
if not rows:
return None
trade_dates: list[date] = []
for ts, *_ in rows:
trade_dates.append(datetime.fromtimestamp(ts / 1000.0, tz=timezone.utc).date())
return OHLCVFrame(
ts_code=symbol,
timeframe=tf,
trade_dates=trade_dates,
open=[r[1] for r in rows],
high=[r[2] for r in rows],
low=[r[3] for r in rows],
close=[r[4] for r in rows],
volume=[r[5] for r in rows],
trade_dates=[
datetime.fromtimestamp(r["ts"] / 1000.0, tz=timezone.utc).date() for r in rows
],
open=[r["open"] for r in rows],
high=[r["high"] for r in rows],
low=[r["low"] for r in rows],
close=[r["close"] for r in rows],
volume=[r["volume"] for r in rows],
)
@@ -219,14 +273,18 @@ def rebuild_monthly_from_daily(symbol: str) -> int:
def backfill_symbol(symbol: str, tfs: Iterable[str] = TF_LIST) -> dict:
"""Pull history for continuous crypto TFs; monthly derived from daily."""
stats = {}
for tf in TF_PROVIDER:
if tf not in tfs and "1M" not in tfs:
"""Pull history for requested TFs; monthly derived from daily when needed."""
wanted = list(dict.fromkeys(tfs))
stats: dict = {}
need_monthly = "1M" in wanted
if need_monthly and "1d" not in wanted:
wanted = ["1d", *wanted]
for tf in wanted:
if tf in LOCAL_ONLY_TFS:
continue
need = LOOKBACK.get(tf, 100)
# need extra daily for monthly history
if tf == "1d":
if tf == "1d" and need_monthly:
need = max(need, LOOKBACK["1M"] * 31)
try:
rows = fetch_candles(symbol, tf, limit=need)
@@ -236,7 +294,8 @@ def backfill_symbol(symbol: str, tfs: Iterable[str] = TF_LIST) -> dict:
logger.warning("backfill %s %s failed: %s", symbol, tf, e)
stats[tf] = 0
time.sleep(0.05)
if "1M" in tfs or True:
if need_monthly:
try:
stats["1M"] = rebuild_monthly_from_daily(symbol)
except Exception as e:
@@ -247,8 +306,11 @@ def backfill_symbol(symbol: str, tfs: Iterable[str] = TF_LIST) -> dict:
def tip_update_symbol(symbol: str, tfs: Iterable[str] = TF_LIST) -> bool:
"""Update forming tip bars (limit=3). Returns True if any bar changed."""
wanted = list(dict.fromkeys(tfs))
changed = False
for tf in TF_PROVIDER:
for tf in wanted:
if tf in LOCAL_ONLY_TFS:
continue
try:
rows = fetch_candles(symbol, tf, limit=3)
if not rows:
@@ -261,15 +323,15 @@ def tip_update_symbol(symbol: str, tfs: Iterable[str] = TF_LIST) -> bool:
except Exception as e:
logger.debug("tip %s %s: %s", symbol, tf, e)
time.sleep(0.02)
# Always rebuild current month tip from daily
before_m = _tip_fingerprint(symbol, "1M")
try:
rebuild_monthly_from_daily(symbol)
except Exception as e:
logger.debug("monthly tip %s: %s", symbol, e)
after_m = _tip_fingerprint(symbol, "1M")
if before_m != after_m:
changed = True
if "1M" in wanted:
before_m = _tip_fingerprint(symbol, "1M")
try:
rebuild_monthly_from_daily(symbol)
except Exception as e:
logger.debug("monthly tip %s: %s", symbol, e)
after_m = _tip_fingerprint(symbol, "1M")
if before_m != after_m:
changed = True
return changed