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 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')}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -123,7 +123,7 @@ constructor() {
|
|||||||
},
|
},
|
||||||
localization: {
|
localization: {
|
||||||
timeFormatter: (timestamp) => {
|
timeFormatter: (timestamp) => {
|
||||||
return TimezoneConfig.formatTickMark(timestamp);
|
return TimezoneConfig.formatDate(timestamp * 1000);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
handleScroll: {
|
handleScroll: {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user