Web 加上资金面/情绪叠图,并收紧默认图面。
默认主/次/次次改为 45m/15m/5m、开始时间一周;SD/CD 按 hist 摆位置和箭头;去掉笔线段背驰与第四类勾选。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,436 @@
|
||||
/* 资金面 + 情绪面。只打本站中转。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
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user