/* 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();
});