Remove 12h timeframe and stabilize chart wheel zoom.
Drop 12h from market chart options and storage, and avoid left-pan reload and tail refresh from resetting the viewport while zooming. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+1
-1
@@ -200,7 +200,7 @@ def purge_1m_bar_cap(db_path: Path | None = None, *, max_bars: int | None = None
|
|||||||
|
|
||||||
|
|
||||||
def purge_retention(db_path: Path | None = None) -> int:
|
def purge_retention(db_path: Path | None = None) -> int:
|
||||||
"""按周期策略清理:5m/1h 一年;1m 保留最近 N 根;12h/1d/1w 不删。"""
|
"""按周期策略清理:5m/1h 一年;1m 保留最近 N 根;1d/1w 不删。"""
|
||||||
n = 0
|
n = 0
|
||||||
n += purge_timeframe_by_days("5m", HUB_KLINE_5M_1H_RETENTION_DAYS, db_path)
|
n += purge_timeframe_by_days("5m", HUB_KLINE_5M_1H_RETENTION_DAYS, db_path)
|
||||||
n += purge_timeframe_by_days("1h", HUB_KLINE_5M_1H_RETENTION_DAYS, db_path)
|
n += purge_timeframe_by_days("1h", HUB_KLINE_5M_1H_RETENTION_DAYS, db_path)
|
||||||
|
|||||||
+5
-10
@@ -15,7 +15,6 @@ CHART_TIMEFRAMES = frozenset(
|
|||||||
"1h",
|
"1h",
|
||||||
"2h",
|
"2h",
|
||||||
"4h",
|
"4h",
|
||||||
"12h",
|
|
||||||
"1d",
|
"1d",
|
||||||
"1w",
|
"1w",
|
||||||
}
|
}
|
||||||
@@ -27,15 +26,14 @@ CHART_TIMEFRAME_ORDER = (
|
|||||||
"1h",
|
"1h",
|
||||||
"2h",
|
"2h",
|
||||||
"4h",
|
"4h",
|
||||||
"12h",
|
|
||||||
"1d",
|
"1d",
|
||||||
"1w",
|
"1w",
|
||||||
)
|
)
|
||||||
DAILY_PLUS_TIMEFRAMES = frozenset({"1d", "1w"})
|
DAILY_PLUS_TIMEFRAMES = frozenset({"1d", "1w"})
|
||||||
|
|
||||||
# 入库 / 同步真源(交易所拉取)
|
# 入库 / 同步真源(交易所拉取)
|
||||||
STORED_TIMEFRAMES = frozenset({"1m", "5m", "1h", "12h", "1d", "1w"})
|
STORED_TIMEFRAMES = frozenset({"1m", "5m", "1h", "1d", "1w"})
|
||||||
PERMANENT_STORED_TIMEFRAMES = frozenset({"12h", "1d", "1w"})
|
PERMANENT_STORED_TIMEFRAMES = frozenset({"1d", "1w"})
|
||||||
YEAR_ROLLING_STORED = frozenset({"5m", "1h"})
|
YEAR_ROLLING_STORED = frozenset({"5m", "1h"})
|
||||||
|
|
||||||
# 展示周期 → 本地聚合源(不落库)
|
# 展示周期 → 本地聚合源(不落库)
|
||||||
@@ -52,10 +50,8 @@ HUB_KLINE_1M_MAX_BARS = max(1000, int(os.getenv("HUB_KLINE_1M_MAX_BARS", "10000"
|
|||||||
HUB_KLINE_5M_1H_RETENTION_DAYS = max(30, int(os.getenv("HUB_KLINE_5M_1H_RETENTION_DAYS", "365")))
|
HUB_KLINE_5M_1H_RETENTION_DAYS = max(30, int(os.getenv("HUB_KLINE_5M_1H_RETENTION_DAYS", "365")))
|
||||||
HUB_KLINE_SEED_BARS = max(100, int(os.getenv("HUB_KLINE_SEED_BARS", "500")))
|
HUB_KLINE_SEED_BARS = max(100, int(os.getenv("HUB_KLINE_SEED_BARS", "500")))
|
||||||
|
|
||||||
# 部分交易所 ccxt 无原生 12h,或原生 K 线间隔异常时从 1h 聚合(仅远程拉取 fallback)
|
# 交易所无原生周期时的远程拉取 fallback(行情区当前无映射)
|
||||||
OHLCV_AGGREGATE_FROM: dict[str, str] = {
|
OHLCV_AGGREGATE_FROM: dict[str, str] = {}
|
||||||
"12h": "1h",
|
|
||||||
}
|
|
||||||
|
|
||||||
TIMEFRAME_MS: dict[str, int] = {
|
TIMEFRAME_MS: dict[str, int] = {
|
||||||
"1m": 60_000,
|
"1m": 60_000,
|
||||||
@@ -126,7 +122,7 @@ def bar_limit_for_timeframe(timeframe: str) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def storage_retention_days(storage_tf: str) -> int | None:
|
def storage_retention_days(storage_tf: str) -> int | None:
|
||||||
"""None 表示不按天截断(1m 按根数;12h/1d/1w 永久)。"""
|
"""None 表示不按天截断(1m 按根数;1d/1w 永久)。"""
|
||||||
tf = normalize_chart_timeframe(storage_tf)
|
tf = normalize_chart_timeframe(storage_tf)
|
||||||
if tf in YEAR_ROLLING_STORED:
|
if tf in YEAR_ROLLING_STORED:
|
||||||
return HUB_KLINE_5M_1H_RETENTION_DAYS
|
return HUB_KLINE_5M_1H_RETENTION_DAYS
|
||||||
@@ -159,7 +155,6 @@ def retention_policy_meta() -> dict[str, Any]:
|
|||||||
"1m": {"mode": "bars", "max_bars": HUB_KLINE_1M_MAX_BARS},
|
"1m": {"mode": "bars", "max_bars": HUB_KLINE_1M_MAX_BARS},
|
||||||
"5m": {"mode": "days", "days": HUB_KLINE_5M_1H_RETENTION_DAYS},
|
"5m": {"mode": "days", "days": HUB_KLINE_5M_1H_RETENTION_DAYS},
|
||||||
"1h": {"mode": "days", "days": HUB_KLINE_5M_1H_RETENTION_DAYS},
|
"1h": {"mode": "days", "days": HUB_KLINE_5M_1H_RETENTION_DAYS},
|
||||||
"12h": {"mode": "permanent"},
|
|
||||||
"1d": {"mode": "permanent"},
|
"1d": {"mode": "permanent"},
|
||||||
"1w": {"mode": "permanent"},
|
"1w": {"mode": "permanent"},
|
||||||
"aggregate_from": dict(CHART_DISPLAY_AGGREGATE_FROM),
|
"aggregate_from": dict(CHART_DISPLAY_AGGREGATE_FROM),
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
"1h": 200,
|
"1h": 200,
|
||||||
"2h": 200,
|
"2h": 200,
|
||||||
"4h": 200,
|
"4h": 200,
|
||||||
"12h": 200,
|
|
||||||
"1d": 200,
|
"1d": 200,
|
||||||
"1w": 150,
|
"1w": 150,
|
||||||
};
|
};
|
||||||
@@ -25,7 +24,6 @@
|
|||||||
"1h": 300,
|
"1h": 300,
|
||||||
"2h": 300,
|
"2h": 300,
|
||||||
"4h": 300,
|
"4h": 300,
|
||||||
"12h": 200,
|
|
||||||
"1d": 200,
|
"1d": 200,
|
||||||
"1w": 150,
|
"1w": 150,
|
||||||
};
|
};
|
||||||
@@ -36,7 +34,6 @@
|
|||||||
"1h": 1000,
|
"1h": 1000,
|
||||||
"2h": 1000,
|
"2h": 1000,
|
||||||
"4h": 1000,
|
"4h": 1000,
|
||||||
"12h": 1000,
|
|
||||||
"1d": 1000,
|
"1d": 1000,
|
||||||
"1w": 500,
|
"1w": 500,
|
||||||
};
|
};
|
||||||
@@ -56,7 +53,6 @@
|
|||||||
"1h": 60 * 60_000,
|
"1h": 60 * 60_000,
|
||||||
"2h": 2 * 60 * 60_000,
|
"2h": 2 * 60 * 60_000,
|
||||||
"4h": 4 * 60 * 60_000,
|
"4h": 4 * 60 * 60_000,
|
||||||
"12h": 12 * 60 * 60_000,
|
|
||||||
"1d": 24 * 60 * 60_000,
|
"1d": 24 * 60 * 60_000,
|
||||||
"1w": 7 * 24 * 60 * 60_000,
|
"1w": 7 * 24 * 60 * 60_000,
|
||||||
};
|
};
|
||||||
@@ -67,7 +63,6 @@
|
|||||||
"60": "1h",
|
"60": "1h",
|
||||||
"120": "2h",
|
"120": "2h",
|
||||||
"240": "4h",
|
"240": "4h",
|
||||||
"720": "12h",
|
|
||||||
"1440": "1d",
|
"1440": "1d",
|
||||||
"10080": "1w",
|
"10080": "1w",
|
||||||
};
|
};
|
||||||
@@ -81,7 +76,6 @@
|
|||||||
"1h": "1小时",
|
"1h": "1小时",
|
||||||
"2h": "2小时",
|
"2h": "2小时",
|
||||||
"4h": "4小时",
|
"4h": "4小时",
|
||||||
"12h": "12小时",
|
|
||||||
"1d": "日线",
|
"1d": "日线",
|
||||||
"1w": "周线",
|
"1w": "周线",
|
||||||
};
|
};
|
||||||
@@ -179,6 +173,8 @@
|
|||||||
let loadingLeft = false;
|
let loadingLeft = false;
|
||||||
let chartDataLoading = false;
|
let chartDataLoading = false;
|
||||||
let chartViewEpoch = 0;
|
let chartViewEpoch = 0;
|
||||||
|
let rangeUiTimer = null;
|
||||||
|
let loadOlderTimer = null;
|
||||||
let priceTagTimer = null;
|
let priceTagTimer = null;
|
||||||
let tfDigitBuf = "";
|
let tfDigitBuf = "";
|
||||||
let tfDigitTimer = null;
|
let tfDigitTimer = null;
|
||||||
@@ -1954,8 +1950,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
chart.timeScale().subscribeVisibleLogicalRangeChange(function (range) {
|
chart.timeScale().subscribeVisibleLogicalRangeChange(function (range) {
|
||||||
updateVisibleRangeMarkers();
|
scheduleRangeUiUpdate();
|
||||||
updatePriceTag();
|
|
||||||
if (
|
if (
|
||||||
!range ||
|
!range ||
|
||||||
chartDataLoading ||
|
chartDataLoading ||
|
||||||
@@ -1967,9 +1962,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (currentChartViewKey() !== lastViewKey) return;
|
if (currentChartViewKey() !== lastViewKey) return;
|
||||||
if (range.from < CHART_LOAD_LEFT_THRESHOLD) {
|
scheduleLoadOlderOnRange(range);
|
||||||
void loadOlderCandles();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("resize", function () {
|
window.addEventListener("resize", function () {
|
||||||
@@ -2032,6 +2025,37 @@
|
|||||||
return range.to >= maxTo - 24;
|
return range.to >= maxTo - 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldLoadOlderOnRange(range) {
|
||||||
|
if (!range || !lastCandles.length) return false;
|
||||||
|
const n = lastCandles.length;
|
||||||
|
const maxTo = n - 1 + RIGHT_OFFSET_BARS;
|
||||||
|
if (range.from >= CHART_LOAD_LEFT_THRESHOLD) return false;
|
||||||
|
// 缩小图表时 from 会变小,但 to 仍靠近最新 — 不应触发左拖补历史
|
||||||
|
if (range.to >= maxTo - 30) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleRangeUiUpdate() {
|
||||||
|
if (rangeUiTimer) clearTimeout(rangeUiTimer);
|
||||||
|
rangeUiTimer = setTimeout(function () {
|
||||||
|
rangeUiTimer = null;
|
||||||
|
updateVisibleRangeMarkers();
|
||||||
|
updatePriceTag();
|
||||||
|
}, 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleLoadOlderOnRange(range) {
|
||||||
|
if (!shouldLoadOlderOnRange(range)) return;
|
||||||
|
if (loadOlderTimer) clearTimeout(loadOlderTimer);
|
||||||
|
loadOlderTimer = setTimeout(function () {
|
||||||
|
loadOlderTimer = null;
|
||||||
|
if (!chart) return;
|
||||||
|
const cur = chart.timeScale().getVisibleLogicalRange();
|
||||||
|
if (!shouldLoadOlderOnRange(cur)) return;
|
||||||
|
void loadOlderCandles();
|
||||||
|
}, 280);
|
||||||
|
}
|
||||||
|
|
||||||
function tailVisibleLogicalRange(candleCount) {
|
function tailVisibleLogicalRange(candleCount) {
|
||||||
const n = Math.max(0, Number(candleCount) || 0);
|
const n = Math.max(0, Number(candleCount) || 0);
|
||||||
if (n <= 0) return null;
|
if (n <= 0) return null;
|
||||||
@@ -2196,20 +2220,18 @@
|
|||||||
if (epochAtStart !== chartViewEpoch) return;
|
if (epochAtStart !== chartViewEpoch) return;
|
||||||
const n = lastCandles.length;
|
const n = lastCandles.length;
|
||||||
const curRange = chart && chart.timeScale().getVisibleLogicalRange();
|
const curRange = chart && chart.timeScale().getVisibleLogicalRange();
|
||||||
if (wasViewingTail) {
|
const minorTailUpdate = Math.abs(n - candleCountBefore) <= 5;
|
||||||
const tailRange = tailVisibleLogicalRange(n);
|
if (
|
||||||
if (tailRange) chart.timeScale().setVisibleLogicalRange(tailRange);
|
|
||||||
} else if (
|
|
||||||
savedRange &&
|
savedRange &&
|
||||||
isVisibleRangeValidForCandles(savedRange, n) &&
|
isVisibleRangeValidForCandles(savedRange, n) &&
|
||||||
Math.abs(n - candleCountBefore) < chartChunkLimit(tf)
|
(minorTailUpdate || !wasViewingTail)
|
||||||
) {
|
) {
|
||||||
chart.timeScale().setVisibleLogicalRange(savedRange);
|
chart.timeScale().setVisibleLogicalRange(savedRange);
|
||||||
} else if (!isVisibleRangeValidForCandles(curRange, n)) {
|
} else if (!isVisibleRangeValidForCandles(curRange, n)) {
|
||||||
const tailRange = tailVisibleLogicalRange(n);
|
const tailRange = tailVisibleLogicalRange(n);
|
||||||
if (tailRange) chart.timeScale().setVisibleLogicalRange(tailRange);
|
if (tailRange) chart.timeScale().setVisibleLogicalRange(tailRange);
|
||||||
}
|
}
|
||||||
updateVisibleRangeMarkers();
|
scheduleRangeUiUpdate();
|
||||||
if (posContext) {
|
if (posContext) {
|
||||||
updateLivePosPnl();
|
updateLivePosPnl();
|
||||||
refreshPosPnlFromBoard();
|
refreshPosPnlFromBoard();
|
||||||
|
|||||||
@@ -93,7 +93,6 @@
|
|||||||
<option value="1h">1h</option>
|
<option value="1h">1h</option>
|
||||||
<option value="2h">2h</option>
|
<option value="2h">2h</option>
|
||||||
<option value="4h">4h</option>
|
<option value="4h">4h</option>
|
||||||
<option value="12h">12h</option>
|
|
||||||
<option value="1d" selected>1d</option>
|
<option value="1d" selected>1d</option>
|
||||||
<option value="1w">1w</option>
|
<option value="1w">1w</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -150,7 +149,6 @@
|
|||||||
<option value="1h">1h</option>
|
<option value="1h">1h</option>
|
||||||
<option value="2h">2h</option>
|
<option value="2h">2h</option>
|
||||||
<option value="4h">4h</option>
|
<option value="4h">4h</option>
|
||||||
<option value="12h">12h</option>
|
|
||||||
<option value="1d">1d</option>
|
<option value="1d">1d</option>
|
||||||
<option value="1w">1w</option>
|
<option value="1w">1w</option>
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -167,37 +167,6 @@ class TestHubOhlcvLib(unittest.TestCase):
|
|||||||
self.assertGreaterEqual(len(ex.calls), 3)
|
self.assertGreaterEqual(len(ex.calls), 3)
|
||||||
self.assertAlmostEqual(out["bars"][-1]["close"], 3.05)
|
self.assertAlmostEqual(out["bars"][-1]["close"], 3.05)
|
||||||
|
|
||||||
def test_aggregate_12h_from_1h_when_exchange_lacks_native(self):
|
|
||||||
"""无原生 12h 时应从 1h 聚合。"""
|
|
||||||
from hub_ohlcv_lib import TIMEFRAME_MS
|
|
||||||
|
|
||||||
h1 = TIMEFRAME_MS["1h"]
|
|
||||||
h12 = TIMEFRAME_MS["12h"]
|
|
||||||
base = (1_700_000_000_000 // h12) * h12
|
|
||||||
one_h = [
|
|
||||||
[base + i * h1, 100.0 + i, 101.0 + i, 99.0 + i, 100.5 + i, 10.0]
|
|
||||||
for i in range(48)
|
|
||||||
]
|
|
||||||
ex = _FakeExchange(
|
|
||||||
[one_h],
|
|
||||||
timeframes={"1h": "1h", "4h": "4h"},
|
|
||||||
)
|
|
||||||
out = fetch_ohlcv_for_hub(
|
|
||||||
symbol="BTC/USDT",
|
|
||||||
timeframe="12h",
|
|
||||||
since_ms=base,
|
|
||||||
limit=4,
|
|
||||||
normalize_symbol_input=lambda s: str(s).strip().upper(),
|
|
||||||
normalize_exchange_symbol=lambda s: f"{s}:USDT" if ":" not in s else s,
|
|
||||||
ensure_markets_loaded=lambda: None,
|
|
||||||
exchange=ex,
|
|
||||||
)
|
|
||||||
self.assertTrue(out.get("ok"))
|
|
||||||
bars = out.get("bars") or []
|
|
||||||
self.assertEqual(len(bars), 4)
|
|
||||||
self.assertTrue(bars_spacing_matches_timeframe(bars, "12h"))
|
|
||||||
self.assertEqual(ex.calls[0]["timeframe"], "1h")
|
|
||||||
|
|
||||||
def test_pagination_stops_when_next_since_reaches_now(self):
|
def test_pagination_stops_when_next_since_reaches_now(self):
|
||||||
"""Gate 等:分页 since 不得越过当前时间,避免 from>to。"""
|
"""Gate 等:分页 since 不得越过当前时间,避免 from>to。"""
|
||||||
from hub_ohlcv_lib import TIMEFRAME_MS
|
from hub_ohlcv_lib import TIMEFRAME_MS
|
||||||
@@ -230,8 +199,8 @@ class TestHubOhlcvLib(unittest.TestCase):
|
|||||||
from hub_ohlcv_lib import TIMEFRAME_MS
|
from hub_ohlcv_lib import TIMEFRAME_MS
|
||||||
|
|
||||||
h1 = TIMEFRAME_MS["1h"]
|
h1 = TIMEFRAME_MS["1h"]
|
||||||
h12 = TIMEFRAME_MS["12h"]
|
h4 = TIMEFRAME_MS["4h"]
|
||||||
base = (1_700_000_000_000 // h12) * h12
|
base = (1_700_000_000_000 // h4) * h4
|
||||||
src = [
|
src = [
|
||||||
{
|
{
|
||||||
"open_time_ms": base + i * h1,
|
"open_time_ms": base + i * h1,
|
||||||
@@ -241,11 +210,11 @@ class TestHubOhlcvLib(unittest.TestCase):
|
|||||||
"close": 1.5,
|
"close": 1.5,
|
||||||
"volume": 1.0,
|
"volume": 1.0,
|
||||||
}
|
}
|
||||||
for i in range(12)
|
for i in range(4)
|
||||||
]
|
]
|
||||||
out = aggregate_ohlcv_bars(src, "12h")
|
out = aggregate_ohlcv_bars(src, "4h")
|
||||||
self.assertEqual(len(out), 1)
|
self.assertEqual(len(out), 1)
|
||||||
self.assertEqual(out[0]["volume"], 12.0)
|
self.assertEqual(out[0]["volume"], 4.0)
|
||||||
self.assertEqual(out[0]["high"], 2.0)
|
self.assertEqual(out[0]["high"], 2.0)
|
||||||
self.assertEqual(out[0]["low"], 0.5)
|
self.assertEqual(out[0]["low"], 0.5)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user