feat: standardize date/time format to DD/MM/YYYY 24h and enhance simulation with stop date and auto-data fetching
This commit is contained in:
@ -46,7 +46,7 @@ const TimezoneConfig = {
|
||||
const parts = formatter.formatToParts(date);
|
||||
const get = (type) => parts.find(p => p.type === type).value;
|
||||
|
||||
return `${get('day')}/${get('month')}/${get('year').slice(-2)} ${get('hour')}:${get('minute')}`;
|
||||
return `${get('day')}/${get('month')}/${get('year')} ${get('hour')}:${get('minute')}`;
|
||||
},
|
||||
|
||||
formatTickMark(timestamp) {
|
||||
@ -55,13 +55,21 @@ const TimezoneConfig = {
|
||||
|
||||
const options = {
|
||||
timeZone: tz,
|
||||
month: '2-digit', day: '2-digit',
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit',
|
||||
hour12: false
|
||||
};
|
||||
|
||||
const formatter = new Intl.DateTimeFormat('en-US', options);
|
||||
return formatter.format(date).replace(',', '');
|
||||
const formatter = new Intl.DateTimeFormat('en-GB', options);
|
||||
const parts = formatter.formatToParts(date);
|
||||
const get = (type) => parts.find(p => p.type === type).value;
|
||||
|
||||
// If it's exactly midnight, just show the date, otherwise show time too
|
||||
const isMidnight = get('hour') === '00' && get('minute') === '00';
|
||||
if (isMidnight) {
|
||||
return `${get('day')}/${get('month')}/${get('year')}`;
|
||||
}
|
||||
return `${get('day')}/${get('month')} ${get('hour')}:${get('minute')}`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -123,7 +123,7 @@ constructor() {
|
||||
},
|
||||
localization: {
|
||||
timeFormatter: (timestamp) => {
|
||||
return TimezoneConfig.formatTickMark(timestamp);
|
||||
return TimezoneConfig.formatDate(timestamp * 1000);
|
||||
},
|
||||
},
|
||||
handleScroll: {
|
||||
|
||||
@ -9,6 +9,26 @@ let posSizeChart = null;
|
||||
|
||||
const STORAGE_KEY = 'ping_pong_settings';
|
||||
|
||||
function formatDisplayDate(timestamp) {
|
||||
if (!timestamp) return '';
|
||||
const date = new Date(timestamp);
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
function parseDisplayDate(str) {
|
||||
if (!str) return null;
|
||||
const regex = /^(\d{2})\/(\d{2})\/(\d{4})\s(\d{2}):(\d{2})$/;
|
||||
const match = str.trim().match(regex);
|
||||
if (!match) return null;
|
||||
const [_, day, month, year, hours, minutes] = match;
|
||||
return new Date(year, month - 1, day, hours, minutes);
|
||||
}
|
||||
|
||||
function getSavedSettings() {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (!saved) return null;
|
||||
@ -22,6 +42,7 @@ function getSavedSettings() {
|
||||
function saveSettings() {
|
||||
const settings = {
|
||||
startDate: document.getElementById('simStartDate').value,
|
||||
stopDate: document.getElementById('simStopDate').value,
|
||||
contractType: document.getElementById('simContractType').value,
|
||||
direction: document.getElementById('simDirection').value,
|
||||
capital: document.getElementById('simCapital').value,
|
||||
@ -69,6 +90,18 @@ export function renderStrategyPanel() {
|
||||
activeIndicators = window.getActiveIndicators?.() || [];
|
||||
const saved = getSavedSettings();
|
||||
|
||||
// Format initial values for display
|
||||
let startDisplay = saved?.startDate || '01/01/2026 00:00';
|
||||
let stopDisplay = saved?.stopDate || '';
|
||||
|
||||
// If the saved value is in ISO format (from previous version), convert it
|
||||
if (startDisplay.includes('T')) {
|
||||
startDisplay = formatDisplayDate(new Date(startDisplay));
|
||||
}
|
||||
if (stopDisplay.includes('T')) {
|
||||
stopDisplay = formatDisplayDate(new Date(stopDisplay));
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-header">
|
||||
@ -77,7 +110,12 @@ export function renderStrategyPanel() {
|
||||
<div class="sidebar-section-content">
|
||||
<div class="sim-input-group">
|
||||
<label>Start Date & Time</label>
|
||||
<input type="datetime-local" id="simStartDate" class="sim-input" value="${saved?.startDate || '2026-01-01T00:00'}">
|
||||
<input type="text" id="simStartDate" class="sim-input" value="${startDisplay}" placeholder="DD/MM/YYYY HH:MM">
|
||||
</div>
|
||||
|
||||
<div class="sim-input-group">
|
||||
<label>Stop Date & Time (Optional)</label>
|
||||
<input type="text" id="simStopDate" class="sim-input" value="${stopDisplay}" placeholder="DD/MM/YYYY HH:MM">
|
||||
</div>
|
||||
|
||||
<div class="sim-input-group">
|
||||
@ -167,11 +205,16 @@ function renderIndicatorChecklist(prefix) {
|
||||
async function runSimulation() {
|
||||
const btn = document.getElementById('runSimulationBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Simulating...';
|
||||
const originalBtnText = btn.textContent;
|
||||
btn.textContent = 'Preparing Data...';
|
||||
|
||||
try {
|
||||
const startVal = document.getElementById('simStartDate').value;
|
||||
const stopVal = document.getElementById('simStopDate').value;
|
||||
|
||||
const config = {
|
||||
startDate: new Date(document.getElementById('simStartDate').value).getTime() / 1000,
|
||||
startDate: new Date(startVal).getTime() / 1000,
|
||||
stopDate: stopVal ? new Date(stopVal).getTime() / 1000 : Math.floor(Date.now() / 1000),
|
||||
contractType: document.getElementById('simContractType').value,
|
||||
direction: document.getElementById('simDirection').value,
|
||||
capital: parseFloat(document.getElementById('simCapital').value),
|
||||
@ -188,16 +231,54 @@ async function runSimulation() {
|
||||
return;
|
||||
}
|
||||
|
||||
const candles = window.dashboard?.allData?.get(window.dashboard?.currentInterval);
|
||||
if (!candles || candles.length === 0) {
|
||||
alert('No candle data available.');
|
||||
return;
|
||||
const interval = window.dashboard?.currentInterval || '1d';
|
||||
|
||||
// 1. Ensure data is loaded for the range
|
||||
let allCandles = window.dashboard?.allData?.get(interval) || [];
|
||||
|
||||
const earliestInCache = allCandles.length > 0 ? allCandles[0].time : Infinity;
|
||||
const latestInCache = allCandles.length > 0 ? allCandles[allCandles.length - 1].time : -Infinity;
|
||||
|
||||
if (config.startDate < earliestInCache || config.stopDate > latestInCache) {
|
||||
btn.textContent = 'Fetching from Server...';
|
||||
console.log(`[Simulation] Data gap detected. Range: ${config.startDate}-${config.stopDate}, Cache: ${earliestInCache}-${latestInCache}`);
|
||||
|
||||
const startISO = new Date(config.startDate * 1000).toISOString();
|
||||
const stopISO = new Date(config.stopDate * 1000).toISOString();
|
||||
|
||||
const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${interval}&start=${startISO}&end=${stopISO}&limit=10000`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.candles && data.candles.length > 0) {
|
||||
const fetchedCandles = 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),
|
||||
volume: parseFloat(c.volume || 0)
|
||||
}));
|
||||
|
||||
// Merge with existing data
|
||||
allCandles = window.dashboard.mergeData(allCandles, fetchedCandles);
|
||||
window.dashboard.allData.set(interval, allCandles);
|
||||
window.dashboard.candleSeries.setData(allCandles);
|
||||
|
||||
// Recalculate indicators
|
||||
btn.textContent = 'Calculating Indicators...';
|
||||
window.drawIndicatorsOnChart?.();
|
||||
// Wait a bit for indicators to calculate (they usually run in background/setTimeout)
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
}
|
||||
|
||||
// Filter candles by start date
|
||||
const simCandles = candles.filter(c => c.time >= config.startDate);
|
||||
btn.textContent = 'Simulating...';
|
||||
|
||||
// Filter candles by the exact range
|
||||
const simCandles = allCandles.filter(c => c.time >= config.startDate && c.time <= config.stopDate);
|
||||
|
||||
if (simCandles.length === 0) {
|
||||
alert('No data available for the selected start date.');
|
||||
alert('No data available for the selected range.');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -205,16 +286,18 @@ async function runSimulation() {
|
||||
const indicatorSignals = {};
|
||||
for (const indId of [...new Set([...config.openIndicators, ...config.closeIndicators])]) {
|
||||
const ind = activeIndicators.find(a => a.id === indId);
|
||||
if (!ind) continue;
|
||||
|
||||
const signalFunc = getSignalFunction(ind.type);
|
||||
const results = ind.cachedResults;
|
||||
|
||||
if (results && signalFunc) {
|
||||
indicatorSignals[indId] = simCandles.map(candle => {
|
||||
const idx = candles.findIndex(c => c.time === candle.time);
|
||||
const idx = allCandles.findIndex(c => c.time === candle.time);
|
||||
if (idx < 1) return null;
|
||||
const values = typeof results[idx] === 'object' && results[idx] !== null ? results[idx] : { ma: results[idx] };
|
||||
const prevValues = typeof results[idx-1] === 'object' && results[idx-1] !== null ? results[idx-1] : { ma: results[idx-1] };
|
||||
return signalFunc(ind, candles[idx], candles[idx-1], values, prevValues);
|
||||
return signalFunc(ind, allCandles[idx], allCandles[idx-1], values, prevValues);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -499,7 +582,20 @@ function displayResults(trades, equityData, config, endPrice, avgPriceData, posS
|
||||
layout: { background: { color: '#131722' }, textColor: '#d1d4dc' },
|
||||
grid: { vertLines: { visible: false }, horzLines: { color: '#2a2e39' } },
|
||||
rightPriceScale: { borderColor: '#2a2e39', autoScale: true },
|
||||
timeScale: { borderColor: '#2a2e39', visible: true, timeVisible: true, secondsVisible: false },
|
||||
timeScale: {
|
||||
borderColor: '#2a2e39',
|
||||
visible: true,
|
||||
timeVisible: true,
|
||||
secondsVisible: false,
|
||||
tickMarkFormatter: (time, tickMarkType, locale) => {
|
||||
return TimezoneConfig.formatTickMark(time);
|
||||
},
|
||||
},
|
||||
localization: {
|
||||
timeFormatter: (timestamp) => {
|
||||
return TimezoneConfig.formatDate(timestamp * 1000);
|
||||
},
|
||||
},
|
||||
handleScroll: true,
|
||||
handleScale: true
|
||||
});
|
||||
@ -523,7 +619,20 @@ function displayResults(trades, equityData, config, endPrice, avgPriceData, posS
|
||||
layout: { background: { color: '#131722' }, textColor: '#d1d4dc' },
|
||||
grid: { vertLines: { visible: false }, horzLines: { color: '#2a2e39' } },
|
||||
rightPriceScale: { borderColor: '#2a2e39', autoScale: true },
|
||||
timeScale: { borderColor: '#2a2e39', visible: true, timeVisible: true, secondsVisible: false },
|
||||
timeScale: {
|
||||
borderColor: '#2a2e39',
|
||||
visible: true,
|
||||
timeVisible: true,
|
||||
secondsVisible: false,
|
||||
tickMarkFormatter: (time, tickMarkType, locale) => {
|
||||
return TimezoneConfig.formatTickMark(time);
|
||||
},
|
||||
},
|
||||
localization: {
|
||||
timeFormatter: (timestamp) => {
|
||||
return TimezoneConfig.formatDate(timestamp * 1000);
|
||||
},
|
||||
},
|
||||
handleScroll: true,
|
||||
handleScale: true
|
||||
});
|
||||
@ -557,6 +666,27 @@ function displayResults(trades, equityData, config, endPrice, avgPriceData, posS
|
||||
equityChart.timeScale().subscribeVisibleTimeRangeChange(() => syncCharts(equityChart, posSizeChart));
|
||||
posSizeChart.timeScale().subscribeVisibleTimeRangeChange(() => syncCharts(posSizeChart, equityChart));
|
||||
}
|
||||
|
||||
// Sync to Main Chart on Click
|
||||
const syncToMain = (param) => {
|
||||
if (!param.time || !window.dashboard || !window.dashboard.chart) return;
|
||||
|
||||
const timeScale = window.dashboard.chart.timeScale();
|
||||
const currentRange = timeScale.getVisibleRange();
|
||||
if (!currentRange) return;
|
||||
|
||||
// Calculate current width to preserve zoom level
|
||||
const width = currentRange.to - currentRange.from;
|
||||
const halfWidth = width / 2;
|
||||
|
||||
timeScale.setVisibleRange({
|
||||
from: param.time - halfWidth,
|
||||
to: param.time + halfWidth
|
||||
});
|
||||
};
|
||||
|
||||
if (equityChart) equityChart.subscribeClick(syncToMain);
|
||||
if (posSizeChart) posSizeChart.subscribeClick(syncToMain);
|
||||
};
|
||||
|
||||
setTimeout(initCharts, 100);
|
||||
|
||||
Reference in New Issue
Block a user