Alpaca Dashboard
This commit is contained in:
@@ -0,0 +1,181 @@
|
|||||||
|
# Alpaca Dashboard
|
||||||
|
|
||||||
|
A sleek, mobile-optimized web dashboard for monitoring your Alpaca trading account in real-time. Built with vanilla HTML, CSS, and JavaScript—no frameworks required.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 📱 **Mobile-First Design** - Optimized for mobile but looks great on desktop too
|
||||||
|
- 🎨 **Modern UI** - Dark theme with smooth animations and gradient effects
|
||||||
|
- 🔒 **Secure** - API credentials never leave your browser
|
||||||
|
- ⚡ **Real-Time Updates** - Auto-refreshes every 30 seconds
|
||||||
|
- 📊 **Portfolio Overview** - View equity, cash, buying power, and daily P&L at a glance
|
||||||
|
- 📈 **Position Tracking** - Monitor all positions with profit/loss calculations
|
||||||
|
- 🔄 **Auto-Detection** - Automatically detects Paper vs Live trading accounts
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- An Alpaca trading account (sign up at [alpaca.markets](https://alpaca.markets))
|
||||||
|
- API Key and Secret from your Alpaca account
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. Clone this repository:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/alpaca-dashboard.git
|
||||||
|
cd alpaca-dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Ensure `alpaca.png` logo file is in the same directory as `alpaca-dashboard.html`
|
||||||
|
|
||||||
|
3. Open `alpaca-dashboard.html` in your web browser
|
||||||
|
|
||||||
|
That's it! No build process, no dependencies to install.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Open `alpaca-dashboard.html` in your web browser
|
||||||
|
2. Enter your Alpaca API Key and API Secret
|
||||||
|
3. Click **GO!** to connect to your account
|
||||||
|
4. View your portfolio data and positions
|
||||||
|
|
||||||
|
The dashboard will automatically:
|
||||||
|
- Detect whether you're using Paper or Live trading credentials
|
||||||
|
- Refresh your data every 30 seconds
|
||||||
|
- Display real-time profit/loss with color-coded indicators
|
||||||
|
|
||||||
|
## Getting Your API Credentials
|
||||||
|
|
||||||
|
1. Log in to your Alpaca account at [app.alpaca.markets](https://app.alpaca.markets)
|
||||||
|
2. Navigate to **Paper Trading** or **Live Trading** (depending on which you want to use)
|
||||||
|
3. Go to **Your API Keys** section
|
||||||
|
4. Generate or view your API Key and Secret Key
|
||||||
|
5. Copy both values into the dashboard login form
|
||||||
|
|
||||||
|
⚠️ **Security Note**: Keep your API credentials secure. Never share them or commit them to version control.
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
The dashboard connects to:
|
||||||
|
- **Paper Trading**: `https://paper-api.alpaca.markets`
|
||||||
|
- **Live Trading**: `https://api.alpaca.markets`
|
||||||
|
|
||||||
|
It automatically tries Paper first, then Live, based on your credentials.
|
||||||
|
|
||||||
|
## Displayed Information
|
||||||
|
|
||||||
|
### Account Summary
|
||||||
|
- **Portfolio Value** - Total equity in your account
|
||||||
|
- **Cash** - Available cash balance
|
||||||
|
- **Buying Power** - Amount available for trading
|
||||||
|
- **Today's P&L** - Profit/loss for the current trading day with percentage
|
||||||
|
|
||||||
|
### Positions Table
|
||||||
|
- Symbol
|
||||||
|
- Quantity
|
||||||
|
- Average Cost
|
||||||
|
- Current Price
|
||||||
|
- Market Value
|
||||||
|
- Unrealized P&L (dollar amount)
|
||||||
|
- Unrealized P&L (percentage)
|
||||||
|
|
||||||
|
## Browser Compatibility
|
||||||
|
|
||||||
|
Works on all modern browsers:
|
||||||
|
- ✅ Chrome/Edge (recommended)
|
||||||
|
- ✅ Firefox
|
||||||
|
- ✅ Safari
|
||||||
|
- ✅ Mobile browsers (iOS Safari, Chrome Mobile)
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
The dashboard uses CSS variables for easy theming. Edit the `:root` section in the `<style>` tag:
|
||||||
|
|
||||||
|
```css
|
||||||
|
: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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- ✅ API credentials are stored only in memory while logged in
|
||||||
|
- ✅ No server-side components—everything runs in your browser
|
||||||
|
- ✅ No data is sent to any third-party services
|
||||||
|
- ✅ Password input fields prevent credential exposure
|
||||||
|
- ⚠️ For additional security, consider using read-only API keys if Alpaca supports them
|
||||||
|
- ⚠️ Always log out when finished, especially on shared devices
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Read-only dashboard (no trading functionality)
|
||||||
|
- Requires active internet connection
|
||||||
|
- No historical data or charts
|
||||||
|
- Session ends when you close the browser tab or click logout
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Potential features for future versions:
|
||||||
|
- [ ] Historical performance charts
|
||||||
|
- [ ] Order history view
|
||||||
|
- [ ] Watchlist functionality
|
||||||
|
- [ ] Price alerts
|
||||||
|
- [ ] Dark/light theme toggle
|
||||||
|
- [ ] Export data to CSV
|
||||||
|
- [ ] Multiple account switching
|
||||||
|
- [ ] PWA support for offline capability
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||||
|
|
||||||
|
1. Fork the project
|
||||||
|
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
This dashboard is for informational purposes only. It is not financial advice and should not be used as the sole basis for investment decisions. Always verify information directly with Alpaca and consult with a qualified financial advisor before making investment decisions.
|
||||||
|
|
||||||
|
The developer is not affiliated with Alpaca Markets and is not responsible for any losses incurred through the use of this dashboard.
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
- [Alpaca Markets](https://alpaca.markets) for their excellent API
|
||||||
|
- Font families: JetBrains Mono and Outfit from Google Fonts
|
||||||
|
- Inspired by modern trading terminals and fintech interfaces
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues and questions:
|
||||||
|
- Open an issue on [GitHub Issues](https://github.com/yourusername/alpaca-dashboard/issues)
|
||||||
|
- Check Alpaca's [API Documentation](https://docs.alpaca.markets)
|
||||||
|
- Visit [Alpaca Community Forum](https://forum.alpaca.markets)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Made with ☕ for traders who want a clean, simple portfolio view**
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
+757
@@ -0,0 +1,757 @@
|
|||||||
|
<!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 {
|
||||||
|
width: clamp(80px, 15vw, 120px);
|
||||||
|
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); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
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:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-green);
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 255, 136, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="apiKey">API Key</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="apiKey"
|
||||||
|
placeholder="PK••••••••••••••••"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="apiSecret">API Secret</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="apiSecret"
|
||||||
|
placeholder="••••••••••••••••"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<button class="btn-go" onclick="login()">GO!</button>
|
||||||
|
<div id="loginError"></div>
|
||||||
|
</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" style="font-family: 'JetBrains Mono', monospace; color: var(--text-secondary); font-size: 14px;"></span>
|
||||||
|
</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
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
const apiKey = document.getElementById('apiKey').value.trim();
|
||||||
|
const apiSecret = document.getElementById('apiSecret').value.trim();
|
||||||
|
const errorDiv = document.getElementById('loginError');
|
||||||
|
|
||||||
|
if (!apiKey || !apiSecret) {
|
||||||
|
errorDiv.innerHTML = '<div class="error">Please enter both API Key and Secret</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
API_KEY = apiKey;
|
||||||
|
API_SECRET = apiSecret;
|
||||||
|
|
||||||
|
// Try paper trading first, then live
|
||||||
|
const endpoints = [
|
||||||
|
{ url: 'https://paper-api.alpaca.markets', name: 'Paper' },
|
||||||
|
{ url: 'https://api.alpaca.markets', name: 'Live' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const endpoint of endpoints) {
|
||||||
|
BASE_URL = endpoint.url;
|
||||||
|
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) {
|
||||||
|
document.getElementById('loginForm').style.display = 'none';
|
||||||
|
document.getElementById('dashboard').style.display = 'block';
|
||||||
|
await loadDashboard();
|
||||||
|
// Auto-refresh every 30 seconds
|
||||||
|
setInterval(loadDashboard, 30000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Continue to next endpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
document.getElementById('accountNumber').textContent = `#${account.account_number}`;
|
||||||
|
|
||||||
|
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 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 = '';
|
||||||
|
document.getElementById('apiKey').value = '';
|
||||||
|
document.getElementById('apiSecret').value = '';
|
||||||
|
document.getElementById('loginForm').style.display = 'block';
|
||||||
|
document.getElementById('dashboard').style.display = 'none';
|
||||||
|
document.getElementById('loginError').innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow Enter key to submit
|
||||||
|
document.getElementById('apiKey').addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') login();
|
||||||
|
});
|
||||||
|
document.getElementById('apiSecret').addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') login();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user