/* 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, `` ]); 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 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 = $('
正在优化阶段判定...
'); $('#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(); });