Fetch native exchange OHLCV per timeframe instead of local aggregation.
Store and serve 15m/2h/4h directly from the exchange so market charts match venue candles. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+74
-12
@@ -31,17 +31,13 @@ CHART_TIMEFRAME_ORDER = (
|
||||
)
|
||||
DAILY_PLUS_TIMEFRAMES = frozenset({"1d", "1w"})
|
||||
|
||||
# 入库 / 同步真源(交易所拉取)
|
||||
STORED_TIMEFRAMES = frozenset({"1m", "5m", "1h", "1d", "1w"})
|
||||
# 入库 / 同步真源(各周期直拉交易所,不做本地聚合)
|
||||
STORED_TIMEFRAMES = frozenset(CHART_TIMEFRAMES)
|
||||
PERMANENT_STORED_TIMEFRAMES = frozenset({"1d", "1w"})
|
||||
YEAR_ROLLING_STORED = frozenset({"5m", "1h"})
|
||||
YEAR_ROLLING_STORED = frozenset({"5m", "15m", "1h", "2h", "4h"})
|
||||
|
||||
# 展示周期 → 本地聚合源(不落库)
|
||||
CHART_DISPLAY_AGGREGATE_FROM: dict[str, str] = {
|
||||
"15m": "5m",
|
||||
"2h": "1h",
|
||||
"4h": "1h",
|
||||
}
|
||||
# 行情区不做展示周期聚合;保留空映射供兼容读取
|
||||
CHART_DISPLAY_AGGREGATE_FROM: dict[str, str] = {}
|
||||
|
||||
SMALL_DISPLAY_TFS = frozenset({"1m", "5m", "15m"})
|
||||
MID_DISPLAY_TFS = frozenset({"1h", "2h", "4h"})
|
||||
@@ -151,13 +147,17 @@ def seed_bar_target(storage_tf: str) -> int:
|
||||
|
||||
|
||||
def retention_policy_meta() -> dict[str, Any]:
|
||||
year = {"mode": "days", "days": HUB_KLINE_5M_1H_RETENTION_DAYS}
|
||||
return {
|
||||
"1m": {"mode": "bars", "max_bars": HUB_KLINE_1M_MAX_BARS},
|
||||
"5m": {"mode": "days", "days": HUB_KLINE_5M_1H_RETENTION_DAYS},
|
||||
"1h": {"mode": "days", "days": HUB_KLINE_5M_1H_RETENTION_DAYS},
|
||||
"5m": dict(year),
|
||||
"15m": dict(year),
|
||||
"1h": dict(year),
|
||||
"2h": dict(year),
|
||||
"4h": dict(year),
|
||||
"1d": {"mode": "permanent"},
|
||||
"1w": {"mode": "permanent"},
|
||||
"aggregate_from": dict(CHART_DISPLAY_AGGREGATE_FROM),
|
||||
"aggregate_from": {},
|
||||
}
|
||||
|
||||
|
||||
@@ -399,6 +399,68 @@ def align_bar_open_ms(open_time_ms: int, period_ms: int) -> int:
|
||||
return (int(open_time_ms) // period_ms) * period_ms
|
||||
|
||||
|
||||
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_ohlcv_bars(
|
||||
bars: list[dict[str, Any]],
|
||||
period_ms: int,
|
||||
start_ms: int | None = None,
|
||||
end_ms: int | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""细周期缺口用上一根收盘价填平,保证聚合后 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 if start_ms is not None else keys[0]), origin, step_ms
|
||||
)
|
||||
aligned_end = max(
|
||||
int(end_ms if end_ms is not None else keys[-1]),
|
||||
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 aggregate_ohlcv_bars(
|
||||
bars: list[dict[str, Any]], target_timeframe: str
|
||||
) -> list[dict[str, Any]]:
|
||||
|
||||
Reference in New Issue
Block a user