feat(hub): add open/close arrows on archive chart with continuous klines
Span chart window across hold period, fill 5m gaps for smooth aggregation, and mark entry/exit with lightweight-charts arrows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -537,12 +537,71 @@ def _to_candles(bars: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
return out
|
||||
|
||||
|
||||
def _snap_to_bar_grid(ts_ms: int, origin_ms: int, step_ms: int) -> int:
|
||||
step = max(1, int(step_ms))
|
||||
origin = int(origin_ms)
|
||||
if ts_ms <= origin:
|
||||
return origin
|
||||
idx = (int(ts_ms) - origin + step - 1) // step
|
||||
return origin + idx * step
|
||||
|
||||
|
||||
def _fill_missing_bars(
|
||||
bars: list[dict[str, Any]],
|
||||
period_ms: int,
|
||||
start_ms: int,
|
||||
end_ms: int,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""5m 缺口用上一根收盘价填平,保证聚合后 K 线时间轴连续。"""
|
||||
by_ts: dict[int, dict[str, Any]] = {}
|
||||
for b in bars or []:
|
||||
try:
|
||||
by_ts[int(b["open_time_ms"])] = b
|
||||
except (KeyError, TypeError, ValueError):
|
||||
continue
|
||||
if not by_ts:
|
||||
return []
|
||||
keys = sorted(by_ts.keys())
|
||||
step_ms = max(1, int(period_ms))
|
||||
origin = keys[0]
|
||||
aligned_start = _snap_to_bar_grid(int(start_ms), origin, step_ms)
|
||||
aligned_end = max(int(end_ms), keys[-1])
|
||||
out: list[dict[str, Any]] = []
|
||||
last: dict[str, Any] | None = None
|
||||
for ts_key in keys:
|
||||
if ts_key <= aligned_start:
|
||||
last = by_ts[ts_key]
|
||||
ts = aligned_start
|
||||
while ts <= aligned_end:
|
||||
cur = by_ts.get(ts)
|
||||
if cur is not None:
|
||||
last = cur
|
||||
out.append(cur)
|
||||
elif last is not None:
|
||||
c = float(last["close"])
|
||||
out.append(
|
||||
{
|
||||
"open_time_ms": ts,
|
||||
"open": c,
|
||||
"high": c,
|
||||
"low": c,
|
||||
"close": c,
|
||||
"volume": 0.0,
|
||||
"filled": True,
|
||||
}
|
||||
)
|
||||
ts += step_ms
|
||||
return out
|
||||
|
||||
|
||||
def resolve_archive_chart(
|
||||
exchange_key: str,
|
||||
symbol: str,
|
||||
timeframe: str = ARCHIVE_DEFAULT_TIMEFRAME,
|
||||
*,
|
||||
anchor_ms: int | None = None,
|
||||
opened_ms: int | None = None,
|
||||
closed_ms: int | None = None,
|
||||
mode: str = "hold",
|
||||
bars: int = ARCHIVE_VISIBLE_BARS_DEFAULT,
|
||||
db_path: Path | None = None,
|
||||
@@ -557,20 +616,40 @@ def resolve_archive_chart(
|
||||
return {"ok": False, "msg": "缺少 exchange_key 或 symbol"}
|
||||
|
||||
period = TIMEFRAME_MS[tf]
|
||||
visible = max(50, min(int(bars or ARCHIVE_VISIBLE_BARS_DEFAULT), 500))
|
||||
anchor = int(anchor_ms) if anchor_ms else _now_ms()
|
||||
half = visible // 2
|
||||
start_ms = max(0, anchor - half * period)
|
||||
end_ms = anchor + half * period
|
||||
period_5m = TIMEFRAME_MS["5m"]
|
||||
hold_open = int(opened_ms) if opened_ms else None
|
||||
hold_close = int(closed_ms) if closed_ms else None
|
||||
if hold_open and hold_close and hold_close >= hold_open:
|
||||
hold_len = hold_close - hold_open
|
||||
pad = max(period * 24, hold_len // 3, period_5m * 12)
|
||||
start_ms = max(0, hold_open - pad)
|
||||
end_ms = hold_close + pad
|
||||
visible = int((end_ms - start_ms) / period) + 8
|
||||
visible = max(50, min(visible, 500))
|
||||
anchor = hold_close if (mode or "hold").strip().lower() != "entry" else hold_open
|
||||
else:
|
||||
visible = max(50, min(int(bars or ARCHIVE_VISIBLE_BARS_DEFAULT), 500))
|
||||
anchor = int(anchor_ms) if anchor_ms else _now_ms()
|
||||
half = visible // 2
|
||||
start_ms = max(0, anchor - half * period)
|
||||
end_ms = anchor + half * period
|
||||
|
||||
raw_5m = load_bars_5m_range(ex_k, sym, start_ms - period * 3, end_ms + period * 3, db_path=db_path)
|
||||
raw_5m = load_bars_5m_range(
|
||||
ex_k,
|
||||
sym,
|
||||
start_ms - period_5m * 6,
|
||||
end_ms + period_5m * 6,
|
||||
db_path=db_path,
|
||||
)
|
||||
if not raw_5m:
|
||||
return {"ok": False, "msg": "档案库暂无 K 线,请等待同步或手动刷新"}
|
||||
|
||||
filled_5m = _fill_missing_bars(raw_5m, period_5m, start_ms - period_5m * 2, end_ms + period_5m * 2)
|
||||
|
||||
if tf == "5m":
|
||||
merged = [b for b in raw_5m if start_ms <= int(b["open_time_ms"]) <= end_ms]
|
||||
merged = [b for b in filled_5m if start_ms <= int(b["open_time_ms"]) <= end_ms]
|
||||
else:
|
||||
agg = aggregate_ohlcv_bars(raw_5m, tf)
|
||||
agg = aggregate_ohlcv_bars(filled_5m, tf)
|
||||
merged = [b for b in agg if start_ms <= int(b["open_time_ms"]) <= end_ms]
|
||||
|
||||
candles = _to_candles(merged)
|
||||
@@ -584,10 +663,13 @@ def resolve_archive_chart(
|
||||
"timeframe": tf,
|
||||
"mode": (mode or "hold").strip().lower(),
|
||||
"anchor_ms": anchor,
|
||||
"opened_ms": hold_open,
|
||||
"closed_ms": hold_close,
|
||||
"window_start_ms": start_ms,
|
||||
"window_end_ms": end_ms,
|
||||
"candles": candles,
|
||||
"bar_count": len(candles),
|
||||
"gaps_filled": sum(1 for b in filled_5m if b.get("filled")),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user