initial commit
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user