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:
BTC Bot
2026-02-13 09:50:08 +01:00
parent 38f0a21f56
commit d7bdfcf716
23 changed files with 3623 additions and 241 deletions

View File

@ -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();
});