fix: UTC+8 market chart times and archive full history K-line load

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-08 16:55:48 +08:00
parent 55a979eee5
commit 38f4280bb8
5 changed files with 109 additions and 14 deletions
+9 -6
View File
@@ -29,10 +29,10 @@ ARCHIVE_DEFAULT_TIMEFRAME = "15m"
ARCHIVE_SEED_LOOKBACK_DAYS = 30 ARCHIVE_SEED_LOOKBACK_DAYS = 30
ARCHIVE_VISIBLE_BARS_DEFAULT = 200 ARCHIVE_VISIBLE_BARS_DEFAULT = 200
ARCHIVE_MAX_CANDLES: dict[str, int] = { ARCHIVE_MAX_CANDLES: dict[str, int] = {
"5m": 4000, "5m": 9000,
"15m": 2500, "15m": 15000,
"1h": 1200, "1h": 4000,
"4h": 600, "4h": 2000,
} }
ARCHIVE_SYNC_INTERVAL_SEC = int(os.getenv("HUB_ARCHIVE_SYNC_INTERVAL_SEC", str(4 * 3600))) ARCHIVE_SYNC_INTERVAL_SEC = int(os.getenv("HUB_ARCHIVE_SYNC_INTERVAL_SEC", str(4 * 3600)))
ARCHIVE_TRADE_DAYS = int(os.getenv("HUB_ARCHIVE_TRADE_DAYS", "365")) ARCHIVE_TRADE_DAYS = int(os.getenv("HUB_ARCHIVE_TRADE_DAYS", "365"))
@@ -159,6 +159,9 @@ def parse_wall_clock_ms(raw: Any, *, tz: ZoneInfo = CHART_DISPLAY_TZ) -> int | N
s = str(raw).strip().replace("Z", "").replace("T", " ") s = str(raw).strip().replace("Z", "").replace("T", " ")
if not s: if not s:
return None return None
if s.isdigit():
v = int(s)
return v if v > 1_000_000_000_000 else v * 1000
for fmt, ln in (("%Y-%m-%d %H:%M:%S", 19), ("%Y-%m-%d %H:%M", 16), ("%Y-%m-%d", 10)): for fmt, ln in (("%Y-%m-%d %H:%M:%S", 19), ("%Y-%m-%d %H:%M", 16), ("%Y-%m-%d", 10)):
try: try:
dt = datetime.strptime(s[:ln], fmt) dt = datetime.strptime(s[:ln], fmt)
@@ -873,8 +876,8 @@ def resolve_archive_chart(
merged = [b for b in agg if start_ms <= int(b["open_time_ms"]) <= end_ms] merged = [b for b in agg if start_ms <= int(b["open_time_ms"]) <= end_ms]
max_n = ARCHIVE_MAX_CANDLES.get(tf, 2000) max_n = ARCHIVE_MAX_CANDLES.get(tf, 2000)
if rm == "history" and merged: if rm == "history" and merged and len(merged) > max_n:
merged = _trim_bars_for_cap(merged, end_ms=end_ms, max_n=max_n) merged = merged[:max_n]
candles = _to_candles(merged) candles = _to_candles(merged)
if not candles: if not candles:
+4 -1
View File
@@ -605,7 +605,10 @@
chart.timeScale().setVisibleLogicalRange({ from: candles.length - 120, to: candles.length + 5 }); chart.timeScale().setVisibleLogicalRange({ from: candles.length - 120, to: candles.length + 5 });
} }
const markHint = markAuto && trades.length > 1 ? " · 自动标注 " + trades.length + " 笔" : tr ? " · 已标注开/平" : ""; const markHint = markAuto && trades.length > 1 ? " · 自动标注 " + trades.length + " 笔" : tr ? " · 已标注开/平" : "";
const histHint = openMs && closeMs ? " · 可拖动/滚轮缩放查看建仓前走势" : ""; const histHint =
openMs && closeMs
? " · 建档30天历史 · 可拖动/滚轮缩放查看建仓前走势"
: "";
setStatus("K 线 " + candles.length + " 根 · " + timeframe + markHint + histHint); setStatus("K 线 " + candles.length + " 根 · " + timeframe + markHint + histHint);
} }
+65 -5
View File
@@ -79,6 +79,69 @@
"1w": "周线", "1w": "周线",
}; };
const TF_DIGIT_TIMEOUT_MS = 650; const TF_DIGIT_TIMEOUT_MS = 650;
const CHART_TZ_OFFSET_SEC = 8 * 60 * 60;
function pad2(n) {
return n < 10 ? "0" + n : String(n);
}
function utcSecToBjDate(utcSec) {
return new Date((Number(utcSec) + CHART_TZ_OFFSET_SEC) * 1000);
}
function formatChartTimeBj(utcSec, withDate) {
const d = utcSecToBjDate(utcSec);
const h = pad2(d.getUTCHours());
const mi = pad2(d.getUTCMinutes());
if (!withDate) return h + ":" + mi;
return (
d.getUTCFullYear() +
"-" +
pad2(d.getUTCMonth() + 1) +
"-" +
pad2(d.getUTCDate()) +
" " +
h +
":" +
mi
);
}
function chartLocalizationBj() {
return {
locale: "zh-CN",
dateFormat: "yyyy-MM-dd",
timeFormatter: function (time) {
if (typeof time === "number") return formatChartTimeBj(time, true);
if (time && typeof time === "object" && time.year) {
return time.year + "-" + pad2(time.month) + "-" + pad2(time.day);
}
return "";
},
tickMarkFormatter: function (time, tickMarkType) {
if (typeof time !== "number") {
if (time && typeof time === "object" && time.year) {
return time.year + "-" + pad2(time.month) + "-" + pad2(time.day);
}
return "";
}
const d = utcSecToBjDate(time);
if (tickMarkType === 0) return String(d.getUTCFullYear());
if (tickMarkType === 1) return pad2(d.getUTCMonth() + 1);
if (tickMarkType === 2) return pad2(d.getUTCDate());
return formatChartTimeBj(time, false);
},
};
}
function buildChartLocalization() {
const loc = chartLocalizationBj();
loc.priceFormatter = function (p) {
return fmtPrice(p);
};
return loc;
}
const chartHost = document.getElementById("market-chart"); const chartHost = document.getElementById("market-chart");
if (!chartHost) return; if (!chartHost) return;
@@ -1649,11 +1712,7 @@
applyPriceFormatToSeries(indSeries.ema55, pf); applyPriceFormatToSeries(indSeries.ema55, pf);
if (chart) { if (chart) {
chart.applyOptions({ chart.applyOptions({
localization: { localization: buildChartLocalization(),
priceFormatter: function (p) {
return fmtPrice(p);
},
},
}); });
} }
} }
@@ -1933,6 +1992,7 @@
horzLines: { visible: false }, horzLines: { visible: false },
}, },
rightPriceScale: { borderColor: tp.border, autoScale: true }, rightPriceScale: { borderColor: tp.border, autoScale: true },
localization: buildChartLocalization(),
timeScale: { timeScale: {
borderColor: tp.border, borderColor: tp.border,
timeVisible: true, timeVisible: true,
+2 -2
View File
@@ -417,8 +417,8 @@
<div id="toast"></div> <div id="toast"></div>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script> <script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script src="/assets/chart_draw.js?v=20260608-market-vol-rank"></script> <script src="/assets/chart_draw.js?v=20260608-market-vol-rank"></script>
<script src="/assets/chart.js?v=20260608-market-vol-rank-v5"></script> <script src="/assets/chart.js?v=20260608-market-tz8"></script>
<script src="/assets/archive.js?v=20260608-hub-archive-tz8"></script> <script src="/assets/archive.js?v=20260608-hub-archive-history"></script>
<script src="/assets/ai_review_render.js?v=2"></script> <script src="/assets/ai_review_render.js?v=2"></script>
<script src="/assets/app.js?v=20260607-hub-archive-v1"></script> <script src="/assets/app.js?v=20260607-hub-archive-v1"></script>
</body> </body>
+29
View File
@@ -216,3 +216,32 @@ def test_parse_wall_clock_ms_uses_utc_plus_8():
assert dt_bj.strftime("%Y-%m-%d %H:%M:%S") == "2026-06-07 20:30:00" assert dt_bj.strftime("%Y-%m-%d %H:%M:%S") == "2026-06-07 20:30:00"
assert ms_to_wall_clock_str(ms) == "2026-06-07 20:30:00" assert ms_to_wall_clock_str(ms) == "2026-06-07 20:30:00"
assert parse_wall_clock_ms("2026-06-07 20:30") == ms assert parse_wall_clock_ms("2026-06-07 20:30") == ms
def test_parse_wall_clock_ms_accepts_epoch_strings():
ms = 1_700_000_000_000
assert parse_wall_clock_ms(str(ms)) == ms
assert parse_wall_clock_ms(str(ms // 1000)) == ms
def test_resolve_archive_chart_history_uses_trade_span_not_200_bars():
with tempfile.TemporaryDirectory() as td:
db = Path(td) / "archive.db"
init_db(db)
opened = 1_700_000_000_000
closed = opened + 20 * 24 * 3600_000
_seed_5m_bars(db, opened - 35 * 24 * 3600_000, 40 * 24 * 12)
out = resolve_archive_chart(
"gate",
"ONDO",
"15m",
opened_ms=opened,
closed_ms=closed,
mode="hold",
bars=200,
range_mode="history",
db_path=db,
)
assert out["ok"] is True
assert out["range_mode"] == "history"
assert out["bar_count"] > 200