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:
DiTus
2026-03-04 09:11:25 +01:00
parent 35ebb0ae87
commit 756b1dbd65
3 changed files with 157 additions and 19 deletions

View File

@ -46,7 +46,7 @@ const TimezoneConfig = {
const parts = formatter.formatToParts(date); const parts = formatter.formatToParts(date);
const get = (type) => parts.find(p => p.type === type).value; 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) { formatTickMark(timestamp) {
@ -55,13 +55,21 @@ const TimezoneConfig = {
const options = { const options = {
timeZone: tz, timeZone: tz,
month: '2-digit', day: '2-digit', year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', hour: '2-digit', minute: '2-digit',
hour12: false hour12: false
}; };
const formatter = new Intl.DateTimeFormat('en-US', options); const formatter = new Intl.DateTimeFormat('en-GB', options);
return formatter.format(date).replace(',', ''); 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')}`;
} }
}; };

View File

@ -123,7 +123,7 @@ constructor() {
}, },
localization: { localization: {
timeFormatter: (timestamp) => { timeFormatter: (timestamp) => {
return TimezoneConfig.formatTickMark(timestamp); return TimezoneConfig.formatDate(timestamp * 1000);
}, },
}, },
handleScroll: { handleScroll: {

View File

@ -9,6 +9,26 @@ let posSizeChart = null;
const STORAGE_KEY = 'ping_pong_settings'; 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() { function getSavedSettings() {
const saved = localStorage.getItem(STORAGE_KEY); const saved = localStorage.getItem(STORAGE_KEY);
if (!saved) return null; if (!saved) return null;
@ -22,6 +42,7 @@ function getSavedSettings() {
function saveSettings() { function saveSettings() {
const settings = { const settings = {
startDate: document.getElementById('simStartDate').value, startDate: document.getElementById('simStartDate').value,
stopDate: document.getElementById('simStopDate').value,
contractType: document.getElementById('simContractType').value, contractType: document.getElementById('simContractType').value,
direction: document.getElementById('simDirection').value, direction: document.getElementById('simDirection').value,
capital: document.getElementById('simCapital').value, capital: document.getElementById('simCapital').value,
@ -69,6 +90,18 @@ export function renderStrategyPanel() {
activeIndicators = window.getActiveIndicators?.() || []; activeIndicators = window.getActiveIndicators?.() || [];
const saved = getSavedSettings(); 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 = ` container.innerHTML = `
<div class="sidebar-section"> <div class="sidebar-section">
<div class="sidebar-section-header"> <div class="sidebar-section-header">
@ -77,7 +110,12 @@ export function renderStrategyPanel() {
<div class="sidebar-section-content"> <div class="sidebar-section-content">
<div class="sim-input-group"> <div class="sim-input-group">
<label>Start Date & Time</label> <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>
<div class="sim-input-group"> <div class="sim-input-group">
@ -167,11 +205,16 @@ function renderIndicatorChecklist(prefix) {
async function runSimulation() { async function runSimulation() {
const btn = document.getElementById('runSimulationBtn'); const btn = document.getElementById('runSimulationBtn');
btn.disabled = true; btn.disabled = true;
btn.textContent = 'Simulating...'; const originalBtnText = btn.textContent;
btn.textContent = 'Preparing Data...';
try { try {
const startVal = document.getElementById('simStartDate').value;
const stopVal = document.getElementById('simStopDate').value;
const config = { 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, contractType: document.getElementById('simContractType').value,
direction: document.getElementById('simDirection').value, direction: document.getElementById('simDirection').value,
capital: parseFloat(document.getElementById('simCapital').value), capital: parseFloat(document.getElementById('simCapital').value),
@ -187,17 +230,55 @@ async function runSimulation() {
alert('Please choose at least one indicator for opening positions.'); alert('Please choose at least one indicator for opening positions.');
return; return;
} }
const interval = window.dashboard?.currentInterval || '1d';
const candles = window.dashboard?.allData?.get(window.dashboard?.currentInterval); // 1. Ensure data is loaded for the range
if (!candles || candles.length === 0) { let allCandles = window.dashboard?.allData?.get(interval) || [];
alert('No candle data available.');
return; 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 btn.textContent = 'Simulating...';
const simCandles = candles.filter(c => c.time >= config.startDate);
// Filter candles by the exact range
const simCandles = allCandles.filter(c => c.time >= config.startDate && c.time <= config.stopDate);
if (simCandles.length === 0) { if (simCandles.length === 0) {
alert('No data available for the selected start date.'); alert('No data available for the selected range.');
return; return;
} }
@ -205,16 +286,18 @@ async function runSimulation() {
const indicatorSignals = {}; const indicatorSignals = {};
for (const indId of [...new Set([...config.openIndicators, ...config.closeIndicators])]) { for (const indId of [...new Set([...config.openIndicators, ...config.closeIndicators])]) {
const ind = activeIndicators.find(a => a.id === indId); const ind = activeIndicators.find(a => a.id === indId);
if (!ind) continue;
const signalFunc = getSignalFunction(ind.type); const signalFunc = getSignalFunction(ind.type);
const results = ind.cachedResults; const results = ind.cachedResults;
if (results && signalFunc) { if (results && signalFunc) {
indicatorSignals[indId] = simCandles.map(candle => { 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; if (idx < 1) return null;
const values = typeof results[idx] === 'object' && results[idx] !== null ? results[idx] : { ma: results[idx] }; 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] }; 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' }, layout: { background: { color: '#131722' }, textColor: '#d1d4dc' },
grid: { vertLines: { visible: false }, horzLines: { color: '#2a2e39' } }, grid: { vertLines: { visible: false }, horzLines: { color: '#2a2e39' } },
rightPriceScale: { borderColor: '#2a2e39', autoScale: true }, 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, handleScroll: true,
handleScale: true handleScale: true
}); });
@ -523,7 +619,20 @@ function displayResults(trades, equityData, config, endPrice, avgPriceData, posS
layout: { background: { color: '#131722' }, textColor: '#d1d4dc' }, layout: { background: { color: '#131722' }, textColor: '#d1d4dc' },
grid: { vertLines: { visible: false }, horzLines: { color: '#2a2e39' } }, grid: { vertLines: { visible: false }, horzLines: { color: '#2a2e39' } },
rightPriceScale: { borderColor: '#2a2e39', autoScale: true }, 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, handleScroll: true,
handleScale: true handleScale: true
}); });
@ -557,6 +666,27 @@ function displayResults(trades, equityData, config, endPrice, avgPriceData, posS
equityChart.timeScale().subscribeVisibleTimeRangeChange(() => syncCharts(equityChart, posSizeChart)); equityChart.timeScale().subscribeVisibleTimeRangeChange(() => syncCharts(equityChart, posSizeChart));
posSizeChart.timeScale().subscribeVisibleTimeRangeChange(() => syncCharts(posSizeChart, equityChart)); 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); setTimeout(initCharts, 100);