Files
alpaca-dashboard/index.html
T

858 lines
29 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alpaca Dashboard</title>
<link rel="icon" type="image/png" href="alpaca.png">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
:root{
--bg-primary:#0a0e14;
--bg-secondary:#12171f;
--bg-card:#1a2332;
--accent-green:#00ff88;
--accent-red:#ff4466;
--accent-blue:#00d4ff;
--accent-yellow:#ffcc00;
--text-primary:#e6edf3;
--text-secondary:#8b949e;
--text-muted:#6e7681;
--border:#30363d;
}
*{
margin:0;
padding:0;
box-sizing:border-box;
}
body{
font-family:'Outfit',sans-serif;
background:var(--bg-primary);
color:var(--text-primary);
min-height:100vh;
overflow-x:hidden;
}
.container{
position:relative;
z-index:1;
max-width:1200px;
margin:0 auto;
padding:12px 12px;
}
header{
text-align:center;
padding:20px 0 16px;
animation:slideDown .6s ease-out;
}
@keyframes slideDown{
from{opacity:0;transform:translateY(-20px);}
to{opacity:1;transform:translateY(0);}
}
.logo-container{
display:flex;
flex-direction:column;
align-items:center;
gap:16px;
}
.logo-image{
width:clamp(180px,30vw,200px);
height:auto;
filter:drop-shadow(0 4px 12px rgba(0,255,136,.3));
}
.logo{
font-family:'JetBrains Mono',monospace;
font-size:clamp(28px,6vw,42px);
font-weight:700;
background:linear-gradient(135deg,var(--accent-green),var(--accent-blue));
-webkit-background-clip:text;
-webkit-text-fill-color:transparent;
background-clip:text;
letter-spacing:-1px;
}
.tagline{
font-size:clamp(14px,3vw,16px);
color:var(--text-secondary);
font-weight:300;
}
.login-card{
background:var(--bg-card);
border:1px solid var(--border);
border-radius:16px;
padding:clamp(24px,5vw,40px);
margin:20px auto;
max-width:500px;
box-shadow:0 8px 32px rgba(0,0,0,.4);
animation:fadeInUp .6s ease-out .2s backwards;
}
@keyframes fadeInUp{
from{opacity:0;transform:translateY(20px);}
to{opacity:1;transform:translateY(0);}
}
.trading-mode-group{
margin-bottom:24px;
}
.mode-options{
display:grid;
grid-template-columns:1fr 1fr;
gap:12px;
}
.mode-option{
position:relative;
}
.mode-option input[type="radio"]{
position:absolute;
opacity:0;
width:0;
height:0;
}
.mode-option-label{
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
padding:16px 12px;
background:var(--bg-secondary);
border:2px solid var(--border);
border-radius:8px;
cursor:pointer;
transition:all .3s ease;
}
.mode-option input[type="radio"]:checked+.mode-option-label{
background:rgba(0,255,136,.1);
border-color:var(--accent-green);
box-shadow:0 0 0 3px rgba(0,255,136,.1);
}
.mode-option-label:hover{
border-color:var(--accent-green);
}
.mode-title{
font-family:'JetBrains Mono',monospace;
font-size:14px;
font-weight:700;
color:var(--text-primary);
margin-bottom:4px;
}
.mode-description{
font-size:11px;
color:var(--text-muted);
text-align:center;
}
.input-group{
margin-bottom:20px;
}
label{
display:block;
font-size:13px;
font-weight:600;
color:var(--text-secondary);
margin-bottom:8px;
text-transform:uppercase;
letter-spacing:.5px;
}
input[type="password"],input[type="text"]{
width:100%;
padding:14px 16px;
background:var(--bg-secondary);
border:1px solid var(--border);
border-radius:8px;
color:var(--text-primary);
font-family:'JetBrains Mono',monospace;
font-size:14px;
transition:all .3s ease;
}
input[type="password"]:focus,input[type="text"]:focus{
outline:none;
border-color:var(--accent-green);
box-shadow:0 0 0 3px rgba(0,255,136,.1);
}
input[type="text"]#apiKey{
-webkit-text-security:disc;
text-security:disc;
}
.btn-go{
width:100%;
padding:16px;
background:linear-gradient(135deg,var(--accent-green),var(--accent-blue));
border:none;
border-radius:8px;
color:var(--bg-primary);
font-family:'Outfit',sans-serif;
font-size:16px;
font-weight:700;
cursor:pointer;
text-transform:uppercase;
letter-spacing:1px;
transition:all .3s ease;
box-shadow:0 4px 16px rgba(0,255,136,.3);
}
.btn-go:hover{
transform:translateY(-2px);
box-shadow:0 6px 24px rgba(0,255,136,.4);
}
.btn-go:active{
transform:translateY(0);
}
.btn-go:disabled{
opacity:.5;
cursor:not-allowed;
}
.api-key-info{
text-align:center;
margin-top:12px;
font-size:11px;
color:var(--text-muted);
}
.api-key-info a{
color:var(--accent-green);
text-decoration:none;
transition:color .3s ease;
}
.api-key-info a:hover{
color:var(--accent-blue);
text-decoration:underline;
}
#dashboard{
display:none;
animation:fadeInUp .6s ease-out;
}
.dashboard-header{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:16px;
flex-wrap:wrap;
gap:12px;
}
.account-info{
display:flex;
align-items:center;
gap:12px;
flex-shrink:1;
min-width:0;
}
.status-badge{
padding:6px 12px;
border-radius:20px;
font-size:12px;
font-weight:600;
text-transform:uppercase;
letter-spacing:.5px;
}
.status-active{
background:rgba(0,255,136,.15);
color:var(--accent-green);
border:1px solid var(--accent-green);
}
.status-paper{
background:rgba(0,212,255,.15);
color:var(--accent-blue);
border:1px solid var(--accent-blue);
}
.account-number{
font-family:'JetBrains Mono',monospace;
color:var(--text-secondary);
font-size:14px;
cursor:pointer;
transition:all .3s ease;
user-select:none;
position:relative;
padding:4px 8px;
border-radius:4px;
white-space:nowrap;
overflow:hidden;
text-overflow:ellipsis;
max-width:100%;
}
.account-number:hover{
color:var(--accent-green);
background:rgba(0,255,136,.1);
}
.account-number:active{
transform:scale(.95);
}
.account-number.copied::after{
content:'Copied!';
position:absolute;
top:-30px;
left:50%;
transform:translateX(-50%);
background:var(--accent-green);
color:var(--bg-primary);
padding:4px 8px;
border-radius:4px;
font-size:11px;
white-space:nowrap;
animation:fadeOut 2s ease-out forwards;
}
@keyframes fadeOut{
0%{opacity:1;}
70%{opacity:1;}
100%{opacity:0;}
}
.stats-grid{
display:grid;
grid-template-columns:repeat(auto-fit,minmax(min(100%,250px),1fr));
gap:12px;
margin-bottom:20px;
}
.stat-card{
background:var(--bg-card);
border:1px solid var(--border);
border-radius:12px;
padding:16px;
transition:all .3s ease;
position:relative;
overflow:hidden;
}
.stat-card::before{
content:'';
position:absolute;
top:0;
left:0;
width:100%;
height:3px;
background:linear-gradient(90deg,var(--accent-green),var(--accent-blue));
transform:scaleX(0);
transform-origin:left;
transition:transform .3s ease;
}
.stat-card:hover::before{
transform:scaleX(1);
}
.stat-card:hover{
transform:translateY(-4px);
box-shadow:0 8px 24px rgba(0,0,0,.3);
}
.stat-label{
font-size:12px;
color:var(--text-muted);
text-transform:uppercase;
letter-spacing:.5px;
margin-bottom:8px;
}
.stat-value{
font-family:'JetBrains Mono',monospace;
font-size:clamp(20px,4vw,28px);
font-weight:700;
color:var(--text-primary);
}
.stat-value.positive{
color:var(--accent-green);
}
.stat-value.negative{
color:var(--accent-red);
}
.stat-change{
font-size:13px;
margin-top:4px;
font-weight:600;
}
.section-title{
font-size:18px;
font-weight:700;
margin-bottom:16px;
display:flex;
align-items:center;
gap:8px;
}
.positions-container{
background:var(--bg-card);
border:1px solid var(--border);
border-radius:12px;
overflow:hidden;
margin-bottom:24px;
}
.positions-scroll{
overflow-x:auto;
}
table{
width:100%;
border-collapse:collapse;
}
thead{
background:var(--bg-secondary);
}
th{
padding:16px;
text-align:left;
font-size:12px;
font-weight:600;
color:var(--text-secondary);
text-transform:uppercase;
letter-spacing:.5px;
white-space:nowrap;
}
td{
padding:16px;
border-top:1px solid var(--border);
font-size:14px;
white-space:nowrap;
}
.symbol{
font-family:'JetBrains Mono',monospace;
font-weight:700;
color:var(--text-primary);
}
.mono{
font-family:'JetBrains Mono',monospace;
}
.loading{
text-align:center;
padding:40px;
color:var(--text-secondary);
}
.spinner{
border:3px solid var(--border);
border-top:3px solid var(--accent-green);
border-radius:50%;
width:40px;
height:40px;
animation:spin 1s linear infinite;
margin:0 auto 16px;
}
@keyframes spin{
0%{transform:rotate(0deg);}
100%{transform:rotate(360deg);}
}
.error{
background:rgba(255,68,102,.1);
border:1px solid var(--accent-red);
color:var(--accent-red);
padding:16px;
border-radius:8px;
margin-top:16px;
font-size:14px;
}
.empty-state{
text-align:center;
padding:60px 20px;
color:var(--text-muted);
}
.empty-state-icon{
font-size:48px;
margin-bottom:16px;
opacity:.5;
}
@media (max-width:768px){
.container{
padding:12px;
}
header{
padding:20px 0 20px;
}
.stats-grid{
grid-template-columns:1fr;
}
th,td{
padding:12px 8px;
font-size:12px;
}
.dashboard-header{
flex-direction:column;
align-items:flex-start;
}
}
@keyframes pulse{
0%,100%{opacity:1;}
50%{opacity:.5;}
}
.refreshing{
animation:pulse 1.5s ease-in-out infinite;
}
/* -----------------------------------------------------------------
Hamburger menu – hidden until login succeeds
----------------------------------------------------------------- */
.hamburger{
position:fixed;
top:20px;
right:20px;
font-size:28px;
cursor:pointer;
color:var(--text-primary);
z-index:10;
display:none; /* hidden on the login page */
}
.menu{
position:fixed;
top:60px;
right:20px;
background:var(--bg-card);
border:1px solid var(--border);
border-radius:8px;
padding:8px 0;
display:none;
min-width:120px;
box-shadow:0 4px 12px rgba(0,0,0,.4);
z-index:10;
}
.menu.show{
display:block;
}
.menu-item{
width:100%;
padding:8px 16px;
background:none;
border:none;
text-align:left;
color:var(--text-primary);
font-family:'Outfit',sans-serif;
font-size:14px;
cursor:pointer;
}
.menu-item:hover{
background:var(--bg-secondary);
}
</style>
</head>
<body>
<div id="menuToggle" class="hamburger">☰</div>
<div id="menu" class="menu">
<button id="menuLogout" class="menu-item">Logout</button>
</div>
<div class="container">
<header>
<div class="logo-container">
<img src="alpaca.png" alt="Alpaca Logo" class="logo-image">
<div class="logo">ALPACA DASHBOARD</div>
</div>
<div class="tagline">Real-time portfolio monitoring</div>
</header>
<div id="loginForm">
<div class="login-card">
<form id="loginFormElement" onsubmit="login(event)">
<div class="trading-mode-group">
<div class="mode-options">
<div class="mode-option">
<input type="radio" id="paperMode" name="tradingMode" value="paper" checked>
<label for="paperMode" class="mode-option-label">
<div class="mode-title">Paper Trading</div>
<div class="mode-description">(Virtual Money)</div>
</label>
</div>
<div class="mode-option">
<input type="radio" id="liveMode" name="tradingMode" value="live">
<label for="liveMode" class="mode-option-label">
<div class="mode-title">Live Trading</div>
<div class="mode-description">(Real Money)</div>
</label>
</div>
</div>
</div>
<div class="input-group">
<label for="apiKey">API Key</label>
<input type="text" id="apiKey" name="username" placeholder="PK••••••••••••••••" autocomplete="username">
</div>
<div class="input-group">
<label for="apiSecret">API Secret</label>
<input type="password" id="apiSecret" name="password" placeholder="••••••••••••••••" autocomplete="current-password">
</div>
<button type="submit" class="btn-go">GO!</button>
<div class="api-key-info">
Visit <a href="https://alpaca.markets" target="_blank">Alpaca Markets</a> to get your API key.
</div>
<div id="loginError"></div>
</form>
</div>
</div>
<div id="dashboard">
<div class="dashboard-header">
<div class="account-info">
<span id="tradingModeBadge" class="status-badge status-active">● LIVE</span>
<span id="accountNumber" class="account-number"
onmouseenter="revealAccountNumber()"
onmouseleave="hideAccountNumber()"
onclick="copyAccountNumber(event)"
oncontextmenu="copyAccountNumber(event)">#PA••••••••</span>
</div>
</div>
<div class="stats-grid" id="statsGrid"></div>
<div class="positions-container">
<div class="positions-scroll">
<table id="positionsTable">
<thead>
<tr>
<th>Symbol</th>
<th>Qty</th>
<th>Avg Cost</th>
<th>Current Price</th>
<th>Market Value</th>
<th>P&amp;L</th>
<th>P&amp;L %</th>
</tr>
</thead>
<tbody id="positionsBody"></tbody>
</table>
</div>
</div>
</div>
</div>
<script>
let API_KEY = '';
let API_SECRET = '';
let BASE_URL = 'https://paper-api.alpaca.markets';
let ACCOUNT_NUMBER = '';
let hideTimeout = null;
let isRevealed = false;
async function login(event) {
event.preventDefault();
const apiKey = document.getElementById('apiKey').value.trim();
const apiSecret = document.getElementById('apiSecret').value.trim();
const errorDiv = document.getElementById('loginError');
const tradingMode = document.querySelector('input[name="tradingMode"]:checked').value;
if (!apiKey || !apiSecret) {
errorDiv.innerHTML = '<div class="error">Please enter both API Key and Secret</div>';
return;
}
API_KEY = apiKey;
API_SECRET = apiSecret;
BASE_URL = tradingMode === 'paper'
? 'https://paper-api.alpaca.markets'
: 'https://api.alpaca.markets';
try {
const response = await fetch(`${BASE_URL}/v2/account`, {
headers: {
'APCA-API-KEY-ID': API_KEY,
'APCA-API-SECRET-KEY': API_SECRET
}
});
if (response.ok) {
updateTradingModeBadge(tradingMode);
document.getElementById('loginForm').style.display = 'none';
document.getElementById('dashboard').style.display = 'block';
// show hamburger now that the user is logged in
document.getElementById('menuToggle').style.display = 'block';
await loadDashboard();
setInterval(loadDashboard, 30000);
return;
}
} catch (error) {
console.error('Login error:', error);
}
errorDiv.innerHTML = '<div class="error">Invalid credentials. Please check your API Key and Secret.</div>';
}
function updateTradingModeBadge(mode) {
const badge = document.getElementById('tradingModeBadge');
if (!badge) return;
badge.classList.remove('status-active', 'status-paper');
if (mode === 'live') {
badge.classList.add('status-active');
badge.textContent = '● LIVE TRADING';
} else {
badge.classList.add('status-paper');
badge.textContent = '● PAPER TRADING';
}
}
async function loadDashboard() {
try {
const [accountData, positions] = await Promise.all([
fetchAlpaca('/v2/account'),
fetchAlpaca('/v2/positions')
]);
displayAccount(accountData);
displayPositions(positions);
} catch (error) {
console.error('Error loading dashboard:', error);
}
}
async function fetchAlpaca(endpoint) {
const response = await fetch(`${BASE_URL}${endpoint}`, {
headers: {
'APCA-API-KEY-ID': API_KEY,
'APCA-API-SECRET-KEY': API_SECRET
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
function displayAccount(account) {
ACCOUNT_NUMBER = account.account_number;
updateAccountDisplay();
const equity = parseFloat(account.equity);
const cash = parseFloat(account.cash);
const buyingPower = parseFloat(account.buying_power);
const plToday = parseFloat(account.equity) - parseFloat(account.last_equity);
const plTodayPercent = (plToday / parseFloat(account.last_equity)) * 100;
const stats = [
{ label: 'Portfolio Value', value: formatCurrency(equity), class: '' },
{ label: 'Cash', value: formatCurrency(cash), class: '' },
{ label: 'Buying Power', value: formatCurrency(buyingPower), class: '' },
{ label: "Today's P&L", value: formatCurrency(plToday), change: `${plToday >= 0 ? '+' : ''}${plTodayPercent.toFixed(2)}%`, class: plToday >= 0 ? 'positive' : 'negative' }
];
const statsGrid = document.getElementById('statsGrid');
statsGrid.innerHTML = stats.map(stat => `
<div class="stat-card">
<div class="stat-label">${stat.label}</div>
<div class="stat-value ${stat.class}">${stat.value}</div>
${stat.change ? `<div class="stat-change ${stat.class}">${stat.change}</div>` : ''}
</div>
`).join('');
}
function getPartialAccountNumber() {
const prefix = ACCOUNT_NUMBER.substring(0, 2);
const dots = '•'.repeat(ACCOUNT_NUMBER.length - 2);
return `#${prefix}${dots}`;
}
function updateAccountDisplay() {
const accountElement = document.getElementById('accountNumber');
if (isRevealed) {
accountElement.textContent = `#${ACCOUNT_NUMBER}`;
} else {
accountElement.textContent = getPartialAccountNumber();
}
}
function revealAccountNumber() {
if (hideTimeout) {
clearTimeout(hideTimeout);
hideTimeout = null;
}
isRevealed = true;
updateAccountDisplay();
}
function hideAccountNumber() {
if (!hideTimeout) {
isRevealed = false;
updateAccountDisplay();
}
}
function copyAccountNumber(event) {
event.preventDefault();
const accountElement = document.getElementById('accountNumber');
isRevealed = true;
updateAccountDisplay();
if (hideTimeout) {
clearTimeout(hideTimeout);
}
navigator.clipboard.writeText(`#${ACCOUNT_NUMBER}`).then(() => {
accountElement.classList.add('copied');
setTimeout(() => {
accountElement.classList.remove('copied');
}, 2000);
}).catch(err => {
console.error('Failed to copy:', err);
});
hideTimeout = setTimeout(() => {
isRevealed = false;
updateAccountDisplay();
hideTimeout = null;
}, 5000);
}
function displayPositions(positions) {
const tbody = document.getElementById('positionsBody');
if (!positions || positions.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="7">
<div class="empty-state">
<div class="empty-state-icon">📊</div>
<div>No positions found</div>
</div>
</td>
</tr>
`;
return;
}
tbody.innerHTML = positions.map(pos => {
const pl = parseFloat(pos.unrealized_pl);
const plPercent = parseFloat(pos.unrealized_plpc) * 100;
const plClass = pl >= 0 ? 'positive' : 'negative';
return `
<tr>
<td class="symbol">${pos.symbol}</td>
<td class="mono">${pos.qty}</td>
<td class="mono">${formatCurrency(pos.avg_entry_price)}</td>
<td class="mono">${formatCurrency(pos.current_price)}</td>
<td class="mono">${formatCurrency(pos.market_value)}</td>
<td class="mono ${plClass}">${formatCurrency(pl)}</td>
<td class="mono ${plClass}">${plPercent >= 0 ? '+' : ''}${plPercent.toFixed(2)}%</td>
</tr>
`;
}).join('');
}
function formatCurrency(value) {
const num = parseFloat(value);
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(num);
}
function logout() {
API_KEY = '';
API_SECRET = '';
ACCOUNT_NUMBER = '';
isRevealed = false;
if (hideTimeout) {
clearTimeout(hideTimeout);
hideTimeout = null;
}
document.getElementById('apiKey').value = '';
document.getElementById('apiSecret').value = '';
document.getElementById('loginForm').style.display = 'block';
document.getElementById('dashboard').style.display = 'none';
document.getElementById('loginError').innerHTML = '';
// hide hamburger again
document.getElementById('menuToggle').style.display = 'none';
document.getElementById('menu').classList.remove('show');
}
/* -----------------------------------------------------------------
Hamburger menu logic
----------------------------------------------------------------- */
document.getElementById('menuToggle').addEventListener('click', () => {
document.getElementById('menu').classList.toggle('show');
});
document.getElementById('menuLogout').addEventListener('click', () => {
document.getElementById('menu').classList.remove('show');
logout();
});
// Close menu when clicking outside of it
document.addEventListener('click', (e) => {
const menu = document.getElementById('menu');
const toggle = document.getElementById('menuToggle');
if (!menu.contains(e.target) && !toggle.contains(e.target)) {
menu.classList.remove('show');
}
});
</script>
</body>
</html>