853 lines
29 KiB
HTML
853 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">
|
||
<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:20px;
|
||
}
|
||
header{
|
||
text-align:center;
|
||
padding:40px 0 30px;
|
||
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: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:.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;
|
||
}
|
||
.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(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 .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&L</th>
|
||
<th>P&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>
|
||
|