| <!DOCTYPE html> |
| <html lang="en" data-theme="light"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>OHLCV Data Sources - Crypto Hub</title> |
| |
| <link rel="stylesheet" href="/static/shared/css/theme-modern.css"> |
| <link rel="stylesheet" href="/static/shared/css/sidebar-modern.css"> |
| |
| <style> |
| body { |
| margin: 0; |
| background: var(--bg-secondary); |
| } |
| |
| .app-layout { |
| display: flex; |
| min-height: 100vh; |
| } |
| |
| .main-content { |
| flex: 1; |
| margin-left: var(--sidebar-width); |
| padding: var(--space-6); |
| transition: margin-left var(--transition-base); |
| } |
| |
| .sidebar-modern.collapsed ~ .main-content { |
| margin-left: var(--sidebar-collapsed-width); |
| } |
| |
| .page-header { |
| margin-bottom: var(--space-8); |
| padding-bottom: var(--space-6); |
| border-bottom: 2px solid var(--border-primary); |
| } |
| |
| h1 { |
| font-size: var(--text-4xl); |
| background: var(--accent-gradient); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| margin-bottom: var(--space-3); |
| } |
| |
| .subtitle { |
| color: var(--text-tertiary); |
| font-size: var(--text-lg); |
| } |
| |
| .controls { |
| display: flex; |
| gap: var(--space-4); |
| margin-bottom: var(--space-6); |
| flex-wrap: wrap; |
| } |
| |
| .control-group { |
| display: flex; |
| flex-direction: column; |
| gap: var(--space-2); |
| } |
| |
| label { |
| font-size: var(--text-sm); |
| font-weight: var(--font-semibold); |
| color: var(--text-secondary); |
| } |
| |
| select, input { |
| padding: var(--space-3) var(--space-4); |
| border: 1px solid var(--border-primary); |
| border-radius: var(--radius-lg); |
| font-size: var(--text-base); |
| background: var(--surface-primary); |
| color: var(--text-primary); |
| transition: all var(--transition-fast); |
| } |
| |
| select:focus, input:focus { |
| outline: none; |
| border-color: var(--accent-primary); |
| box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.1); |
| } |
| |
| .btn { |
| padding: var(--space-3) var(--space-6); |
| border-radius: var(--radius-lg); |
| font-weight: var(--font-semibold); |
| font-size: var(--text-base); |
| cursor: pointer; |
| transition: all var(--transition-base); |
| border: none; |
| display: inline-flex; |
| align-items: center; |
| gap: var(--space-2); |
| } |
| |
| .btn-primary { |
| background: var(--accent-gradient); |
| color: white; |
| box-shadow: var(--shadow-md); |
| } |
| |
| .btn-primary:hover { |
| transform: translateY(-2px); |
| box-shadow: var(--shadow-lg); |
| } |
| |
| .btn-secondary { |
| background: var(--surface-secondary); |
| color: var(--text-primary); |
| border: 1px solid var(--border-primary); |
| } |
| |
| .btn-secondary:hover { |
| background: var(--surface-hover); |
| } |
| |
| .stats-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
| gap: var(--space-4); |
| margin-bottom: var(--space-8); |
| } |
| |
| .stat-card { |
| background: var(--surface-primary); |
| padding: var(--space-5); |
| border-radius: var(--radius-xl); |
| border: 1px solid var(--border-primary); |
| box-shadow: var(--shadow-sm); |
| } |
| |
| .stat-value { |
| font-size: var(--text-3xl); |
| font-weight: var(--font-extrabold); |
| color: var(--accent-primary); |
| margin-bottom: var(--space-2); |
| } |
| |
| .stat-label { |
| font-size: var(--text-sm); |
| color: var(--text-tertiary); |
| font-weight: var(--font-medium); |
| } |
| |
| .results { |
| background: var(--surface-primary); |
| border-radius: var(--radius-xl); |
| padding: var(--space-6); |
| border: 1px solid var(--border-primary); |
| box-shadow: var(--shadow-md); |
| margin-bottom: var(--space-6); |
| } |
| |
| .results h2 { |
| margin-bottom: var(--space-4); |
| display: flex; |
| align-items: center; |
| gap: var(--space-3); |
| } |
| |
| .source-list { |
| display: flex; |
| flex-direction: column; |
| gap: var(--space-3); |
| } |
| |
| .source-item { |
| display: flex; |
| align-items: center; |
| gap: var(--space-4); |
| padding: var(--space-4); |
| background: var(--bg-secondary); |
| border-radius: var(--radius-lg); |
| border: 1px solid var(--border-primary); |
| } |
| |
| .source-priority { |
| width: 40px; |
| height: 40px; |
| border-radius: var(--radius-md); |
| background: var(--accent-gradient); |
| color: white; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-weight: var(--font-bold); |
| font-size: var(--text-lg); |
| } |
| |
| .source-info { |
| flex: 1; |
| } |
| |
| .source-name { |
| font-weight: var(--font-semibold); |
| color: var(--text-primary); |
| margin-bottom: 2px; |
| } |
| |
| .source-details { |
| font-size: var(--text-sm); |
| color: var(--text-tertiary); |
| } |
| |
| .source-status { |
| padding: var(--space-2) var(--space-4); |
| border-radius: var(--radius-full); |
| font-size: var(--text-sm); |
| font-weight: var(--font-bold); |
| } |
| |
| .status-success { |
| background: rgba(16, 185, 129, 0.1); |
| color: var(--color-success); |
| } |
| |
| .status-failed { |
| background: rgba(239, 68, 68, 0.1); |
| color: var(--color-danger); |
| } |
| |
| .status-pending { |
| background: rgba(245, 158, 11, 0.1); |
| color: var(--color-warning); |
| } |
| |
| .chart-container { |
| height: 400px; |
| margin-top: var(--space-6); |
| background: var(--surface-primary); |
| border-radius: var(--radius-lg); |
| padding: var(--space-4); |
| } |
| |
| .data-table { |
| width: 100%; |
| border-collapse: collapse; |
| margin-top: var(--space-4); |
| } |
| |
| .data-table th, |
| .data-table td { |
| padding: var(--space-3); |
| text-align: left; |
| border-bottom: 1px solid var(--border-primary); |
| } |
| |
| .data-table th { |
| background: var(--bg-secondary); |
| font-weight: var(--font-semibold); |
| color: var(--text-secondary); |
| font-size: var(--text-sm); |
| text-transform: uppercase; |
| letter-spacing: 0.5px; |
| } |
| |
| .data-table td { |
| font-size: var(--text-sm); |
| color: var(--text-primary); |
| } |
| |
| .data-table tr:hover { |
| background: var(--surface-hover); |
| } |
| |
| .loading { |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| padding: var(--space-8); |
| gap: var(--space-3); |
| color: var(--text-tertiary); |
| } |
| |
| .spinner { |
| width: 24px; |
| height: 24px; |
| border: 3px solid var(--border-primary); |
| border-top-color: var(--accent-primary); |
| border-radius: 50%; |
| animation: spin 1s linear infinite; |
| } |
| |
| @keyframes spin { |
| to { transform: rotate(360deg); } |
| } |
| |
| @media (max-width: 1024px) { |
| .main-content { |
| margin-left: 0; |
| } |
| } |
| |
| @media (max-width: 768px) { |
| .controls { |
| flex-direction: column; |
| } |
| |
| .stats-grid { |
| grid-template-columns: 1fr; |
| } |
| } |
| </style> |
| |
| <script src="/static/js/api-config.js"></script> |
| <script> |
| |
| window.apiReady = new Promise((resolve) => { |
| if (window.apiClient) { |
| console.log('✅ API Client ready'); |
| resolve(window.apiClient); |
| } else { |
| console.error('❌ API Client not loaded'); |
| } |
| }); |
| </script> |
|
|
| </head> |
| <body> |
| <div class="app-layout"> |
| |
| <div id="sidebar-container"></div> |
|
|
| |
| <main class="main-content"> |
| <div class="page-header"> |
| <h1>OHLCV Data Sources</h1> |
| <p class="subtitle">Comprehensive candlestick data from 15+ integrated sources with automatic fallback</p> |
| </div> |
|
|
| |
| <div class="controls"> |
| <div class="control-group"> |
| <label for="symbol-select">Symbol</label> |
| <select id="symbol-select"> |
| <option value="bitcoin">Bitcoin (BTC)</option> |
| <option value="ethereum">Ethereum (ETH)</option> |
| <option value="cardano">Cardano (ADA)</option> |
| <option value="solana">Solana (SOL)</option> |
| <option value="ripple">XRP (XRP)</option> |
| <option value="polkadot">Polkadot (DOT)</option> |
| </select> |
| </div> |
|
|
| <div class="control-group"> |
| <label for="timeframe-select">Timeframe</label> |
| <select id="timeframe-select"> |
| <option value="1m">1 Minute</option> |
| <option value="5m">5 Minutes</option> |
| <option value="15m">15 Minutes</option> |
| <option value="30m">30 Minutes</option> |
| <option value="1h">1 Hour</option> |
| <option value="4h">4 Hours</option> |
| <option value="1d" selected>1 Day</option> |
| <option value="1w">1 Week</option> |
| <option value="1M">1 Month</option> |
| </select> |
| </div> |
|
|
| <div class="control-group"> |
| <label for="limit-input">Candles</label> |
| <input type="number" id="limit-input" value="100" min="10" max="1000" step="10"> |
| </div> |
|
|
| <div class="control-group"> |
| <label> </label> |
| <button class="btn btn-primary" id="fetch-btn"> |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M3 3v18h18"/> |
| <path d="M18 7l-5 5-4-4-5 5"/> |
| </svg> |
| Fetch OHLCV Data |
| </button> |
| </div> |
|
|
| <div class="control-group"> |
| <label> </label> |
| <button class="btn btn-secondary" id="test-all-btn"> |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M9 11l3 3L22 4"/> |
| <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/> |
| </svg> |
| Test All Sources |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="stats-grid" id="stats-grid"> |
| <div class="stat-card"> |
| <div class="stat-value" id="total-sources">12</div> |
| <div class="stat-label">Total OHLCV Sources</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" id="success-rate">--</div> |
| <div class="stat-label">Success Rate</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" id="candles-loaded">0</div> |
| <div class="stat-label">Candles Loaded</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" id="cache-size">0</div> |
| <div class="stat-label">Cached Queries</div> |
| </div> |
| </div> |
|
|
| |
| <div class="results"> |
| <h2> |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M4 11a9 9 0 0 1 9 9"/> |
| <path d="M4 4a16 16 0 0 1 16 16"/> |
| <circle cx="5" cy="19" r="2"/> |
| </svg> |
| Available OHLCV Sources |
| </h2> |
| <div class="source-list" id="source-list"> |
| <div class="loading"> |
| <div class="spinner"></div> |
| <span>Loading sources...</span> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="results" id="test-results" style="display: none;"> |
| <h2> |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M9 11l3 3L22 4"/> |
| <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/> |
| </svg> |
| Test Results |
| </h2> |
| <div id="test-results-content"></div> |
| </div> |
|
|
| |
| <div class="results" id="data-preview" style="display: none;"> |
| <h2> |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <rect x="3" y="3" width="18" height="18" rx="2"/> |
| <path d="M3 9h18"/> |
| <path d="M9 21V9"/> |
| </svg> |
| OHLCV Data Preview |
| <span style="margin-left: auto; font-size: var(--text-sm); font-weight: normal; color: var(--text-tertiary);" id="data-source">Source: --</span> |
| </h2> |
| <div style="overflow-x: auto;"> |
| <table class="data-table" id="data-table"> |
| <thead> |
| <tr> |
| <th>Date/Time</th> |
| <th>Open</th> |
| <th>High</th> |
| <th>Low</th> |
| <th>Close</th> |
| <th>Volume</th> |
| </tr> |
| </thead> |
| <tbody id="data-table-body"> |
| <tr> |
| <td colspan="6" style="text-align: center; padding: var(--space-8);">No data loaded</td> |
| </tr> |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </main> |
| </div> |
|
|
| <script type="module"> |
| import ohlcvClient from '/static/shared/js/ohlcv-client.js'; |
| import sidebarManager from '/static/shared/js/sidebar-manager.js'; |
| |
| |
| fetch('/static/shared/layouts/sidebar-modern.html') |
| .then(r => r.text()) |
| .then(html => { |
| document.getElementById('sidebar-container').innerHTML = html; |
| }); |
| |
| |
| function displaySources() { |
| const sources = ohlcvClient.listSources(); |
| document.getElementById('total-sources').textContent = sources.length; |
| |
| const listHtml = sources.map(source => ` |
| <div class="source-item"> |
| <div class="source-priority">${source.priority}</div> |
| <div class="source-info"> |
| <div class="source-name">${source.name}</div> |
| <div class="source-details"> |
| Max: ${source.maxLimit.toLocaleString()} candles |
| ${source.needsAuth ? '• API Key Required' : '• No Auth'} |
| ${source.needsProxy ? '• Needs Proxy' : '• Direct'} |
| </div> |
| </div> |
| <div class="source-status status-pending">Ready</div> |
| </div> |
| `).join(''); |
| |
| document.getElementById('source-list').innerHTML = listHtml; |
| } |
| |
| |
| async function fetchOHLCV() { |
| const symbol = document.getElementById('symbol-select').value; |
| const timeframe = document.getElementById('timeframe-select').value; |
| const limit = parseInt(document.getElementById('limit-input').value); |
| |
| const btn = document.getElementById('fetch-btn'); |
| btn.disabled = true; |
| btn.innerHTML = '<div class="spinner"></div> Fetching...'; |
| |
| try { |
| console.log(`📊 Fetching ${symbol} ${timeframe} (${limit} candles)...`); |
| const data = await ohlcvClient.getOHLCV(symbol, timeframe, limit); |
| |
| console.log(`✅ Loaded ${data.length} candles`); |
| displayData(data, symbol, timeframe); |
| updateStats(); |
| |
| btn.disabled = false; |
| btn.innerHTML = ` |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M3 3v18h18"/> |
| <path d="M18 7l-5 5-4-4-5 5"/> |
| </svg> |
| Fetch OHLCV Data |
| `; |
| } catch (error) { |
| console.error('❌ Failed to fetch OHLCV:', error); |
| alert(`Failed to fetch OHLCV data: ${error.message}`); |
| |
| btn.disabled = false; |
| btn.innerHTML = ` |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M3 3v18h18"/> |
| <path d="M18 7l-5 5-4-4-5 5"/> |
| </svg> |
| Fetch OHLCV Data |
| `; |
| } |
| } |
| |
| |
| function displayData(data, symbol, timeframe) { |
| document.getElementById('data-preview').style.display = 'block'; |
| document.getElementById('candles-loaded').textContent = data.length.toLocaleString(); |
| |
| |
| const stats = ohlcvClient.getStats(); |
| const lastSuccess = stats.recentRequests.reverse().find(r => r.success); |
| if (lastSuccess) { |
| document.getElementById('data-source').textContent = `Source: ${lastSuccess.source}`; |
| } |
| |
| |
| const tableBody = document.getElementById('data-table-body'); |
| const displayData = data.slice(-20).reverse(); |
| |
| tableBody.innerHTML = displayData.map(candle => ` |
| <tr> |
| <td>${new Date(candle.timestamp).toLocaleString()}</td> |
| <td>$${candle.open?.toFixed(2) || 'N/A'}</td> |
| <td>$${candle.high?.toFixed(2) || 'N/A'}</td> |
| <td>$${candle.low?.toFixed(2) || 'N/A'}</td> |
| <td>$${candle.close?.toFixed(2) || 'N/A'}</td> |
| <td>${candle.volume ? candle.volume.toFixed(2) : 'N/A'}</td> |
| </tr> |
| `).join(''); |
| } |
| |
| |
| async function testAllSources() { |
| const symbol = document.getElementById('symbol-select').value; |
| const timeframe = document.getElementById('timeframe-select').value; |
| |
| const btn = document.getElementById('test-all-btn'); |
| btn.disabled = true; |
| btn.innerHTML = '<div class="spinner"></div> Testing...'; |
| |
| document.getElementById('test-results').style.display = 'block'; |
| document.getElementById('test-results-content').innerHTML = ` |
| <div class="loading"> |
| <div class="spinner"></div> |
| <span>Testing all sources... This may take a minute...</span> |
| </div> |
| `; |
| |
| try { |
| const results = await ohlcvClient.testAllSources(symbol, timeframe, 10); |
| |
| const resultsHtml = ` |
| <div class="source-list"> |
| ${results.map(result => ` |
| <div class="source-item"> |
| <div class="source-priority">${result.priority}</div> |
| <div class="source-info"> |
| <div class="source-name">${result.source}</div> |
| <div class="source-details"> |
| ${result.status === 'SUCCESS' |
| ? `${result.candles} candles in ${result.duration}` |
| : result.error} |
| </div> |
| </div> |
| <div class="source-status ${result.status === 'SUCCESS' ? 'status-success' : 'status-failed'}"> |
| ${result.status} |
| </div> |
| </div> |
| `).join('')} |
| </div> |
| `; |
| |
| document.getElementById('test-results-content').innerHTML = resultsHtml; |
| updateStats(); |
| |
| btn.disabled = false; |
| btn.innerHTML = ` |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M9 11l3 3L22 4"/> |
| <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/> |
| </svg> |
| Test All Sources |
| `; |
| } catch (error) { |
| console.error('Test failed:', error); |
| document.getElementById('test-results-content').innerHTML = ` |
| <div style="color: var(--color-danger); padding: var(--space-4);"> |
| Test failed: ${error.message} |
| </div> |
| `; |
| |
| btn.disabled = false; |
| btn.innerHTML = ` |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M9 11l3 3L22 4"/> |
| <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/> |
| </svg> |
| Test All Sources |
| `; |
| } |
| } |
| |
| |
| function updateStats() { |
| const stats = ohlcvClient.getStats(); |
| document.getElementById('success-rate').textContent = stats.successRate; |
| document.getElementById('cache-size').textContent = stats.cacheSize; |
| } |
| |
| |
| document.getElementById('fetch-btn').addEventListener('click', fetchOHLCV); |
| document.getElementById('test-all-btn').addEventListener('click', testAllSources); |
| |
| |
| displaySources(); |
| updateStats(); |
| |
| |
| window.ohlcvClient = ohlcvClient; |
| console.log('💡 OHLCV Client ready! Try: ohlcvClient.getOHLCV("bitcoin", "1d", 100)'); |
| </script> |
| </body> |
| </html> |
|
|
|
|