将根目录引擎迁入 chanlun/ 并保留兼容 shim;拆分 TF_DF 与 web 服务; 前端模块化;strategies 改用 chanlun 导入;补充 ESS 文档与 golden 回归。 Co-authored-by: Cursor <cursoragent@cursor.com>
572 lines
21 KiB
JavaScript
572 lines
21 KiB
JavaScript
/* 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();
|
|
});
|
|
|