diff --git a/hub_symbol_archive_lib.py b/hub_symbol_archive_lib.py index c3e671d..ca0ee46 100644 --- a/hub_symbol_archive_lib.py +++ b/hub_symbol_archive_lib.py @@ -29,10 +29,10 @@ ARCHIVE_DEFAULT_TIMEFRAME = "15m" ARCHIVE_SEED_LOOKBACK_DAYS = 30 ARCHIVE_VISIBLE_BARS_DEFAULT = 200 ARCHIVE_MAX_CANDLES: dict[str, int] = { - "5m": 4000, - "15m": 2500, - "1h": 1200, - "4h": 600, + "5m": 9000, + "15m": 15000, + "1h": 4000, + "4h": 2000, } 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")) @@ -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", " ") if not s: 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)): try: 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] max_n = ARCHIVE_MAX_CANDLES.get(tf, 2000) - if rm == "history" and merged: - merged = _trim_bars_for_cap(merged, end_ms=end_ms, max_n=max_n) + if rm == "history" and merged and len(merged) > max_n: + merged = merged[:max_n] candles = _to_candles(merged) if not candles: diff --git a/manual_trading_hub/static/archive.js b/manual_trading_hub/static/archive.js index c367acd..a4e9100 100644 --- a/manual_trading_hub/static/archive.js +++ b/manual_trading_hub/static/archive.js @@ -605,7 +605,10 @@ chart.timeScale().setVisibleLogicalRange({ from: candles.length - 120, to: candles.length + 5 }); } 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); } diff --git a/manual_trading_hub/static/chart.js b/manual_trading_hub/static/chart.js index 3638306..3baae74 100644 --- a/manual_trading_hub/static/chart.js +++ b/manual_trading_hub/static/chart.js @@ -79,6 +79,69 @@ "1w": "周线", }; 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"); if (!chartHost) return; @@ -1649,11 +1712,7 @@ applyPriceFormatToSeries(indSeries.ema55, pf); if (chart) { chart.applyOptions({ - localization: { - priceFormatter: function (p) { - return fmtPrice(p); - }, - }, + localization: buildChartLocalization(), }); } } @@ -1933,6 +1992,7 @@ horzLines: { visible: false }, }, rightPriceScale: { borderColor: tp.border, autoScale: true }, + localization: buildChartLocalization(), timeScale: { borderColor: tp.border, timeVisible: true, diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 67e75e0..fe368d7 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -417,8 +417,8 @@
- - + + diff --git a/tests/test_hub_symbol_archive_lib.py b/tests/test_hub_symbol_archive_lib.py index ddd74bd..1f28522 100644 --- a/tests/test_hub_symbol_archive_lib.py +++ b/tests/test_hub_symbol_archive_lib.py @@ -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 ms_to_wall_clock_str(ms) == "2026-06-07 20:30:00" 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