initial commit

This commit is contained in:
coddard
2026-05-18 00:31:08 +03:00
commit 0096c8819b
26 changed files with 2851 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
{% extends "layout.html" %}
{% block title %}Real-Time Chart — IBKR Dashboard{% endblock %}
{% block extra_head %}
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
{% endblock %}
{% block content %}
<div class="chart-section">
<div id="controls">
<div class="control-group">
<input type="text" id="symbolInput" placeholder="Enter symbol (e.g. AAPL)" value="{{ symbol }}">
<button onclick="subscribeSymbol()">Load Chart</button>
</div>
<div class="loading-indicator" id="loadingIndicator">Loading...</div>
</div>
<div id="chart"></div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
const chart = LightweightCharts.createChart(document.getElementById('chart'), {
width: document.getElementById('chart').clientWidth,
height: 600,
layout: { background: { color: '#ffffff' }, textColor: '#333' },
grid: { vertLines: { color: '#f0f0f0' }, horzLines: { color: '#f0f0f0' } },
timeScale: { timeVisible: true, secondsVisible: false }
});
const series = chart.addCandlestickSeries();
let eventSource = null;
function cleanupConnections() {
if (eventSource) {
console.log('Closing EventSource connection');
eventSource.close();
eventSource = null;
}
}
async function subscribeSymbol() {
cleanupConnections();
const symbol = document.getElementById("symbolInput").value.toUpperCase();
document.getElementById("loadingIndicator").classList.add("show");
document.getElementById("chart").classList.add("loading");
const historyResponse = await fetch(`/history?symbol=${symbol}`);
const historicalData = await historyResponse.json();
series.setData(historicalData);
await fetch("/subscribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ symbol })
});
eventSource = new EventSource("/stream");
eventSource.addEventListener("candle", (event) => {
const candleData = JSON.parse(event.data);
series.update(candleData);
});
eventSource.addEventListener("error", (event) => {
console.error('SSE Error:', event);
});
eventSource.addEventListener("open", () => {
console.log('SSE Connection Opened');
document.getElementById("loadingIndicator").classList.remove("show");
document.getElementById("chart").classList.remove("loading");
});
}
window.addEventListener('DOMContentLoaded', () => {
const urlParams = new URLSearchParams(window.location.search);
const symbol = urlParams.get('symbol');
if (symbol) {
document.getElementById("symbolInput").value = symbol;
subscribeSymbol();
}
});
window.addEventListener('resize', () => {
chart.applyOptions({ width: document.getElementById('chart').clientWidth });
});
window.addEventListener('beforeunload', cleanupConnections);
window.addEventListener('pagehide', cleanupConnections);
</script>
{% endblock %}
+29
View File
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}IBKR Trading Dashboard{% endblock %}</title>
<link rel="stylesheet" href="/static/styles.css">
{% block extra_head %}{% endblock %}
</head>
<body>
<nav>
<div class="nav-container">
<div class="nav-brand">
<a href="/">IBKR Dashboard</a>
</div>
<ul class="nav-links">
<li><a href="/">Real-Time Chart</a></li>
<li><a href="/scanner">Market Scanner</a></li>
<li><a href="/tradelog">Trade Log</a></li>
<li><a href="/portfolio">Portfolio</a></li>
</ul>
</div>
</nav>
<main class="container">
{% block content %}{% endblock %}
</main>
{% block extra_scripts %}{% endblock %}
</body>
</html>
+108
View File
@@ -0,0 +1,108 @@
{% extends "layout.html" %}
{% block title %}Portfolio — IBKR Dashboard{% endblock %}
{% block content %}
<h1>Portfolio</h1>
<div class="portfolio-summary" id="pnlCards">
<div class="summary-card">
<div class="card-label">Net Liquidation</div>
<div class="card-value" id="netLiq"></div>
</div>
<div class="summary-card">
<div class="card-label">Total Cash</div>
<div class="card-value" id="totalCash"></div>
</div>
<div class="summary-card">
<div class="card-label">Unrealized P&amp;L</div>
<div class="card-value" id="unrealizedPnl"></div>
</div>
<div class="summary-card">
<div class="card-label">Realized P&amp;L</div>
<div class="card-value" id="realizedPnl"></div>
</div>
</div>
<div class="dashboard-container" style="margin-top: 24px;">
<table>
<thead>
<tr>
<th>Symbol</th>
<th>Position</th>
<th>Avg Cost</th>
<th>Chart</th>
</tr>
</thead>
<tbody id="positionsBody">
<tr><td colspan="4" class="empty-state">Loading positions...</td></tr>
</tbody>
</table>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
function fmt(val) {
if (val === undefined || val === null) return '—';
return '$' + parseFloat(val).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function colorClass(val) {
const n = parseFloat(val);
if (isNaN(n)) return '';
return n >= 0 ? 'positive' : 'negative';
}
async function loadPnl() {
try {
const res = await fetch('/portfolio/pnl');
const d = await res.json();
document.getElementById('netLiq').textContent = fmt(d.NetLiquidation);
document.getElementById('totalCash').textContent = fmt(d.TotalCashValue);
const upEl = document.getElementById('unrealizedPnl');
upEl.textContent = fmt(d.UnrealizedPnL);
upEl.className = 'card-value ' + colorClass(d.UnrealizedPnL);
const rpEl = document.getElementById('realizedPnl');
rpEl.textContent = fmt(d.RealizedPnL);
rpEl.className = 'card-value ' + colorClass(d.RealizedPnL);
} catch (e) {
console.error('PnL fetch error:', e);
}
}
async function loadPositions() {
try {
const res = await fetch('/portfolio/data');
const data = await res.json();
const tbody = document.getElementById('positionsBody');
if (!Array.isArray(data) || data.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">No open positions.</td></tr>';
return;
}
tbody.innerHTML = data.map(p => `
<tr>
<td><a class="symbol-link" href="/?symbol=${p.symbol}">${p.symbol}</a></td>
<td>${p.position}</td>
<td class="price-cell">${fmt(p.avg_cost)}</td>
<td><a class="symbol-link" href="/?symbol=${p.symbol}">View Chart</a></td>
</tr>
`).join('');
} catch (e) {
console.error('Positions fetch error:', e);
}
}
function refresh() {
loadPnl();
loadPositions();
}
window.addEventListener('DOMContentLoaded', () => {
refresh();
setInterval(refresh, 30000);
});
</script>
{% endblock %}
+215
View File
@@ -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 %}
+41
View File
@@ -0,0 +1,41 @@
{% extends "layout.html" %}
{% block title %}Trade Log — IBKR Dashboard{% endblock %}
{% block content %}
<h1>Trade Log</h1>
<div class="dashboard-container">
{% if trades %}
<table>
<thead>
<tr>
<th>Time (UTC)</th>
<th>Symbol</th>
<th>Action</th>
<th>Quantity</th>
<th>Fill Price</th>
</tr>
</thead>
<tbody>
{% for trade in trades %}
<tr>
<td>{{ trade.timestamp }}</td>
<td class="symbol-cell">
<a href="/?symbol={{ trade.symbol }}" class="symbol-link">{{ trade.symbol }}</a>
</td>
<td>
<span class="action-tag {{ 'buy' if trade.action == 'BUY' else 'sell' }}">{{ trade.action }}</span>
</td>
<td>{{ trade.quantity }}</td>
<td class="price-cell">${{ '%.2f'|format(trade.price) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
<p>No trades have been recorded yet. Executed trades from webhooks will appear here.</p>
</div>
{% endif %}
</div>
{% endblock %}