feat(web): 增量自动刷新、结构区修复与默认指标/周期
自动刷新常态只拉 recent 尾部 K,每 1 分钟全量重算缠论;修复结构区缓存导入;默认指标/4h·1h·15m/近30天;同步 ECR-009 screener 相关改动。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+92
-30
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user