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:
@@ -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:
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user