|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>TradingView-like Charting Software</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script> |
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom"></script> |
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/papaparse.min.js"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
<style> |
|
.chart-container { |
|
position: relative; |
|
height: 70vh; |
|
width: 100%; |
|
} |
|
.candle { |
|
position: relative; |
|
width: 6px; |
|
} |
|
.candle::before { |
|
content: ''; |
|
position: absolute; |
|
width: 100%; |
|
height: 3px; |
|
background-color: currentColor; |
|
top: 50%; |
|
transform: translateY(-50%); |
|
} |
|
.candle.up { |
|
color: #26a69a; |
|
} |
|
.candle.down { |
|
color: #ef5350; |
|
} |
|
.indicator-line { |
|
position: absolute; |
|
height: 1px; |
|
width: 100%; |
|
background-color: rgba(255,255,255,0.2); |
|
} |
|
.tooltip { |
|
position: absolute; |
|
padding: 8px; |
|
background: rgba(0, 0, 0, 0.8); |
|
color: white; |
|
border-radius: 4px; |
|
pointer-events: none; |
|
font-size: 12px; |
|
z-index: 100; |
|
display: none; |
|
} |
|
.resize-handle { |
|
position: absolute; |
|
right: 0; |
|
bottom: 0; |
|
width: 10px; |
|
height: 10px; |
|
background: #3b82f6; |
|
cursor: nwse-resize; |
|
z-index: 10; |
|
} |
|
.chartjs-tooltip { |
|
opacity: 1; |
|
position: absolute; |
|
background: rgba(0, 0, 0, 0.7); |
|
color: white; |
|
border-radius: 3px; |
|
-webkit-transition: all .1s ease; |
|
transition: all .1s ease; |
|
pointer-events: none; |
|
-webkit-transform: translate(-50%, 0); |
|
transform: translate(-50%, 0); |
|
padding: 4px; |
|
font-size: 12px; |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-900 text-gray-200"> |
|
<div class="container mx-auto px-4 py-6"> |
|
<header class="flex justify-between items-center mb-6"> |
|
<h1 class="text-2xl font-bold text-blue-400">TradeVision Charting</h1> |
|
<div class="flex space-x-4"> |
|
<button id="saveLayoutBtn" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md"> |
|
<i class="fas fa-save mr-2"></i>Save Layout |
|
</button> |
|
<button id="loadLayoutBtn" class="px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md"> |
|
<i class="fas fa-folder-open mr-2"></i>Load Layout |
|
</button> |
|
</div> |
|
</header> |
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-4 gap-4"> |
|
|
|
<div class="lg:col-span-1 bg-gray-800 rounded-lg p-4"> |
|
<div class="mb-6"> |
|
<h2 class="text-lg font-semibold mb-3 border-b border-gray-700 pb-2">Data Import</h2> |
|
<div class="space-y-3"> |
|
<div> |
|
<label class="block text-sm font-medium mb-1">Upload CSV File</label> |
|
<input type="file" id="csvFileInput" accept=".csv" class="block w-full text-sm text-gray-400 |
|
file:mr-4 file:py-2 file:px-4 |
|
file:rounded-md file:border-0 |
|
file:text-sm file:font-semibold |
|
file:bg-blue-500 file:text-white |
|
hover:file:bg-blue-600 |
|
cursor-pointer |
|
bg-gray-700 rounded-md"> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium mb-1">Chart Type</label> |
|
<select id="chartTypeSelect" class="w-full bg-gray-700 border border-gray-600 rounded-md px-3 py-2"> |
|
<option value="candlestick">Candlestick</option> |
|
<option value="line">Line</option> |
|
<option value="ohlc">OHLC Bars</option> |
|
<option value="area">Area</option> |
|
</select> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium mb-1">Timeframe</label> |
|
<select id="timeframeSelect" class="w-full bg-gray-700 border border-gray-600 rounded-md px-3 py-2"> |
|
<option value="1m">1 Minute</option> |
|
<option value="5m">5 Minutes</option> |
|
<option value="15m">15 Minutes</option> |
|
<option value="30m">30 Minutes</option> |
|
<option value="1h">1 Hour</option> |
|
<option value="4h">4 Hours</option> |
|
<option value="1d">1 Day</option> |
|
<option value="1w">1 Week</option> |
|
</select> |
|
</div> |
|
<button id="loadDataBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md"> |
|
<i class="fas fa-chart-line mr-2"></i>Load Data |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="mb-6"> |
|
<h2 class="text-lg font-semibold mb-3 border-b border-gray-700 pb-2">Indicators</h2> |
|
<div class="space-y-3"> |
|
<select id="indicatorSelect" class="w-full bg-gray-700 border border-gray-600 rounded-md px-3 py-2"> |
|
<option value="">Select Indicator...</option> |
|
<option value="sma">Simple Moving Average</option> |
|
<option value="ema">Exponential Moving Average</option> |
|
<option value="rsi">Relative Strength Index</option> |
|
<option value="macd">MACD</option> |
|
<option value="bollinger">Bollinger Bands</option> |
|
</select> |
|
<div id="indicatorParams" class="hidden space-y-2 p-2 bg-gray-700 rounded-md"> |
|
|
|
</div> |
|
<button id="addIndicatorBtn" class="w-full bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded-md"> |
|
<i class="fas fa-plus mr-2"></i>Add Indicator |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div> |
|
<h2 class="text-lg font-semibold mb-3 border-b border-gray-700 pb-2">Active Indicators</h2> |
|
<ul id="activeIndicatorsList" class="space-y-2"> |
|
|
|
</ul> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="lg:col-span-3 bg-gray-800 rounded-lg p-4"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<div class="flex space-x-2"> |
|
<button id="zoomInBtn" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 rounded-md"> |
|
<i class="fas fa-search-plus"></i> |
|
</button> |
|
<button id="zoomOutBtn" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 rounded-md"> |
|
<i class="fas fa-search-minus"></i> |
|
</button> |
|
<button id="zoomFitBtn" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 rounded-md"> |
|
<i class="fas fa-expand"></i> |
|
</button> |
|
</div> |
|
<div class="flex space-x-2"> |
|
<button id="drawLineBtn" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 rounded-md"> |
|
<i class="fas fa-slash"></i> Line |
|
</button> |
|
<button id="drawHorizontalBtn" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 rounded-md"> |
|
<i class="fas fa-grip-lines"></i> Horizontal |
|
</button> |
|
<button id="clearDrawingsBtn" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 rounded-md"> |
|
<i class="fas fa-trash-alt"></i> Clear |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="chart-container"> |
|
<canvas id="priceChart"></canvas> |
|
<div id="tooltip" class="tooltip"></div> |
|
</div> |
|
|
|
<div class="mt-4"> |
|
<div class="chart-container" style="height: 150px;"> |
|
<canvas id="volumeChart"></canvas> |
|
</div> |
|
</div> |
|
|
|
<div class="mt-4 flex justify-between items-center text-sm text-gray-400"> |
|
<div id="timeRangeDisplay">Time range: -</div> |
|
<div id="priceDisplay">Price: -</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
const priceChartCtx = document.getElementById('priceChart').getContext('2d'); |
|
const volumeChartCtx = document.getElementById('volumeChart').getContext('2d'); |
|
|
|
let priceChart, volumeChart; |
|
let chartData = []; |
|
let activeIndicators = []; |
|
let drawings = []; |
|
let isDrawing = false; |
|
let currentDrawing = null; |
|
|
|
|
|
function initCharts() { |
|
priceChart = new Chart(priceChartCtx, { |
|
type: 'candlestick', |
|
data: { |
|
datasets: [{ |
|
label: 'Price', |
|
data: [], |
|
borderColor: 'rgba(75, 192, 192, 1)', |
|
borderWidth: 1, |
|
color: { |
|
up: '#26a69a', |
|
down: '#ef5350', |
|
unchanged: '#999', |
|
}, |
|
}] |
|
}, |
|
options: { |
|
responsive: true, |
|
maintainAspectRatio: false, |
|
scales: { |
|
x: { |
|
type: 'time', |
|
time: { |
|
unit: 'day' |
|
}, |
|
grid: { |
|
color: 'rgba(255, 255, 255, 0.1)' |
|
}, |
|
ticks: { |
|
color: 'rgba(255, 255, 255, 0.7)' |
|
} |
|
}, |
|
y: { |
|
position: 'right', |
|
grid: { |
|
color: 'rgba(255, 255, 255, 0.1)' |
|
}, |
|
ticks: { |
|
color: 'rgba(255, 255, 255, 0.7)' |
|
} |
|
} |
|
}, |
|
plugins: { |
|
legend: { |
|
display: false |
|
}, |
|
tooltip: { |
|
enabled: false, |
|
external: function(context) { |
|
const tooltip = document.getElementById('tooltip'); |
|
if (context.tooltip.opacity === 0) { |
|
tooltip.style.display = 'none'; |
|
return; |
|
} |
|
|
|
const data = context.tooltip.dataPoints[0].raw; |
|
tooltip.innerHTML = ` |
|
<div><strong>Date:</strong> ${new Date(data.x).toLocaleString()}</div> |
|
<div><strong>Open:</strong> ${data.o}</div> |
|
<div><strong>High:</strong> ${data.h}</div> |
|
<div><strong>Low:</strong> ${data.l}</div> |
|
<div><strong>Close:</strong> ${data.c}</div> |
|
<div><strong>Volume:</strong> ${data.v || 0}</div> |
|
`; |
|
|
|
tooltip.style.display = 'block'; |
|
tooltip.style.left = context.tooltip.caretX + 'px'; |
|
tooltip.style.top = context.tooltip.caretY + 'px'; |
|
} |
|
}, |
|
zoom: { |
|
pan: { |
|
enabled: true, |
|
mode: 'xy', |
|
modifierKey: 'ctrl' |
|
}, |
|
zoom: { |
|
wheel: { |
|
enabled: true, |
|
}, |
|
pinch: { |
|
enabled: true |
|
}, |
|
mode: 'xy', |
|
} |
|
} |
|
}, |
|
interaction: { |
|
intersect: false, |
|
mode: 'index', |
|
}, |
|
onHover: function(event, chartElement) { |
|
if (event.native) { |
|
const x = event.native.x; |
|
const y = event.native.y; |
|
document.getElementById('priceDisplay').textContent = `Price: ${y}`; |
|
} |
|
} |
|
} |
|
}); |
|
|
|
volumeChart = new Chart(volumeChartCtx, { |
|
type: 'bar', |
|
data: { |
|
datasets: [{ |
|
label: 'Volume', |
|
data: [], |
|
backgroundColor: function(context) { |
|
const index = context.dataIndex; |
|
if (index > 0) { |
|
const current = chartData[index]; |
|
const previous = chartData[index - 1]; |
|
return current.c >= previous.c ? 'rgba(38, 166, 154, 0.7)' : 'rgba(239, 83, 80, 0.7)'; |
|
} |
|
return 'rgba(153, 153, 153, 0.7)'; |
|
}, |
|
borderColor: 'rgba(0, 0, 0, 0.1)', |
|
borderWidth: 1 |
|
}] |
|
}, |
|
options: { |
|
responsive: true, |
|
maintainAspectRatio: false, |
|
scales: { |
|
x: { |
|
type: 'time', |
|
display: false, |
|
grid: { |
|
display: false |
|
} |
|
}, |
|
y: { |
|
display: false, |
|
beginAtZero: true |
|
} |
|
}, |
|
plugins: { |
|
legend: { |
|
display: false |
|
}, |
|
tooltip: { |
|
enabled: false |
|
} |
|
} |
|
} |
|
}); |
|
} |
|
|
|
|
|
function parseCSV(file) { |
|
return new Promise((resolve, reject) => { |
|
Papa.parse(file, { |
|
header: true, |
|
complete: function(results) { |
|
resolve(results.data); |
|
}, |
|
error: function(error) { |
|
reject(error); |
|
} |
|
}); |
|
}); |
|
} |
|
|
|
|
|
function processData(rawData) { |
|
return rawData.map(item => { |
|
|
|
let date; |
|
if (item.date) { |
|
date = new Date(item.date); |
|
} else if (item.timestamp) { |
|
date = new Date(parseInt(item.timestamp)); |
|
} else if (item.time) { |
|
date = new Date(item.time); |
|
} else { |
|
|
|
for (const key in item) { |
|
if (key.match(/date|time|timestamp/i)) { |
|
date = new Date(item[key]); |
|
break; |
|
} |
|
} |
|
} |
|
|
|
|
|
let o, h, l, c, v; |
|
|
|
if (item.open && item.high && item.low && item.close) { |
|
o = parseFloat(item.open); |
|
h = parseFloat(item.high); |
|
l = parseFloat(item.low); |
|
c = parseFloat(item.close); |
|
} else { |
|
|
|
const numericValues = []; |
|
for (const key in item) { |
|
if (!isNaN(parseFloat(item[key]))) { |
|
numericValues.push(parseFloat(item[key])); |
|
} |
|
} |
|
|
|
if (numericValues.length >= 4) { |
|
o = numericValues[0]; |
|
h = numericValues[1]; |
|
l = numericValues[2]; |
|
c = numericValues[3]; |
|
} |
|
} |
|
|
|
v = item.volume ? parseFloat(item.volume) : 0; |
|
|
|
return { |
|
x: date, |
|
o: o, |
|
h: h, |
|
l: l, |
|
c: c, |
|
v: v |
|
}; |
|
}).filter(item => !isNaN(item.x.getTime()) && !isNaN(item.o) && !isNaN(item.h) && !isNaN(item.l) && !isNaN(item.c)); |
|
} |
|
|
|
|
|
function updateCharts(data) { |
|
chartData = data; |
|
|
|
|
|
priceChart.data.datasets[0].data = data; |
|
|
|
|
|
volumeChart.data.datasets[0].data = data.map(item => ({ |
|
x: item.x, |
|
y: item.v |
|
})); |
|
|
|
|
|
if (data.length > 0) { |
|
const startDate = new Date(data[0].x).toLocaleDateString(); |
|
const endDate = new Date(data[data.length - 1].x).toLocaleDateString(); |
|
document.getElementById('timeRangeDisplay').textContent = `Time range: ${startDate} to ${endDate}`; |
|
} |
|
|
|
priceChart.update(); |
|
volumeChart.update(); |
|
} |
|
|
|
|
|
document.getElementById('loadDataBtn').addEventListener('click', async function() { |
|
const fileInput = document.getElementById('csvFileInput'); |
|
const chartType = document.getElementById('chartTypeSelect').value; |
|
|
|
if (fileInput.files.length === 0) { |
|
alert('Please select a CSV file first.'); |
|
return; |
|
} |
|
|
|
try { |
|
const rawData = await parseCSV(fileInput.files[0]); |
|
const processedData = processData(rawData); |
|
|
|
if (processedData.length === 0) { |
|
alert('No valid data found in the CSV file.'); |
|
return; |
|
} |
|
|
|
|
|
priceChart.config.type = chartType === 'candlestick' ? 'candlestick' : 'line'; |
|
|
|
updateCharts(processedData); |
|
} catch (error) { |
|
console.error('Error loading CSV:', error); |
|
alert('Error loading CSV file. Please check the file format.'); |
|
} |
|
}); |
|
|
|
|
|
document.getElementById('chartTypeSelect').addEventListener('change', function() { |
|
const chartType = this.value; |
|
priceChart.config.type = chartType === 'candlestick' ? 'candlestick' : 'line'; |
|
priceChart.update(); |
|
}); |
|
|
|
|
|
document.getElementById('indicatorSelect').addEventListener('change', function() { |
|
const indicator = this.value; |
|
const paramsDiv = document.getElementById('indicatorParams'); |
|
|
|
if (!indicator) { |
|
paramsDiv.classList.add('hidden'); |
|
return; |
|
} |
|
|
|
paramsDiv.innerHTML = ''; |
|
paramsDiv.classList.remove('hidden'); |
|
|
|
switch (indicator) { |
|
case 'sma': |
|
paramsDiv.innerHTML = ` |
|
<div> |
|
<label class="block text-sm font-medium mb-1">Period</label> |
|
<input type="number" value="20" min="1" max="200" class="w-full bg-gray-600 border border-gray-500 rounded-md px-2 py-1"> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium mb-1">Color</label> |
|
<input type="color" value="#FFA500" class="w-full bg-gray-600 border border-gray-500 rounded-md px-2 py-1"> |
|
</div> |
|
`; |
|
break; |
|
case 'ema': |
|
paramsDiv.innerHTML = ` |
|
<div> |
|
<label class="block text-sm font-medium mb-1">Period</label> |
|
<input type="number" value="20" min="1" max="200" class="w-full bg-gray-600 border border-gray-500 rounded-md px-2 py-1"> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium mb-1">Color</label> |
|
<input type="color" value="#FFA500" class="w-full bg-gray-600 border border-gray-500 rounded-md px-2 py-1"> |
|
</div> |
|
`; |
|
break; |
|
case 'rsi': |
|
paramsDiv.innerHTML = ` |
|
<div> |
|
<label class="block text-sm font-medium mb-1">Period</label> |
|
<input type="number" value="14" min="1" max="200" class="w-full bg-gray-600 border border-gray-500 rounded-md px-2 py-1"> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium mb-1">Overbought</label> |
|
<input type="number" value="70" min="1" max="100" class="w-full bg-gray-600 border border-gray-500 rounded-md px-2 py-1"> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium mb-1">Oversold</label> |
|
<input type="number" value="30" min="1" max="100" class="w-full bg-gray-600 border border-gray-500 rounded-md px-2 py-1"> |
|
</div> |
|
`; |
|
break; |
|
case 'macd': |
|
paramsDiv.innerHTML = ` |
|
<div> |
|
<label class="block text-sm font-medium mb-1">Fast Period</label> |
|
<input type="number" value="12" min="1" max="50" class="w-full bg-gray-600 border border-gray-500 rounded-md px-2 py-1"> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium mb-1">Slow Period</label> |
|
<input type="number" value="26" min="1" max="50" class="w-full bg-gray-600 border border-gray-500 rounded-md px-2 py-1"> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium mb-1">Signal Period</label> |
|
<input type="number" value="9" min="1" max="50" class="w-full bg-gray-600 border border-gray-500 rounded-md px-2 py-1"> |
|
</div> |
|
`; |
|
break; |
|
case 'bollinger': |
|
paramsDiv.innerHTML = ` |
|
<div> |
|
<label class="block text-sm font-medium mb-1">Period</label> |
|
<input type="number" value="20" min="1" max="200" class="w-full bg-gray-600 border border-gray-500 rounded-md px-2 py-1"> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium mb-1">Standard Deviations</label> |
|
<input type="number" value="2" min="1" max="5" step="0.1" class="w-full bg-gray-600 border border-gray-500 rounded-md px-2 py-1"> |
|
</div> |
|
`; |
|
break; |
|
} |
|
}); |
|
|
|
|
|
document.getElementById('addIndicatorBtn').addEventListener('click', function() { |
|
const indicator = document.getElementById('indicatorSelect').value; |
|
|
|
if (!indicator) { |
|
alert('Please select an indicator first.'); |
|
return; |
|
} |
|
|
|
if (chartData.length === 0) { |
|
alert('Please load data first.'); |
|
return; |
|
} |
|
|
|
const paramsDiv = document.getElementById('indicatorParams'); |
|
const inputs = paramsDiv.querySelectorAll('input'); |
|
const params = {}; |
|
|
|
inputs.forEach(input => { |
|
params[input.previousElementSibling.textContent.trim()] = input.type === 'color' ? input.value : parseFloat(input.value); |
|
}); |
|
|
|
const indicatorId = Date.now(); |
|
const indicatorConfig = { |
|
id: indicatorId, |
|
type: indicator, |
|
params: params, |
|
color: params.Color || '#FFA500' |
|
}; |
|
|
|
activeIndicators.push(indicatorConfig); |
|
updateIndicatorsList(); |
|
applyIndicator(indicatorConfig); |
|
}); |
|
|
|
|
|
function updateIndicatorsList() { |
|
const list = document.getElementById('activeIndicatorsList'); |
|
list.innerHTML = ''; |
|
|
|
activeIndicators.forEach(indicator => { |
|
const li = document.createElement('li'); |
|
li.className = 'flex justify-between items-center bg-gray-700 p-2 rounded-md'; |
|
li.innerHTML = ` |
|
<span>${indicator.type.toUpperCase()} (${Object.entries(indicator.params) |
|
.filter(([key]) => key !== 'Color') |
|
.map(([key, val]) => `${key}: ${val}`).join(', ')})</span> |
|
<button class="text-red-400 hover:text-red-300" data-id="${indicator.id}"> |
|
<i class="fas fa-times"></i> |
|
</button> |
|
`; |
|
list.appendChild(li); |
|
}); |
|
|
|
|
|
document.querySelectorAll('#activeIndicatorsList button').forEach(button => { |
|
button.addEventListener('click', function() { |
|
const id = parseInt(this.getAttribute('data-id')); |
|
removeIndicator(id); |
|
}); |
|
}); |
|
} |
|
|
|
|
|
function removeIndicator(id) { |
|
|
|
activeIndicators = activeIndicators.filter(ind => ind.id !== id); |
|
|
|
|
|
priceChart.data.datasets = priceChart.data.datasets.filter(ds => !ds.indicatorId || ds.indicatorId !== id); |
|
|
|
updateIndicatorsList(); |
|
priceChart.update(); |
|
} |
|
|
|
|
|
function applyIndicator(config) { |
|
const data = chartData; |
|
|
|
switch (config.type) { |
|
case 'sma': |
|
const smaPeriod = config.params.Period || 20; |
|
const smaValues = calculateSMA(data, smaPeriod); |
|
|
|
priceChart.data.datasets.push({ |
|
label: `SMA(${smaPeriod})`, |
|
data: data.map((item, i) => ({ |
|
x: item.x, |
|
y: smaValues[i] |
|
})), |
|
borderColor: config.color, |
|
borderWidth: 2, |
|
pointRadius: 0, |
|
fill: false, |
|
tension: 0.1, |
|
indicatorId: config.id |
|
}); |
|
break; |
|
|
|
case 'ema': |
|
const emaPeriod = config.params.Period || 20; |
|
const emaValues = calculateEMA(data, emaPeriod); |
|
|
|
priceChart.data.datasets.push({ |
|
label: `EMA(${emaPeriod})`, |
|
data: data.map((item, i) => ({ |
|
x: item.x, |
|
y: emaValues[i] |
|
})), |
|
borderColor: config.color, |
|
borderWidth: 2, |
|
pointRadius: 0, |
|
fill: false, |
|
tension: 0.1, |
|
indicatorId: config.id |
|
}); |
|
break; |
|
|
|
case 'rsi': |
|
const rsiPeriod = config.params.Period || 14; |
|
const rsiValues = calculateRSI(data, rsiPeriod); |
|
const overbought = config.params.Overbought || 70; |
|
const oversold = config.params.Oversold || 30; |
|
|
|
|
|
priceChart.data.datasets.push({ |
|
label: `RSI(${rsiPeriod})`, |
|
data: data.map((item, i) => ({ |
|
x: item.x, |
|
y: rsiValues[i] |
|
})), |
|
borderColor: config.color, |
|
borderWidth: 2, |
|
pointRadius: 0, |
|
fill: false, |
|
tension: 0.1, |
|
indicatorId: config.id, |
|
yAxisID: 'rsi-axis' |
|
}); |
|
|
|
|
|
priceChart.data.datasets.push({ |
|
label: 'Overbought', |
|
data: data.map(item => ({ |
|
x: item.x, |
|
y: overbought |
|
})), |
|
borderColor: 'rgba(255, 0, 0, 0.5)', |
|
borderWidth: 1, |
|
borderDash: [5, 5], |
|
pointRadius: 0, |
|
fill: false, |
|
indicatorId: config.id, |
|
yAxisID: 'rsi-axis' |
|
}); |
|
|
|
|
|
priceChart.data.datasets.push({ |
|
label: 'Oversold', |
|
data: data.map(item => ({ |
|
x: item.x, |
|
y: oversold |
|
})), |
|
borderColor: 'rgba(0, 255, 0, 0.5)', |
|
borderWidth: 1, |
|
borderDash: [5, 5], |
|
pointRadius: 0, |
|
fill: false, |
|
indicatorId: config.id, |
|
yAxisID: 'rsi-axis' |
|
}); |
|
|
|
|
|
if (!priceChart.options.scales['rsi-axis']) { |
|
priceChart.options.scales['rsi-axis'] = { |
|
type: 'linear', |
|
display: false, |
|
min: 0, |
|
max: 100, |
|
grid: { |
|
drawOnChartArea: false |
|
} |
|
}; |
|
} |
|
break; |
|
} |
|
|
|
priceChart.update(); |
|
} |
|
|
|
|
|
function calculateSMA(data, period) { |
|
const sma = []; |
|
|
|
for (let i = 0; i < data.length; i++) { |
|
if (i < period - 1) { |
|
sma.push(null); |
|
continue; |
|
} |
|
|
|
let sum = 0; |
|
for (let j = 0; j < period; j++) { |
|
sum += data[i - j].c; |
|
} |
|
|
|
sma.push(sum / period); |
|
} |
|
|
|
return sma; |
|
} |
|
|
|
function calculateEMA(data, period) { |
|
const ema = []; |
|
const multiplier = 2 / (period + 1); |
|
|
|
|
|
let sum = 0; |
|
for (let i = 0; i < period; i++) { |
|
sum += data[i].c; |
|
ema.push(null); |
|
} |
|
ema[period - 1] = sum / period; |
|
|
|
|
|
for (let i = period; i < data.length; i++) { |
|
ema[i] = (data[i].c - ema[i - 1]) * multiplier + ema[i - 1]; |
|
} |
|
|
|
return ema; |
|
} |
|
|
|
function calculateRSI(data, period) { |
|
const rsi = []; |
|
let avgGain = 0; |
|
let avgLoss = 0; |
|
|
|
|
|
for (let i = 1; i <= period; i++) { |
|
const change = data[i].c - data[i - 1].c; |
|
if (change > 0) { |
|
avgGain += change; |
|
} else { |
|
avgLoss += Math.abs(change); |
|
} |
|
|
|
rsi.push(null); |
|
} |
|
|
|
avgGain /= period; |
|
avgLoss /= period; |
|
|
|
|
|
const rs = avgLoss === 0 ? 100 : avgGain / avgLoss; |
|
rsi[period] = 100 - (100 / (1 + rs)); |
|
|
|
|
|
for (let i = period + 1; i < data.length; i++) { |
|
const change = data[i].c - data[i - 1].c; |
|
let gain = 0; |
|
let loss = 0; |
|
|
|
if (change > 0) { |
|
gain = change; |
|
} else { |
|
loss = Math.abs(change); |
|
} |
|
|
|
avgGain = (avgGain * (period - 1) + gain) / period; |
|
avgLoss = (avgLoss * (period - 1) + loss) / period; |
|
|
|
const rs = avgLoss === 0 ? 100 : avgGain / avgLoss; |
|
rsi[i] = 100 - (100 / (1 + rs)); |
|
} |
|
|
|
return rsi; |
|
} |
|
|
|
|
|
document.getElementById('zoomInBtn').addEventListener('click', function() { |
|
priceChart.zoom(1.1); |
|
}); |
|
|
|
document.getElementById('zoomOutBtn').addEventListener('click', function() { |
|
priceChart.zoom(0.9); |
|
}); |
|
|
|
document.getElementById('zoomFitBtn').addEventListener('click', function() { |
|
priceChart.resetZoom(); |
|
}); |
|
|
|
|
|
document.getElementById('drawLineBtn').addEventListener('click', function() { |
|
activateDrawingTool('line'); |
|
}); |
|
|
|
document.getElementById('drawHorizontalBtn').addEventListener('click', function() { |
|
activateDrawingTool('horizontal'); |
|
}); |
|
|
|
document.getElementById('clearDrawingsBtn').addEventListener('click', function() { |
|
clearDrawings(); |
|
}); |
|
|
|
function activateDrawingTool(tool) { |
|
|
|
|
|
alert(`Drawing tool activated: ${tool}. In a full implementation, you would be able to draw on the chart.`); |
|
} |
|
|
|
function clearDrawings() { |
|
|
|
drawings = []; |
|
|
|
alert('All drawings cleared. In a full implementation, the drawings would be removed from the chart.'); |
|
} |
|
|
|
|
|
initCharts(); |
|
}); |
|
</script> |
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Sanzhar7/tr" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
</html> |