216 lines
8.0 KiB
HTML
216 lines
8.0 KiB
HTML
{% extends "layout.html" %}
|
|
|
|
{% block title %}Market Scanner — IBKR Dashboard{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="scanner-container">
|
|
<div class="scanner-header">
|
|
<h1 class="scanner-title">Market Scanner</h1>
|
|
<div class="scanner-controls">
|
|
<div class="control-group">
|
|
<label for="scanType">Scan Type</label>
|
|
<select id="scanType">
|
|
<option value="HOT_BY_VOLUME" selected>Unusual Volume</option>
|
|
<option value="MOST_ACTIVE">Most Active</option>
|
|
<option value="TOP_PERC_GAIN">Top Gainers</option>
|
|
<option value="TOP_PERC_LOSE">Top Losers</option>
|
|
<option value="HIGH_OPEN_GAP">High Open Gap</option>
|
|
</select>
|
|
</div>
|
|
<div class="control-group">
|
|
<label for="minPrice">Min Price ($)</label>
|
|
<input type="number" id="minPrice" placeholder="e.g. 5.00" step="0.01" min="0">
|
|
</div>
|
|
<div class="control-group">
|
|
<label for="minVolume">Min Volume</label>
|
|
<input type="number" id="minVolume" placeholder="e.g. 100000" step="1000" min="0">
|
|
</div>
|
|
<div class="control-group">
|
|
<label for="minMarketCap">Min Market Cap ($M)</label>
|
|
<input type="number" id="minMarketCap" placeholder="e.g. 100" step="10" min="0">
|
|
</div>
|
|
<div class="control-group">
|
|
<button class="scan-button" id="scanButton" onclick="runScanner()">Run Scan</button>
|
|
</div>
|
|
<div class="control-group">
|
|
<button class="scan-button export-button" id="exportButton" onclick="exportCSV()">Export CSV</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="results-container" id="resultsContainer">
|
|
<div class="results-header">
|
|
<span>Scanner Results</span>
|
|
<span class="results-count" id="resultsCount" style="display:none;">0 results</span>
|
|
</div>
|
|
<div id="scanResults"></div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
runScanner();
|
|
});
|
|
|
|
async function runScanner() {
|
|
const button = document.getElementById('scanButton');
|
|
const scanResults = document.getElementById('scanResults');
|
|
const resultsCount = document.getElementById('resultsCount');
|
|
|
|
button.disabled = true;
|
|
button.textContent = 'Scanning...';
|
|
|
|
scanResults.innerHTML = `
|
|
<div class="loading-state">
|
|
<div class="scanner-spinner"></div>
|
|
<p>Running scanner...</p>
|
|
</div>
|
|
`;
|
|
resultsCount.style.display = 'none';
|
|
|
|
try {
|
|
const payload = {
|
|
scan_type: document.getElementById('scanType').value,
|
|
min_price: parseFloat(document.getElementById('minPrice').value) || 0,
|
|
min_volume: parseInt(document.getElementById('minVolume').value) || 0,
|
|
min_market_cap: parseFloat(document.getElementById('minMarketCap').value) || 0,
|
|
};
|
|
|
|
const response = await fetch('/scanner', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'ok') {
|
|
displayScanResults(data.results, payload.scan_type);
|
|
} else {
|
|
showError(data.message || 'Scanner request failed');
|
|
}
|
|
} catch (err) {
|
|
showError('Network error: ' + err.message);
|
|
} finally {
|
|
button.disabled = false;
|
|
button.textContent = 'Run Scan';
|
|
}
|
|
}
|
|
|
|
function displayScanResults(results, scanType) {
|
|
const scanResults = document.getElementById('scanResults');
|
|
const resultsCount = document.getElementById('resultsCount');
|
|
|
|
resultsCount.textContent = `${results.length} result${results.length !== 1 ? 's' : ''}`;
|
|
resultsCount.style.display = 'inline-block';
|
|
|
|
if (results.length === 0) {
|
|
scanResults.innerHTML = '<div class="loading-state"><p>No results found. Try adjusting your filters.</p></div>';
|
|
return;
|
|
}
|
|
|
|
const grid = document.createElement('div');
|
|
grid.className = 'results-grid';
|
|
|
|
results.forEach((result) => {
|
|
const card = document.createElement('div');
|
|
card.className = 'result-card';
|
|
card.onclick = () => loadSymbolFromScan(result.symbol);
|
|
|
|
card.innerHTML = `
|
|
<img
|
|
class="chart-thumbnail"
|
|
src="${result.chart_image}"
|
|
alt="${result.symbol} chart"
|
|
onerror="handleChartError(this, '${result.symbol}')"
|
|
>
|
|
<div class="symbol-details">
|
|
<div class="symbol-name">${result.symbol}</div>
|
|
<div class="symbol-company">${result.company}</div>
|
|
<div class="symbol-exchange">${result.exchange || ''}</div>
|
|
</div>
|
|
`;
|
|
|
|
grid.appendChild(card);
|
|
});
|
|
|
|
scanResults.innerHTML = '';
|
|
scanResults.appendChild(grid);
|
|
}
|
|
|
|
function loadSymbolFromScan(symbol) {
|
|
window.location.href = '/?symbol=' + symbol;
|
|
}
|
|
|
|
function formatVolume(volume) {
|
|
if (!volume) return '';
|
|
if (volume >= 1_000_000) return (volume / 1_000_000).toFixed(1) + 'M';
|
|
if (volume >= 1_000) return (volume / 1_000).toFixed(1) + 'K';
|
|
return volume.toString();
|
|
}
|
|
|
|
function handleChartError(imgElement, symbol) {
|
|
imgElement.style.display = 'none';
|
|
const placeholder = document.createElement('div');
|
|
placeholder.className = 'chart-placeholder';
|
|
placeholder.textContent = symbol;
|
|
imgElement.parentNode.insertBefore(placeholder, imgElement);
|
|
}
|
|
|
|
function showError(message) {
|
|
const scanResults = document.getElementById('scanResults');
|
|
scanResults.innerHTML = `
|
|
<div class="loading-state">
|
|
<p style="color: #e74c3c;">Error: ${message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function exportCSV() {
|
|
const button = document.getElementById('exportButton');
|
|
button.disabled = true;
|
|
button.textContent = 'Exporting...';
|
|
|
|
try {
|
|
const payload = {
|
|
scan_type: document.getElementById('scanType').value,
|
|
min_price: parseFloat(document.getElementById('minPrice').value) || 0,
|
|
min_volume: parseInt(document.getElementById('minVolume').value) || 0,
|
|
min_market_cap: parseFloat(document.getElementById('minMarketCap').value) || 0,
|
|
};
|
|
|
|
const response = await fetch('/scanner/export', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
showError('Export failed');
|
|
return;
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
const disposition = response.headers.get('Content-Disposition') || '';
|
|
const filenameMatch = disposition.match(/filename=([^\s;]+)/);
|
|
const filename = filenameMatch ? filenameMatch[1] : 'scanner_export.csv';
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
} catch (err) {
|
|
showError('Export error: ' + err.message);
|
|
} finally {
|
|
button.disabled = false;
|
|
button.textContent = 'Export CSV';
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|