868 lines
29 KiB
HTML
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&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.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>
|