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