feat(web): 增量自动刷新、结构区修复与默认指标/周期
自动刷新常态只拉 recent 尾部 K,每 1 分钟全量重算缠论;修复结构区缓存导入;默认指标/4h·1h·15m/近30天;同步 ECR-009 screener 相关改动。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+47
-34
@@ -61,12 +61,18 @@ def _compress_phases(points: list[tuple[str, str]]) -> list[dict]:
|
||||
return segs
|
||||
|
||||
|
||||
def annotate_frame(frame: OHLCVFrame, step: int | None = None) -> dict:
|
||||
def annotate_frame(
|
||||
frame: OHLCVFrame,
|
||||
step: int | None = None,
|
||||
*,
|
||||
role: str | None = None,
|
||||
) -> dict:
|
||||
"""Pure annotation: phase bands + event markers + latest levels.
|
||||
|
||||
``step`` defaults by timeframe to keep interactive charts snappy.
|
||||
``role`` is the D/W/M rule alias (1d/1w/1M). Defaults to frame.timeframe.
|
||||
``step`` defaults by role to keep interactive charts snappy.
|
||||
"""
|
||||
tf = frame.timeframe
|
||||
tf = role or frame.timeframe
|
||||
min_bars = _MIN_BARS.get(tf, 30)
|
||||
if step is None:
|
||||
step = {"1d": 2, "1w": 1, "1M": 1}.get(tf, 2)
|
||||
@@ -227,17 +233,21 @@ def annotate_symbol(
|
||||
freq: str,
|
||||
end_date: date | None = None,
|
||||
lookback: int = 180,
|
||||
*,
|
||||
combo_id: str | None = None,
|
||||
) -> dict:
|
||||
"""IO + annotate for one symbol (used by API).
|
||||
|
||||
For daily charts, phase bands come from **weekly** structure (Wyckoff
|
||||
primary timeframe), while event markers / levels come from daily.
|
||||
For the combo *low* chart, phase bands come from **mid** structure,
|
||||
while event markers / levels come from the low TF.
|
||||
"""
|
||||
from crypto_wyckoff.io import latest_daily_trade_date, load_frames_batch
|
||||
from crypto_wyckoff.combos import ROLE_HIGH, ROLE_LOW, ROLE_MID, get_combo
|
||||
from crypto_wyckoff.io import load_frame
|
||||
|
||||
if freq not in ("1d", "1w", "1M"):
|
||||
raise ValueError(f"unsupported freq: {freq}")
|
||||
ed = end_date or latest_daily_trade_date()
|
||||
combo = get_combo(combo_id)
|
||||
allowed = {combo["low"], combo["mid"], combo["high"]}
|
||||
if freq not in allowed:
|
||||
raise ValueError(f"freq {freq} not in combo {combo['id']} ({combo['label']})")
|
||||
empty = {
|
||||
"ts_code": ts_code,
|
||||
"freq": freq,
|
||||
@@ -247,25 +257,22 @@ def annotate_symbol(
|
||||
"zones": [],
|
||||
"bars": 0,
|
||||
"phase_source": freq,
|
||||
"cycles": [],
|
||||
"combo_id": combo["id"],
|
||||
}
|
||||
if ed is None:
|
||||
return empty
|
||||
_ = end_date
|
||||
|
||||
if freq == "1d":
|
||||
daily_frames = load_frames_batch("1d", ed, lookback, ts_codes=[ts_code])
|
||||
weekly_frames = load_frames_batch("1w", ed, max(60, lookback // 3), ts_codes=[ts_code])
|
||||
daily = daily_frames.get(ts_code)
|
||||
weekly = weekly_frames.get(ts_code)
|
||||
if daily is None:
|
||||
if freq == combo["low"]:
|
||||
low = load_frame(ts_code, combo["low"], lookback)
|
||||
mid = load_frame(ts_code, combo["mid"], max(60, lookback // 3))
|
||||
if low is None:
|
||||
return empty
|
||||
d_ann = annotate_frame(daily)
|
||||
w_ann = annotate_frame(weekly) if weekly is not None else {"phases": []}
|
||||
cycles = _cycle_segments(weekly) if weekly is not None else []
|
||||
d_ann = annotate_frame(low, role=ROLE_LOW)
|
||||
w_ann = annotate_frame(mid, role=ROLE_MID) if mid is not None else {"phases": []}
|
||||
cycles = _cycle_segments(mid, role=ROLE_MID) if mid is not None else []
|
||||
levels = d_ann.get("levels") or {}
|
||||
# Prefer weekly cycle on the latest levels for zone labeling
|
||||
if cycles:
|
||||
levels = {**levels, "cycle": cycles[-1].get("cycle") or levels.get("cycle")}
|
||||
# latest non-None weekly phase
|
||||
for p in reversed(w_ann.get("phases") or []):
|
||||
if p.get("phase") not in (None, "None"):
|
||||
levels = {**levels, "phase": p["phase"]}
|
||||
@@ -273,29 +280,30 @@ def annotate_symbol(
|
||||
return {
|
||||
"ts_code": ts_code,
|
||||
"freq": freq,
|
||||
"end_date": ed.isoformat(),
|
||||
"end_date": low.trade_dates[-1].isoformat() if low.trade_dates else None,
|
||||
"phases": w_ann.get("phases") or [],
|
||||
"events": d_ann.get("events") or [],
|
||||
"levels": d_ann.get("levels") or {},
|
||||
"zones": _build_range_zones(daily, cycles, levels),
|
||||
"zones": _build_range_zones(low, cycles, levels),
|
||||
"bars": d_ann.get("bars", 0),
|
||||
"phase_source": "1w",
|
||||
"phase_source": combo["mid"],
|
||||
"cycles": cycles,
|
||||
"combo_id": combo["id"],
|
||||
}
|
||||
|
||||
frames = load_frames_batch(freq, ed, lookback, ts_codes=[ts_code])
|
||||
frame = frames.get(ts_code)
|
||||
role = ROLE_MID if freq == combo["mid"] else ROLE_HIGH
|
||||
frame = load_frame(ts_code, freq, lookback)
|
||||
if frame is None:
|
||||
return empty
|
||||
out = annotate_frame(frame)
|
||||
out = annotate_frame(frame, role=role)
|
||||
out["ts_code"] = ts_code
|
||||
out["freq"] = freq
|
||||
out["end_date"] = ed.isoformat()
|
||||
out["end_date"] = frame.trade_dates[-1].isoformat() if frame.trade_dates else None
|
||||
out["phase_source"] = freq
|
||||
out["cycles"] = _cycle_segments(frame)
|
||||
out["cycles"] = _cycle_segments(frame, role=ROLE_HIGH if role == ROLE_HIGH else ROLE_MID)
|
||||
out["zones"] = _build_range_zones(frame, out["cycles"], out.get("levels") or {})
|
||||
if freq == "1M":
|
||||
# Monthly chart: cycle bands are more meaningful than phase
|
||||
out["combo_id"] = combo["id"]
|
||||
if role == ROLE_HIGH:
|
||||
if not any(p.get("phase") not in (None, "None") for p in out["phases"]):
|
||||
out["phases"] = [
|
||||
{"start": c["start"], "end": c["end"], "phase": c["cycle"]}
|
||||
@@ -305,9 +313,14 @@ def annotate_symbol(
|
||||
return out
|
||||
|
||||
|
||||
def _cycle_segments(frame: OHLCVFrame, step: int | None = None) -> list[dict]:
|
||||
def _cycle_segments(
|
||||
frame: OHLCVFrame,
|
||||
step: int | None = None,
|
||||
*,
|
||||
role: str | None = None,
|
||||
) -> list[dict]:
|
||||
"""Walk-forward cycle labels compressed to segments."""
|
||||
tf = frame.timeframe
|
||||
tf = role or frame.timeframe
|
||||
min_bars = _MIN_BARS.get(tf, 30)
|
||||
if step is None:
|
||||
step = {"1d": 3, "1w": 1, "1M": 1}.get(tf, 2)
|
||||
|
||||
Reference in New Issue
Block a user