cryptotrader-pro / components /custom-indicator.js
lonestar108's picture
gimme a nie indicator panel that allows me to script a custom indicator in javascript
aa8bc9a verified
class CustomIndicator extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
background: #1e293b;
border-radius: 0.5rem;
padding: 1rem;
margin-top: 1rem;
border: 1px solid #334155;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #334155;
}
.title {
font-weight: bold;
font-size: 1.125rem;
}
.controls {
display: flex;
gap: 0.5rem;
}
.editor-container {
position: relative;
height: 300px;
margin-bottom: 1rem;
}
#editor {
width: 100%;
height: 100%;
border-radius: 0.375rem;
background: #0f172a;
color: #f8fafc;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
padding: 0.5rem;
border: 1px solid #334155;
resize: none;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
cursor: pointer;
border: none;
display: flex;
align-items: center;
gap: 0.5rem;
}
.btn-run {
background: #3b82f6;
color: white;
}
.btn-reset {
background: #334155;
color: white;
}
.output {
margin-top: 1rem;
padding: 0.75rem;
background: #0f172a;
border-radius: 0.375rem;
border: 1px solid #334155;
font-family: monospace;
font-size: 0.875rem;
white-space: pre-wrap;
min-height: 100px;
max-height: 200px;
overflow-y: auto;
}
.error {
color: #ef4444;
}
.success {
color: #10b981;
}
</style>
<div class="header">
<div class="title">Custom Indicator</div>
<div class="controls">
<button class="btn btn-reset" id="reset-btn">
<i data-feather="trash-2"></i> Reset
</button>
<button class="btn btn-run" id="run-btn">
<i data-feather="play"></i> Run
</button>
</div>
</div>
<div class="editor-container">
<textarea id="editor">// Custom Indicator Script
// Access price data with: data.close, data.open, data.high, data.low
// Must return an object with format: { value: number, color?: string, text?: string }
function calculateIndicator(data) {
// Example: Simple Moving Average (SMA)
if (data.length < 14) return null;
const closePrices = data.map(d => d.close);
const sum = closePrices.slice(-14).reduce((a, b) => a + b, 0);
const sma = sum / 14;
return {
value: sma,
color: sma > data[data.length-1].close ? '#ef4444' : '#10b981',
text: \`SMA(14): \${sma.toFixed(2)}\`
};
}
return calculateIndicator(data);</textarea>
</div>
<div class="output" id="output"></div>
`;
if (window.feather) {
window.feather.replace({ class: 'feather-inline' });
}
this.shadowRoot.getElementById('run-btn').addEventListener('click', () => this.runScript());
this.shadowRoot.getElementById('reset-btn').addEventListener('click', () => this.resetScript());
// Sample data for testing
this.sampleData = Array.from({ length: 30 }, (_, i) => ({
close: 25000 + Math.sin(i/3) * 1000 + Math.random() * 500,
open: 25000 + Math.sin(i/3) * 1000 + Math.random() * 500,
high: 25000 + Math.sin(i/3) * 1000 + Math.random() * 600,
low: 25000 + Math.sin(i/3) * 1000 - Math.random() * 400,
volume: 1000 + Math.random() * 5000
}));
}
runScript() {
const editor = this.shadowRoot.getElementById('editor');
const output = this.shadowRoot.getElementById('output');
try {
output.innerHTML = '';
output.classList.remove('error');
// Create a function from the editor content
const indicatorFn = new Function('data', `
${editor.value}
`);
// Execute with sample data
const result = indicatorFn(this.sampleData);
if (result === null || result === undefined) {
output.innerHTML = 'Indicator returned no data (null)';
} else if (typeof result === 'object' && 'value' in result) {
output.classList.add('success');
output.innerHTML = `Indicator value: ${result.value}\n\n`;
if (result.text) {
output.innerHTML += `Display text: ${result.text}\n\n`;
}
if (result.color) {
output.innerHTML += `Color: <span style="color:${result.color}">${result.color}</span>`;
}
// Emit event with the indicator function
this.dispatchEvent(new CustomEvent('indicator-created', {
detail: {
fn: indicatorFn,
config: result
}
}));
} else {
throw new Error('Indicator must return an object with at least a "value" property');
}
} catch (err) {
output.classList.add('error');
output.innerHTML = err.message;
console.error(err);
}
}
resetScript() {
const editor = this.shadowRoot.getElementById('editor');
const output = this.shadowRoot.getElementById('output');
editor.value = `// Custom Indicator Script
// Access price data with: data.close, data.open, data.high, data.low
// Must return an object with format: { value: number, color?: string, text?: string }
function calculateIndicator(data) {
// Example: Simple Moving Average (SMA)
if (data.length < 14) return null;
const closePrices = data.map(d => d.close);
const sum = closePrices.slice(-14).reduce((a, b) => a + b, 0);
const sma = sum / 14;
return {
value: sma,
color: sma > data[data.length-1].close ? '#ef4444' : '#10b981',
text: \`SMA(14): \${sma.toFixed(2)}\`
};
}
return calculateIndicator(data);`;
output.innerHTML = '';
output.classList.remove('error', 'success');
this.dispatchEvent(new CustomEvent('indicator-removed'));
}
}
customElements.define('custom-indicator', CustomIndicator);