Files
alpaca-dashboard/index.html
T

988 lines
30 KiB
HTML
Raw Normal View History

2026-01-26 21:16:46 -07:00
<!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;
}
/* Animated background grid */
.bg-grid {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
linear-gradient(var(--border) 1px, transparent 1px),
linear-gradient(90deg, var(--border) 1px, transparent 1px);
background-size: 50px 50px;
opacity: 0.3;
animation: gridMove 20s linear infinite;
pointer-events: none;
z-index: 0;
}
@keyframes gridMove {
0% { transform: translate(0, 0); }
100% { transform: translate(50px, 50px); }
}
/* Gradient orb effects */
.orb {
position: fixed;
border-radius: 50%;
filter: blur(80px);
opacity: 0.15;
pointer-events: none;
z-index: 0;
}
.orb-1 {
width: 300px;
height: 300px;
background: var(--accent-green);
top: -100px;
right: -100px;
animation: float 15s ease-in-out infinite;
}
.orb-2 {
width: 400px;
height: 400px;
background: var(--accent-blue);
bottom: -150px;
left: -150px;
animation: float 20s ease-in-out infinite reverse;
}
@keyframes float {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(30px, -30px) scale(1.1); }
}
.container {
position: relative;
z-index: 1;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* Header */
header {
text-align: center;
padding: 40px 0 30px;
animation: slideDown 0.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 {
2026-01-26 22:46:33 -07:00
width: clamp(180px, 30vw, 200px);
2026-01-26 21:16:46 -07:00
height: auto;
filter: drop-shadow(0 4px 12px rgba(0, 255, 136, 0.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 */
.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, 0.4);
animation: fadeInUp 0.6s ease-out 0.2s backwards;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* Trading Mode Toggle */
.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 0.3s ease;
}
.mode-option input[type="radio"]:checked + .mode-option-label {
background: rgba(0, 255, 136, 0.1);
border-color: var(--accent-green);
box-shadow: 0 0 0 3px rgba(0, 255, 136, 0.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;
}
2026-01-26 21:16:46 -07:00
.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: 0.5px;
}
input[type="password"],
input[type="text"] {
2026-01-26 21:16:46 -07:00
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 0.3s ease;
}
input[type="password"]:focus,
input[type="text"]:focus {
2026-01-26 21:16:46 -07:00
outline: none;
border-color: var(--accent-green);
box-shadow: 0 0 0 3px rgba(0, 255, 136, 0.1);
}
/* Make text input look like password field */
input[type="text"]#apiKey {
-webkit-text-security: disc;
text-security: disc;
}
2026-01-26 21:16:46 -07:00
.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 0.3s ease;
box-shadow: 0 4px 16px rgba(0, 255, 136, 0.3);
}
.btn-go:hover {
transform: translateY(-2px);
box-shadow: 0 6px 24px rgba(0, 255, 136, 0.4);
}
.btn-go:active {
transform: translateY(0);
}
.btn-go:disabled {
opacity: 0.5;
cursor: not-allowed;
}
2026-01-27 18:49:52 -07:00
.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 0.3s ease;
}
.api-key-info a:hover {
color: var(--accent-blue);
text-decoration: underline;
}
2026-01-26 21:16:46 -07:00
/* Dashboard */
#dashboard {
display: none;
animation: fadeInUp 0.6s ease-out;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
flex-wrap: wrap;
gap: 12px;
}
.account-info {
display: flex;
align-items: center;
gap: 12px;
}
.status-badge {
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-active {
background: rgba(0, 255, 136, 0.15);
color: var(--accent-green);
border: 1px solid var(--accent-green);
}
.account-number {
font-family: 'JetBrains Mono', monospace;
color: var(--text-secondary);
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
user-select: none;
position: relative;
padding: 4px 8px;
border-radius: 4px;
}
.account-number:hover {
color: var(--accent-green);
background: rgba(0, 255, 136, 0.1);
}
.account-number:active {
transform: scale(0.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; }
}
2026-01-26 21:16:46 -07:00
.btn-logout {
padding: 8px 16px;
background: transparent;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-secondary);
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-logout:hover {
border-color: var(--accent-red);
color: var(--accent-red);
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
transition: all 0.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 0.3s ease;
}
.stat-card:hover::before {
transform: scaleX(1);
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
.stat-label {
font-size: 12px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.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;
}
/* Positions Table */
.section-title {
font-size: 18px;
font-weight: 700;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.section-title::before {
content: '';
width: 4px;
height: 20px;
background: var(--accent-green);
border-radius: 2px;
}
.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: 0.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 */
.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 */
.error {
background: rgba(255, 68, 102, 0.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: 0.5;
}
/* Responsive */
@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;
}
}
/* Refresh animation */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.refreshing {
animation: pulse 1.5s ease-in-out infinite;
}
</style>
</head>
<body>
<div class="bg-grid"></div>
<div class="orb orb-1"></div>
<div class="orb orb-2"></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>
<!-- Login Form -->
<div id="loginForm">
<div class="login-card">
<form id="loginFormElement" onsubmit="login(event)">
<!-- Trading Mode Selection -->
<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>
<!-- API Credentials -->
<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>
2026-01-26 21:16:46 -07:00
</div>
</div>
<!-- Dashboard -->
<div id="dashboard">
<div class="dashboard-header">
<div class="account-info">
<span 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>
2026-01-26 21:16:46 -07:00
</div>
<button class="btn-logout" onclick="logout()">Logout</button>
</div>
<div class="stats-grid" id="statsGrid">
<!-- Stats will be inserted here -->
</div>
<h2 class="section-title">Positions</h2>
<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&L</th>
<th>P&L %</th>
</tr>
</thead>
<tbody id="positionsBody">
<!-- Positions will be inserted here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
let API_KEY = '';
let API_SECRET = '';
let BASE_URL = 'https://paper-api.alpaca.markets'; // Default to paper trading
let ACCOUNT_NUMBER = '';
let hideTimeout = null;
let isRevealed = false;
2026-01-26 21:16:46 -07:00
async function login(event) {
event.preventDefault(); // Prevent form submission
2026-01-26 21:16:46 -07:00
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;
2026-01-26 21:16:46 -07:00
if (!apiKey || !apiSecret) {
errorDiv.innerHTML = '<div class="error">Please enter both API Key and Secret</div>';
return;
}
API_KEY = apiKey;
API_SECRET = apiSecret;
// Set BASE_URL based on selected trading mode
BASE_URL = tradingMode === 'paper'
? 'https://paper-api.alpaca.markets'
: 'https://api.alpaca.markets';
2026-01-26 21:16:46 -07:00
try {
const response = await fetch(`${BASE_URL}/v2/account`, {
headers: {
'APCA-API-KEY-ID': API_KEY,
'APCA-API-SECRET-KEY': API_SECRET
2026-01-26 21:16:46 -07:00
}
});
if (response.ok) {
document.getElementById('loginForm').style.display = 'none';
document.getElementById('dashboard').style.display = 'block';
await loadDashboard();
// Auto-refresh every 30 seconds
setInterval(loadDashboard, 30000);
return;
2026-01-26 21:16:46 -07:00
}
} catch (error) {
console.error('Login error:', error);
2026-01-26 21:16:46 -07:00
}
errorDiv.innerHTML = '<div class="error">Invalid credentials. Please check your API Key and Secret.</div>';
}
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;
// Set initial display with first 3 characters visible
updateAccountDisplay();
2026-01-26 21:16:46 -07:00
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: `${plTodayPercent >= 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() {
// Show first 3 characters after # and mask the rest
const prefix = ACCOUNT_NUMBER.substring(0, 2); // PA or similar
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() {
// Clear any pending hide timeout
if (hideTimeout) {
clearTimeout(hideTimeout);
hideTimeout = null;
}
isRevealed = true;
updateAccountDisplay();
}
function hideAccountNumber() {
// Only hide if not clicked (click sets a 5 second timer)
if (!hideTimeout) {
isRevealed = false;
updateAccountDisplay();
}
}
function copyAccountNumber(event) {
event.preventDefault();
const accountElement = document.getElementById('accountNumber');
// Reveal the account number
isRevealed = true;
updateAccountDisplay();
// Clear any existing timeout
if (hideTimeout) {
clearTimeout(hideTimeout);
}
// Copy to clipboard
navigator.clipboard.writeText(`#${ACCOUNT_NUMBER}`).then(() => {
// Add copied class for visual feedback
accountElement.classList.add('copied');
// Remove the copied class after animation completes
setTimeout(() => {
accountElement.classList.remove('copied');
}, 2000);
}).catch(err => {
console.error('Failed to copy:', err);
});
// Set a timeout to hide after 5 seconds
hideTimeout = setTimeout(() => {
isRevealed = false;
updateAccountDisplay();
hideTimeout = null;
}, 5000);
}
2026-01-26 21:16:46 -07:00
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;
}
2026-01-26 21:16:46 -07:00
document.getElementById('apiKey').value = '';
document.getElementById('apiSecret').value = '';
document.getElementById('loginForm').style.display = 'block';
document.getElementById('dashboard').style.display = 'none';
document.getElementById('loginError').innerHTML = '';
}
</script>
</body>
</html>