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