优化:折线图初始化数据
This commit is contained in:
parent
fef3d86fc2
commit
1a63d2d6e1
@ -191,9 +191,10 @@ export interface FetchCryptoChartResponse {
|
|||||||
*/
|
*/
|
||||||
async function fetchBinanceKlines(
|
async function fetchBinanceKlines(
|
||||||
binanceSymbol: string,
|
binanceSymbol: string,
|
||||||
limit: number
|
limit: number,
|
||||||
|
interval: '1s' | '1m' = '1m'
|
||||||
): Promise<CryptoChartPoint[]> {
|
): Promise<CryptoChartPoint[]> {
|
||||||
const url = `${BINANCE_REST}/klines?symbol=${binanceSymbol}&interval=1m&limit=${limit}`
|
const url = `${BINANCE_REST}/klines?symbol=${binanceSymbol}&interval=${interval}&limit=${limit}`
|
||||||
const res = await fetch(url)
|
const res = await fetch(url)
|
||||||
if (!res.ok) throw new Error(`Binance API: ${res.status}`)
|
if (!res.ok) throw new Error(`Binance API: ${res.status}`)
|
||||||
const raw = (await res.json()) as [number, string, string, string, string, number, ...unknown[]][]
|
const raw = (await res.json()) as [number, string, string, string, string, number, ...unknown[]][]
|
||||||
@ -223,9 +224,41 @@ async function fetchCoinGeckoChart(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 拉取加密货币价格历史
|
* 拉取加密货币价格历史
|
||||||
* 优先 Binance(1 分钟粒度),不支持时回退 CoinGecko
|
* 优先 Binance(1 分钟粒度;30S 场景为 1 秒粒度以铺满视窗),不支持时回退 CoinGecko
|
||||||
*/
|
*/
|
||||||
const THIRTY_SEC_MS = 30 * 1000
|
/**
|
||||||
|
* 30S 细粒度模式下内存中保留的历史跨度(毫秒)。
|
||||||
|
* 需 ≥ 图表 timeScale 实际展示的时间宽度;仅用「最近 30s」裁剪时会把已拉取的 1s K 线丢掉大半,屏内左侧会变空。
|
||||||
|
*/
|
||||||
|
export const CRYPTO_30S_RETENTION_MS = 120 * 1000
|
||||||
|
|
||||||
|
/** 30S 初始:拉取的 1s K 线条数,应略大于 {@link CRYPTO_30S_RETENTION_MS}(秒)以抵消网络延迟与 K 线闭合同步 */
|
||||||
|
const CRYPTO_30S_INITIAL_1S_LIMIT = 150
|
||||||
|
|
||||||
|
/** 加密货币分时折线图 X 轴 / 补点统一时间步长(ms),与图表 timeScale 一致 */
|
||||||
|
export const CRYPTO_CHART_TIME_STEP_MS = 100
|
||||||
|
|
||||||
|
/** 将毫秒时间戳对齐到 {@link CRYPTO_CHART_TIME_STEP_MS} 网格(向下取整,单调递增友好) */
|
||||||
|
export function alignCryptoChartTimeMs(tsMs: number): number {
|
||||||
|
const step = CRYPTO_CHART_TIME_STEP_MS
|
||||||
|
return Math.floor(tsMs / step) * step
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同一时间对齐戳保留最后一条,避免与 realtime 合并时重复键 */
|
||||||
|
function dedupeLatestByTime(points: CryptoChartPoint[]): CryptoChartPoint[] {
|
||||||
|
if (points.length <= 1) return points
|
||||||
|
const sorted = [...points].sort((a, b) => a[0] - b[0])
|
||||||
|
const out: CryptoChartPoint[] = []
|
||||||
|
for (const pt of sorted) {
|
||||||
|
const last = out[out.length - 1]
|
||||||
|
if (last && last[0] === pt[0]) {
|
||||||
|
out[out.length - 1] = pt
|
||||||
|
} else {
|
||||||
|
out.push(pt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchCryptoChart(
|
export async function fetchCryptoChart(
|
||||||
params: FetchCryptoChartParams
|
params: FetchCryptoChartParams
|
||||||
@ -243,10 +276,21 @@ export async function fetchCryptoChart(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (useBinance) {
|
if (useBinance) {
|
||||||
let points = await fetchBinanceKlines(binanceSymbol, limit)
|
let points: CryptoChartPoint[]
|
||||||
if (is30S) {
|
if (is30S) {
|
||||||
const cutoff = Date.now() - THIRTY_SEC_MS
|
points = await fetchBinanceKlines(
|
||||||
points = points.filter(([ts]) => ts >= cutoff)
|
binanceSymbol,
|
||||||
|
Math.min(CRYPTO_30S_INITIAL_1S_LIMIT, 1000),
|
||||||
|
'1s'
|
||||||
|
)
|
||||||
|
const cutoff = Date.now() - CRYPTO_30S_RETENTION_MS
|
||||||
|
points = dedupeLatestByTime(
|
||||||
|
points
|
||||||
|
.map(([ts, p]) => [alignCryptoChartTimeMs(ts), p] as CryptoChartPoint)
|
||||||
|
.filter(([ts]) => ts >= cutoff)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
points = await fetchBinanceKlines(binanceSymbol, limit, '1m')
|
||||||
}
|
}
|
||||||
return { code: 0, data: points, msg: 'ok' }
|
return { code: 0, data: points, msg: 'ok' }
|
||||||
}
|
}
|
||||||
@ -254,7 +298,7 @@ export async function fetchCryptoChart(
|
|||||||
const days = RANGE_TO_DAYS[fetchRange] ?? 7
|
const days = RANGE_TO_DAYS[fetchRange] ?? 7
|
||||||
let points = await fetchCoinGeckoChart(coinId, days)
|
let points = await fetchCoinGeckoChart(coinId, days)
|
||||||
if (is30S) {
|
if (is30S) {
|
||||||
const cutoff = Date.now() - THIRTY_SEC_MS
|
const cutoff = Date.now() - CRYPTO_30S_RETENTION_MS
|
||||||
points = points.filter(([ts]) => ts >= cutoff)
|
points = points.filter(([ts]) => ts >= cutoff)
|
||||||
}
|
}
|
||||||
return { code: 0, data: points, msg: 'ok' }
|
return { code: 0, data: points, msg: 'ok' }
|
||||||
@ -278,15 +322,6 @@ interface BinanceAggTradeMsg {
|
|||||||
|
|
||||||
const CRYPTO_UPDATE_THROTTLE_MS = 80
|
const CRYPTO_UPDATE_THROTTLE_MS = 80
|
||||||
|
|
||||||
/** 加密货币分时折线图 X 轴 / 补点统一时间步长(ms),与图表 timeScale 一致 */
|
|
||||||
export const CRYPTO_CHART_TIME_STEP_MS = 100
|
|
||||||
|
|
||||||
/** 将毫秒时间戳对齐到 {@link CRYPTO_CHART_TIME_STEP_MS} 网格(向下取整,单调递增友好) */
|
|
||||||
export function alignCryptoChartTimeMs(tsMs: number): number {
|
|
||||||
const step = CRYPTO_CHART_TIME_STEP_MS
|
|
||||||
return Math.floor(tsMs / step) * step
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订阅 Binance 归集交易 WebSocket(aggTrade),实现更丝滑的实时走势
|
* 订阅 Binance 归集交易 WebSocket(aggTrade),实现更丝滑的实时走势
|
||||||
* 参考:https://developers.binance.com/docs/zh-CN/binance-spot-api-docs/web-socket-streams#归集交易
|
* 参考:https://developers.binance.com/docs/zh-CN/binance-spot-api-docs/web-socket-streams#归集交易
|
||||||
|
|||||||
@ -414,6 +414,7 @@ import {
|
|||||||
subscribeCryptoRealtime,
|
subscribeCryptoRealtime,
|
||||||
alignCryptoChartTimeMs,
|
alignCryptoChartTimeMs,
|
||||||
CRYPTO_CHART_TIME_STEP_MS,
|
CRYPTO_CHART_TIME_STEP_MS,
|
||||||
|
CRYPTO_30S_RETENTION_MS,
|
||||||
} from '../api/cryptoChart'
|
} from '../api/cryptoChart'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@ -1225,7 +1226,7 @@ const lineColor = '#2563eb'
|
|||||||
/** 叠加价格轴 ID:曲线绑定在 overlay scale,主图占满宽;Y 轴数字由 InPanePriceScalePrimitive 画在图内 */
|
/** 叠加价格轴 ID:曲线绑定在 overlay scale,主图占满宽;Y 轴数字由 InPanePriceScalePrimitive 画在图内 */
|
||||||
const CHART_OVERLAY_PRICE_SCALE_ID = 'overlay-price'
|
const CHART_OVERLAY_PRICE_SCALE_ID = 'overlay-price'
|
||||||
/** 右侧留白(px):fitContent 时压缩可用宽度,线头略离开物理右缘,lastPrice 水波能露出一半且不依赖超大 rightOffset(bars) */
|
/** 右侧留白(px):fitContent 时压缩可用宽度,线头略离开物理右缘,lastPrice 水波能露出一半且不依赖超大 rightOffset(bars) */
|
||||||
const CHART_RIPPLE_RIGHT_PAD_PX = 28
|
const CHART_RIPPLE_RIGHT_PAD_PX = 0
|
||||||
|
|
||||||
function ensureChartSeries() {
|
function ensureChartSeries() {
|
||||||
if (!chartInstance || !chartContainerRef.value) return
|
if (!chartInstance || !chartContainerRef.value) return
|
||||||
@ -1392,7 +1393,7 @@ function applyCryptoRealtimePoint(point: [number, number]) {
|
|||||||
let updatePoint: [number, number] | null = null
|
let updatePoint: [number, number] | null = null
|
||||||
|
|
||||||
if (range === '30S') {
|
if (range === '30S') {
|
||||||
const cutoff = Date.now() - 30 * 1000
|
const cutoff = Date.now() - CRYPTO_30S_RETENTION_MS
|
||||||
if (prevLastT != null && ts === prevLastT) {
|
if (prevLastT != null && ts === prevLastT) {
|
||||||
list[list.length - 1] = [ts, price]
|
list[list.length - 1] = [ts, price]
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user