Files

868 lines
29 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<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:16px;
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:8px;
}
header{
padding:12px 0 12px;
}
.logo-image{
width:120px;
}
.logo{
font-size:24px;
}
.tagline{
font-size:12px;
}
.stats-grid{
grid-template-columns:1fr;
gap:8px;
margin-bottom:12px;
}
.stat-card{
padding:12px;
}
.dashboard-header{
margin-bottom:12px;
}
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{
position:fixed;
top:20px;
right:20px;
font-size:28px;
cursor:pointer;
color:var(--text-primary);
z-index:10;
display:none;
}
.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.activeElement.blur();
document.getElementById('loginForm').style.display = 'none';
document.getElementById('dashboard').style.display = 'block';
document.getElementById('menuToggle').style.display = 'block';
window.scrollTo(0, 0);
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 = '';
document.getElementById('menuToggle').style.display = 'none';
document.getElementById('menu').classList.remove('show');
}
document.getElementById('menuToggle').addEventListener('click', () => {
document.getElementById('menu').classList.toggle('show');
});
document.getElementById('menuLogout').addEventListener('click', () => {
document.getElementById('menu').classList.remove('show');
logout();
});
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>