@@ -1,8 +1,7 @@
/ * *
* 中控行情区 : 单图 + 周期切换 , 数据来自 / api / chart / ohlcv ( 本地库优先 ) 。
* 中控行情区 : K 线 + 底部成交量 ; 十字线时显示 OHLCV ; 可视区间高低点 。
* /
( function ( ) {
const TF _ORDER = [ "1m" , "5m" , "15m" , "1h" , "4h" , "1d" , "1w" ] ;
const chartHost = document . getElementById ( "market-chart" ) ;
if ( ! chartHost ) return ;
@@ -12,6 +11,7 @@
const elRefresh = document . getElementById ( "market-refresh" ) ;
const elStatus = document . getElementById ( "market-status" ) ;
const elUpdated = document . getElementById ( "market-updated" ) ;
const elOverlay = document . querySelector ( ".market-ohlcv-overlay" ) ;
const elO = document . getElementById ( "mkt-o" ) ;
const elH = document . getElementById ( "mkt-h" ) ;
const elL = document . getElementById ( "mkt-l" ) ;
@@ -22,9 +22,11 @@
let chart = null ;
let candleSeries = null ;
let volumeSeries = null ;
let priceTick = null ;
let rangeMarkers = [ ] ;
let lastCandles = [ ] ;
let candleByTime = { } ;
let chartMeta = null ;
let loadToken = 0 ;
let marketInited = false ;
@@ -38,6 +40,33 @@
return n . toFixed ( 2 ) ;
}
function fmtPrice ( v ) {
if ( v == null || Number . isNaN ( Number ( v ) ) ) return "-" ;
const n = Number ( v ) ;
if ( n === 0 ) return "0" ;
const tick = priceTick ;
if ( tick && tick > 0 ) {
let decimals = tick >= 1 ? 0 : Math . max ( 0 , Math . min ( 12 , Math . round ( - Math . log10 ( tick ) ) ) ) ;
let text = n . toFixed ( decimals ) ;
if ( text . indexOf ( "." ) >= 0 ) text = text . replace ( /\.?0+$/ , "" ) ;
return text ;
}
const av = Math . abs ( n ) ;
let d = 8 ;
if ( av >= 10000 ) d = 2 ;
else if ( av >= 100 ) d = 3 ;
else if ( av >= 1 ) d = 4 ;
else if ( av >= 0.01 ) d = 6 ;
let text = n . toFixed ( d ) ;
if ( text . indexOf ( "." ) >= 0 ) text = text . replace ( /\.?0+$/ , "" ) ;
return text ;
}
function setOverlayVisible ( on ) {
if ( ! elOverlay ) return ;
elOverlay . classList . toggle ( "is-active" , ! ! on ) ;
}
function paintOhlcv ( bar ) {
if ( ! bar ) {
[ "o" , "h" , "l" , "c" , "v" ] . forEach ( function ( k ) {
@@ -46,15 +75,43 @@
} ) ;
return ;
}
if ( elO ) elO . textContent = bar . open != null ? String ( bar . open ) : "-" ;
if ( elH ) elH . textContent = bar . high != null ? String ( bar . high ) : "-" ;
if ( elL ) elL . textContent = bar . low != null ? String ( bar . low ) : "-" ;
if ( elC ) elC . textContent = bar . close != null ? String ( bar . close ) : "-" ;
if ( elO ) elO . textContent = fmtPrice ( bar . open ) ;
if ( elH ) elH . textContent = fmtPrice ( bar . high ) ;
if ( elL ) elL . textContent = fmtPrice ( bar . low ) ;
if ( elC ) elC . textContent = fmtPrice ( bar . close ) ;
if ( elV ) elV . textContent = fmtVol ( bar . volume ) ;
}
function hideOhlcvOverlay ( ) {
setOverlayVisible ( false ) ;
paintOhlcv ( null ) ;
}
function indexCandles ( candles ) {
candleByTime = { } ;
( candles || [ ] ) . forEach ( function ( c ) {
if ( c && c . time != null ) candleByTime [ c . time ] = c ;
} ) ;
}
function candleAtTime ( t ) {
if ( t == null ) return null ;
return candleByTime [ t ] || null ;
}
function buildVolumeData ( candles ) {
return ( candles || [ ] ) . map ( function ( c ) {
const up = Number ( c . close ) >= Number ( c . open ) ;
return {
time : c . time ,
value : Number ( c . volume ) || 0 ,
color : up ? "rgba(0, 255, 157, 0.5)" : "rgba(255, 77, 109, 0.5)" ,
} ;
} ) ;
}
function ensureChart ( ) {
if ( chart && candleSeries ) return true ;
if ( chart && candleSeries && volumeSeries ) return true ;
if ( ! window . LightweightCharts ) {
if ( elStatus ) {
elStatus . className = "market-status err" ;
@@ -64,40 +121,77 @@
}
chart = LightweightCharts . createChart ( chartHost , {
layout : { background : { color : "#0a1018" } , textColor : "#b8d4e8" } ,
grid : { vertLines : { color : "#1a2838" } , horzLines : { color : "#1a2838" } } ,
grid : {
vertLines : { visible : false } ,
horzLines : { visible : false } ,
} ,
rightPriceScale : { borderColor : "#2a4058" } ,
timeScale : { borderColor : "#2a4058" , timeVisible : true , secondsVisible : false } ,
crosshair : { mode : LightweightCharts . CrosshairMode ? LightweightCharts . CrosshairMode . Normal : 0 } ,
crosshair : {
mode : LightweightCharts . CrosshairMode
? LightweightCharts . CrosshairMode . Normal
: 0 ,
} ,
} ) ;
const opts = {
const candleOpts = {
upColor : "#00ff9d" ,
downColor : "#ff4d6d" ,
borderVisible : false ,
wickUpColor : "#00ff9d" ,
wickDownColor : "#ff4d6d" ,
} ;
if ( typeof chart . addCandlestickSeries === "function" ) {
candleSeries = chart . addCandlestickSeries ( opts ) ;
candleSeries = chart . addCandlestickSeries ( candleOpts ) ;
} else if (
typeof chart . addSeries === "function" &&
window . LightweightCharts &&
window . LightweightCharts . CandlestickSeries
) {
candleSeries = chart . addSeries ( window . LightweightCharts . CandlestickSeries , opts ) ;
candleSeries = chart . addSeries ( window . LightweightCharts . CandlestickSeries , candleOpts ) ;
}
if ( ! candleSeries ) return false ;
const volOpts = {
priceFormat : { type : "volume" } ,
priceScaleId : "volume" ,
lastValueVisible : false ,
} ;
if ( typeof chart . addHistogramSeries === "function" ) {
volumeSeries = chart . addHistogramSeries ( volOpts ) ;
} else if (
typeof chart . addSeries === "function" &&
window . LightweightCharts &&
window . LightweightCharts . HistogramSeries
) {
volumeSeries = chart . addSeries ( window . LightweightCharts . HistogramSeries , volOpts ) ;
}
if ( ! volumeSeries ) return false ;
chart . priceScale ( "right" ) . applyOptions ( {
scaleMargins : { top : 0.06 , bottom : 0.28 } ,
} ) ;
chart . priceScale ( "volume" ) . applyOptions ( {
scaleMargins : { top : 0.78 , bottom : 0 } ,
} ) ;
chart . subscribeCrosshairMove ( function ( param ) {
if ( ! param || ! param . time || ! param . seriesData ) return ;
const d = param . seriesData . get ( candleSeries ) ;
if ( ! d ) return ;
paintOhlcv ( {
open : d . open ,
high : d . high ,
low : d . low ,
close : d . close ,
volume : d . volume ,
} ) ;
if ( ! param || param . time == null ) {
hideOhlcvOverlay ( ) ;
return ;
}
const bar = candleAtTime ( param . time ) ;
if ( ! bar ) {
hideOhlcvOverlay ( ) ;
return ;
}
setOverlayVisible ( true ) ;
paintOhlcv ( bar ) ;
} ) ;
chart . timeScale ( ) . subscribeVisibleLogicalRangeChange ( function ( ) {
updateVisibleRangeMarkers ( ) ;
} ) ;
window . addEventListener ( "resize" , function ( ) {
@@ -105,6 +199,7 @@
chart . applyOptions ( { width : chartHost . clientWidth , height : chartHost . clientHeight } ) ;
} ) ;
chart . applyOptions ( { width : chartHost . clientWidth , height : chartHost . clientHeight } ) ;
hideOhlcvOverlay ( ) ;
return true ;
}
@@ -117,35 +212,47 @@
rangeMarkers = [ ] ;
}
function addRangeMarkers ( data ) {
function updateVisibleRangeMarkers ( ) {
clearMarkers ( ) ;
if ( ! candleSeries || ! data ) return ;
const hi = data . range _high ;
const lo = data . range _low ;
if ( hi && hi . price != null ) {
rangeMarkers . push (
candleSeries . createPriceLine ( {
price : Number ( hi . price ) ,
color : "#ffb84d" ,
lineWidth : 1 ,
lineStyle : 2 ,
axisLabelVisible : true ,
title : "区间高" ,
} )
) ;
}
if ( lo && lo . price != null ) {
rangeMarkers . push (
candleSeries . createPriceLine ( {
price : Number ( lo . price ) ,
color : "#4cd97f" ,
lineWidth : 1 ,
lineStyle : 2 ,
axisLabelVisible : true ,
title : "区间低" ,
} )
) ;
if ( ! candleSeries || ! chart || ! lastCandles . length ) return ;
const range = chart . timeScale ( ) . getVisibleLogicalRange ( ) ;
if ( ! range ) return ;
const from = Math . max ( 0 , Math . floor ( range . from ) ) ;
const to = Math . min ( lastCandles . length - 1 , Math . ceil ( range . to ) ) ;
if ( to < from ) return ;
let hi = null ;
let lo = null ;
for ( let i = from ; i <= to ; i ++ ) {
const c = lastCandles [ i ] ;
if ( ! c ) continue ;
if ( ! hi || c . high > hi . high ) hi = c ;
if ( ! lo || c . low < lo . low ) lo = c ;
}
if ( ! hi || ! lo ) return ;
rangeMarkers . push (
candleSeries . createPriceLine ( {
price : Number ( hi . high ) ,
color : "#ffb84d" ,
lineWidth : 1 ,
lineStyle : 2 ,
axisLabelVisible : true ,
title : "可视高" ,
} )
) ;
rangeMarkers . push (
candleSeries . createPriceLine ( {
price : Number ( lo . low ) ,
color : "#4cd97f" ,
lineWidth : 1 ,
lineStyle : 2 ,
axisLabelVisible : true ,
title : "可视低" ,
} )
) ;
}
function readQuery ( ) {
@@ -158,6 +265,11 @@
if ( tf && elTf ) elTf . value = tf ;
}
function applyDefaults ( ) {
if ( elSymbol && ! elSymbol . value . trim ( ) ) elSymbol . value = "BTC/USDT" ;
if ( elTf && ! elTf . value ) elTf . value = "1d" ;
}
async function loadMeta ( ) {
const r = await fetch ( "/api/chart/meta" , { credentials : "same-origin" } ) ;
chartMeta = await r . json ( ) ;
@@ -170,13 +282,14 @@
elExchange . appendChild ( opt ) ;
} ) ;
readQuery ( ) ;
applyDefaults ( ) ;
}
async function loadChart ( force ) {
if ( ! ensureChart ( ) ) return ;
const exKey = ( elExchange && elExchange . value ) || "" ;
const sym = ( elSymbol && elSymbol . value . trim ( ) . toUpperCase ( ) ) || "" ;
const tf = ( elTf && elTf . value ) || "5m " ;
const tf = ( elTf && elTf . value ) || "1d " ;
if ( ! exKey || ! sym ) {
if ( elStatus ) {
elStatus . className = "market-status err" ;
@@ -189,6 +302,7 @@
elStatus . className = "market-status" ;
elStatus . textContent = "加载中…" ;
}
hideOhlcvOverlay ( ) ;
if ( elSymLabel ) elSymLabel . textContent = sym ;
if ( elTfLabel ) elTfLabel . textContent = tf ;
@@ -212,23 +326,19 @@
priceTick = data . price _tick ;
lastCandles = data . candles ;
candleSeries . setData ( data . candles ) ;
indexCandles ( lastCandles ) ;
candleSeries . setData ( lastCandles ) ;
volumeSeries . setData ( buildVolumeData ( lastCandles ) ) ;
chart . timeScale ( ) . fitContent ( ) ;
addRangeMarkers ( data ) ;
const ohlcv = data . ohlcv || { } ;
paintOhlcv ( {
open : ohlcv . open ,
high : ohlcv . high ,
low : ohlcv . low ,
close : ohlcv . close ,
volume : ohlcv . volume ,
} ) ;
updateVisibleRangeMarkers ( ) ;
const limit = data . limit || lastCandles . length ;
let hint =
"已加载 " +
data . candles . length +
" 根(库 " +
" 根(目标 " +
limit +
")· 库 " +
( data . from _cache || 0 ) +
" / 新拉 " +
( data . fetched || 0 ) +
@@ -274,9 +384,11 @@
} ) ;
}
const btnLoad = document . getElementById ( "market-load" ) ;
if ( btnLoad ) btnLoad . addEventListener ( "click" , function ( ) {
loadChart ( false ) ;
} ) ;
if ( btnLoad ) {
btnLoad . addEventListener ( "click" , function ( ) {
loadChart ( false ) ;
} ) ;
}
}
window . hubMarketChart = {
@@ -293,7 +405,10 @@
} ,
} ;
if ( document . getElementById ( "page-market" ) && ! document . getElementById ( "page-market" ) . classList . contains ( "hidden" ) ) {
if (
document . getElementById ( "page-market" ) &&
! document . getElementById ( "page-market" ) . classList . contains ( "hidden" )
) {
window . hubMarketChart . init ( ) ;
}
} ) ( ) ;