- 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.
869 lines
30 KiB
Python
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>
|
|
"""
|