feat: implement strategy metadata and dashboard simulation panel
- Added display_name and description to BaseStrategy - Updated MA44 and MA125 strategies with metadata - Added /api/v1/strategies endpoint for dynamic discovery - Added Strategy Simulation panel to dashboard with date picker and tooltips - Implemented JS polling for backtest results in dashboard - Added performance test scripts and DB connection guide - Expanded indicator config to all 15 timeframes
This commit is contained in:
@ -396,13 +396,173 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
/* Simulation Panel Styles */
|
||||
.sim-strategies {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.sim-strategy-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.sim-strategy-option:hover {
|
||||
background: var(--tv-hover);
|
||||
}
|
||||
|
||||
.sim-strategy-option input[type="radio"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sim-strategy-option label {
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.sim-strategy-info {
|
||||
color: var(--tv-text-secondary);
|
||||
cursor: help;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.sim-strategy-info:hover {
|
||||
background: var(--tv-hover);
|
||||
color: var(--tv-text);
|
||||
}
|
||||
|
||||
/* Tooltip */
|
||||
.sim-strategy-info:hover::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
right: 0;
|
||||
background: var(--tv-panel-bg);
|
||||
border: 1px solid var(--tv-border);
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
width: 250px;
|
||||
z-index: 100;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
color: var(--tv-text);
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sim-input-group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sim-input-group label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--tv-text-secondary);
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sim-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: var(--tv-bg);
|
||||
border: 1px solid var(--tv-border);
|
||||
border-radius: 4px;
|
||||
color: var(--tv-text);
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.sim-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--tv-blue);
|
||||
}
|
||||
|
||||
.sim-run-btn {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: var(--tv-blue);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: opacity 0.2s;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.sim-run-btn:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.sim-run-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.sim-results {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--tv-border);
|
||||
}
|
||||
|
||||
.sim-stat-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 6px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.sim-stat-row span:first-child {
|
||||
color: var(--tv-text-secondary);
|
||||
}
|
||||
|
||||
.sim-value {
|
||||
font-weight: 600;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.sim-value.positive { color: var(--tv-green); }
|
||||
.sim-value.negative { color: var(--tv-red); }
|
||||
|
||||
.loading-strategies {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
color: var(--tv-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
.ta-content {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.ta-content {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.ta-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@ -834,18 +994,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ta-section">
|
||||
<div class="ta-section-title">Price Info</div>
|
||||
<div class="ta-level">
|
||||
<span class="ta-level-label">Current</span>
|
||||
<span class="ta-level-value">$${data.current_price.toLocaleString()}</span>
|
||||
<div class="ta-section" id="simulationPanel">
|
||||
<div class="ta-section-title">Strategy Simulation</div>
|
||||
|
||||
<!-- Date picker -->
|
||||
<div class="sim-input-group" style="margin: 0 0 8px 0;">
|
||||
<label style="font-size: 10px; text-transform: uppercase; color: var(--tv-text-secondary);">Start Date:</label>
|
||||
<input type="datetime-local" id="simStartDate" class="sim-input" style="margin-top: 2px;">
|
||||
</div>
|
||||
<div style="font-size: 12px; color: var(--tv-text-secondary); margin-top: 8px;">
|
||||
Based on last 200 candles<br>
|
||||
Strategy: Trend following with MA crossovers
|
||||
|
||||
<!-- Strategies loaded dynamically here -->
|
||||
<div id="strategyList" class="sim-strategies" style="max-height: 100px; overflow-y: auto;">
|
||||
<div class="loading-strategies">Loading strategies...</div>
|
||||
</div>
|
||||
|
||||
<button class="sim-run-btn" onclick="runSimulation()" id="runSimBtn" disabled style="padding: 6px; font-size: 12px; margin-top: 6px;">
|
||||
Run Simulation
|
||||
</button>
|
||||
|
||||
<!-- Results -->
|
||||
<div id="simResults" class="sim-results" style="display: none; margin-top: 8px; padding-top: 8px;">
|
||||
<div class="sim-stat-row" style="padding: 2px 0; font-size: 11px;">
|
||||
<span>Trades:</span>
|
||||
<span id="simTrades" class="sim-value">--</span>
|
||||
</div>
|
||||
<div class="sim-stat-row" style="padding: 2px 0; font-size: 11px;">
|
||||
<span>Win Rate:</span>
|
||||
<span id="simWinRate" class="sim-value">--</span>
|
||||
</div>
|
||||
<div class="sim-stat-row" style="padding: 2px 0; font-size: 11px;">
|
||||
<span>Total P&L:</span>
|
||||
<span id="simPnL" class="sim-value">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Load strategies after simulation panel is rendered
|
||||
setTimeout(() => {
|
||||
loadStrategies();
|
||||
setDefaultStartDate();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
updateStats(candle) {
|
||||
@ -891,6 +1080,226 @@
|
||||
window.open(geminiUrl, '_blank');
|
||||
}
|
||||
|
||||
// Load strategies on page load
|
||||
async function loadStrategies() {
|
||||
try {
|
||||
console.log('Fetching strategies from API...');
|
||||
|
||||
// Add timeout to fetch
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
|
||||
|
||||
const response = await fetch('/api/v1/strategies', {
|
||||
signal: controller.signal
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Strategies loaded:', data);
|
||||
|
||||
if (!data.strategies) {
|
||||
throw new Error('Invalid response format: missing strategies array');
|
||||
}
|
||||
|
||||
renderStrategies(data.strategies);
|
||||
} catch (error) {
|
||||
console.error('Error loading strategies:', error);
|
||||
|
||||
let errorMessage = error.message;
|
||||
if (error.name === 'AbortError') {
|
||||
errorMessage = 'Request timeout - API server not responding';
|
||||
} else if (error.message.includes('Failed to fetch')) {
|
||||
errorMessage = 'Cannot connect to API server - is it running?';
|
||||
}
|
||||
|
||||
document.getElementById('strategyList').innerHTML =
|
||||
`<div class="loading-strategies" style="color: var(--tv-red);">
|
||||
${errorMessage}<br>
|
||||
<small>Check console (F12) for details</small>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Render strategy list
|
||||
function renderStrategies(strategies) {
|
||||
const container = document.getElementById('strategyList');
|
||||
|
||||
if (!strategies || strategies.length === 0) {
|
||||
container.innerHTML = '<div class="loading-strategies">No strategies available</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = strategies.map((s, index) => `
|
||||
<div class="sim-strategy-option">
|
||||
<input type="radio" name="strategy" id="strat_${s.id}"
|
||||
value="${s.id}" ${index === 0 ? 'checked' : ''}>
|
||||
<label for="strat_${s.id}">${s.name}</label>
|
||||
<span class="sim-strategy-info" data-tooltip="${s.description}">ⓘ</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Enable run button
|
||||
document.getElementById('runSimBtn').disabled = false;
|
||||
}
|
||||
|
||||
// Run simulation
|
||||
async function runSimulation() {
|
||||
const selectedStrategy = document.querySelector('input[name="strategy"]:checked');
|
||||
|
||||
if (!selectedStrategy) {
|
||||
alert('Please select a strategy');
|
||||
return;
|
||||
}
|
||||
|
||||
const strategyId = selectedStrategy.value;
|
||||
const startDateInput = document.getElementById('simStartDate').value;
|
||||
|
||||
if (!startDateInput) {
|
||||
alert('Please select a start date');
|
||||
return;
|
||||
}
|
||||
|
||||
// Format date for API
|
||||
const startDate = new Date(startDateInput).toISOString().split('T')[0];
|
||||
|
||||
// Disable button during simulation
|
||||
const runBtn = document.getElementById('runSimBtn');
|
||||
runBtn.disabled = true;
|
||||
runBtn.textContent = 'Running...';
|
||||
|
||||
try {
|
||||
// Trigger backtest via API
|
||||
const response = await fetch('/api/v1/backtests', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
symbol: 'BTC',
|
||||
intervals: [window.dashboard?.currentInterval || '1d'],
|
||||
start_date: startDate
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.message) {
|
||||
// Show that simulation is running
|
||||
runBtn.textContent = 'Running...';
|
||||
|
||||
// Poll for results
|
||||
setTimeout(() => {
|
||||
pollForBacktestResults(strategyId, startDate);
|
||||
}, 2000); // Wait 2 seconds then poll
|
||||
} else {
|
||||
alert('Failed to start simulation');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error running simulation:', error);
|
||||
alert('Error running simulation: ' + error.message);
|
||||
// Reset button only on error
|
||||
runBtn.disabled = false;
|
||||
runBtn.textContent = 'Run Simulation';
|
||||
}
|
||||
// Button stays as "Running..." until polling completes or times out
|
||||
}
|
||||
|
||||
// Poll for backtest results
|
||||
async function pollForBacktestResults(strategyId, startDate, attempts = 0) {
|
||||
const runBtn = document.getElementById('runSimBtn');
|
||||
|
||||
if (attempts > 30) { // Stop after 30 attempts (60 seconds)
|
||||
console.log('Backtest polling timeout');
|
||||
runBtn.textContent = 'Run Simulation';
|
||||
runBtn.disabled = false;
|
||||
|
||||
// Show timeout message in results area
|
||||
const simResults = document.getElementById('simResults');
|
||||
if (simResults) {
|
||||
simResults.innerHTML = `
|
||||
<div class="sim-stat-row" style="color: var(--tv-text-secondary); font-size: 11px; text-align: center;">
|
||||
<span>Simulation timeout - no results found after 60s.<br>Check server logs or try again.</span>
|
||||
</div>
|
||||
`;
|
||||
simResults.style.display = 'block';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/backtests?limit=5');
|
||||
const backtests = await response.json();
|
||||
|
||||
// Find the most recent backtest that matches our criteria
|
||||
const recentBacktest = backtests.find(bt =>
|
||||
bt.strategy && bt.strategy.includes(strategyId) ||
|
||||
bt.created_at > new Date(Date.now() - 60000).toISOString() // Created in last minute
|
||||
);
|
||||
|
||||
if (recentBacktest && recentBacktest.results) {
|
||||
// Parse JSON string if needed (database stores results as text)
|
||||
const parsedBacktest = {
|
||||
...recentBacktest,
|
||||
results: typeof recentBacktest.results === 'string'
|
||||
? JSON.parse(recentBacktest.results)
|
||||
: recentBacktest.results
|
||||
};
|
||||
// Results found! Display them
|
||||
displayBacktestResults(parsedBacktest);
|
||||
runBtn.textContent = 'Run Simulation';
|
||||
runBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// No results yet, poll again in 2 seconds
|
||||
setTimeout(() => {
|
||||
pollForBacktestResults(strategyId, startDate, attempts + 1);
|
||||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error polling for backtest results:', error);
|
||||
runBtn.textContent = 'Run Simulation';
|
||||
runBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Display backtest results in the UI
|
||||
function displayBacktestResults(backtest) {
|
||||
// Parse JSON string if needed (database stores results as text)
|
||||
const results = typeof backtest.results === 'string'
|
||||
? JSON.parse(backtest.results)
|
||||
: backtest.results;
|
||||
|
||||
// Update the results display
|
||||
document.getElementById('simTrades').textContent = results.total_trades || '--';
|
||||
document.getElementById('simWinRate').textContent = results.win_rate ? results.win_rate.toFixed(1) + '%' : '--';
|
||||
|
||||
const pnlElement = document.getElementById('simPnL');
|
||||
const pnl = results.total_pnl || 0;
|
||||
pnlElement.textContent = (pnl >= 0 ? '+' : '') + '$' + pnl.toFixed(2);
|
||||
pnlElement.className = 'sim-value ' + (pnl >= 0 ? 'positive' : 'negative');
|
||||
|
||||
// Show results section
|
||||
document.getElementById('simResults').style.display = 'block';
|
||||
|
||||
console.log('Backtest results:', backtest);
|
||||
}
|
||||
|
||||
// Set default start date (7 days ago)
|
||||
function setDefaultStartDate() {
|
||||
const startDateInput = document.getElementById('simStartDate');
|
||||
if (startDateInput) {
|
||||
const sevenDaysAgo = new Date();
|
||||
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
||||
// Format as datetime-local: YYYY-MM-DDTHH:mm
|
||||
startDateInput.value = sevenDaysAgo.toISOString().slice(0, 16);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.dashboard = new TradingDashboard();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user