// localStorage.removeItem("tradingview.chartproperties"); // 在初始化 Highcharts 图表之前清除 TradingView 配置 localStorage.removeItem("tradingview.chartproperties"); // 然后继续初始化 Highcharts 或处理 Datafeed 类 // const datafeed = new Datafeed(vm); // const chart = Highcharts.stockChart('container', { // // Highcharts 配置 // }); import config from './config' $(function() { // Light Theme for Highcharts let light = { chart: { backgroundColor: "#ffffff", borderColor: "#e6e6e6", borderWidth: 1 }, title: { style: { color: '#333', fontSize: '16px' } }, xAxis: { gridLineColor: '#dcdee0', labels: { style: { color: '#333', fontSize: '9px' } }, lineColor: '#dcdee0' }, yAxis: { gridLineColor: '#dcdee0', labels: { style: { color: '#333', fontSize: '9px' } }, lineColor: '#dcdee0' }, legend: { itemStyle: { color: '#333' } }, plotOptions: { candlestick: { upColor: '#2EBD85', downColor: '#F4465D', borderColor: '#2EBD85', borderWidth: 1 } } }; // Dark Theme for Highcharts let dark = { chart: { backgroundColor: "#2b2b37", borderColor: "#49495F", borderWidth: 1 }, title: { style: { color: '#fff', fontSize: '16px' } }, xAxis: { gridLineColor: '#49495F', labels: { style: { color: '#fff', fontSize: '9px' } }, lineColor: '#49495F' }, yAxis: { gridLineColor: '#49495F', labels: { style: { color: '#fff', fontSize: '9px' } }, lineColor: '#49495F' }, legend: { itemStyle: { color: '#fff' } }, plotOptions: { candlestick: { upColor: '#2EBD85', downColor: '#F4465D', borderColor: '#2EBD85', borderWidth: 1 } } }; // Final TV Style (light and dark theme options) let tvStyle = { light, dark }; class Datafeed { constructor(vm) { this.self = vm; // 传入的数据和配置 } // onReady 方法:传递图表支持的配置 onReady(callback) { setTimeout(() => { callback({ supports_search: false, supports_group_request: false, supported_resolutions: this.self.resolutions, supports_marks: true, supports_timescale_marks: true, supports_time: true }); }, 30); } // resolveSymbol 方法:解析符号,返回图表所需的元数据 resolveSymbol(symbolName, onSymbolResolvedCallback, onResolveErrorCallback) { setTimeout(() => { let data = this.defaultSymbol(); if (this.self.resolveSymbol) { this.self.resolveSymbol((res) => { onSymbolResolvedCallback(Object.assign(data, res)); }); } else { onSymbolResolvedCallback(data); } }, 60); } // getBars 方法:获取数据,转换为 Highcharts K线图所需的格式 getBars(symbolInfo, resolution, rangeStartDate, rangeEndDate, onDataCallback, onErrorCallback, isFirstCall) { let page = this.page > 3 ? 3 : this.page; let data = { symbol: symbolInfo.name, period: this.resolution(resolution), form: rangeStartDate, to: rangeEndDate, size: page * 200, zip: 2 } this.page++; this.isLoad = true; this.unSub(); $.get(this.url, data).then(res => { // 解析返回的数据 let arr = this.unzip(res.data.data).map((item) => { // 假设返回的数据为 {timestamp, open, high, low, close, volume} 格式 return [ item.timestamp, // Highstock 需要的是 Unix 时间戳 item.open, item.high, item.low, item.close, item.volume ]; }); // 更新 Highstock 图表的数据 if (isFirstCall) { // 第一次加载时,使用 `setData` 或 `addSeries` 方法来添加数据 this.chart.series[0].setData(arr, true); // 假设你已经有一个 Highstock 图表实例并且已经初始化了一个 series } else { // 如果不是第一次调用,使用 `addPoint` 或 `addData` 追加数据 this.chart.series[0].addData(arr, true); // `addData` 会追加数据并重新渲染 } // 调用 Highstock 回调函数 onDataCallback(arr); // 创建消息并订阅 this.msg = this.createMsg(); this.sub(); }).catch(err => { // 处理错误,返回空数据 onDataCallback([]); }); } // subscribeBars 方法:订阅数据更新 subscribeBars(symbolInfo, resolution, onRealtimeCallback, listenerGUID) { this.self.subscribeBars(symbolInfo, resolution, (bar) => { // 转换并推送实时更新的 K线数据 const updatedBar = { x: bar.time * 1000, // 时间戳转换为毫秒 open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume || 0 }; onRealtimeCallback(updatedBar); }); } // unsubscribeBars 方法:取消订阅数据更新 unsubscribeBars(listenerGUID) { this.self.unsubscribeBars(listenerGUID); } // 默认的符号信息 defaultSymbol() { return { 'timezone': 'Asia/Shanghai', 'minmov': 1, 'minmov2': 0, 'fractional': true, 'session': '24x7', 'has_intraday': true, 'has_no_volume': false, 'has_daily': true, 'has_weekly_and_monthly': true, 'pricescale': 10000 }; } } class Page { constructor() { this.datafeed = undefined; // 数据源 this.page = 1; this.onRealtimeCallback = undefined; this.TView = undefined; this.interval = this.getQuery('interval') || '60'; // 默认 60 分钟 this.symbolName = this.getQuery('symbol'); // 从 URL 获取 symbol this.theme = this.getQuery('theme') || 'dark'; // 默认 dark 主题 this.lang = this.getQuery('lang') || 'en'; // 默认语言 this.resolutions = this.getQuery('resolutions') || ["5", "15", "30", "60", "240", "1D", "1W", "1M" ]; this.isLoad = false; this.url = this.getQuery('getLinkUrl'); // 获取链接 this.TVID = "tradingview_10798345"; // TV ID this.Ws = undefined; this.msg = ''; this.contract = this.getQuery('contract'); this.init(); // 初始化 this.studies = []; // 配置项 this.tvBars = []; // 数据 } // 获取路径上的参数 getQuery(name) { const urlParams = new URLSearchParams(window.location.search); return urlParams.get(name); // 返回 URL 查询参数的值 } // 初始化 init() { console.log(123123); this.linkSocket(); // 如果需要WebSocket连接 // 数据模型 - 如果使用自定义数据源,需确保数据适配Highcharts this.datafeed = new Datafeed(this); // 初始化 Highcharts 图表 this.createChart(); } // 创建Highcharts图表 createChart() { console.log('111', this.tvBars); const chartOptions = { chart: { renderTo: 'tradingview_10798345', // 图表容器ID type: 'candlestick', // 设置图表类型为蜡烛图 backgroundColor: this.theme === 'light' ? '#fff' : '#2b2b37', // 根据主题选择背景颜色 height: 500, // 设置图表高度 events: { load: function() { const chart = this; // 使用 chart 来绑定 mousemove 事件 chart.container.addEventListener('mousemove', function(event) { // 获取鼠标位置的时间戳 const time = chart.xAxis[0].toValue(event.offsetX); const resolutionTime = this .getResolutionTime(); // 获取分辨率的时间周期 const fTime = Math.floor(time / resolutionTime) * resolutionTime; // 根据分辨率计算时间 console.log('fTime', fTime); chart.showKlineQuoter(fTime); // 显示对应的K线数据 }); } } }, title: { // text: `${this.symbolName} Kline Chart`, // 标题显示symbol名称 style: { color: this.theme === 'light' ? '#000' : '#fff' // 依据主题设置标题颜色 } }, subtitle: { // text: `Interval: ${this.interval}`, // 显示时间间隔 style: { color: this.theme === 'light' ? '#000' : '#fff' // 依据主题设置副标题颜色 } }, xAxis: { type: 'datetime', // 设置x轴为时间格式 labels: { style: { color: this.theme === 'light' ? '#000' : '#fff' // 依据主题设置x轴标签颜色 } } }, yAxis: { title: { text: 'Price', // y轴标题 style: { color: this.theme === 'light' ? '#000' : '#fff' // 依据主题设置y轴标题颜色 } }, labels: { style: { color: this.theme === 'light' ? '#000' : '#fff' // 依据主题设置y轴标签颜色 } }, }, series: [{ name: 'Price', data: this.tvBars, // K线数据,假设数据格式已适配 // data: [ // [Date.UTC(2025, 2, 1), 100], // [Date.UTC(2025, 2, 2), 105], // [Date.UTC(2025, 2, 3), 110] // ], type: 'candlestick', // 设置图表类型为蜡烛图 upColor: '#4fff33', // 设置上涨时的颜色 color: '#f00707', // 设置下跌时的颜色 lineColor: '#f00707', // 设置线的颜色 dataGrouping: { enabled: true, // 启用数据分组 groupPixelWidth: 10 // 每个分组的宽度 } }], plotOptions: { candlestick: { color: '#f00707', // 下跌时的颜色 upColor: '#4fff33' // 上涨时的颜色 } }, navigation: { buttonOptions: { enabled: false // 禁用导航按钮 } }, tooltip: { pointFormatter: function() { // 格式化鼠标悬停时显示的tooltip内容 return `Time: ${new Date(this.x).toLocaleString()}
Open: ${this.open}
High: ${this.high}
Low: ${this.low}
Close: ${this.close}
`; } } }; // 渲染Highcharts图表 Highcharts.stockChart(chartOptions); } upsertTvBars(isFirstCall, bars) { if (isFirstCall) { this.tvBars = []; // 初始化 tvBars 数组 } bars.forEach((bar) => { // 确保不会重复添加时间戳相同的条目 if (this.tvBars.length === 0 || !this.tvBars.find(o => o.time === bar.time)) { // 将数据转换为 Highcharts K 线图需要的格式:[时间戳, 开盘价, 最高价, 最低价, 收盘价] const highchartsBar = [ bar.time, // 时间戳 bar.open, // 开盘价 bar.high, // 最高价 bar.low, // 最低价 bar.close // 收盘价 ]; this.tvBars.push(highchartsBar); } }); // 按照时间戳排序 this.tvBars.sort((a, b) => { if (a[0] < b[0]) return -1; if (a[0] > b[0]) return 1; return 0; }); // 更新 Highcharts 图表的数据 this.updateChart(); } showKlineQuoter(time) { if (this.tvBars.length === 0) return; const { from, to } = this.chart.xAxis[0].getExtremes(); // 获取 visible range const bar = this.tvBars.find(o => o.time === time * 1000); // 查找对应的 bar if (!bar) return; const barTime = bar.time / 1000 + 60 * 60 * 8; // 时区调整 const fromSize = barTime - from; const toSize = to - barTime; // 更新 TV Quoter 面板 const tvQuoter = $('#tv-quoter'); tvQuoter.css({ visibility: 'visible', }); // 调整 Quoter 面板的位置 if (fromSize > toSize) { tvQuoter.css({ left: '10px', right: 'auto' }); } else { tvQuoter.css({ right: '10px', left: 'auto' }); } // 计算涨跌幅 let zhangdiefu = (bar.close - bar.open) / bar.open; let zhangdiee = bar.close - bar.open; // 更新 quoter 显示的数据 tvQuoter.find('[data-name="date"]').text(this.timestampToTime(bar.time)); tvQuoter.find('[data-name="date_lang"]').text(this.lang == 'zh-CN' ? '时间' : 'Date'); tvQuoter.find('[data-name="open"]').text(bar.open); tvQuoter.find('[data-name="open_lang"]').text(this.lang == 'zh-CN' ? '开' : 'Open'); tvQuoter.find('[data-name="high"]').text(bar.high); tvQuoter.find('[data-name="high_lang"]').text(this.lang == 'zh-CN' ? '高' : 'H'); tvQuoter.find('[data-name="low"]').text(bar.low); tvQuoter.find('[data-name="low_lang"]').text(this.lang == 'zh-CN' ? '低' : 'L'); tvQuoter.find('[data-name="close"]').text(bar.close); tvQuoter.find('[data-name="close_lang"]').text(this.lang == 'zh-CN' ? '收' : 'Close'); tvQuoter.find('[data-name="volume"]').text((+bar.volume).toFixed()); tvQuoter.find('[data-name="volume_lang"]').text(this.lang == 'zh-CN' ? '成交量' : 'Executed'); tvQuoter.find('[data-name="zhangdiefu"]').text((zhangdiefu * 100).toFixed(2) + '%'); tvQuoter.find('[data-name="zhangdiefu_lang"]').text(this.lang == 'zh-CN' ? '涨跌幅' : 'Change%'); tvQuoter.find('[data-name="zhangdiefu"]').css('color', zhangdiee > 0 ? '#53b987' : '#eb4d5c'); tvQuoter.find('[data-name="zhangdiee"]').text(zhangdiee.toFixed(4)); tvQuoter.find('[data-name="zhangdiee_lang"]').text(this.lang == 'zh-CN' ? '涨跌额' : 'Change'); tvQuoter.find('[data-name="zhangdiee"]').css('color', zhangdiee > 0 ? '#53b987' : '#eb4d5c'); // Highcharts 更新数据:例如,更新显示的特定点的 hover 信息 let chart = this.chart; // 获取 Highcharts 图表对象 // 查找与给定时间相匹配的点 const point = chart.series[0].data.find(p => p.x === bar.time); if (point) { // 更新图表的 tooltip 或者标记 chart.tooltip.refresh(point); // 刷新 tooltip 显示当前点的数据 } } timestampToTime(timestamp) { const date = new Date(timestamp); const yyyy = `${date.getFullYear()}`; const yy = `${date.getFullYear()}`.substr(2); const MM = `0${date.getMonth() + 1}`.slice(-2); const dd = `0${date.getDate()}`.slice(-2); const HH = `0${date.getHours()}`.slice(-2); const mm = `0${date.getMinutes()}`.slice(-2); // 获取Highcharts的分辨率(假设你已经设置了chart的分辨率) const resolution = this.TView.chart().resolution(); let dateStr = ''; if (resolution === 'D' || resolution === 'W' || resolution === 'M') { // 日期格式为 yyyy-MM-dd dateStr = `${yyyy}-${MM}-${dd}`; } else { // 日期格式为 yy-MM-dd HH:mm dateStr = `${yy}-${MM}-${dd} ${HH}:${mm}`; } return dateStr; } getResolutionTime() { const chart = this.TView.chart(); const resolution = chart.resolution(); // 获取图表的分辨率(例如:'1', '5', 'D', 'W'等) switch (resolution) { case '1': return 60; // 1分钟 case '5': return 5 * 60; // 5分钟 case '10': return 10 * 60; // 10分钟 case '15': return 15 * 60; // 15分钟 case '30': return 30 * 60; // 30分钟 case '60': return 60 * 60; // 1小时 case '120': return 120 * 60; // 2小时 case '240': return 4 * 60 * 60; // 4小时 case 'D': return 24 * 60 * 60; // 1天 case 'W': return 7 * 24 * 60 * 60; // 1周 case 'M': return 4 * 7 * 24 * 60 * 60; // 1月(假设每月4周) default: return 1; // 如果分辨率没有匹配项,默认返回1秒 } } createStudy() { let chart = this.TView.chart(); // 获取图表实例 let theme = this.theme; // 获取主题 // 假设你的数据是存储在 `data` 中,这里会为每条移动平均线创建一个新的 `series` const data = this.data; // 图表的数据 // 计算不同周期的移动平均线 const movingAverage = (data, period) => { let maData = []; for (let i = period - 1; i < data.length; i++) { let sum = 0; for (let j = i - period + 1; j <= i; j++) { sum += data[j][1]; // 假设 `data[j][1]` 是价格 } maData.push([data[i][0], sum / period]); // 将时间戳和均值添加到结果 } return maData; }; // 定义三个不同周期的移动平均线 let ma5 = movingAverage(data, 5); let ma10 = movingAverage(data, 10); let ma20 = movingAverage(data, 20); // 创建图表 chart.update({ series: [{ name: 'MA 5', data: ma5, type: 'line', color: theme === "light" ? "#efc149" : 'rgb(238, 218, 154)', lineWidth: 3 }, { name: 'MA 10', data: ma10, type: 'line', color: theme === "light" ? "#7fcec0" : 'rgb(123, 201, 187)', lineWidth: 3 }, { name: 'MA 20', data: ma20, type: 'line', color: "rgb(194, 148, 247)", lineWidth: 3 } ] }); } // 连接socket并更新Highcharts图表数据 linkSocket() { // 连接socket this.Ws = new Ws(this.getQuery('ws')); // 监听WebSocket的消息 this.Ws.on('message', (evt) => { console.log('Received message:', evt); // 打印事件内容 // 检测ping消息并回应pong if (evt.cmd === 'ping' || evt.type === 'ping') { this.Ws.send({ cmd: 'pong' }); } // 处理特定的消息 if (evt.sub === this.msg) { // 假设evt.data是图表数据的更新,按需求处理 const updatedData = this.getMap(evt.data); console.log('updatedData', updatedData); // 更新Highcharts图表数据 // this.updateChartData(updatedData); console.log('123123',updatedData); this.onRealtimeCallback(this.getMap(updatedData)) } }); } // 更新Highcharts图表数据 updateChartData(newData) { // 假设newData是数组形式的数据,[时间戳, 值] const series = this.chart.series[0]; // 获取第一个数据系列 // 向图表中添加新的数据点 series.addPoint(newData, true, true); // 参数: 新数据, 是否重新计算坐标轴, 是否移除旧数据点 // 或者更新整个数据系列 // series.setData(newData); } // 图表语言映射 chartLang() { switch (this.lang) { case "zh-CN": return 'zh'; case "zh-TW": return 'zh_TW'; case "tr": return 'tr'; case "jp": return 'ja'; case "kor": return 'ko'; case "de": return 'de_DE'; case "fra": return 'fr'; case "it": return 'it'; case "pt": return 'pt'; case "spa": return 'es'; default: return 'en'; } } getMap(data) { return [ data.id * 1000, data.close * 1, data.open * 1, data.high * 1, data.low * 1, data.vol * 1, ]; } resolveSymbol(call) { // 返回符号信息 call({ 'name': this.symbolName.toUpperCase(), // 名称 'description': this.symbolName.toUpperCase(), // 描述 'ticker': this.symbolName.toUpperCase(), // 股票代码或符号 'supported_resolutions': this.resolutions, // 支持的时间分辨率 // 可以根据需要扩展其他属性,Highcharts 本身没有对 'description' 等字段的强制要求 }); } // 获取数据 getBars(symbolInfo, resolution, rangeStartDate, rangeEndDate, onDataCallback, onErrorCallback, isFirstCall) { let page = this.page > 3 ? 3 : this.page; let data = { symbol: symbolInfo.name, period: this.resolution(resolution), form: rangeStartDate, to: rangeEndDate, size: page * 200, zip: 2 }; this.page++; this.isLoad = true; this.unSub(); // 取消订阅 // 使用 AJAX 请求获取数据 $.get(this.url, data).then(res => { // 解压并转换数据为 Highcharts 可用格式 let arr = this.unzip(res.data.data).map(item => { return this.getMap(item); // 假设 getMap(item) 返回 Highcharts 可识别的格式 }); // 更新数据到 Highcharts 图表 this.upsertTvBars(isFirstCall, arr); // 这里假设你已经在 Highcharts 中设置了图表 onDataCallback(arr); // 调用回调,传递数据 // 更新消息和订阅机制 this.msg = this.createMsg(); this.sub(); // 重新订阅 }).catch(err => { // 错误回调,返回空数据 onDataCallback([]); }); } // 解压并适配 Highcharts 数据格式 unzip(b64Data) { // 解码 Base64 字符串 let u8 = atob(b64Data); // 使用 pako 解压 let jiya = pako.inflate(u8); // 将 Uint8Array 转换为字符串 let str = this.Uint8ArrayToString(jiya); // 解析为 JSON 格式 let jsonData = JSON.parse(str); // 假设解压后的数据是一个数组,并且每个数据项包含时间戳和价格值 // 比如 jsonData = [{time: 1625479000000, value: 123}, ...] // 将数据转换为 Highcharts 可识别的格式 [[timestamp, value], ...] let highchartsData = jsonData.map(item => { return [item.time, item.value]; // 假设数据项有 'time' 和 'value' 字段 }); return highchartsData; } // 将 Uint8Array 转换为字符串的函数 Uint8ArrayToString(fileData) { var dataString = ""; for (var i = 0; i < fileData.length; i++) { dataString += String.fromCharCode(fileData[i]); } return dataString; } // 将解压后的 JSON 数据适配为 Highcharts 数据格式 unzipAndAdaptToHighcharts(b64Data) { // 1. 解码 Base64 字符串 let u8 = atob(b64Data); // 2. 使用 pako 解压 Base64 字符串 let jiya = pako.inflate(u8); // 3. 将解压后的 Uint8Array 转换为字符串 let str = this.Uint8ArrayToString(jiya); // 4. 解析 JSON 数据 let jsonData = JSON.parse(str); // 5. 假设解压后的数据格式为 [{time: timestamp, value: dataValue}, ...] // 6. 转换为 Highcharts 支持的数据格式 [[timestamp, value], ...] let highchartsData = jsonData.map(item => { return [item.time, item.value]; // 假设每个数据项有 'time' 和 'value' 字段 }); return highchartsData; } // 获取传给后台的精度 resolution(resolution) { let T = ""; if (isNaN(resolution * 1)) { T = resolution .replace("D", "day") .replace("W", "week") .replace("M", "mon"); } else { if (resolution > 60) { T = Math.floor(resolution / 60) + "hour"; } else { T = resolution + "min"; } } return T; } // 获取推送回调,适配为 Highcharts 数据更新机制 subscribeBars(symbolInfo, resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback) { this.onRealtimeCallback = onRealtimeCallback; if (!this.symbolName) { setTimeout(() => { // 在Highcharts中实现数据更新机制,这里模拟重新加载数据 onResetCacheNeededCallback(); }, 100); } // 假设我们通过某种方式获取到新的数据,比如通过API或WebSocket // 这个地方模拟新的数据推送 const newData = [ // 这里的数据是模拟的,格式为 [timestamp, value] [Date.now(), Math.random() * 100], // 时间戳和随机值 ]; // 更新 Highcharts 图表的数据 if (this.chart) { // 假设 chart 是 Highcharts 的实例 this.chart.series[0].addPoint(newData[0], true, true); // 添加新数据点 } // 使用 onRealtimeCallback 更新图表数据 if (onRealtimeCallback) { onRealtimeCallback(newData); } } // 生成符号的名称,处理为小写 getSymbol(name) { return name.split("/").join("").toLowerCase(); } // 生成订阅消息的内容 createMsg() { if (this.contract) { return `swapKline_${this.symbolName}_${this.resolution(this.interval)}`; } else { return `Kline_${this.getSymbol(this.symbolName)}_${this.resolution(this.interval)}`; } } // 解析时间粒度 resolution(resolution) { let T = ""; if (isNaN(resolution * 1)) { T = resolution.replace("D", "day").replace("W", "week").replace("M", "mon"); } else { if (resolution > 60) { T = Math.floor(resolution / 60) + "hour"; } else { T = resolution + "min"; } } return T; } // 订阅消息,推送给 WebSocket sub() { console.log('订阅消息:', this.msg); this.Ws.send({ cmd: "sub", msg: this.msg }); } // 取消订阅 unSub() { if (!this.msg) return; this.Ws.send({ cmd: "unsub", msg: this.msg }); } } new Page() })