/* 资金面 + 情绪面。只打本站中转。OI 叠主图左侧,其余叠情绪副图。 */ window.ChanDeriv = (function () { var snapshotReq = 0; var latestReq = 0; var overlayReq = 0; var caches = {}; var METRICS = [ { id: 'oi', checkbox: 'showOi', metric: 'open_interest_history', field: 'open_interest_amount', seriesKey: 'oiSeries', target: 'main', color: 'rgba(123, 31, 162, 0.85)', title: 'OI', priceFormat: { type: 'volume' } }, { id: 'taker', checkbox: 'showDeriv', metric: 'taker_buy_sell_ratio', field: 'buy_sell_ratio', seriesKey: 'takerSeries', target: 'sentiment', color: '#0d9488', title: '买卖比', priceFormat: { type: 'price', precision: 3 } }, { id: 'lsAccount', checkbox: 'showDeriv', metric: 'long_short_account_ratio', field: 'long_short_ratio', seriesKey: 'lsAccountSeries', target: 'sentiment', color: '#2563eb', title: '多空', priceFormat: { type: 'price', precision: 3 } }, { id: 'lsTop', checkbox: 'showDeriv', metric: 'top_long_short_position_ratio', field: 'long_short_ratio', seriesKey: 'lsTopSeries', target: 'sentiment', color: '#ea580c', title: '大户', priceFormat: { type: 'price', precision: 3 } }, { id: 'funding', checkbox: 'showDeriv', metric: 'funding_rate_history', field: 'funding_rate', seriesKey: 'fundingHistSeries', target: 'sentiment', color: '#7c3aed', title: '费率%', scale: 'funding', mul: 100, priceFormat: { type: 'price', precision: 4 } } ]; function isCrypto() { return ($('#dataSource').val() || 'crypto') === 'crypto'; } function currentSymbol() { return $('#symbol').val() || 'BTC/USDT:USDT'; } function fmtOi(n) { if (n == null || !isFinite(Number(n))) return '—'; return Number(n).toLocaleString('en-US', { maximumFractionDigits: 1 }); } function fmtFunding(n) { if (n == null || !isFinite(Number(n))) return '—'; return (Number(n) * 100).toFixed(4) + '%'; } function fmtChg(n) { if (n == null || !isFinite(Number(n))) return '—'; var v = Number(n); return (v > 0 ? '+' : '') + v.toFixed(2) + '%'; } function fmtBasis(n) { if (n == null || !isFinite(Number(n))) return '—'; var v = Number(n); return (v > 0 ? '+' : '') + v.toFixed(2); } function fmtRatio(n) { if (n == null || !isFinite(Number(n))) return '—'; return Number(n).toFixed(3); } function setChip(id, text, tone) { var el = document.getElementById(id); if (!el) return; el.textContent = text; el.classList.remove('up', 'down'); if (tone) el.classList.add(tone); } function tone(n, invert) { if (!isFinite(n) || n === 0) return null; var up = n > 0; if (invert) up = !up; return up ? 'up' : 'down'; } function renderDerivEmpty() { setChip('derivOi', 'OI —'); setChip('derivOiChg', 'Δ —'); setChip('derivFunding', '费率 —'); setChip('derivBasis', '基差 —'); var src = document.getElementById('derivSrc'); if (src) src.textContent = ''; } function renderSentimentEmpty() { setChip('derivTaker', '买卖比 —'); setChip('derivLs', '多空 —'); setChip('derivTop', '大户 —'); } function setVisible(on) { var bar = document.getElementById('derivBar'); var wrap = document.getElementById('showSentimentWrap'); if (bar) bar.style.display = on ? '' : 'none'; if (wrap) wrap.style.display = on ? '' : 'none'; if (!on) clearAllSeries(); } function activeKlines() { var data = (typeof currentData !== 'undefined') ? currentData : null; if (!data) return []; if ($('#subSubPeriodKline').is(':checked') && data.sub_sub_kline_data) return data.sub_sub_kline_data; if ($('#elementPeriodKline').is(':checked') && data.element_kline_data) return data.element_kline_data; return data.kline_data || []; } function klineRangeMs() { var rows = activeKlines(); if (!rows.length) return null; var start = new Date(rows[0].date).getTime(); var end = new Date(rows[rows.length - 1].date).getTime(); if (!isFinite(start) || !isFinite(end)) return null; return { start: start, end: end }; } function toPoints(rows, field, mul) { var out = []; var lastT = null; var factor = mul || 1; (rows || []).forEach(function (row) { var ms = Number(row.timestamp); var val = Number(row[field]); if (!isFinite(ms) || !isFinite(val)) return; var t = Math.floor(ms / 1000); var v = val * factor; if (lastT === t) { out[out.length - 1].value = v; return; } lastT = t; out.push({ time: t, value: v }); }); return out; } function candleTimesSec() { var rows = activeKlines(); var times = []; (rows || []).forEach(function (k) { var t = Math.floor(new Date(k.date).getTime() / 1000); if (isFinite(t)) times.push(t); }); return times; } function alignToTimes(src, times) { if (!src || !src.length || !times || !times.length) return []; var out = []; var j = 0; var lastVal; for (var i = 0; i < times.length; i++) { var t = times[i]; while (j < src.length && src[j].time <= t) { lastVal = src[j].value; j++; } if (lastVal !== undefined) out.push({ time: t, value: lastVal }); } return out; } function syncSentimentTime() { if (!tvWidget || !tvWidget.mainChart || !tvWidget.sentimentChart) return; try { var vr = tvWidget.mainChart.timeScale().getVisibleRange(); var lr = tvWidget.mainChart.timeScale().getVisibleLogicalRange(); var opts = tvWidget.mainChart.timeScale().options ? tvWidget.mainChart.timeScale().options() : null; if (opts) { tvWidget.sentimentChart.timeScale().applyOptions({ barSpacing: opts.barSpacing, rightOffset: opts.rightOffset }); } if (vr) tvWidget.sentimentChart.timeScale().setVisibleRange(vr); if (lr) tvWidget.sentimentChart.timeScale().setVisibleLogicalRange(lr); } catch (e) {} } function targetChart(spec) { if (!tvWidget) return null; if (spec.target === 'main') return tvWidget.mainChart; return tvWidget.sentimentChart || null; } function safeRemove(chart, seriesKey) { var series = tvWidget && tvWidget.series && tvWidget.series[seriesKey]; if (series && chart) { try { chart.removeSeries(series); } catch (e) {} } if (tvWidget && tvWidget.series) tvWidget.series[seriesKey] = null; } function clearAllSeries() { METRICS.forEach(function (spec) { safeRemove(targetChart(spec), spec.seriesKey); }); safeRemove(tvWidget && tvWidget.sentimentChart, 'sentimentBaseSeries'); try { if (tvWidget && tvWidget.mainChart) { tvWidget.mainChart.applyOptions({ leftPriceScale: { visible: false } }); } } catch (e) {} } function drawSeries(spec, points) { var chart = targetChart(spec); if (!chart || !points || !points.length) return; safeRemove(chart, spec.seriesKey); if (spec.target === 'main') { chart.applyOptions({ leftPriceScale: { visible: true, borderVisible: false, scaleMargins: { top: 0.08, bottom: 0.12 } } }); } var opts = { color: spec.color, lineWidth: 1, title: spec.title, lastValueVisible: true, priceLineVisible: false, priceFormat: spec.priceFormat }; if (spec.target === 'main') opts.priceScaleId = 'left'; if (spec.scale) { opts.priceScaleId = spec.scale; chart.priceScale(spec.scale).applyOptions({ scaleMargins: { top: 0.15, bottom: 0.1 }, borderVisible: false }); } var series = chart.addLineSeries(opts); series.setData(points); tvWidget.series[spec.seriesKey] = series; if (spec.target === 'sentiment') syncSentimentTime(); if (spec.target === 'main' && typeof window._redrawFxBoxVerticalOverlay === 'function') { window._redrawFxBoxVerticalOverlay(); setTimeout(window._redrawFxBoxVerticalOverlay, 50); } } function drawRatioBaseline(points) { var chart = tvWidget && tvWidget.sentimentChart; if (!chart || !points || !points.length) return; safeRemove(chart, 'sentimentBaseSeries'); var baseline = points.map(function (p) { return { time: p.time, value: 1 }; }); var series = chart.addLineSeries({ color: 'rgba(120, 120, 120, 0.45)', lineWidth: 1, lineStyle: 2, lastValueVisible: false, priceLineVisible: false, title: '1.0' }); series.setData(baseline); tvWidget.series.sentimentBaseSeries = series; } function loadOne(spec, range, symbol, req) { var checked = $('#' + spec.checkbox).is(':checked'); var chart = targetChart(spec); if (!checked) { safeRemove(chart, spec.seriesKey); if (spec.id === 'oi') { try { if (tvWidget && tvWidget.mainChart) { tvWidget.mainChart.applyOptions({ leftPriceScale: { visible: false } }); } } catch (e) {} if (typeof window._redrawFxBoxVerticalOverlay === 'function') { window._redrawFxBoxVerticalOverlay(); } } return; } if (!chart) return; var times = candleTimesSec(); var key = [symbol, spec.metric, times[0] || '', times[times.length - 1] || '', times.length].join(':'); var cached = caches[spec.id]; function paint(raw) { var aligned = spec.target === 'main' || spec.target === 'sentiment' ? alignToTimes(raw, times) : raw; if (!aligned.length) aligned = raw; drawSeries(spec, aligned); if (spec.target === 'sentiment') maybeDrawBaseline(); } if (cached && cached.raw && cached.raw.length) { paint(cached.raw); if (cached.key === key) return; } if (!range || !window.ChanApi || !ChanApi.sentimentMetrics) return; ChanApi.sentimentMetrics({ metric: spec.metric, symbol: symbol, start: range.start - 8 * 60 * 60 * 1000, end: range.end, limit: 2000 }).then(function (payload) { if (req !== overlayReq) return; var raw = toPoints(payload && payload.data, spec.field, spec.mul); if (!raw.length) return; caches[spec.id] = { key: key, raw: raw }; if ($('#' + spec.checkbox).is(':checked') && targetChart(spec)) paint(raw); }).catch(function (err) { if (req !== overlayReq) return; console.warn(spec.title + ' 序列不可用', err); }); } function maybeDrawBaseline() { var times = candleTimesSec(); if (!times.length) return; drawRatioBaseline(times.map(function (t) { return { time: t, value: 1 }; })); syncSentimentTime(); } function loadOverlays() { if (!isCrypto()) { clearAllSeries(); return; } if (!tvWidget || !tvWidget.mainChart) return; var range = klineRangeMs(); var symbol = currentSymbol(); var req = ++overlayReq; METRICS.forEach(function (spec) { loadOne(spec, range, symbol, req); }); maybeDrawBaseline(); } function loadSnapshot() { if (!isCrypto() || !window.ChanApi || !ChanApi.derivatives) return; var req = ++snapshotReq; ChanApi.derivatives({ symbol: currentSymbol() }).then(function (row) { if (req !== snapshotReq) return; var chg = Number(row.oi_change_pct); var fund = Number(row.funding_rate); var basis = Number(row.basis); setChip('derivOi', 'OI ' + fmtOi(row.open_interest)); setChip('derivOiChg', 'Δ ' + fmtChg(row.oi_change_pct), tone(chg)); setChip('derivFunding', '费率 ' + fmtFunding(row.funding_rate), tone(fund)); setChip('derivBasis', '基差 ' + fmtBasis(row.basis), tone(basis)); var src = document.getElementById('derivSrc'); if (src) src.textContent = row.exchange || ''; }).catch(function (err) { if (req !== snapshotReq) return; console.warn('资金面快照不可用', err); renderDerivEmpty(); var src = document.getElementById('derivSrc'); if (src) src.textContent = '不可用'; }); } function loadLatest() { if (!isCrypto() || !window.ChanApi || !ChanApi.sentimentLatest) return; var req = ++latestReq; ChanApi.sentimentLatest({ symbol: currentSymbol() }).then(function (payload) { if (req !== latestReq) return; var data = (payload && payload.data) || {}; var taker = data.taker_buy_sell_ratio || {}; var ls = data.long_short_account_ratio || {}; var top = data.top_long_short_position_ratio || {}; var takerN = Number(taker.buy_sell_ratio); var lsN = Number(ls.long_short_ratio); var topN = Number(top.long_short_ratio); setChip('derivTaker', '买卖比 ' + fmtRatio(taker.buy_sell_ratio), isFinite(takerN) ? (takerN >= 1 ? 'up' : 'down') : null); setChip('derivLs', '多空 ' + fmtRatio(ls.long_short_ratio), isFinite(lsN) ? (lsN >= 1 ? 'up' : 'down') : null); setChip('derivTop', '大户 ' + fmtRatio(top.long_short_ratio), isFinite(topN) ? (topN >= 1 ? 'up' : 'down') : null); }).catch(function (err) { if (req !== latestReq) return; console.warn('情绪快照不可用', err); renderSentimentEmpty(); }); } function sync(opts) { opts = opts || {}; var show = isCrypto(); setVisible(show); if (!show) return; loadSnapshot(); loadLatest(); if (opts.overlay !== false) loadOverlays(); } return { sync: sync, setVisible: setVisible, loadOiOverlay: loadOverlays, loadOverlays: loadOverlays }; })();