refactor: 缠论引擎包化与 Web 分层(ECR-001)
将根目录引擎迁入 chanlun/ 并保留兼容 shim;拆分 TF_DF 与 web 服务; 前端模块化;strategies 改用 chanlun 导入;补充 ESS 文档与 golden 回归。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,571 @@
|
||||
/* trend.js */
|
||||
function initTrendTables() {
|
||||
if (!FEATURES.trendFilter) return; // 未启用则跳过初始化
|
||||
if (!trendTable) {
|
||||
trendTable = $('#trendFilterTable').DataTable({
|
||||
paging: true,
|
||||
searching: false,
|
||||
info: true,
|
||||
order: [[4, 'desc']],
|
||||
});
|
||||
}
|
||||
if (!trendDetailTable) {
|
||||
trendDetailTable = $('#trendDetailTable').DataTable({
|
||||
paging: true,
|
||||
searching: false,
|
||||
info: true,
|
||||
order: [[0, 'desc']],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function bindTrendControls() {
|
||||
// 双向绑定强度滑块与数字框
|
||||
$('#trendMinStrength').on('input change', function(){
|
||||
$('#trendMinStrengthNum').val($(this).val());
|
||||
});
|
||||
$('#trendMinStrengthNum').on('input change', function(){
|
||||
let v = Math.max(0, Math.min(100, parseFloat($(this).val()||0)));
|
||||
$(this).val(v);
|
||||
$('#trendMinStrength').val(v);
|
||||
});
|
||||
|
||||
// 周期变化时,自动填充当前时间回溯300根K线的时间范围
|
||||
$('#trendTimeframe').on('change', function(){
|
||||
const tf = $(this).val();
|
||||
const step = window.timeframeToMs(tf) || (60*60*1000);
|
||||
const now = new Date();
|
||||
const endMs = now.getTime();
|
||||
const startMs = endMs - 300 * step;
|
||||
const toLocal = (ms) => new Date(ms - new Date(ms).getTimezoneOffset()*60000).toISOString().slice(0,16);
|
||||
$('#trendEnd').val(toLocal(endMs));
|
||||
$('#trendStart').val(toLocal(startMs));
|
||||
});
|
||||
|
||||
$('#btnTrendFilter').on('click', async function(){
|
||||
await runTrendFilter();
|
||||
});
|
||||
}
|
||||
|
||||
async function runTrendFilter() {
|
||||
if (!FEATURES.trendFilter) return; // 未启用则早退
|
||||
initTrendTables();
|
||||
trendTable.clear().draw();
|
||||
|
||||
const timeframe = $('#trendTimeframe').val();
|
||||
const direction = $('#trendDirection').val();
|
||||
const stage = $('#trendStage').val();
|
||||
const minStrength = $('#trendMinStrength').val();
|
||||
const symbols = $('#trendSymbols').val();
|
||||
let start = $('#trendStart').val();
|
||||
let end = $('#trendEnd').val();
|
||||
|
||||
// 前端必须提供时间范围:若为空,自动以当前时间回溯300根
|
||||
if (!start || !end) {
|
||||
const step = window.timeframeToMs(timeframe) || (60*60*1000);
|
||||
const now = Date.now();
|
||||
const startMsAuto = now - 300 * step;
|
||||
const toLocal = (ms) => new Date(ms - new Date(ms).getTimezoneOffset()*60000).toISOString().slice(0,16);
|
||||
if (!end) $('#trendEnd').val(toLocal(now));
|
||||
if (!start) $('#trendStart').val(toLocal(startMsAuto));
|
||||
start = $('#trendStart').val();
|
||||
end = $('#trendEnd').val();
|
||||
}
|
||||
|
||||
let startMs = start ? new Date(start).getTime() : '';
|
||||
let endMs = end ? new Date(end).getTime() : '';
|
||||
|
||||
const params = $.param({
|
||||
timeframe: timeframe,
|
||||
direction: direction || '',
|
||||
stage: stage || '',
|
||||
min_strength: minStrength,
|
||||
symbols: symbols || '',
|
||||
start_time: startMs || '',
|
||||
end_time: endMs || ''
|
||||
});
|
||||
|
||||
// 显示筛选状态
|
||||
$('#trendFilterStatus').show();
|
||||
try {
|
||||
const res = await $.getJSON(`/api/trend_filter?${params}`);
|
||||
// 初筛后端结果,再次用前端方向筛选(避免后端噪声)
|
||||
const dirVal = $('#trendDirection').val();
|
||||
const rows = (res.results || [])
|
||||
.filter(r => {
|
||||
if (!dirVal) return true;
|
||||
return r.direction === dirVal;
|
||||
})
|
||||
.map(r => [
|
||||
r.symbol,
|
||||
new Date(r.time).toLocaleString('zh-CN', { timeZone: $('#timezone').val() || 'Asia/Shanghai' }),
|
||||
r.direction === 'bull' ? '多头' : (r.direction === 'bear' ? '空头' : '盘整'),
|
||||
r.stage === 'early' ? '初期' : (r.stage === 'mid' ? '中期' : '末期'),
|
||||
r.strength,
|
||||
r.close,
|
||||
r.ema5,
|
||||
r.ema10,
|
||||
r.ema26,
|
||||
r.ema52,
|
||||
`<button class="btn btn-sm btn-outline-primary" data-symbol="${r.symbol}" data-timeframe="${timeframe}">查看</button>`
|
||||
]);
|
||||
trendTable.rows.add(rows).draw();
|
||||
|
||||
// 绑定查看按钮
|
||||
$('#trendFilterTable').off('click', 'button').on('click', 'button', function(){
|
||||
const sym = $(this).data('symbol');
|
||||
const tf = $(this).data('timeframe');
|
||||
loadTrendDetail(sym, tf, startMs, endMs);
|
||||
});
|
||||
|
||||
// 精细化阶段判定(前端基于明细重算)
|
||||
refineTrendStages(Array.from(new Set((res.results||[]).map(r => r.symbol))).slice(0, 20), timeframe, startMs, endMs);
|
||||
} catch (e) {
|
||||
alert('趋势筛选失败: ' + e);
|
||||
} finally {
|
||||
$('#trendFilterStatus').hide();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTrendDetail(symbol, timeframe, startMs, endMs) {
|
||||
const params = $.param({
|
||||
symbol: symbol,
|
||||
timeframe: timeframe,
|
||||
start_time: startMs || '',
|
||||
end_time: endMs || '',
|
||||
timezone: $('#timezone').val() || 'Asia/Shanghai'
|
||||
});
|
||||
|
||||
// 显示详情加载状态
|
||||
$('#trendDetailStatus').show();
|
||||
try {
|
||||
const data = await $.getJSON(`/api/trend_detail?${params}`);
|
||||
// 填表
|
||||
trendDetailTable.clear();
|
||||
(data.kline_data || []).forEach(row => {
|
||||
trendDetailTable.row.add([
|
||||
new Date(row.timestamp).toLocaleString('zh-CN', { timeZone: data.timezone }),
|
||||
row.open, row.high, row.low, row.close, row.volume,
|
||||
row.ema5, row.ema10, row.ema26, row.ema52
|
||||
]);
|
||||
});
|
||||
trendDetailTable.draw();
|
||||
|
||||
// 画图
|
||||
drawTrendChart(data);
|
||||
} catch (e) {
|
||||
alert('加载趋势详情失败: ' + e);
|
||||
} finally {
|
||||
$('#trendDetailStatus').hide();
|
||||
}
|
||||
}
|
||||
|
||||
function drawTrendChart(data) {
|
||||
const container = document.getElementById('trendChartContainer');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
const chart = LightweightCharts.createChart(container, {
|
||||
layout: { background: { color: '#ffffff' }, textColor: '#333' },
|
||||
rightPriceScale: { visible: true },
|
||||
timeScale: { timeVisible: true, secondsVisible: false },
|
||||
crosshair: { mode: LightweightCharts.CrosshairMode.Normal },
|
||||
grid: { vertLines: { color: '#eee' }, horzLines: { color: '#eee' } },
|
||||
autoSize: true
|
||||
});
|
||||
trendChart = chart;
|
||||
|
||||
const candle = chart.addCandlestickSeries();
|
||||
// 关闭均线的价格线与最后值标签,仅保留K线的当前价格虚线
|
||||
const ema5 = chart.addLineSeries({ color: '#ff0000', lineWidth: 2, lastValueVisible: false, priceLineVisible: false });
|
||||
const ema10 = chart.addLineSeries({ color: '#2962FF', lineWidth: 2, lastValueVisible: false, priceLineVisible: false });
|
||||
const ema26 = chart.addLineSeries({ color: '#008000', lineWidth: 2, lastValueVisible: false, priceLineVisible: false });
|
||||
const ema52 = chart.addLineSeries({ color: '#800080', lineWidth: 2, lastValueVisible: false, priceLineVisible: false });
|
||||
|
||||
const k = (data.kline_data || []).map(r => ({
|
||||
time: Math.floor(r.timestamp / 1000),
|
||||
open: Number(r.open), high: Number(r.high), low: Number(r.low), close: Number(r.close)
|
||||
}));
|
||||
candle.setData(k);
|
||||
|
||||
// 前端过滤均线前导缺失/无效值,避免绘制为0
|
||||
const sanitizeMA = (field) => {
|
||||
const rows = data.kline_data || [];
|
||||
const out = [];
|
||||
let started = false;
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i];
|
||||
const raw = r[field];
|
||||
const v = Number(raw);
|
||||
const valid = Number.isFinite(v) && v > 0;
|
||||
if (!started) {
|
||||
if (!valid) continue;
|
||||
started = true;
|
||||
}
|
||||
if (!valid) continue;
|
||||
out.push({ time: Math.floor(r.timestamp / 1000), value: v });
|
||||
}
|
||||
return out;
|
||||
};
|
||||
ema5.setData(sanitizeMA('ema5'));
|
||||
ema10.setData(sanitizeMA('ema10'));
|
||||
ema26.setData(sanitizeMA('ema26'));
|
||||
ema52.setData(sanitizeMA('ema52'));
|
||||
|
||||
// 趋势线(使用返回的拟合参数)
|
||||
const trend = data.trend_line || null;
|
||||
if (trend && k.length > 1) {
|
||||
const L = Math.min(trend.length, k.length);
|
||||
const startIdx = k.length - L;
|
||||
const lineData = [];
|
||||
for (let i = 0; i < L; i++) {
|
||||
const y = trend.slope * i + trend.intercept;
|
||||
const point = { time: k[startIdx + i].time, value: y };
|
||||
lineData.push(point);
|
||||
}
|
||||
const trendSeries = chart.addLineSeries({ color: '#ffa500', lineWidth: 2, lineStyle: 0, lastValueVisible: false, priceLineVisible: false });
|
||||
trendSeries.setData(lineData);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 前端精细化阶段判定 =====
|
||||
function computeEMA() {
|
||||
if (window.App && window.App.Indicators && typeof window.App.Indicators.computeEMA === 'function') {
|
||||
return window.App.Indicators.computeEMA.apply(null, arguments);
|
||||
}
|
||||
console.warn('computeEMA 未就绪,返回空数组');
|
||||
return [];
|
||||
}
|
||||
|
||||
function computeMACDSeries(close) {
|
||||
const ema12 = computeEMA(close, 12);
|
||||
const ema26 = computeEMA(close, 26);
|
||||
const macd = close.map((_, i) => (ema12[i] != null && ema26[i] != null) ? (ema12[i] - ema26[i]) : null);
|
||||
const signal = computeEMA(macd.map(v => v ?? null), 9);
|
||||
const hist = macd.map((v, i) => (v != null && signal[i] != null) ? (v - signal[i]) : null);
|
||||
return { macd, signal, hist };
|
||||
}
|
||||
|
||||
function slope(series, win) {
|
||||
const n = series.length;
|
||||
const k = Math.min(win, n);
|
||||
if (k < 3) return 0;
|
||||
const y = series.slice(n - k).filter(v => v != null && isFinite(v));
|
||||
if (y.length < 3) return 0;
|
||||
const x = [...Array(y.length).keys()];
|
||||
const xm = x.reduce((a,b)=>a+b,0)/x.length;
|
||||
const ym = y.reduce((a,b)=>a+b,0)/y.length;
|
||||
let num = 0, den = 0;
|
||||
for (let i=0;i<x.length;i++){ num += (x[i]-xm)*(y[i]-ym); den += (x[i]-xm)*(x[i]-xm); }
|
||||
return den ? num/den : 0;
|
||||
}
|
||||
|
||||
function classifyStageFrontend(kline, directionHint) {
|
||||
const close = kline.map(r => Number(r.close));
|
||||
const ema26 = computeEMA(close, 26);
|
||||
const ema52 = computeEMA(close, 52);
|
||||
const last = close[close.length-1];
|
||||
const e26 = ema26[ema26.length-1];
|
||||
const e52 = ema52[ema52.length-1];
|
||||
const s26 = slope(ema26, 20);
|
||||
const s52 = slope(ema52, 30);
|
||||
const dist52 = (e52 && isFinite(e52)) ? (last - e52)/e52 : 0;
|
||||
const { hist } = computeMACDSeries(close);
|
||||
const recent = hist.slice(-9).filter(v => v != null);
|
||||
const earlier = hist.slice(-18, -9).filter(v => v != null);
|
||||
const growth = (recent.length && earlier.length) ? (avgAbs(recent) - avgAbs(earlier)) : 0;
|
||||
|
||||
function avgAbs(arr){ return arr.reduce((a,b)=>a+Math.abs(b),0)/arr.length; }
|
||||
|
||||
let direction = directionHint;
|
||||
if (!direction) {
|
||||
if (e26 > e52 && s26 > 0 && s52 > 0) direction = 'bull';
|
||||
else if (e26 < e52 && s26 < 0 && s52 < 0) direction = 'bear';
|
||||
else direction = 'sideways';
|
||||
}
|
||||
|
||||
let stage = 'early';
|
||||
const ad = Math.abs(dist52);
|
||||
if (direction === 'bull') {
|
||||
if (ad < 0.03 && growth > 0) stage = 'early';
|
||||
else if (ad < 0.10 && (growth >= 0 || s26 > 0)) stage = 'mid';
|
||||
else stage = 'late';
|
||||
} else if (direction === 'bear') {
|
||||
if (ad < 0.03 && growth > 0) stage = 'early';
|
||||
else if (ad < 0.10 && (growth >= 0 || s26 < 0)) stage = 'mid';
|
||||
else stage = 'late';
|
||||
} else {
|
||||
stage = 'early';
|
||||
}
|
||||
return { direction, stage };
|
||||
}
|
||||
|
||||
async function refineTrendStages(symbols, timeframe, startMs, endMs) {
|
||||
if (!symbols || symbols.length === 0) return;
|
||||
// 在表头上方提示
|
||||
const info = $('<div class="text-muted mb-2" id="refineInfo">正在优化阶段判定...</div>');
|
||||
$('#trendFilterTable').before(info);
|
||||
const tz = $('#timezone').val() || 'Asia/Shanghai';
|
||||
const selectedDir = $('#trendDirection').val(); // bull/bear/sideways/''
|
||||
for (const sym of symbols) {
|
||||
try {
|
||||
const params = $.param({ symbol: sym, timeframe, start_time: startMs, end_time: endMs, timezone: tz });
|
||||
const data = await $.getJSON(`/api/trend_detail?${params}`);
|
||||
const { direction, stage } = classifyStageFrontend(data.kline_data || [], null);
|
||||
// 若与选择的方向不一致,则在前端移除该行,避免"选择多头仍出现空头/盘整"
|
||||
if (selectedDir && direction !== selectedDir) {
|
||||
if (trendTable) {
|
||||
trendTable.rows().every(function(){
|
||||
const rowData = this.data();
|
||||
if (rowData && rowData[0] === sym) {
|
||||
this.remove();
|
||||
}
|
||||
});
|
||||
trendTable.draw(false);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 否则更新该行方向与阶段展示
|
||||
$('#trendFilterTable tbody tr').each(function(){
|
||||
const tds = $(this).find('td');
|
||||
if (tds.eq(0).text() === sym) {
|
||||
tds.eq(2).text(direction === 'bull' ? '多头' : (direction === 'bear' ? '空头' : '盘整'));
|
||||
tds.eq(3).text(stage === 'early' ? '初期' : stage === 'mid' ? '中期' : '末期');
|
||||
}
|
||||
});
|
||||
} catch(e) {
|
||||
// 忽略单个失败
|
||||
}
|
||||
}
|
||||
info.remove();
|
||||
}
|
||||
|
||||
// 页面初始化时绑定控件
|
||||
$(function(){
|
||||
initTrendTables();
|
||||
bindTrendControls();
|
||||
});
|
||||
var tvWidget = {
|
||||
mainChart: null,
|
||||
volumeChart: null,
|
||||
macdChart: null,
|
||||
chanMacdChart: null, // 新增ChanMACD图表
|
||||
series: {
|
||||
candleSeries: null,
|
||||
barSeries: null,
|
||||
lineSeries: null,
|
||||
areaSeries: null,
|
||||
baselineSeries: null,
|
||||
renkoSeries: null,
|
||||
volumeSeries: null,
|
||||
atrLineSeries: null,
|
||||
macdLineSeries: null,
|
||||
signalLineSeries: null,
|
||||
histogramSeries: null,
|
||||
mainBiSeries: [],
|
||||
mainSegSeries: [],
|
||||
mainZsSeries: [],
|
||||
mainUncompletedZsSeries: [],
|
||||
elementBiSeries: [],
|
||||
elementSegSeries: [],
|
||||
elementZsSeries: [],
|
||||
elementUncompletedZsSeries: [],
|
||||
subSubBiSeries: [],
|
||||
subSubSegSeries: [],
|
||||
subSubZsSeries: [],
|
||||
subSubUncompletedZsSeries: [],
|
||||
mainBollingerSeries: [],
|
||||
elementBollingerSeries: [],
|
||||
maSeries: [], // 添加均线系列
|
||||
bbSeries: [], // 添加布林带系列
|
||||
ema52Series: [], // 添加EMA52系列数组
|
||||
chanMacdLineSeries: null, // ChanMACD线
|
||||
chanMacdSignalSeries: null, // ChanMACD信号线
|
||||
chanMacdHistSeries: null, // ChanMACD柱状图
|
||||
chanMacdSegSeries: [], // ChanMACD段
|
||||
chanMacdUnitTFSeries: [], // ChanMACD UnitTF
|
||||
chanMacdHistSetSeries: [] // ChanMACD HistSet
|
||||
},
|
||||
state: {
|
||||
isInitialized: false,
|
||||
visibleRange: null,
|
||||
logicalRange: null
|
||||
}
|
||||
};
|
||||
|
||||
// 默认EMA初始化哨兵,防止删除后再次被自动添加
|
||||
var hasInitializedDefaultMAs = false;
|
||||
|
||||
// 买卖点类型定义
|
||||
const TRADE_POINT_TYPE = {
|
||||
BUY1: 1, // 一类买点
|
||||
BUY2: 2, // 二类买点
|
||||
BUY3: 3, // 三类买点
|
||||
SELL1: -1, // 一类卖点
|
||||
SELL2: -2, // 二类卖点
|
||||
SELL3: -3 // 三类卖点
|
||||
};
|
||||
|
||||
// 买卖点样式定义
|
||||
const TRADE_POINT_STYLE = {
|
||||
[TRADE_POINT_TYPE.BUY1]: {color: '#FF1744', shape: 'arrowUp', text: '买1', size: 2},
|
||||
[TRADE_POINT_TYPE.BUY2]: {color: '#F50057', shape: 'circle', text: '买2', size: 2},
|
||||
[TRADE_POINT_TYPE.BUY3]: {color: '#D500F9', shape: 'square', text: '买3', size: 2},
|
||||
[TRADE_POINT_TYPE.SELL1]: {color: '#00E676', shape: 'arrowDown', text: '卖1', size: 2},
|
||||
[TRADE_POINT_TYPE.SELL2]: {color: '#00B0FF', shape: 'circle', text: '卖2', size: 2},
|
||||
[TRADE_POINT_TYPE.SELL3]: {color: '#FFEA00', shape: 'square', text: '卖3', size: 2}
|
||||
};
|
||||
|
||||
// 定义标记垂直偏移系数 - 合约市场通常波动较大,减小偏移防止显示在范围外
|
||||
const TRADE_POINT_OFFSET = {
|
||||
[TRADE_POINT_TYPE.BUY1]: 0, // 一类买点向下偏移2.0%的价格
|
||||
[TRADE_POINT_TYPE.BUY2]: 0, // 二类买点向下偏移1.5%的价格
|
||||
[TRADE_POINT_TYPE.BUY3]: 0, // 三类买点向下偏移1.0%的价格
|
||||
[TRADE_POINT_TYPE.SELL1]: 0, // 一类卖点向上偏移2.0%的价格
|
||||
[TRADE_POINT_TYPE.SELL2]: 0,// 二类卖点向上偏移1.5%的价格
|
||||
[TRADE_POINT_TYPE.SELL3]: 0 // 三类卖点向上偏移1.0%的价格
|
||||
};
|
||||
|
||||
// 更改为基于价格百分比的垂直偏移 - 合约市场适用的更小偏移
|
||||
const PRICE_PERCENT_OFFSET = {
|
||||
[TRADE_POINT_TYPE.BUY1]: 0, // 一类买点向下偏移价格的0.2%
|
||||
[TRADE_POINT_TYPE.BUY2]: 0, // 二类买点向下偏移价格的0.15%
|
||||
[TRADE_POINT_TYPE.BUY3]: 0, // 三类买点向下偏移价格的0.1%
|
||||
[TRADE_POINT_TYPE.SELL1]: 0, // 一类卖点向上偏移价格的0.2%
|
||||
[TRADE_POINT_TYPE.SELL2]: 0, // 二类卖点向上偏移价格的0.15%
|
||||
[TRADE_POINT_TYPE.SELL3]: 0 // 三类卖点向上偏移价格的0.1%
|
||||
};
|
||||
|
||||
// 对于高价格标的如BTC,设置零偏移,完全不影响价格显示
|
||||
const USE_FIXED_OFFSET = true; // 是否使用固定偏移而非百分比
|
||||
const PRICE_FIXED_OFFSET = {
|
||||
[TRADE_POINT_TYPE.BUY1]: 0, // 一类买点零偏移
|
||||
[TRADE_POINT_TYPE.BUY2]: 0, // 二类买点零偏移
|
||||
[TRADE_POINT_TYPE.BUY3]: 0, // 三类买点零偏移
|
||||
[TRADE_POINT_TYPE.SELL1]: 0, // 一类卖点零偏移
|
||||
[TRADE_POINT_TYPE.SELL2]: 0, // 二类卖点零偏移
|
||||
[TRADE_POINT_TYPE.SELL3]: 0 // 三类卖点零偏移
|
||||
};
|
||||
|
||||
// 同一时间点的标记堆叠间距系数
|
||||
const STACK_OFFSET_FACTOR = 5; // 增加堆叠标记的间距
|
||||
|
||||
// 添加CSS样式定义买卖点标记的样式
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.textContent = `
|
||||
.point-tooltip {
|
||||
position: absolute;
|
||||
background: rgba(40, 40, 40, 0.9);
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
max-width: 300px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.buy-point {
|
||||
color: #ff1744;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sell-point {
|
||||
color: #00e676;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.buy-marker {
|
||||
background-color: #ff1744;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.sell-marker {
|
||||
background-color: #00e676;
|
||||
border: 2px solid white;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styleElement);
|
||||
|
||||
// 添加买卖点悬浮提示元素
|
||||
const tooltipElement = document.createElement('div');
|
||||
tooltipElement.className = 'point-tooltip';
|
||||
// document.body.appendChild(tooltipElement);
|
||||
|
||||
// 添加自定义十字线信息显示
|
||||
const crosshairTooltip = document.createElement('div');
|
||||
crosshairTooltip.className = 'crosshair-tooltip';
|
||||
crosshairTooltip.style.position = 'absolute';
|
||||
crosshairTooltip.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
|
||||
crosshairTooltip.style.color = 'white';
|
||||
crosshairTooltip.style.padding = '5px 10px';
|
||||
crosshairTooltip.style.borderRadius = '4px';
|
||||
crosshairTooltip.style.fontSize = '12px';
|
||||
crosshairTooltip.style.zIndex = '1000';
|
||||
crosshairTooltip.style.pointerEvents = 'none';
|
||||
crosshairTooltip.style.display = 'none';
|
||||
// document.body.appendChild(crosshairTooltip);
|
||||
|
||||
// 助手函数:转换UTC时间到所选时区
|
||||
function convertToTimezone(utcDate, timezone) {
|
||||
return new Date(utcDate).toLocaleString('zh-CN', {
|
||||
timeZone: timezone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取UTC时间戳(秒)
|
||||
function getTimestamp(dateStr) {
|
||||
return new Date(dateStr).getTime() / 1000;
|
||||
}
|
||||
|
||||
// 获取时区偏移量(小时)
|
||||
function getTimezoneOffset(timezone) {
|
||||
// 手动定义已知时区的偏移量
|
||||
const offsets = {
|
||||
'UTC': 0,
|
||||
'Asia/Shanghai': 8,
|
||||
'America/New_York': -4, // 夏令时可能是-4,冬令时是-5
|
||||
'Europe/London': 0, // 夏令时可能是+1,冬令时是0
|
||||
'Europe/Berlin': 1, // 夏令时可能是+2,冬令时是+1
|
||||
'Asia/Tokyo': 9
|
||||
};
|
||||
|
||||
return offsets[timezone] || 0;
|
||||
}
|
||||
|
||||
// 从日期字符串获取时间戳,应用时区偏移
|
||||
function getAdjustedTimestamp(dateStr, applyOffset = true) {
|
||||
const date = new Date(dateStr);
|
||||
const timestamp = Math.floor(date.getTime() / 1000);
|
||||
|
||||
if (!applyOffset) {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
// 不再手动调整时区偏移,使用JavaScript的内置时区支持
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
// 添加时区选择器变更事件
|
||||
$('#timezone').change(function() {
|
||||
if (currentData) {
|
||||
// 重新渲染图表和数据表以使用新的时区
|
||||
initTradingView($('#symbol').val(), $('#timeframe').val());
|
||||
updateTables(currentData);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加MACD复选框变更事件
|
||||
$('#showMacd').change(function() {
|
||||
updateChartDisplay();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user