|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const tradingViewCharts = {};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function createCandlestickChart(canvasId, data, options = {}) {
|
|
|
const ctx = document.getElementById(canvasId);
|
|
|
if (!ctx) return null;
|
|
|
|
|
|
|
|
|
if (tradingViewCharts[canvasId]) {
|
|
|
tradingViewCharts[canvasId].destroy();
|
|
|
}
|
|
|
|
|
|
const {
|
|
|
symbol = 'BTC',
|
|
|
timeframe = '1D',
|
|
|
showVolume = true,
|
|
|
showIndicators = true
|
|
|
} = options;
|
|
|
|
|
|
|
|
|
const labels = data.map(d => new Date(d.time).toLocaleDateString());
|
|
|
const opens = data.map(d => d.open);
|
|
|
const highs = data.map(d => d.high);
|
|
|
const lows = data.map(d => d.low);
|
|
|
const closes = data.map(d => d.close);
|
|
|
const volumes = data.map(d => d.volume || 0);
|
|
|
|
|
|
|
|
|
const colors = data.map((d, i) => {
|
|
|
if (i === 0) return closes[i] >= opens[i] ? '#10B981' : '#EF4444';
|
|
|
return closes[i] >= closes[i - 1] ? '#10B981' : '#EF4444';
|
|
|
});
|
|
|
|
|
|
const datasets = [
|
|
|
{
|
|
|
label: 'Price',
|
|
|
data: closes,
|
|
|
borderColor: '#00D4FF',
|
|
|
backgroundColor: 'rgba(0, 212, 255, 0.1)',
|
|
|
borderWidth: 2,
|
|
|
fill: true,
|
|
|
tension: 0.1,
|
|
|
pointRadius: 0,
|
|
|
pointHoverRadius: 6,
|
|
|
pointHoverBackgroundColor: '#00D4FF',
|
|
|
pointHoverBorderColor: '#fff',
|
|
|
pointHoverBorderWidth: 2,
|
|
|
yAxisID: 'y'
|
|
|
}
|
|
|
];
|
|
|
|
|
|
if (showVolume) {
|
|
|
datasets.push({
|
|
|
label: 'Volume',
|
|
|
data: volumes,
|
|
|
type: 'bar',
|
|
|
backgroundColor: colors.map(c => c + '40'),
|
|
|
borderColor: colors,
|
|
|
borderWidth: 1,
|
|
|
yAxisID: 'y1',
|
|
|
order: 2
|
|
|
});
|
|
|
}
|
|
|
|
|
|
tradingViewCharts[canvasId] = new Chart(ctx, {
|
|
|
type: 'line',
|
|
|
data: { labels, datasets },
|
|
|
options: {
|
|
|
responsive: true,
|
|
|
maintainAspectRatio: false,
|
|
|
interaction: {
|
|
|
mode: 'index',
|
|
|
intersect: false
|
|
|
},
|
|
|
plugins: {
|
|
|
legend: {
|
|
|
display: true,
|
|
|
position: 'top',
|
|
|
align: 'end',
|
|
|
labels: {
|
|
|
usePointStyle: true,
|
|
|
padding: 15,
|
|
|
font: {
|
|
|
size: 12,
|
|
|
weight: 600,
|
|
|
family: "'Manrope', sans-serif"
|
|
|
},
|
|
|
color: '#E2E8F0'
|
|
|
}
|
|
|
},
|
|
|
tooltip: {
|
|
|
enabled: true,
|
|
|
backgroundColor: 'rgba(15, 23, 42, 0.98)',
|
|
|
titleColor: '#00D4FF',
|
|
|
bodyColor: '#E2E8F0',
|
|
|
borderColor: 'rgba(0, 212, 255, 0.5)',
|
|
|
borderWidth: 1,
|
|
|
padding: 16,
|
|
|
displayColors: true,
|
|
|
boxPadding: 8,
|
|
|
usePointStyle: true,
|
|
|
callbacks: {
|
|
|
title: function(context) {
|
|
|
return context[0].label;
|
|
|
},
|
|
|
label: function(context) {
|
|
|
if (context.datasetIndex === 0) {
|
|
|
return `Price: $${context.parsed.y.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
|
} else {
|
|
|
return `Volume: ${context.parsed.y.toLocaleString()}`;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
scales: {
|
|
|
x: {
|
|
|
grid: {
|
|
|
display: false,
|
|
|
color: 'rgba(255, 255, 255, 0.05)'
|
|
|
},
|
|
|
ticks: {
|
|
|
color: '#94A3B8',
|
|
|
font: {
|
|
|
size: 11,
|
|
|
family: "'Manrope', sans-serif"
|
|
|
},
|
|
|
maxRotation: 0,
|
|
|
autoSkip: true,
|
|
|
maxTicksLimit: 12
|
|
|
},
|
|
|
border: {
|
|
|
display: false
|
|
|
}
|
|
|
},
|
|
|
y: {
|
|
|
type: 'linear',
|
|
|
position: 'left',
|
|
|
grid: {
|
|
|
color: 'rgba(255, 255, 255, 0.05)',
|
|
|
drawBorder: false
|
|
|
},
|
|
|
ticks: {
|
|
|
color: '#94A3B8',
|
|
|
font: {
|
|
|
size: 11,
|
|
|
family: "'Manrope', sans-serif"
|
|
|
},
|
|
|
callback: function(value) {
|
|
|
return '$' + value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 });
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
y1: showVolume ? {
|
|
|
type: 'linear',
|
|
|
position: 'right',
|
|
|
grid: {
|
|
|
display: false,
|
|
|
drawBorder: false
|
|
|
},
|
|
|
ticks: {
|
|
|
display: false
|
|
|
}
|
|
|
} : undefined
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
return tradingViewCharts[canvasId];
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function createAdvancedLineChart(canvasId, priceData, indicators = {}) {
|
|
|
const ctx = document.getElementById(canvasId);
|
|
|
if (!ctx) return null;
|
|
|
|
|
|
if (tradingViewCharts[canvasId]) {
|
|
|
tradingViewCharts[canvasId].destroy();
|
|
|
}
|
|
|
|
|
|
const labels = priceData.map(d => new Date(d.time || d.timestamp).toLocaleDateString());
|
|
|
const prices = priceData.map(d => d.price || d.value);
|
|
|
|
|
|
|
|
|
const ma20 = indicators.ma20 || calculateMA(prices, 20);
|
|
|
const ma50 = indicators.ma50 || calculateMA(prices, 50);
|
|
|
const rsi = indicators.rsi || calculateRSI(prices, 14);
|
|
|
|
|
|
const datasets = [
|
|
|
{
|
|
|
label: 'Price',
|
|
|
data: prices,
|
|
|
borderColor: '#00D4FF',
|
|
|
backgroundColor: 'rgba(0, 212, 255, 0.1)',
|
|
|
borderWidth: 2.5,
|
|
|
fill: true,
|
|
|
tension: 0.1,
|
|
|
pointRadius: 0,
|
|
|
pointHoverRadius: 6,
|
|
|
yAxisID: 'y',
|
|
|
order: 1
|
|
|
}
|
|
|
];
|
|
|
|
|
|
if (indicators.showMA20) {
|
|
|
datasets.push({
|
|
|
label: 'MA 20',
|
|
|
data: ma20,
|
|
|
borderColor: '#8B5CF6',
|
|
|
backgroundColor: 'transparent',
|
|
|
borderWidth: 1.5,
|
|
|
borderDash: [5, 5],
|
|
|
fill: false,
|
|
|
tension: 0.1,
|
|
|
pointRadius: 0,
|
|
|
yAxisID: 'y',
|
|
|
order: 2
|
|
|
});
|
|
|
}
|
|
|
|
|
|
if (indicators.showMA50) {
|
|
|
datasets.push({
|
|
|
label: 'MA 50',
|
|
|
data: ma50,
|
|
|
borderColor: '#EC4899',
|
|
|
backgroundColor: 'transparent',
|
|
|
borderWidth: 1.5,
|
|
|
borderDash: [5, 5],
|
|
|
fill: false,
|
|
|
tension: 0.1,
|
|
|
pointRadius: 0,
|
|
|
yAxisID: 'y',
|
|
|
order: 3
|
|
|
});
|
|
|
}
|
|
|
|
|
|
tradingViewCharts[canvasId] = new Chart(ctx, {
|
|
|
type: 'line',
|
|
|
data: { labels, datasets },
|
|
|
options: {
|
|
|
responsive: true,
|
|
|
maintainAspectRatio: false,
|
|
|
interaction: {
|
|
|
mode: 'index',
|
|
|
intersect: false
|
|
|
},
|
|
|
plugins: {
|
|
|
legend: {
|
|
|
display: true,
|
|
|
position: 'top',
|
|
|
align: 'end',
|
|
|
labels: {
|
|
|
usePointStyle: true,
|
|
|
padding: 15,
|
|
|
font: {
|
|
|
size: 12,
|
|
|
weight: 600,
|
|
|
family: "'Manrope', sans-serif"
|
|
|
},
|
|
|
color: '#E2E8F0'
|
|
|
}
|
|
|
},
|
|
|
tooltip: {
|
|
|
enabled: true,
|
|
|
backgroundColor: 'rgba(15, 23, 42, 0.98)',
|
|
|
titleColor: '#00D4FF',
|
|
|
bodyColor: '#E2E8F0',
|
|
|
borderColor: 'rgba(0, 212, 255, 0.5)',
|
|
|
borderWidth: 1,
|
|
|
padding: 16,
|
|
|
displayColors: true,
|
|
|
boxPadding: 8
|
|
|
}
|
|
|
},
|
|
|
scales: {
|
|
|
x: {
|
|
|
grid: {
|
|
|
display: false
|
|
|
},
|
|
|
ticks: {
|
|
|
color: '#94A3B8',
|
|
|
font: {
|
|
|
size: 11,
|
|
|
family: "'Manrope', sans-serif"
|
|
|
},
|
|
|
maxRotation: 0,
|
|
|
autoSkip: true
|
|
|
},
|
|
|
border: {
|
|
|
display: false
|
|
|
}
|
|
|
},
|
|
|
y: {
|
|
|
grid: {
|
|
|
color: 'rgba(255, 255, 255, 0.05)',
|
|
|
drawBorder: false
|
|
|
},
|
|
|
ticks: {
|
|
|
color: '#94A3B8',
|
|
|
font: {
|
|
|
size: 11,
|
|
|
family: "'Manrope', sans-serif"
|
|
|
},
|
|
|
callback: function(value) {
|
|
|
return '$' + value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
return tradingViewCharts[canvasId];
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function calculateMA(data, period) {
|
|
|
const result = [];
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
|
if (i < period - 1) {
|
|
|
result.push(null);
|
|
|
} else {
|
|
|
const sum = data.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
|
|
|
result.push(sum / period);
|
|
|
}
|
|
|
}
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function calculateRSI(data, period = 14) {
|
|
|
const result = [];
|
|
|
const gains = [];
|
|
|
const losses = [];
|
|
|
|
|
|
for (let i = 1; i < data.length; i++) {
|
|
|
const change = data[i] - data[i - 1];
|
|
|
gains.push(change > 0 ? change : 0);
|
|
|
losses.push(change < 0 ? Math.abs(change) : 0);
|
|
|
}
|
|
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
|
if (i < period) {
|
|
|
result.push(null);
|
|
|
} else {
|
|
|
const avgGain = gains.slice(i - period, i).reduce((a, b) => a + b, 0) / period;
|
|
|
const avgLoss = losses.slice(i - period, i).reduce((a, b) => a + b, 0) / period;
|
|
|
const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
|
|
|
const rsi = 100 - (100 / (1 + rs));
|
|
|
result.push(rsi);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function createVolumeChart(canvasId, volumeData) {
|
|
|
const ctx = document.getElementById(canvasId);
|
|
|
if (!ctx) return null;
|
|
|
|
|
|
if (tradingViewCharts[canvasId]) {
|
|
|
tradingViewCharts[canvasId].destroy();
|
|
|
}
|
|
|
|
|
|
const labels = volumeData.map(d => new Date(d.time).toLocaleDateString());
|
|
|
const volumes = volumeData.map(d => d.volume);
|
|
|
const colors = volumeData.map((d, i) => {
|
|
|
if (i === 0) return '#10B981';
|
|
|
return volumes[i] >= volumes[i - 1] ? '#10B981' : '#EF4444';
|
|
|
});
|
|
|
|
|
|
tradingViewCharts[canvasId] = new Chart(ctx, {
|
|
|
type: 'bar',
|
|
|
data: {
|
|
|
labels,
|
|
|
datasets: [{
|
|
|
label: 'Volume',
|
|
|
data: volumes,
|
|
|
backgroundColor: colors.map(c => c + '60'),
|
|
|
borderColor: colors,
|
|
|
borderWidth: 1
|
|
|
}]
|
|
|
},
|
|
|
options: {
|
|
|
responsive: true,
|
|
|
maintainAspectRatio: false,
|
|
|
plugins: {
|
|
|
legend: {
|
|
|
display: false
|
|
|
},
|
|
|
tooltip: {
|
|
|
backgroundColor: 'rgba(15, 23, 42, 0.98)',
|
|
|
titleColor: '#00D4FF',
|
|
|
bodyColor: '#E2E8F0',
|
|
|
borderColor: 'rgba(0, 212, 255, 0.5)',
|
|
|
borderWidth: 1,
|
|
|
padding: 12
|
|
|
}
|
|
|
},
|
|
|
scales: {
|
|
|
x: {
|
|
|
grid: {
|
|
|
display: false
|
|
|
},
|
|
|
ticks: {
|
|
|
color: '#94A3B8',
|
|
|
font: {
|
|
|
size: 10,
|
|
|
family: "'Manrope', sans-serif"
|
|
|
}
|
|
|
},
|
|
|
border: {
|
|
|
display: false
|
|
|
}
|
|
|
},
|
|
|
y: {
|
|
|
grid: {
|
|
|
color: 'rgba(255, 255, 255, 0.05)',
|
|
|
drawBorder: false
|
|
|
},
|
|
|
ticks: {
|
|
|
color: '#94A3B8',
|
|
|
font: {
|
|
|
size: 10,
|
|
|
family: "'Manrope', sans-serif"
|
|
|
},
|
|
|
callback: function(value) {
|
|
|
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'B';
|
|
|
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'M';
|
|
|
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'K';
|
|
|
return value;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
return tradingViewCharts[canvasId];
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function destroyChart(canvasId) {
|
|
|
if (tradingViewCharts[canvasId]) {
|
|
|
tradingViewCharts[canvasId].destroy();
|
|
|
delete tradingViewCharts[canvasId];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function updateChart(canvasId, newData) {
|
|
|
if (tradingViewCharts[canvasId]) {
|
|
|
tradingViewCharts[canvasId].data = newData;
|
|
|
tradingViewCharts[canvasId].update();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|