Files
Chan/web/static/js/app/deriv_ui.js
T
jackyu66gitandCursor c22cd48f36 Web 加上资金面/情绪叠图,并收紧默认图面。
默认主/次/次次改为 45m/15m/5m、开始时间一周;SD/CD 按 hist 摆位置和箭头;去掉笔线段背驰与第四类勾选。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-12 02:25:44 +08:00

437 lines
14 KiB
JavaScript

/* 资金面 + 情绪面。只打本站中转。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
};
})();