Files
Market-Data-Downloader/ui_template_gaps.py
lewismac b391427023 feat: Add indicator coverage monitoring to gaps page
- Add "Indicator Coverage" tab alongside existing "Gap Detection" tab
- Display coverage summary with total/complete/incomplete combinations
- Show detailed coverage table with progress bars and status badges
- Add backfill buttons for individual symbol/interval pairs
- Add bulk backfill option for all incomplete indicators
- Include filter and search functionality for both tabs
- Show missing indicator counts and details per combination
- Real-time refresh capabilities for both gaps and indicators
- Maintain all existing gap detection functionality
- Provide visual progress bars showing coverage percentages
- Support batch operations with confirmation dialogs

This integrates indicator coverage monitoring into the existing gaps
interface, providing a unified data quality dashboard for monitoring
both OHLCV gaps and technical indicator completeness.
2025-10-09 09:07:40 +01:00

869 lines
30 KiB
Python

#!/usr/bin/env python3
"""
ui_template_gaps.py - Data Gap Monitoring Interface
Provides visual interface for tracking and filling data gaps and monitoring technical indicator coverage
"""
def get_gaps_monitoring_html():
"""Return the gaps monitoring page HTML with indicator coverage section"""
return """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gap Monitoring & Indicator Coverage - Crypto Data Collector</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
margin-bottom: 30px;
}
.header h1 {
color: #333;
font-size: 32px;
margin-bottom: 10px;
}
.header p {
color: #666;
font-size: 16px;
}
.nav-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
background: white;
padding: 15px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.nav-tab {
padding: 12px 24px;
background: #f0f0f0;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: all 0.3s;
}
.nav-tab:hover {
background: #e0e0e0;
}
.nav-tab.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.card {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
margin-bottom: 30px;
}
.card h2 {
color: #333;
font-size: 24px;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #667eea;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
}
.stat-card h3 {
font-size: 14px;
opacity: 0.9;
margin-bottom: 10px;
}
.stat-card .value {
font-size: 32px;
font-weight: bold;
}
.table-container {
overflow-x: auto;
margin-top: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
th {
background: #f8f9fa;
font-weight: 600;
color: #333;
position: sticky;
top: 0;
}
tr:hover {
background: #f8f9fa;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.status-excellent {
background: #d4edda;
color: #155724;
}
.status-good {
background: #d1ecf1;
color: #0c5460;
}
.status-warning {
background: #fff3cd;
color: #856404;
}
.status-critical {
background: #f8d7da;
color: #721c24;
}
.progress-bar {
width: 100%;
height: 24px;
background: #e0e0e0;
border-radius: 12px;
overflow: hidden;
position: relative;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
transition: width 0.3s;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
font-weight: bold;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s;
margin: 2px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:hover {
background: #218838;
}
.btn-warning {
background: #ffc107;
color: #333;
}
.btn-warning:hover {
background: #e0a800;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.loading-spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.alert {
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
.alert-info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-warning {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeeba;
}
.alert-danger {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.filter-bar {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.filter-bar input,
.filter-bar select {
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.filter-bar input {
flex: 1;
min-width: 200px;
}
.indicator-details {
font-size: 12px;
color: #666;
margin-top: 5px;
}
.missing-list {
max-height: 200px;
overflow-y: auto;
font-size: 12px;
color: #666;
margin-top: 5px;
padding: 10px;
background: #f8f9fa;
border-radius: 6px;
}
.back-link {
display: inline-block;
color: #667eea;
text-decoration: none;
margin-bottom: 20px;
font-weight: 500;
}
.back-link:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<a href="/" class="back-link">← Back to Dashboard</a>
<h1>📊 Data Quality Monitor</h1>
<p>Track and resolve data gaps and ensure 100% technical indicator coverage</p>
</div>
<div class="nav-tabs">
<button class="nav-tab active" onclick="switchTab('gaps')">Gap Detection</button>
<button class="nav-tab" onclick="switchTab('indicators')">Indicator Coverage</button>
</div>
<!-- GAPS TAB -->
<div id="gaps-tab" class="tab-content active">
<div class="card">
<h2>Trading Pairs Gap Status</h2>
<div id="gaps-loading" class="loading">
<div class="loading-spinner"></div>
<p>Loading gap data...</p>
</div>
<div id="gaps-content" style="display: none;">
<div class="filter-bar">
<input type="text" id="gap-search" placeholder="Search symbol or interval...">
<select id="gap-filter">
<option value="all">All Pairs</option>
<option value="has-gaps">With Gaps Only</option>
<option value="complete">Complete Only</option>
</select>
<button class="btn btn-primary" onclick="loadGapsData()">🔄 Refresh</button>
<button class="btn btn-success" onclick="fillAllGaps()">Fill All Gaps</button>
</div>
<div class="table-container">
<table id="gaps-table">
<thead>
<tr>
<th>Symbol</th>
<th>Interval</th>
<th>Coverage</th>
<th>Total Records</th>
<th>Missing Records</th>
<th>Gaps Count</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="gaps-tbody">
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- INDICATORS TAB -->
<div id="indicators-tab" class="tab-content">
<div class="card">
<h2>Technical Indicator Coverage Summary</h2>
<div id="indicator-summary-loading" class="loading">
<div class="loading-spinner"></div>
<p>Loading indicator coverage summary...</p>
</div>
<div id="indicator-summary-content" style="display: none;">
<div class="stats-grid">
<div class="stat-card">
<h3>Total Combinations</h3>
<div class="value" id="total-combinations">0</div>
</div>
<div class="stat-card">
<h3>Complete (≥99.9%)</h3>
<div class="value" id="complete-combinations">0</div>
</div>
<div class="stat-card">
<h3>Incomplete</h3>
<div class="value" id="incomplete-combinations">0</div>
</div>
<div class="stat-card">
<h3>Average Coverage</h3>
<div class="value" id="avg-coverage">0%</div>
</div>
</div>
</div>
</div>
<div class="card">
<h2>Indicator Coverage by Symbol/Interval</h2>
<div id="indicators-loading" class="loading">
<div class="loading-spinner"></div>
<p>Loading indicator coverage data...</p>
</div>
<div id="indicators-content" style="display: none;">
<div class="filter-bar">
<input type="text" id="indicator-search" placeholder="Search symbol or interval...">
<select id="indicator-filter">
<option value="all">All Pairs</option>
<option value="incomplete">Incomplete Only (<100%)</option>
<option value="complete">Complete Only (≥99.9%)</option>
</select>
<button class="btn btn-primary" onclick="loadIndicatorData()">🔄 Refresh</button>
<button class="btn btn-success" onclick="backfillAllIndicators()">Backfill All Incomplete</button>
</div>
<div class="table-container">
<table id="indicators-table">
<thead>
<tr>
<th>Symbol</th>
<th>Interval</th>
<th>Coverage</th>
<th>Total OHLCV</th>
<th>Complete Records</th>
<th>Incomplete Records</th>
<th>Missing Indicators</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="indicators-tbody">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
let gapsData = [];
let indicatorData = [];
// Tab switching
function switchTab(tab) {
// Update tab buttons
document.querySelectorAll('.nav-tab').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(tab + '-tab').classList.add('active');
// Load data if needed
if (tab === 'gaps' && gapsData.length === 0) {
loadGapsData();
} else if (tab === 'indicators' && indicatorData.length === 0) {
loadIndicatorData();
loadIndicatorSummary();
}
}
// Load gaps data
async function loadGapsData() {
const loading = document.getElementById('gaps-loading');
const content = document.getElementById('gaps-content');
loading.style.display = 'block';
content.style.display = 'none';
try {
const response = await fetch('/api/gaps/all-pairs');
const data = await response.json();
if (data.status === 'success') {
gapsData = data.data || [];
renderGapsTable();
content.style.display = 'block';
} else {
alert('Error loading gaps data: ' + data.message);
}
} catch (error) {
console.error('Error loading gaps:', error);
alert('Failed to load gaps data');
} finally {
loading.style.display = 'none';
}
}
// Render gaps table
function renderGapsTable() {
const tbody = document.getElementById('gaps-tbody');
const search = document.getElementById('gap-search').value.toLowerCase();
const filter = document.getElementById('gap-filter').value;
let filtered = gapsData.filter(item => {
const matchSearch = item.symbol.toLowerCase().includes(search) ||
item.interval.toLowerCase().includes(search);
let matchFilter = true;
if (filter === 'has-gaps') {
matchFilter = item.gaps_count > 0;
} else if (filter === 'complete') {
matchFilter = item.gaps_count === 0;
}
return matchSearch && matchFilter;
});
tbody.innerHTML = filtered.map(item => {
const coverage = item.coverage_percent || 0;
const statusClass = coverage >= 99 ? 'status-excellent' :
coverage >= 95 ? 'status-good' :
coverage >= 80 ? 'status-warning' : 'status-critical';
return `
<tr>
<td><strong>${item.symbol}</strong></td>
<td>${item.interval}</td>
<td>
<div class="progress-bar">
<div class="progress-fill" style="width: ${coverage}%">
${coverage.toFixed(2)}%
</div>
</div>
</td>
<td>${item.total_records.toLocaleString()}</td>
<td>${item.missing_records.toLocaleString()}</td>
<td>${item.gaps_count}</td>
<td><span class="status-badge ${statusClass}">
${coverage >= 99 ? 'Excellent' : coverage >= 95 ? 'Good' : coverage >= 80 ? 'Fair' : 'Critical'}
</span></td>
<td>
${item.gaps_count > 0 ?
`<button class="btn btn-warning" onclick="fillGap('${item.symbol}', '${item.interval}')">Fill Gaps</button>` :
`<button class="btn btn-secondary" disabled>No Gaps</button>`
}
</td>
</tr>
`;
}).join('');
}
// Load indicator coverage data
async function loadIndicatorData() {
const loading = document.getElementById('indicators-loading');
const content = document.getElementById('indicators-content');
loading.style.display = 'block';
content.style.display = 'none';
try {
const response = await fetch('/api/indicators/coverage/all');
const data = await response.json();
if (data.status === 'success') {
indicatorData = data.data || [];
renderIndicatorTable();
content.style.display = 'block';
} else {
alert('Error loading indicator data: ' + data.message);
}
} catch (error) {
console.error('Error loading indicators:', error);
alert('Failed to load indicator coverage data');
} finally {
loading.style.display = 'none';
}
}
// Load indicator summary
async function loadIndicatorSummary() {
const loading = document.getElementById('indicator-summary-loading');
const content = document.getElementById('indicator-summary-content');
loading.style.display = 'block';
try {
const response = await fetch('/api/indicators/summary');
const data = await response.json();
if (data.status === 'success') {
const summary = data.data;
document.getElementById('total-combinations').textContent = summary.total_combinations;
document.getElementById('complete-combinations').textContent = summary.complete_combinations;
document.getElementById('incomplete-combinations').textContent = summary.incomplete_combinations;
document.getElementById('avg-coverage').textContent = summary.average_coverage.toFixed(2) + '%';
content.style.display = 'block';
}
} catch (error) {
console.error('Error loading summary:', error);
} finally {
loading.style.display = 'none';
}
}
// Render indicator table
function renderIndicatorTable() {
const tbody = document.getElementById('indicators-tbody');
const search = document.getElementById('indicator-search').value.toLowerCase();
const filter = document.getElementById('indicator-filter').value;
let filtered = indicatorData.filter(item => {
const matchSearch = item.symbol.toLowerCase().includes(search) ||
item.interval.toLowerCase().includes(search);
let matchFilter = true;
if (filter === 'incomplete') {
matchFilter = item.coverage_percent < 100;
} else if (filter === 'complete') {
matchFilter = item.coverage_percent >= 99.9;
}
return matchSearch && matchFilter;
});
tbody.innerHTML = filtered.map(item => {
const coverage = item.coverage_percent || 0;
const statusClass = coverage >= 99.9 ? 'status-excellent' :
coverage >= 90 ? 'status-good' :
coverage >= 50 ? 'status-warning' : 'status-critical';
const missingIndicators = item.missing_indicators || [];
const missingDetails = missingIndicators.length > 0 ?
`<div class="indicator-details">${missingIndicators.length} indicators incomplete</div>` : '';
return `
<tr>
<td><strong>${item.symbol}</strong></td>
<td>${item.interval}</td>
<td>
<div class="progress-bar">
<div class="progress-fill" style="width: ${coverage}%">
${coverage.toFixed(2)}%
</div>
</div>
</td>
<td>${item.total_ohlcv.toLocaleString()}</td>
<td>${item.complete_records.toLocaleString()}</td>
<td>${item.incomplete_records.toLocaleString()}</td>
<td>
${missingIndicators.length > 0 ?
`<span class="status-badge status-warning">${missingIndicators.length}</span>` :
`<span class="status-badge status-excellent">Complete</span>`
}
${missingDetails}
</td>
<td>
${coverage < 99.9 ?
`<button class="btn btn-success" onclick="backfillIndicator('${item.symbol}', '${item.interval}')">Backfill</button>` :
`<button class="btn btn-secondary" disabled>Complete</button>`
}
</td>
</tr>
`;
}).join('');
}
// Fill gap for specific symbol/interval
async function fillGap(symbol, interval) {
if (!confirm(`Fill gaps for ${symbol} ${interval}?`)) return;
try {
const response = await fetch('/api/gaps/auto-fill', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
symbol: symbol,
intervals: [interval],
fill_genuine_gaps: true
})
});
const data = await response.json();
if (data.status === 'success') {
alert(`Gap fill completed for ${symbol} ${interval}`);
loadGapsData();
} else {
alert('Error filling gaps: ' + data.message);
}
} catch (error) {
console.error('Error:', error);
alert('Failed to fill gaps');
}
}
// Fill all gaps
async function fillAllGaps() {
if (!confirm('Fill all gaps? This may take a while.')) return;
const pairsWithGaps = gapsData.filter(item => item.gaps_count > 0);
if (pairsWithGaps.length === 0) {
alert('No gaps to fill!');
return;
}
alert(`Starting gap fill for ${pairsWithGaps.length} pairs...`);
for (const item of pairsWithGaps) {
try {
await fetch('/api/gaps/auto-fill', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
symbol: item.symbol,
intervals: [item.interval],
fill_genuine_gaps: true
})
});
} catch (error) {
console.error(`Error filling ${item.symbol}:`, error);
}
}
alert('Gap fill process completed!');
loadGapsData();
}
// Backfill indicator for specific symbol/interval
async function backfillIndicator(symbol, interval) {
if (!confirm(`Backfill indicators for ${symbol} ${interval}?`)) return;
const btn = event.target;
btn.disabled = true;
btn.textContent = 'Processing...';
try {
const response = await fetch(`/api/indicators/backfill/${symbol}/${interval}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ batch_size: 200 })
});
const data = await response.json();
if (data.status === 'success') {
const result = data.data;
alert(`Backfill completed!\nCoverage: ${result.coverage_before}% → ${result.coverage_after}%\nIndicators added: ${result.indicators_added}`);
loadIndicatorData();
loadIndicatorSummary();
} else {
alert('Error backfilling: ' + data.message);
}
} catch (error) {
console.error('Error:', error);
alert('Failed to backfill indicators');
} finally {
btn.disabled = false;
btn.textContent = 'Backfill';
}
}
// Backfill all incomplete indicators
async function backfillAllIndicators() {
if (!confirm('Backfill all incomplete indicators? This may take a while.')) return;
try {
const response = await fetch('/api/indicators/backfill-all', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
batch_size: 200,
min_coverage_threshold: 99.9
})
});
const data = await response.json();
if (data.status === 'success') {
const summary = data.data;
alert(`Backfill completed!\nProcessed: ${summary.processed}\nSuccessful: ${summary.successful}\nFailed: ${summary.failed}`);
loadIndicatorData();
loadIndicatorSummary();
} else {
alert('Error: ' + data.message);
}
} catch (error) {
console.error('Error:', error);
alert('Failed to backfill indicators');
}
}
// Search and filter handlers
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('gap-search')?.addEventListener('input', renderGapsTable);
document.getElementById('gap-filter')?.addEventListener('change', renderGapsTable);
document.getElementById('indicator-search')?.addEventListener('input', renderIndicatorTable);
document.getElementById('indicator-filter')?.addEventListener('change', renderIndicatorTable);
// Load initial data
loadGapsData();
});
</script>
</body>
</html>
"""