feat: implement interactive Best Moving Averages panel based on 1D timeframe
This commit is contained in:
@ -23,10 +23,62 @@ constructor() {
|
|||||||
this.lastCandleTimestamp = null;
|
this.lastCandleTimestamp = null;
|
||||||
this.simulationMarkers = [];
|
this.simulationMarkers = [];
|
||||||
this.avgPriceSeries = null;
|
this.avgPriceSeries = null;
|
||||||
|
this.dailyMAData = new Map(); // timestamp -> { ma44, ma125, price }
|
||||||
|
this.currentMouseTime = null;
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadDailyMAData() {
|
||||||
|
try {
|
||||||
|
// Use 1d interval for this calculation
|
||||||
|
const interval = '1d';
|
||||||
|
let candles = this.allData.get(interval);
|
||||||
|
|
||||||
|
if (!candles || candles.length < 125) {
|
||||||
|
const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${interval}&limit=1000`);
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.candles && data.candles.length > 0) {
|
||||||
|
candles = data.candles.reverse().map(c => ({
|
||||||
|
time: Math.floor(new Date(c.time).getTime() / 1000),
|
||||||
|
open: parseFloat(c.open),
|
||||||
|
high: parseFloat(c.high),
|
||||||
|
low: parseFloat(c.low),
|
||||||
|
close: parseFloat(c.close)
|
||||||
|
}));
|
||||||
|
this.allData.set(interval, candles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candles && candles.length >= 44) {
|
||||||
|
const ma44 = this.calculateSimpleSMA(candles, 44);
|
||||||
|
const ma125 = this.calculateSimpleSMA(candles, 125);
|
||||||
|
|
||||||
|
this.dailyMAData.clear();
|
||||||
|
candles.forEach((c, i) => {
|
||||||
|
this.dailyMAData.set(c.time, {
|
||||||
|
price: c.close,
|
||||||
|
ma44: ma44[i],
|
||||||
|
ma125: ma125[i]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DailyMA] Error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateSimpleSMA(candles, period) {
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < candles.length; i++) {
|
||||||
|
sum += candles[i].close;
|
||||||
|
if (i >= period) sum -= candles[i - period].close;
|
||||||
|
if (i >= period - 1) results[i] = sum / period;
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
setSimulationMarkers(markers) {
|
setSimulationMarkers(markers) {
|
||||||
this.simulationMarkers = markers || [];
|
this.simulationMarkers = markers || [];
|
||||||
this.updateSignalMarkers();
|
this.updateSignalMarkers();
|
||||||
@ -166,6 +218,17 @@ constructor() {
|
|||||||
|
|
||||||
this.chart.timeScale().subscribeVisibleLogicalRangeChange(this.onVisibleRangeChange.bind(this));
|
this.chart.timeScale().subscribeVisibleLogicalRangeChange(this.onVisibleRangeChange.bind(this));
|
||||||
|
|
||||||
|
// Subscribe to crosshair movement for Best Moving Averages updates
|
||||||
|
this.chart.subscribeCrosshairMove(param => {
|
||||||
|
if (param.time) {
|
||||||
|
this.currentMouseTime = param.time;
|
||||||
|
this.renderTA();
|
||||||
|
} else {
|
||||||
|
this.currentMouseTime = null;
|
||||||
|
this.renderTA();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
this.chart.applyOptions({
|
this.chart.applyOptions({
|
||||||
width: chartContainer.clientWidth,
|
width: chartContainer.clientWidth,
|
||||||
@ -357,7 +420,8 @@ constructor() {
|
|||||||
async loadInitialData() {
|
async loadInitialData() {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.loadData(2000, true),
|
this.loadData(2000, true),
|
||||||
this.loadStats()
|
this.loadStats(),
|
||||||
|
this.loadDailyMAData()
|
||||||
]);
|
]);
|
||||||
this.hasInitialLoad = true;
|
this.hasInitialLoad = true;
|
||||||
this.loadTA();
|
this.loadTA();
|
||||||
@ -465,6 +529,7 @@ async loadNewData() {
|
|||||||
window.drawIndicatorsOnChart?.();
|
window.drawIndicatorsOnChart?.();
|
||||||
window.updateIndicatorCandles?.();
|
window.updateIndicatorCandles?.();
|
||||||
|
|
||||||
|
this.loadDailyMAData();
|
||||||
await this.loadSignals();
|
await this.loadSignals();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -719,9 +784,6 @@ async loadSignals() {
|
|||||||
const trendClass = data.trend.direction.toLowerCase();
|
const trendClass = data.trend.direction.toLowerCase();
|
||||||
const signalClass = data.trend.signal.toLowerCase();
|
const signalClass = data.trend.signal.toLowerCase();
|
||||||
|
|
||||||
const ma44Change = data.moving_averages.price_vs_ma44;
|
|
||||||
const ma125Change = data.moving_averages.price_vs_ma125;
|
|
||||||
|
|
||||||
document.getElementById('taInterval').textContent = this.currentInterval.toUpperCase();
|
document.getElementById('taInterval').textContent = this.currentInterval.toUpperCase();
|
||||||
document.getElementById('taLastUpdate').textContent = new Date().toLocaleTimeString();
|
document.getElementById('taLastUpdate').textContent = new Date().toLocaleTimeString();
|
||||||
|
|
||||||
@ -752,6 +814,34 @@ async loadSignals() {
|
|||||||
|
|
||||||
const summaryBadge = '';
|
const summaryBadge = '';
|
||||||
|
|
||||||
|
// Best Moving Averages Logic (1D based)
|
||||||
|
let displayMA = { ma44: null, ma125: null, price: null, time: null };
|
||||||
|
|
||||||
|
if (this.currentMouseTime && this.dailyMAData.size > 0) {
|
||||||
|
// Find the 1D candle that includes this mouse time
|
||||||
|
const dayTimestamp = Math.floor(this.currentMouseTime / 86400) * 86400;
|
||||||
|
if (this.dailyMAData.has(dayTimestamp)) {
|
||||||
|
displayMA = { ...this.dailyMAData.get(dayTimestamp), time: dayTimestamp };
|
||||||
|
} else {
|
||||||
|
// Fallback to latest if specific day not found
|
||||||
|
const keys = Array.from(this.dailyMAData.keys()).sort((a, b) => b - a);
|
||||||
|
const latestKey = keys[0];
|
||||||
|
displayMA = { ...this.dailyMAData.get(latestKey), time: latestKey };
|
||||||
|
}
|
||||||
|
} else if (this.dailyMAData.size > 0) {
|
||||||
|
const keys = Array.from(this.dailyMAData.keys()).sort((a, b) => b - a);
|
||||||
|
const latestKey = keys[0];
|
||||||
|
displayMA = { ...this.dailyMAData.get(latestKey), time: latestKey };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ma44Value = displayMA.ma44;
|
||||||
|
const ma125Value = displayMA.ma125;
|
||||||
|
const currentPrice = displayMA.price;
|
||||||
|
|
||||||
|
const ma44Change = (ma44Value && currentPrice) ? ((currentPrice - ma44Value) / ma44Value * 100) : null;
|
||||||
|
const ma125Change = (ma125Value && currentPrice) ? ((currentPrice - ma125Value) / ma125Value * 100) : null;
|
||||||
|
const maDateStr = displayMA.time ? TimezoneConfig.formatDate(displayMA.time * 1000).split(' ')[0] : 'Latest';
|
||||||
|
|
||||||
document.getElementById('taContent').innerHTML = `
|
document.getElementById('taContent').innerHTML = `
|
||||||
<div class="ta-section">
|
<div class="ta-section">
|
||||||
<div class="ta-section-title">
|
<div class="ta-section-title">
|
||||||
@ -762,18 +852,21 @@ async loadSignals() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ta-section">
|
<div class="ta-section">
|
||||||
<div class="ta-section-title">Moving Averages</div>
|
<div class="ta-section-title" style="display: flex; justify-content: space-between;">
|
||||||
|
<span>Best Moving Averages</span>
|
||||||
|
<span style="font-size: 10px; font-weight: normal; color: var(--tv-blue);">${maDateStr} (1D)</span>
|
||||||
|
</div>
|
||||||
<div class="ta-ma-row">
|
<div class="ta-ma-row">
|
||||||
<span class="ta-ma-label">MA 44</span>
|
<span class="ta-ma-label">MA 44</span>
|
||||||
<span class="ta-ma-value">
|
<span class="ta-ma-value">
|
||||||
${data.moving_averages.ma_44 ? data.moving_averages.ma_44.toFixed(2) : 'N/A'}
|
${ma44Value ? ma44Value.toFixed(2) : 'N/A'}
|
||||||
${ma44Change !== null ? `<span class="ta-ma-change ${ma44Change >= 0 ? 'positive' : 'negative'}">${ma44Change >= 0 ? '+' : ''}${ma44Change.toFixed(1)}%</span>` : ''}
|
${ma44Change !== null ? `<span class="ta-ma-change ${ma44Change >= 0 ? 'positive' : 'negative'}">${ma44Change >= 0 ? '+' : ''}${ma44Change.toFixed(1)}%</span>` : ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ta-ma-row">
|
<div class="ta-ma-row">
|
||||||
<span class="ta-ma-label">MA 125</span>
|
<span class="ta-ma-label">MA 125</span>
|
||||||
<span class="ta-ma-value">
|
<span class="ta-ma-value">
|
||||||
${data.moving_averages.ma_125 ? data.moving_averages.ma_125.toFixed(2) : 'N/A'}
|
${ma125Value ? ma125Value.toFixed(2) : 'N/A'}
|
||||||
${ma125Change !== null ? `<span class="ta-ma-change ${ma125Change >= 0 ? 'positive' : 'negative'}">${ma125Change >= 0 ? '+' : ''}${ma125Change.toFixed(1)}%</span>` : ''}
|
${ma125Change !== null ? `<span class="ta-ma-change ${ma125Change >= 0 ? 'positive' : 'negative'}">${ma125Change >= 0 ? '+' : ''}${ma125Change.toFixed(1)}%</span>` : ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user