Create an HTML web page for a simulated stock trading platform. The theme can switch between dark and light. The top of the page shows the user's available funds, total assets, market value, and daily gains/losses. The user can view stock quotes in a table with stock names, latest prices, and changes. There is a trading panel for buying and selling, with options for market or limit orders, and inputs for stock codes and quantities. The bottom of the page has a settings button, including theme switching, market status toggling, and clearing records.
HTML
Preview
Copy
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TradeSim Pro | Live Market Simulation</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
dark: {
bg: '#0f172a',
card: '#1e293b',
border: '#334155'
},
light: {
bg: '#f8fafc',
card: '#ffffff',
border: '#e2e8f0'
},
trade: {
up: '#10b981', // Emerald 500
down: '#ef4444', // Red 500
accent: '#3b82f6' // Blue 500
}
},
fontFamily: {
mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', "Liberation Mono", "Courier New", 'monospace'],
},
animation: {
'pulse-fast': 'pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'flash-green': 'flashGreen 0.5s ease-out',
'flash-red': 'flashRed 0.5s ease-out',
},
keyframes: {
flashGreen: {
'0%': { backgroundColor: 'rgba(16, 185, 129, 0.2)' },
'100%': { backgroundColor: 'transparent' },
},
flashRed: {
'0%': { backgroundColor: 'rgba(239, 68, 68, 0.2)' },
'100%': { backgroundColor: 'transparent' },
}
}
}
}
}
</script>
<style>
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #64748b;
}
.glass-panel {
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
/* Toggle Switch */
.toggle-checkbox:checked {
right: 0;
border-color: #3b82f6;
}
.toggle-checkbox:checked + .toggle-label {
background-color: #3b82f6;
}
/* CRT Effect for Dark Mode (Optional subtle scanline) */
.dark .crt::before {
content: " ";
display: block;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.1) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.03), rgba(0, 255, 0, 0.01), rgba(0, 0, 255, 0.03));
z-index: 2;
background-size: 100% 2px, 3px 100%;
pointer-events: none;
}
</style>
</head>
<body class="bg-light-bg text-slate-800 dark:bg-dark-bg dark:text-slate-100 transition-colors duration-300 font-sans h-screen flex flex-col overflow-hidden selection:bg-blue-500 selection:text-white">
<!-- Top Navigation / Header -->
<header class="h-16 border-b border-light-border dark:border-dark-border bg-light-card dark:bg-dark-card flex items-center justify-between px-6 shadow-sm z-20 relative">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-lg flex items-center justify-center text-white font-bold shadow-lg shadow-blue-500/30">
T
</div>
<h1 class="text-xl font-bold tracking-tight">Trade<span class="text-blue-500">Sim</span> Pro</h1>
<span id="market-status-badge" class="ml-4 px-2 py-0.5 rounded text-xs font-medium bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-800 flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span> MARKET OPEN
</span>
</div>
<!-- Portfolio Summary (Top) -->
<div class="hidden md:flex items-center gap-8 text-sm">
<div class="flex flex-col items-end">
<span class="text-slate-500 dark:text-slate-400 text-xs uppercase tracking-wider">Available Funds</span>
<span id="header-funds" class="font-mono font-semibold text-lg">$0.00</span>
</div>
<div class="w-px h-8 bg-slate-200 dark:bg-slate-700"></div>
<div class="flex flex-col items-end">
<span class="text-slate-500 dark:text-slate-400 text-xs uppercase tracking-wider">Total Equity</span>
<span id="header-equity" class="font-mono font-bold text-lg">$0.00</span>
</div>
<div class="flex flex-col items-end">
<span class="text-slate-500 dark:text-slate-400 text-xs uppercase tracking-wider">Day P/L</span>
<div class="flex items-center gap-1">
<span id="header-pl-icon"></span>
<span id="header-pl" class="font-mono font-bold text-lg">$0.00</span>
</div>
</div>
</div>
<button id="settings-btn" class="p-2 rounded-full hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors text-slate-500 dark:text-slate-400">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path><circle cx="12" cy="12" r="3"></circle></svg>
</button>
</header>
<!-- Main Layout -->
<div class="flex flex-1 overflow-hidden relative">
<!-- Left Column: Market Watch & Chart (70%) -->
<main class="flex-1 flex flex-col border-r border-light-border dark:border-dark-border min-w-0 bg-light-bg dark:bg-dark-bg relative">
<!-- Chart Area -->
<div class="h-1/2 border-b border-light-border dark:border-dark-border p-4 flex flex-col bg-light-card dark:bg-dark-card m-4 rounded-xl shadow-sm border border-light-border dark:border-dark-border">
<div class="flex justify-between items-center mb-2">
<div>
<h2 id="chart-title" class="text-lg font-bold flex items-center gap-2">
<span class="text-slate-400">Select a stock</span>
</h2>
<p id="chart-subtitle" class="text-xs text-slate-500">--</p>
</div>
<div class="flex gap-2">
<button class="px-3 py-1 text-xs font-medium rounded bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700">1H</button>
<button class="px-3 py-1 text-xs font-medium rounded bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700">1D</button>
<button class="px-3 py-1 text-xs font-medium rounded bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400">1W</button>
</div>
</div>
<div class="flex-1 relative w-full h-full min-h-0">
<canvas id="mainChart"></canvas>
</div>
</div>
<!-- Market Watch Table -->
<div class="flex-1 overflow-hidden flex flex-col bg-light-bg dark:bg-dark-bg">
<div class="px-6 py-3 border-b border-light-border dark:border-dark-border flex justify-between items-center bg-light-card dark:bg-dark-card">
<h3 class="font-semibold text-sm uppercase tracking-wider text-slate-500 dark:text-slate-400">Market Watch</h3>
<div class="relative">
<input type="text" placeholder="Search symbol..." class="bg-slate-100 dark:bg-slate-800 border-none rounded-md text-xs px-3 py-1.5 focus:ring-1 focus:ring-blue-500 outline-none w-48 text-slate-700 dark:text-slate-200">
<svg class="absolute right-2 top-1.5 w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
</div>
</div>
<div class="overflow-y-auto flex-1">
<table class="w-full text-left border-collapse">
<thead class="bg-slate-50 dark:bg-slate-800/50 sticky top-0 z-10 backdrop-blur-sm">
<tr>
<th class="px-6 py-3 text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">Symbol</th>
<th class="px-6 py-3 text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider text-right">Price</th>
<th class="px-6 py-3 text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider text-right">Change</th>
<th class="px-6 py-3 text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider text-right">Vol</th>
<th class="px-6 py-3 text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider text-center">Action</th>
</tr>
</thead>
<tbody id="stock-table-body" class="divide-y divide-light-border dark:divide-dark-border text-sm">
<!-- Rows generated by JS -->
</tbody>
</table>
</div>
</div>
</main>
<!-- Right Column: Trading & Portfolio (30%) -->
<aside class="w-96 bg-light-card dark:bg-dark-card flex flex-col shadow-xl z-10">
<!-- Trading Panel -->
<div class="p-6 border-b border-light-border dark:border-dark-border">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg">Trade</h3>
<div class="flex bg-slate-100 dark:bg-slate-800 p-1 rounded-lg">
<button id="btn-buy-mode" class="px-4 py-1 rounded-md text-xs font-bold bg-white dark:bg-slate-700 shadow-sm text-emerald-600 dark:text-emerald-400 transition-all">BUY</button>
<button id="btn-sell-mode" class="px-4 py-1 rounded-md text-xs font-bold text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 transition-all">SELL</button>
</div>
</div>
<form id="trade-form" onsubmit="return false;" class="space-y-4">
<div>
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Symbol</label>
<div class="relative">
<input type="text" id="input-symbol" class="w-full bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg px-3 py-2 font-mono font-bold text-slate-800 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none uppercase" placeholder="e.g. AAPL">
<div id="symbol-search-results" class="absolute top-full left-0 w-full bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg mt-1 shadow-xl hidden z-50 max-h-40 overflow-y-auto"></div>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Quantity</label>
<input type="number" id="input-qty" min="1" value="10" class="w-full bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg px-3 py-2 font-mono text-slate-800 dark:text-white focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div>
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Order Type</label>
<select id="input-type" class="w-full bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-800 dark:text-white focus:ring-2 focus:ring-blue-500 outline-none appearance-none">
<option value="market">Market</option>
<option value="limit">Limit</option>
</select>
</div>
</div>
<div id="limit-price-container" class="hidden">
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Limit Price</label>
<input type="number" id="input-price" step="0.01" class="w-full bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg px-3 py-2 font-mono text-slate-800 dark:text-white focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div class="pt-2">
<div class="flex justify-between text-xs mb-1 text-slate-500 dark:text-slate-400">
<span>Est. Total</span>
<span id="est-total" class="font-mono font-bold text-slate-700 dark:text-slate-200">$0.00</span>
</div>
<button id="btn-execute" type="button" class="w-full py-3 rounded-lg font-bold text-white shadow-lg transform active:scale-95 transition-all bg-emerald-500 hover:bg-emerald-600 shadow-emerald-500/30">
BUY SHARES
</button>
</div>
</form>
</div>
<!-- Portfolio Holdings -->
<div class="flex-1 flex flex-col overflow-hidden">
<div class="px-6 py-3 border-b border-light-border dark:border-dark-border bg-slate-50 dark:bg-slate-800/30">
<h3 class="font-semibold text-sm uppercase tracking-wider text-slate-500 dark:text-slate-400">Your Positions</h3>
</div>
<div class="overflow-y-auto flex-1 p-4 space-y-3" id="portfolio-list">
<!-- Portfolio items injected here -->
<div class="text-center py-10 text-slate-400 text-sm">
No open positions yet.
</div>
</div>
</div>
</aside>
</div>
<!-- Settings Modal -->
<div id="settings-modal" class="fixed inset-0 z-50 hidden">
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity" id="settings-backdrop"></div>
<div class="absolute right-0 top-0 h-full w-80 bg-light-card dark:bg-dark-card shadow-2xl transform transition-transform translate-x-full duration-300 flex flex-col" id="settings-panel">
<div class="p-6 border-b border-light-border dark:border-dark-border flex justify-between items-center">
<h2 class="text-xl font-bold">Settings</h2>
<button id="close-settings" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
</div>
<div class="p-6 space-y-8 flex-1 overflow-y-auto">
<!-- Theme Toggle -->
<div class="flex items-center justify-between">
<div>
<h4 class="font-medium text-slate-900 dark:text-white">Dark Mode</h4>
<p class="text-xs text-slate-500">Toggle between light and dark themes</p>
</div>
<div class="relative inline-block w-12 mr-2 align-middle select-none transition duration-200 ease-in">
<input type="checkbox" name="toggle" id="theme-toggle" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer transition-all duration-300 left-0 border-slate-300"/>
<label for="theme-toggle" class="toggle-label block overflow-hidden h-6 rounded-full bg-slate-300 cursor-pointer transition-colors duration-300"></label>
</div>
</div>
<!-- Market Status -->
<div class="flex items-center justify-between">
<div>
<h4 class="font-medium text-slate-900 dark:text-white">Market Status</h4>
<p class="text-xs text-slate-500">Pause/Resume price updates</p>
</div>
<button id="toggle-market-btn" class="px-4 py-2 rounded-lg text-sm font-bold bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-800">
PAUSE MARKET
</button>
</div>
<!-- Reset -->
<div class="pt-6 border-t border-light-border dark:border-dark-border">
<h4 class="font-medium text-red-500 mb-2">Danger Zone</h4>
<button id="reset-btn" class="w-full py-2 border border-red-200 dark:border-red-900/50 text-red-500 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors text-sm font-medium">
Reset Portfolio & History
</button>
</div>
</div>
<div class="p-6 border-t border-light-border dark:border-dark-border text-center text-xs text-slate-400">
TradeSim Pro v1.0.2
</div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toast-container" class="fixed bottom-6 right-6 z-50 flex flex-col gap-2 pointer-events-none"></div>
<script>
// --- Application State ---
const state = {
cash: 10000.00,
holdings: {}, // { 'AAPL': { qty: 10, avgPrice: 150.00 } }
marketOpen: true,
theme: 'dark',
selectedStock: null,
tradeMode: 'buy', // 'buy' or 'sell'
stocks: [
{ symbol: 'AAPL', name: 'Apple Inc.', price: 175.50, open: 174.00, vol: 54000000 },
{ symbol: 'MSFT', name: 'Microsoft Corp.', price: 330.20, open: 328.00, vol: 22000000 },
{ symbol: 'GOOGL', name: 'Alphabet Inc.', price: 135.80, open: 136.50, vol: 18000000 },
{ symbol: 'AMZN', name: 'Amazon.com', price: 128.90, open: 127.00, vol: 35000000 },
{ symbol: 'TSLA', name: 'Tesla Inc.', price: 245.60, open: 250.00, vol: 98000000 },
{ symbol: 'NVDA', name: 'NVIDIA Corp.', price: 460.10, open: 455.00, vol: 45000000 },
{ symbol: 'META', name: 'Meta Platforms', price: 305.40, open: 300.00, vol: 15000000 },
{ symbol: 'NFLX', name: 'Netflix Inc.', price: 440.20, open: 435.00, vol: 5000000 },
{ symbol: 'AMD', name: 'Adv. Micro Dev.', price: 105.30, open: 104.00, vol: 60000000 },
{ symbol: 'INTC', name: 'Intel Corp.', price: 35.40, open: 36.00, vol: 30000000 },
],
history: [] // For chart
};
// --- DOM Elements ---
const els = {
headerFunds: document.getElementById('header-funds'),
headerEquity: document.getElementById('header-equity'),
headerPL: document.getElementById('header-pl'),
headerPLIcon: document.getElementById('header-pl-icon'),
tableBody: document.getElementById('stock-table-body'),
chartCanvas: document.getElementById('mainChart'),
chartTitle: document.getElementById('chart-title'),
chartSubtitle: document.getElementById('chart-subtitle'),
inputSymbol: document.getElementById('input-symbol'),
inputQty: document.getElementById('input-qty'),
inputType: document.getElementById('input-type'),
inputPrice: document.getElementById('input-price'),
limitContainer: document.getElementById('limit-price-container'),
estTotal: document.getElementById('est-total'),
btnExecute: document.getElementById('btn-execute'),
btnBuyMode: document.getElementById('btn-buy-mode'),
btnSellMode: document.getElementById('btn-sell-mode'),
portfolioList: document.getElementById('portfolio-list'),
settingsModal: document.getElementById('settings-modal'),
settingsPanel: document.getElementById('settings-panel'),
settingsBackdrop: document.getElementById('settings-backdrop'),
settingsBtn: document.getElementById('settings-btn'),
closeSettingsBtn: document.getElementById('close-settings'),
themeToggle: document.getElementById('theme-toggle'),
toggleMarketBtn: document.getElementById('toggle-market-btn'),
marketStatusBadge: document.getElementById('market-status-badge'),
resetBtn: document.getElementById('reset-btn'),
symbolSearchResults: document.getElementById('symbol-search-results')
};
let chartInstance = null;
// --- Initialization ---
function init() {
// Generate initial history for charts
state.stocks.forEach(s => {
s.history = generateRandomHistory(s.price, 50);
s.prevPrice = s.price;
});
// Set initial selected stock
selectStock(state.stocks[0]);
// Render
updatePortfolioUI();
renderMarketTable();
initChart();
// Start Simulation
setInterval(simulateMarket, 2000);
// Event Listeners
setupEventListeners();
// Initial UI Update
updateTradeUI();
}
// --- Logic: Market Simulation ---
function generateRandomHistory(basePrice, count) {
let data = [];
let price = basePrice * 0.9;
for(let i=0; i<count; i++) {
price = price * (1 + (Math.random() * 0.04 - 0.02));
data.push(price);
}
// Ensure last point connects to current
data[data.length-1] = basePrice;
return data;
}
function simulateMarket() {
if (!state.marketOpen) return;
state.stocks.forEach(stock => {
stock.prevPrice = stock.price;
// Random walk -2% to +2%
const change = (Math.random() * 0.04) - 0.02;
stock.price = stock.price * (1 + change);
// Update history
stock.history.shift();
stock.history.push(stock.price);
// Update Volume slightly
stock.vol += Math.floor(Math.random() * 1000);
});
renderMarketTable();
updateChart();
updatePortfolioUI(); // To update live P/L on holdings
}
// --- Logic: Trading ---
function executeTrade() {
const symbol = els.inputSymbol.value.toUpperCase();
const qty = parseInt(els.inputQty.value);
const type = els.inputType.value;
const mode = state.tradeMode;
if (!symbol || !qty || qty <= 0) {
showToast('Invalid input', 'error');
return;
}
const stock = state.stocks.find(s => s.symbol === symbol);
if (!stock) {
showToast('Symbol not found', 'error');
return;
}
let price = stock.price;
if (type === 'limit') {
const limit = parseFloat(els.inputPrice.value);
if (!limit) {
showToast('Enter a limit price', 'error');
return;
}
// Simulation: Limit orders fill immediately if price is "reasonable" (within 1%)
if (Math.abs(limit - price) / price > 0.01) {
showToast(`Limit order for ${symbol} placed (Simulated)`, 'info');
return;
}
price = limit;
}
const total = price * qty;
if (mode === 'buy') {
if (total > state.cash) {
showToast('Insufficient funds', 'error');
return;
}
state.cash -= total;
if (!state.holdings[symbol]) {
state.holdings[symbol] = { qty: 0, avgPrice: 0 };
}
// Weighted average cost basis
const oldVal = state.holdings[symbol].qty * state.holdings[symbol].avgPrice;
const newVal = qty * price;
state.holdings[symbol].qty += qty;
state.holdings[symbol].avgPrice = (oldVal + newVal) / state.holdings[symbol].qty;
showToast(`Bought ${qty} shares of ${symbol} @ $${price.toFixed(2)}`, 'success');
} else {
// Sell
if (!state.holdings[symbol] || state.holdings[symbol].qty < qty) {
showToast('Insufficient shares', 'error');
return;
}
state.cash += total;
state.holdings[symbol].qty -= qty;
if (state.holdings[symbol].qty === 0) {
delete state.holdings[symbol];
}
showToast(`Sold ${qty} shares of ${symbol} @ $${price.toFixed(2)}`, 'success');
}
updatePortfolioUI();
renderPortfolioList();
}
// --- UI: Rendering ---
function renderMarketTable() {
els.tableBody.innerHTML = '';
state.stocks.forEach(stock => {
const change = stock.price - stock.open;
const changePct = (change / stock.open) * 100;
const isUp = change >= 0;
const flashClass = stock.price > stock.prevPrice ? 'animate-flash-green' : (stock.price < stock.prevPrice ? 'animate-flash-red' : '');
const tr = document.createElement('tr');
tr.className = `hover:bg-slate-50 dark:hover:bg-slate-800/50 cursor-pointer transition-colors border-b border-light-border dark:border-dark-border ${flashClass}`;
tr.onclick = () => selectStock(stock);
if (state.selectedStock?.symbol === stock.symbol) {
tr.classList.add('bg-blue-50', 'dark:bg-blue-900/20');
}
tr.innerHTML = `
<td class="px-6 py-3">
<div class="flex items-center">
<div class="h-8 w-8 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-xs font-bold mr-3 text-slate-600 dark:text-slate-300">
${stock.symbol[0]}
</div>
<div>
<div class="font-bold text-slate-900 dark:text-white">${stock.symbol}</div>
<div class="text-xs text-slate-500">${stock.name}</div>
</div>
</div>
</td>
<td class="px-6 py-3 text-right font-mono font-medium text-slate-700 dark:text-slate-200">
$${stock.price.toFixed(2)}
</td>
<td class="px-6 py-3 text-right font-mono font-medium ${isUp ? 'text-emerald-500' : 'text-red-500'}">
${isUp ? '+' : ''}${change.toFixed(2)} (${changePct.toFixed(2)}%)
</td>
<td class="px-6 py-3 text-right text-xs text-slate-500 font-mono">
${(stock.vol / 1000000).toFixed(1)}M
</td>
<td class="px-6 py-3 text-center">
<button onclick="event.stopPropagation(); quickTrade('${stock.symbol}')" class="text-xs bg-slate-100 dark:bg-slate-800 hover:bg-blue-100 dark:hover:bg-blue-900/30 text-blue-600 dark:text-blue-400 px-2 py-1 rounded transition-colors">
Trade
</button>
</td>
`;
els.tableBody.appendChild(tr);
});
}
function renderPortfolioList() {
els.portfolioList.innerHTML = '';
const symbols = Object.keys(state.holdings);
if (symbols.length === 0) {
els.portfolioList.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-slate-400"><svg class="w-12 h-12 mb-2 opacity-20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg><p class="text-sm">No positions</p></div>`;
return;
}
symbols.forEach(sym => {
const pos = state.holdings[sym];
const stock = state.stocks.find(s => s.symbol === sym);
const currentPrice = stock ? stock.price : pos.avgPrice;
const marketVal = pos.qty * currentPrice;
const costBasis = pos.qty * pos.avgPrice;
const pl = marketVal - costBasis;
const plPct = (pl / costBasis) * 100;
const isProfit = pl >= 0;
const div = document.createElement('div');
div.className = 'bg-slate-50 dark:bg-slate-800/50 rounded-lg p-3 border border-slate-100 dark:border-slate-700';
div.innerHTML = `
<div class="flex justify-between items-start mb-1">
<div>
<span class="font-bold text-slate-900 dark:text-white">${sym}</span>
<span class="text-xs text-slate-500 ml-1">${pos.qty} shares</span>
</div>
<div class="text-right">
<div class="font-mono font-medium text-slate-800 dark:text-slate-200">$${marketVal.toFixed(2)}</div>
<div class="text-xs font-mono ${isProfit ? 'text-emerald-500' : 'text-red-500'}">
${isProfit ? '+' : ''}$${pl.toFixed(2)} (${plPct.toFixed(1)}%)
</div>
</div>
</div>
<div class="w-full bg-slate-200 dark:bg-slate-700 h-1 rounded-full mt-2 overflow-hidden">
<div class="bg-blue-500 h-1 rounded-full" style="width: ${Math.min(Math.abs(plPct), 100)}%"></div>
</div>
`;
els.portfolioList.appendChild(div);
});
}
function updatePortfolioUI() {
// Calculate Totals
let marketValue = 0;
let dayPL = 0;
Object.keys(state.holdings).forEach(sym => {
const pos = state.holdings[sym];
const stock = state.stocks.find(s => s.symbol === sym);
if (stock) {
marketValue += pos.qty * stock.price;
dayPL += pos.qty * (stock.price - stock.open);
}
});
const totalEquity = state.cash + marketValue;
// Update Header
els.headerFunds.innerText = formatCurrency(state.cash);
els.headerEquity.innerText = formatCurrency(totalEquity);
const plElem = els.headerPL;
const plIcon = els.headerPLIcon;
plElem.innerText = (dayPL >= 0 ? '+' : '') + formatCurrency(dayPL);
plElem.className = `font-mono font-bold text-lg ${dayPL >= 0 ? 'text-emerald-500' : 'text-red-500'}`;
plIcon.innerHTML = dayPL >= 0
? '<svg class="w-4 h-4 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path></svg>'
: '<svg class="w-4 h-4 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 17h8m0 0V9m0 8l-8-8-4 4-6-6"></path></svg>';
renderPortfolioList();
}
function updateTradeUI() {
const isBuy = state.tradeMode === 'buy';
// Toggle Buttons
if (isBuy) {
els.btnBuyMode.className = "px-4 py-1 rounded-md text-xs font-bold bg-white dark:bg-slate-700 shadow-sm text-emerald-600 dark:text-emerald-400 transition-all";
els.btnSellMode.className = "px-4 py-1 rounded-md text-xs font-bold text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 transition-all";
els.btnExecute.className = "w-full py-3 rounded-lg font-bold text-white shadow-lg transform active:scale-95 transition-all bg-emerald-500 hover:bg-emerald-600 shadow-emerald-500/30";
els.btnExecute.innerText = "BUY SHARES";
} else {
els.btnSellMode.className = "px-4 py-1 rounded-md text-xs font-bold bg-white dark:bg-slate-700 shadow-sm text-red-600 dark:text-red-400 transition-all";
els.btnBuyMode.className = "px-4 py-1 rounded-md text-xs font-bold text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 transition-all";
els.btnExecute.className = "w-full py-3 rounded-lg font-bold text-white shadow-lg transform active:scale-95 transition-all bg-red-500 hover:bg-red-600 shadow-red-500/30";
els.btnExecute.innerText = "SELL SHARES";
}
// Calculation
const qty = parseInt(els.inputQty.value) || 0;
const symbol = els.inputSymbol.value.toUpperCase();
const stock = state.stocks.find(s => s.symbol === symbol);
let price = 0;
if (els.inputType.value === 'market' && stock) {
price = stock.price;
} else {
price = parseFloat(els.inputPrice.value) || 0;
}
const total = qty * price;
els.estTotal.innerText = formatCurrency(total);
}
// --- Chart.js Integration ---
function initChart() {
const ctx = els.chartCanvas.getContext('2d');
// Gradient
const gradient = ctx.createLinearGradient(0, 0, 0, 400);
gradient.addColorStop(0, 'rgba(59, 130, 246, 0.5)'); // Blue top
gradient.addColorStop(1, 'rgba(59, 130, 246, 0)'); // Transparent bottom
chartInstance = new Chart(ctx, {
type: 'line',
data: {
labels: Array(50).fill(''), // Dummy labels
datasets: [{
label: 'Price',
data: [],
borderColor: '#3b82f6',
backgroundColor: gradient,
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 4,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
return '$' + context.parsed.y.toFixed(2);
}
}
}
},
scales: {
x: { display: false },
y: {
display: true,
position: 'right',
grid: {
color: 'rgba(148, 163, 184, 0.1)'
},
ticks: {
color: '#64748b',
callback: function(value) { return '$' + value; }
}
}
},
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false
}
}
});
}
function updateChart() {
if (!state.selectedStock || !chartInstance) return;
const stock = state.selectedStock;
chartInstance.data.datasets[0].data = stock.history;
// Dynamic color based on trend
const isUp = stock.price >= stock.open;
const color = isUp ? '#10b981' : '#ef4444';
chartInstance.data.datasets[0].borderColor = color;
// Update gradient
const ctx = els.chartCanvas.getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, 0, 400);
gradient.addColorStop(0, isUp ? 'rgba(16, 185, 129, 0.5)' : 'rgba(239, 68, 68, 0.5)');
gradient.addColorStop(1, 'rgba(0,0,0,0)');
chartInstance.data.datasets[0].backgroundColor = gradient;
chartInstance.update('none'); // 'none' for performance
}
function selectStock(stock) {
state.selectedStock = stock;
els.inputSymbol.value = stock.symbol;
// Update Chart Header
const change = stock.price - stock.open;
const changePct = (change / stock.open) * 100;
const isUp = change >= 0;
els.chartTitle.innerHTML = `
<span class="text-slate-900 dark:text-white">${stock.symbol}</span>
<span class="text-sm font-normal text-slate-500 ml-2">${stock.name}</span>
<span class="ml-3 text-sm font-mono ${isUp ? 'text-emerald-500' : 'text-red-500'}">
${isUp ? '▲' : '▼'} ${Math.abs(change).toFixed(2)} (${Math.abs(changePct).toFixed(2)}%)
</span>
`;
els.chartSubtitle.innerText = `Last updated: ${new Date().toLocaleTimeString()}`;
updateChart();
renderMarketTable(); // To update highlighting
}
// --- Utilities ---
function formatCurrency(num) {
return '$' + num.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,');
}
function showToast(message, type = 'info') {
const toast = document.createElement('div');
const colors = {
success: 'bg-emerald-500',
error: 'bg-red-500',
info: 'bg-blue-500'
};
toast.className = `${colors[type]} text-white px-4 py-3 rounded-lg shadow-lg transform transition-all duration-300 translate-y-10 opacity-0 flex items-center gap-2 pointer-events-auto min-w-[200px]`;
toast.innerHTML = `
<span class="font-medium text-sm">${message}</span>
`;
document.getElementById('toast-container').appendChild(toast);
// Animate in
requestAnimationFrame(() => {
toast.classList.remove('translate-y-10', 'opacity-0');
});
// Remove after 3s
setTimeout(() => {
toast.classList.add('translate-y-10', 'opacity-0');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
function quickTrade(symbol) {
els.inputSymbol.value = symbol;
// Scroll to trade panel on mobile if needed, or just focus
els.inputQty.focus();
showToast(`Selected ${symbol}. Review quantity and click Buy/Sell.`, 'info');
}
// --- Event Listeners ---
function setupEventListeners() {
// Trade Form Inputs
[els.inputQty, els.inputSymbol, els.inputPrice, els.inputType].forEach(el => {
el.addEventListener('input', updateTradeUI);
});
// Buy/Sell Mode Toggle
els.btnBuyMode.addEventListener('click', () => {
state.tradeMode = 'buy';
updateTradeUI();
});
els.btnSellMode.addEventListener('click', () => {
state.tradeMode = 'sell';
updateTradeUI();
});
// Execute Trade
els.btnExecute.addEventListener('click', executeTrade);
// Limit Order Toggle
els.inputType.addEventListener('change', (e) => {
if (e.target.value === 'limit') {
els.limitContainer.classList.remove('hidden');
if(state.selectedStock) els.inputPrice.value = state.selectedStock.price.toFixed(2);
} else {
els.limitContainer.classList.add('hidden');
}
updateTradeUI();
});
// Settings Modal
els.settingsBtn.addEventListener('click', () => {
els.settingsModal.classList.remove('hidden');
setTimeout(() => {
els.settingsPanel.classList.remove('translate-x-full');
}, 10);
});
const closeSettings = () => {
els.settingsPanel.classList.add('translate-x-full');
setTimeout(() => {
els.settingsModal.classList.add('hidden');
}, 300);
};
els.closeSettingsBtn.addEventListener('click', closeSettings);
els.settingsBackdrop.addEventListener('click', closeSettings);
// Theme Toggle
els.themeToggle.checked = document.documentElement.classList.contains('dark');
els.themeToggle.addEventListener('change', (e) => {
if (e.target.checked) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
});
// Market Toggle
els.toggleMarketBtn.addEventListener('click', () => {
state.marketOpen = !state.marketOpen;
if (state.marketOpen) {
els.toggleMarketBtn.innerText = "PAUSE MARKET";
els.toggleMarketBtn.className = "px-4 py-2 rounded-lg text-sm font-bold bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-800";
els.marketStatusBadge.innerHTML = `<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span> MARKET OPEN`;
showToast('Market Resumed', 'success');
} else {
els.toggleMarketBtn.innerText = "RESUME MARKET";
els.toggleMarketBtn.className = "px-4 py-2 rounded-lg text-sm font-bold bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400 border border-amber-200 dark:border-amber-800";
els.marketStatusBadge.innerHTML = `<span class="w-1.5 h-1.5 rounded-full bg-amber-500"></span> MARKET PAUSED`;
showToast('Market Paused', 'info');
}
});
// Reset
els.resetBtn.addEventListener('click', () => {
if(confirm('Reset all data? This cannot be undone.')) {
state.cash = 10000;
state.holdings = {};
updatePortfolioUI();
showToast('Portfolio Reset', 'success');
}
});
// Symbol Search (Simple Filter)
els.inputSymbol.addEventListener('input', (e) => {
const val = e.target.value.toUpperCase();
if (val.length < 1) {
els.symbolSearchResults.classList.add('hidden');
return;
}
const matches = state.stocks.filter(s => s.symbol.includes(val) || s.name.toUpperCase().includes(val));
els.symbolSearchResults.innerHTML = '';
if (matches.length > 0) {
els.symbolSearchResults.classList.remove('hidden');
matches.forEach(s => {
const div = document.createElement('div');
div.className = "px-4 py-2 hover:bg-slate-100 dark:hover:bg-slate-700 cursor-pointer text-sm text-slate-700 dark:text-slate-200";
div.innerText = `${s.symbol} - ${s.name}`;
div.onclick = () => {
els.inputSymbol.value = s.symbol;
els.symbolSearchResults.classList.add('hidden');
selectStock(s);
updateTradeUI();
};
els.symbolSearchResults.appendChild(div);
});
} else {
els.symbolSearchResults.classList.add('hidden');
}
});
// Hide search results on click outside
document.addEventListener('click', (e) => {
if (!els.inputSymbol.contains(e.target) && !els.symbolSearchResults.contains(e.target)) {
els.symbolSearchResults.classList.add('hidden');
}
});
}
// Run
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>