增加K线
This commit is contained in:
+93
-4
@@ -15,12 +15,36 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
_RATE_LIMIT_CODES = {418, 429}
|
_RATE_LIMIT_CODES = {418, 429}
|
||||||
_SYMBOLS_CACHE_FILE = ROOT_DIR / "data" / "symbols_cache.json"
|
_SYMBOLS_CACHE_FILE = ROOT_DIR / "data" / "symbols_cache.json"
|
||||||
|
_SYMBOL_META_CACHE_FILE = ROOT_DIR / "data" / "symbol_meta_cache.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _precision_from_tick_size(tick_size: str) -> int:
|
||||||
|
tick = tick_size.strip()
|
||||||
|
if "." not in tick:
|
||||||
|
return 0
|
||||||
|
dec = tick.split(".", 1)[1]
|
||||||
|
trimmed = dec.rstrip("0")
|
||||||
|
return len(trimmed) if trimmed else len(dec)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_symbol_price_meta(symbol_info: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
tick_size = "0.01"
|
||||||
|
for f in symbol_info.get("filters", []):
|
||||||
|
if f.get("filterType") == "PRICE_FILTER":
|
||||||
|
tick_size = str(f.get("tickSize", tick_size))
|
||||||
|
break
|
||||||
|
precision = _precision_from_tick_size(tick_size)
|
||||||
|
api_precision = symbol_info.get("pricePrecision")
|
||||||
|
if isinstance(api_precision, int) and api_precision > precision:
|
||||||
|
precision = api_precision
|
||||||
|
return {"tick_size": tick_size, "price_precision": precision}
|
||||||
|
|
||||||
|
|
||||||
class BinanceFuturesClient:
|
class BinanceFuturesClient:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.base = settings.binance_fapi_base.rstrip("/")
|
self.base = settings.binance_fapi_base.rstrip("/")
|
||||||
self._symbols_cache: list[str] | None = None
|
self._symbols_cache: list[str] | None = None
|
||||||
|
self._symbol_meta_cache: dict[str, dict[str, Any]] | None = None
|
||||||
self._client: httpx.AsyncClient | None = None
|
self._client: httpx.AsyncClient | None = None
|
||||||
self._throttle_lock = asyncio.Lock()
|
self._throttle_lock = asyncio.Lock()
|
||||||
self._last_request_at: float = 0.0
|
self._last_request_at: float = 0.0
|
||||||
@@ -130,6 +154,69 @@ class BinanceFuturesClient:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Save symbols cache file failed: %s", e)
|
logger.warning("Save symbols cache file failed: %s", e)
|
||||||
|
|
||||||
|
def _load_symbol_meta_file(self) -> dict[str, dict[str, Any]] | None:
|
||||||
|
try:
|
||||||
|
if _SYMBOL_META_CACHE_FILE.is_file():
|
||||||
|
data = json.loads(_SYMBOL_META_CACHE_FILE.read_text(encoding="utf-8"))
|
||||||
|
if isinstance(data, dict):
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Load symbol meta cache file failed: %s", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _save_symbol_meta_file(self, meta: dict[str, dict[str, Any]]) -> None:
|
||||||
|
try:
|
||||||
|
_SYMBOL_META_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
_SYMBOL_META_CACHE_FILE.write_text(
|
||||||
|
json.dumps(meta, ensure_ascii=False),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Save symbol meta cache file failed: %s", e)
|
||||||
|
|
||||||
|
async def _ensure_symbol_meta(self) -> dict[str, dict[str, Any]]:
|
||||||
|
if self._symbol_meta_cache:
|
||||||
|
return self._symbol_meta_cache
|
||||||
|
|
||||||
|
if self.is_rate_limited():
|
||||||
|
cached = self._load_symbol_meta_file()
|
||||||
|
if cached:
|
||||||
|
self._symbol_meta_cache = cached
|
||||||
|
return cached
|
||||||
|
raise BinanceRateLimitedError(self.rate_limit_remaining_sec(), "symbol_meta")
|
||||||
|
|
||||||
|
try:
|
||||||
|
info = await self._get("/fapi/v1/exchangeInfo")
|
||||||
|
meta: dict[str, dict[str, Any]] = {}
|
||||||
|
for s in info.get("symbols", []):
|
||||||
|
if (
|
||||||
|
s.get("contractType") == "PERPETUAL"
|
||||||
|
and s.get("quoteAsset") == "USDT"
|
||||||
|
and s.get("status") == "TRADING"
|
||||||
|
):
|
||||||
|
sym = s["symbol"]
|
||||||
|
meta[sym] = _parse_symbol_price_meta(s)
|
||||||
|
self._symbol_meta_cache = meta
|
||||||
|
self._save_symbol_meta_file(meta)
|
||||||
|
return meta
|
||||||
|
except BinanceRateLimitedError:
|
||||||
|
cached = self._load_symbol_meta_file()
|
||||||
|
if cached:
|
||||||
|
self._symbol_meta_cache = cached
|
||||||
|
return cached
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def get_symbol_price_meta(self, symbol: str) -> dict[str, Any]:
|
||||||
|
sym = symbol.upper().strip()
|
||||||
|
meta_map = await self._ensure_symbol_meta()
|
||||||
|
if sym in meta_map:
|
||||||
|
return meta_map[sym]
|
||||||
|
return {"tick_size": "0.01", "price_precision": 2}
|
||||||
|
|
||||||
|
def clear_symbol_cache(self) -> None:
|
||||||
|
self._symbols_cache = None
|
||||||
|
self._symbol_meta_cache = None
|
||||||
|
|
||||||
async def get_usdt_perpetual_symbols(self) -> list[str]:
|
async def get_usdt_perpetual_symbols(self) -> list[str]:
|
||||||
if self._symbols_cache:
|
if self._symbols_cache:
|
||||||
return self._symbols_cache
|
return self._symbols_cache
|
||||||
@@ -145,15 +232,20 @@ class BinanceFuturesClient:
|
|||||||
try:
|
try:
|
||||||
info = await self._get("/fapi/v1/exchangeInfo")
|
info = await self._get("/fapi/v1/exchangeInfo")
|
||||||
symbols = []
|
symbols = []
|
||||||
|
meta: dict[str, dict[str, Any]] = {}
|
||||||
for s in info.get("symbols", []):
|
for s in info.get("symbols", []):
|
||||||
if (
|
if (
|
||||||
s.get("contractType") == "PERPETUAL"
|
s.get("contractType") == "PERPETUAL"
|
||||||
and s.get("quoteAsset") == "USDT"
|
and s.get("quoteAsset") == "USDT"
|
||||||
and s.get("status") == "TRADING"
|
and s.get("status") == "TRADING"
|
||||||
):
|
):
|
||||||
symbols.append(s["symbol"])
|
sym = s["symbol"]
|
||||||
|
symbols.append(sym)
|
||||||
|
meta[sym] = _parse_symbol_price_meta(s)
|
||||||
self._symbols_cache = sorted(symbols)
|
self._symbols_cache = sorted(symbols)
|
||||||
|
self._symbol_meta_cache = meta
|
||||||
self._save_symbols_file(self._symbols_cache)
|
self._save_symbols_file(self._symbols_cache)
|
||||||
|
self._save_symbol_meta_file(meta)
|
||||||
logger.info("Loaded %d USDT perpetual symbols", len(self._symbols_cache))
|
logger.info("Loaded %d USDT perpetual symbols", len(self._symbols_cache))
|
||||||
return self._symbols_cache
|
return self._symbols_cache
|
||||||
except BinanceRateLimitedError:
|
except BinanceRateLimitedError:
|
||||||
@@ -164,9 +256,6 @@ class BinanceFuturesClient:
|
|||||||
return cached
|
return cached
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def clear_symbol_cache(self) -> None:
|
|
||||||
self._symbols_cache = None
|
|
||||||
|
|
||||||
async def get_24hr_tickers(self) -> list[dict]:
|
async def get_24hr_tickers(self) -> list[dict]:
|
||||||
data = await self._get("/fapi/v1/ticker/24hr")
|
data = await self._get("/fapi/v1/ticker/24hr")
|
||||||
return data if isinstance(data, list) else []
|
return data if isinstance(data, list) else []
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from .config import ROOT_DIR, settings
|
|||||||
from .funding_store import get_funding_bundle
|
from .funding_store import get_funding_bundle
|
||||||
from .kline_store import get_candles, get_daily_candles, sync_daily_klines, sync_klines
|
from .kline_store import get_candles, get_daily_candles, sync_daily_klines, sync_klines
|
||||||
from .chart_intervals import CHART_INTERVALS, limit_for_interval, validate_interval
|
from .chart_intervals import CHART_INTERVALS, limit_for_interval, validate_interval
|
||||||
|
from .binance import binance_client
|
||||||
from .db import get_latest_snapshot, init_db, log_push, save_snapshot
|
from .db import get_latest_snapshot, init_db, log_push, save_snapshot
|
||||||
from .exceptions import BinanceRateLimitedError
|
from .exceptions import BinanceRateLimitedError
|
||||||
from .period_api import get_period_top30
|
from .period_api import get_period_top30
|
||||||
@@ -28,6 +29,14 @@ logging.getLogger("httpx").setLevel(logging.WARNING)
|
|||||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def _chart_price_meta(sym: str) -> dict:
|
||||||
|
try:
|
||||||
|
return await binance_client.get_symbol_price_meta(sym)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("price meta %s fallback: %s", sym, e)
|
||||||
|
return {"tick_size": "0.01", "price_precision": 2}
|
||||||
|
|
||||||
WEB_DIR = ROOT_DIR / "web"
|
WEB_DIR = ROOT_DIR / "web"
|
||||||
|
|
||||||
|
|
||||||
@@ -165,6 +174,7 @@ async def api_chart(
|
|||||||
candles, source = await get_candles(
|
candles, source = await get_candles(
|
||||||
sym, iv, limit or default_limit, force_refresh=refresh
|
sym, iv, limit or default_limit, force_refresh=refresh
|
||||||
)
|
)
|
||||||
|
price_meta = await _chart_price_meta(sym)
|
||||||
return {
|
return {
|
||||||
"symbol": sym,
|
"symbol": sym,
|
||||||
"interval": iv,
|
"interval": iv,
|
||||||
@@ -172,6 +182,7 @@ async def api_chart(
|
|||||||
"candles": candles,
|
"candles": candles,
|
||||||
"source": source,
|
"source": source,
|
||||||
"intervals": list(CHART_INTERVALS),
|
"intervals": list(CHART_INTERVALS),
|
||||||
|
**price_meta,
|
||||||
}
|
}
|
||||||
except BinanceRateLimitedError as e:
|
except BinanceRateLimitedError as e:
|
||||||
raise HTTPException(503, f"币安限流,请 {e.retry_after_sec} 秒后再试") from e
|
raise HTTPException(503, f"币安限流,请 {e.retry_after_sec} 秒后再试") from e
|
||||||
@@ -188,6 +199,7 @@ async def api_chart_daily(symbol: str, limit: int | None = None, refresh: bool =
|
|||||||
raise HTTPException(400, "invalid symbol")
|
raise HTTPException(400, "invalid symbol")
|
||||||
try:
|
try:
|
||||||
candles, source = await get_daily_candles(sym, limit, force_refresh=refresh)
|
candles, source = await get_daily_candles(sym, limit, force_refresh=refresh)
|
||||||
|
price_meta = await _chart_price_meta(sym)
|
||||||
return {
|
return {
|
||||||
"symbol": sym,
|
"symbol": sym,
|
||||||
"interval": "1d",
|
"interval": "1d",
|
||||||
@@ -195,6 +207,7 @@ async def api_chart_daily(symbol: str, limit: int | None = None, refresh: bool =
|
|||||||
"candles": candles,
|
"candles": candles,
|
||||||
"source": source,
|
"source": source,
|
||||||
"intervals": list(CHART_INTERVALS),
|
"intervals": list(CHART_INTERVALS),
|
||||||
|
**price_meta,
|
||||||
}
|
}
|
||||||
except BinanceRateLimitedError as e:
|
except BinanceRateLimitedError as e:
|
||||||
raise HTTPException(503, f"币安限流,请 {e.retry_after_sec} 秒后再试") from e
|
raise HTTPException(503, f"币安限流,请 {e.retry_after_sec} 秒后再试") from e
|
||||||
|
|||||||
+160
-7
@@ -40,6 +40,8 @@ let lwcChart = null;
|
|||||||
let lwcCandleSeries = null;
|
let lwcCandleSeries = null;
|
||||||
let lwcVolumeSeries = null;
|
let lwcVolumeSeries = null;
|
||||||
let lwcResizeObserver = null;
|
let lwcResizeObserver = null;
|
||||||
|
let lwcPriceLines = [];
|
||||||
|
const symbolPriceMeta = new Map();
|
||||||
|
|
||||||
function cacheKey(symbol, interval) {
|
function cacheKey(symbol, interval) {
|
||||||
return `${symbol}:${interval}`;
|
return `${symbol}:${interval}`;
|
||||||
@@ -69,11 +71,18 @@ function loadKlineFromLS(symbol, interval) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveKlineToLS(symbol, interval, candles, source) {
|
function saveKlineToLS(symbol, interval, candles, source, priceMeta) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
LS_KLINE_PREFIX + symbol + "_" + interval,
|
LS_KLINE_PREFIX + symbol + "_" + interval,
|
||||||
JSON.stringify({ ts: Date.now(), candles, source, interval })
|
JSON.stringify({
|
||||||
|
ts: Date.now(),
|
||||||
|
candles,
|
||||||
|
source,
|
||||||
|
interval,
|
||||||
|
tick_size: priceMeta?.tick_size,
|
||||||
|
price_precision: priceMeta?.price_precision,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
/* quota */
|
/* quota */
|
||||||
@@ -88,6 +97,53 @@ function sourceLabel(source) {
|
|||||||
return "同步";
|
return "同步";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseMinMove(tickSize) {
|
||||||
|
const n = Number(tickSize);
|
||||||
|
return Number.isFinite(n) && n > 0 ? n : 0.01;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPrice(price, precision) {
|
||||||
|
return Number(price).toFixed(precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rememberPriceMeta(symbol, meta) {
|
||||||
|
if (!meta?.tick_size) return null;
|
||||||
|
const priceMeta = {
|
||||||
|
tick_size: meta.tick_size,
|
||||||
|
price_precision: Number(meta.price_precision ?? 2),
|
||||||
|
};
|
||||||
|
symbolPriceMeta.set(symbol, priceMeta);
|
||||||
|
return priceMeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPriceMeta(symbol, fallback) {
|
||||||
|
return (
|
||||||
|
symbolPriceMeta.get(symbol) ||
|
||||||
|
(fallback?.tick_size ? rememberPriceMeta(symbol, fallback) : null) || {
|
||||||
|
tick_size: "0.01",
|
||||||
|
price_precision: 2,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findCandleExtremes(candles, interval) {
|
||||||
|
let maxHigh = -Infinity;
|
||||||
|
let minLow = Infinity;
|
||||||
|
let highTime = null;
|
||||||
|
let lowTime = null;
|
||||||
|
for (const c of candles) {
|
||||||
|
if (c.high > maxHigh) {
|
||||||
|
maxHigh = c.high;
|
||||||
|
highTime = toLwcTime(c.time, interval);
|
||||||
|
}
|
||||||
|
if (c.low < minLow) {
|
||||||
|
minLow = c.low;
|
||||||
|
lowTime = toLwcTime(c.time, interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { maxHigh, minLow, highTime, lowTime };
|
||||||
|
}
|
||||||
|
|
||||||
function toLwcTime(ms, interval) {
|
function toLwcTime(ms, interval) {
|
||||||
if (interval === "1d" || interval === "1w") {
|
if (interval === "1d" || interval === "1w") {
|
||||||
const d = new Date(ms);
|
const d = new Date(ms);
|
||||||
@@ -261,7 +317,14 @@ async function fetchKlines(symbol, interval = DEFAULT_MINI_INTERVAL) {
|
|||||||
|
|
||||||
const ls = loadKlineFromLS(symbol, interval);
|
const ls = loadKlineFromLS(symbol, interval);
|
||||||
if (ls) {
|
if (ls) {
|
||||||
const result = { candles: ls.candles, source: ls.source || "browser", interval };
|
const priceMeta = rememberPriceMeta(symbol, ls);
|
||||||
|
const result = {
|
||||||
|
candles: ls.candles,
|
||||||
|
source: ls.source || "browser",
|
||||||
|
interval,
|
||||||
|
tick_size: priceMeta?.tick_size,
|
||||||
|
price_precision: priceMeta?.price_precision,
|
||||||
|
};
|
||||||
chartDataCache.set(key, result);
|
chartDataCache.set(key, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -273,13 +336,16 @@ async function fetchKlines(symbol, interval = DEFAULT_MINI_INTERVAL) {
|
|||||||
throw new Error(err.detail || res.statusText);
|
throw new Error(err.detail || res.statusText);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
const priceMeta = rememberPriceMeta(symbol, data);
|
||||||
const result = {
|
const result = {
|
||||||
candles: data.candles || [],
|
candles: data.candles || [],
|
||||||
source: data.source || "db",
|
source: data.source || "db",
|
||||||
interval,
|
interval,
|
||||||
|
tick_size: priceMeta.tick_size,
|
||||||
|
price_precision: priceMeta.price_precision,
|
||||||
};
|
};
|
||||||
chartDataCache.set(key, result);
|
chartDataCache.set(key, result);
|
||||||
saveKlineToLS(symbol, interval, result.candles, result.source);
|
saveKlineToLS(symbol, interval, result.candles, result.source, priceMeta);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,6 +374,7 @@ async function loadMiniChart(box) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function destroyLwcChart() {
|
function destroyLwcChart() {
|
||||||
|
clearHighLowAnnotations();
|
||||||
if (lwcResizeObserver) {
|
if (lwcResizeObserver) {
|
||||||
lwcResizeObserver.disconnect();
|
lwcResizeObserver.disconnect();
|
||||||
lwcResizeObserver = null;
|
lwcResizeObserver = null;
|
||||||
@@ -320,6 +387,85 @@ function destroyLwcChart() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearHighLowAnnotations() {
|
||||||
|
if (lwcCandleSeries) {
|
||||||
|
lwcPriceLines.forEach((line) => {
|
||||||
|
try {
|
||||||
|
lwcCandleSeries.removePriceLine(line);
|
||||||
|
} catch {
|
||||||
|
/* already removed */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
lwcCandleSeries.setMarkers([]);
|
||||||
|
}
|
||||||
|
lwcPriceLines = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySeriesPriceFormat(priceMeta) {
|
||||||
|
if (!lwcCandleSeries) return;
|
||||||
|
const precision = priceMeta.price_precision;
|
||||||
|
const minMove = parseMinMove(priceMeta.tick_size);
|
||||||
|
lwcCandleSeries.applyOptions({
|
||||||
|
priceFormat: {
|
||||||
|
type: "price",
|
||||||
|
precision,
|
||||||
|
minMove,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyHighLowAnnotations(candles, interval, priceMeta) {
|
||||||
|
if (!lwcCandleSeries || !candles.length) return;
|
||||||
|
clearHighLowAnnotations();
|
||||||
|
|
||||||
|
const { maxHigh, minLow, highTime, lowTime } = findCandleExtremes(candles, interval);
|
||||||
|
if (!Number.isFinite(maxHigh) || !Number.isFinite(minLow)) return;
|
||||||
|
|
||||||
|
const precision = priceMeta.price_precision;
|
||||||
|
const highText = formatPrice(maxHigh, precision);
|
||||||
|
const lowText = formatPrice(minLow, precision);
|
||||||
|
|
||||||
|
lwcPriceLines.push(
|
||||||
|
lwcCandleSeries.createPriceLine({
|
||||||
|
price: maxHigh,
|
||||||
|
color: COLORS.up,
|
||||||
|
lineWidth: 1,
|
||||||
|
lineStyle: LightweightCharts.LineStyle.Dotted,
|
||||||
|
axisLabelVisible: true,
|
||||||
|
title: `最高 ${highText}`,
|
||||||
|
}),
|
||||||
|
lwcCandleSeries.createPriceLine({
|
||||||
|
price: minLow,
|
||||||
|
color: COLORS.down,
|
||||||
|
lineWidth: 1,
|
||||||
|
lineStyle: LightweightCharts.LineStyle.Dotted,
|
||||||
|
axisLabelVisible: true,
|
||||||
|
title: `最低 ${lowText}`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const markers = [];
|
||||||
|
if (highTime != null) {
|
||||||
|
markers.push({
|
||||||
|
time: highTime,
|
||||||
|
position: "aboveBar",
|
||||||
|
color: COLORS.up,
|
||||||
|
shape: "circle",
|
||||||
|
text: `高 ${highText}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (lowTime != null) {
|
||||||
|
markers.push({
|
||||||
|
time: lowTime,
|
||||||
|
position: "belowBar",
|
||||||
|
color: COLORS.down,
|
||||||
|
shape: "circle",
|
||||||
|
text: `低 ${lowText}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
lwcCandleSeries.setMarkers(markers);
|
||||||
|
}
|
||||||
|
|
||||||
function ensureLwcChart(container) {
|
function ensureLwcChart(container) {
|
||||||
if (typeof LightweightCharts === "undefined") {
|
if (typeof LightweightCharts === "undefined") {
|
||||||
container.innerHTML = '<p class="chart-lwc-fallback">图表库加载失败</p>';
|
container.innerHTML = '<p class="chart-lwc-fallback">图表库加载失败</p>';
|
||||||
@@ -380,16 +526,20 @@ function ensureLwcChart(container) {
|
|||||||
return lwcChart;
|
return lwcChart;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLwcChart(candles, interval) {
|
function renderLwcChart(candles, interval, priceMeta) {
|
||||||
const container = document.getElementById("chart-modal-container");
|
const container = document.getElementById("chart-modal-container");
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
if (!lwcChart) ensureLwcChart(container);
|
if (!lwcChart) ensureLwcChart(container);
|
||||||
if (!lwcCandleSeries || !lwcVolumeSeries) return;
|
if (!lwcCandleSeries || !lwcVolumeSeries) return;
|
||||||
|
|
||||||
|
const meta = getPriceMeta(chartModalSymbol, priceMeta);
|
||||||
|
applySeriesPriceFormat(meta);
|
||||||
|
|
||||||
const { ohlc, vol } = candlesToLwc(candles, interval);
|
const { ohlc, vol } = candlesToLwc(candles, interval);
|
||||||
lwcCandleSeries.setData(ohlc);
|
lwcCandleSeries.setData(ohlc);
|
||||||
lwcVolumeSeries.setData(vol);
|
lwcVolumeSeries.setData(vol);
|
||||||
|
applyHighLowAnnotations(candles, interval, meta);
|
||||||
lwcChart.timeScale().fitContent();
|
lwcChart.timeScale().fitContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,9 +569,12 @@ async function loadModalChart(interval) {
|
|||||||
if (hint) hint.textContent = "加载中…";
|
if (hint) hint.textContent = "加载中…";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { candles, source } = await fetchKlines(chartModalSymbol, interval);
|
const { candles, source, tick_size, price_precision } = await fetchKlines(
|
||||||
|
chartModalSymbol,
|
||||||
|
interval
|
||||||
|
);
|
||||||
if (!candles.length) throw new Error("无K线数据");
|
if (!candles.length) throw new Error("无K线数据");
|
||||||
renderLwcChart(candles, interval);
|
renderLwcChart(candles, interval, { tick_size, price_precision });
|
||||||
updateModalMeta(candles, source, interval);
|
updateModalMeta(candles, source, interval);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (hint) hint.textContent = `加载失败: ${e.message}`;
|
if (hint) hint.textContent = `加载失败: ${e.message}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user