stock market simulation
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Virtual Stock Simulator - Live P&L</title>
<style>
:root {
--bg: #0f172a;
--card: #1e293b;
--text: #f8fafc;
--green: #10b981;
--red: #ef4444;
--blue: #3b82f6;
}
body {
background-color: var(--bg);
color: var(--text);
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
}
.app-container {
width: 100%;
max-width: 650px;
}
header {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
border-bottom: 2px solid #334155;
padding-bottom: 15px;
margin-bottom: 25px;
}
h1 { margin: 0; font-size: 26px; grid-column: span 2; }
.financial-card {
padding: 12px 20px;
border-radius: 8px;
}
.balance-box {
background: linear-gradient(135deg, #1e3a8a, var(--blue));
}
.pnl-box {
background-color: var(--card);
border: 1px solid #334155;
}
.financial-card p { margin: 0; font-size: 11px; opacity: 0.8; text-transform: uppercase; letter-spacing: 0.5px; }
.financial-card h2 { margin: 5px 0 0 0; font-size: 20px; }
/* Search Component */
.search-box {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
input[type="text"] {
flex: 1;
padding: 12px;
background: var(--card);
border: 1px solid #475569;
color: white;
border-radius: 6px;
font-size: 16px;
}
button {
padding: 12px 20px;
border: none;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
font-size: 14px;
}
button:hover { opacity: 0.9; }
.btn-search { background-color: var(--blue); color: white; }
.btn-buy { background-color: var(--green); color: white; }
.btn-sell { background-color: var(--red); color: white; }
/* Quote Display Card */
.stock-card {
background-color: var(--card);
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
display: none;
}
.stock-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.stock-header h3 { margin: 0; font-size: 24px; }
.price-tag { font-size: 24px; font-weight: bold; color: var(--green); }
.trade-controls {
display: flex;
align-items: center;
gap: 10px;
border-top: 1px solid #334155;
padding-top: 15px;
}
input[type="number"] {
width: 80px;
padding: 10px;
background: var(--bg);
border: 1px solid #475569;
color: white;
border-radius: 6px;
}
/* Portfolio Layout */
.portfolio-section h3 {
border-bottom: 2px solid #334155;
padding-bottom: 8px;
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.refresh-badge {
font-size: 11px;
background-color: #334155;
padding: 4px 8px;
border-radius: 20px;
color: #94a3b8;
}
.portfolio-item {
background-color: var(--card);
padding: 15px;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
border-left: 4px solid #475569;
transition: border-color 0.3s;
}
.portfolio-item.profit { border-left-color: var(--green); }
.portfolio-item.loss { border-left-color: var(--red); }
.portfolio-item h4 { margin: 0 0 5px 0; font-size: 18px; }
.portfolio-item p { margin: 0; font-size: 13px; color: #94a3b8; }
.profit-text { color: var(--green) !important; font-weight: bold; }
.loss-text { color: var(--red) !important; font-weight: bold; }
.empty-state { text-align: center; color: #64748b; font-style: italic; margin-top: 20px; }
.loading { color: #94a3b8; font-style: italic; display: none; margin-bottom: 15px; }
</style>
</head>
<body>
<div class="app-container">
<header>
<h1>PaperTrade Live Dashboard</h1>
<div class="financial-card balance-box">
<p>Cash Balance</p>
<h2 id="balance-display">500,000.00 Coins</h2>
</div>
<div class="financial-card pnl-box">
<p>Total Live P&L</p>
<h2 id="total-pnl-display">0.00 Coins</h2>
</div>
</header>
<div class="search-box">
<input type="text" id="search-input" placeholder="Ticker (e.g., AAPL, TSLA, BTC-USD)">
<button class="btn-search" onclick="searchStock()">Search</button>
</div>
<div class="loading" id="loading-text">Fetching live market streaming feeds...</div>
<div class="stock-card" id="stock-display-card">
<div class="stock-header">
<div>
<h3 id="display-ticker">AAPL</h3>
<span style="font-size:12px; color:#94a3b8;">Real-Time Yahoo Price Execution</span>
</div>
<div class="price-tag" id="display-price">0.00 Coins</div>
</div>
<div class="trade-controls">
<label for="trade-qty">Qty:</label>
<input type="number" id="trade-qty" min="1" value="1">
<button class="btn-buy" onclick="executeBuy()">Buy</button>
<button class="btn-sell" onclick="executeSell()">Sell</button>
</div>
</div>
<div class="portfolio-section">
<h3>
<span>Your Holdings / Portfolio</span>
<span class="refresh-badge" id="refresh-status">Live Tracking Active</span>
</h3>
<div id="portfolio-list">
<p class="empty-state">Your portfolio is currently empty.</p>
</div>
</div>
</div>
<script>
// --- Application State ---
let walletBalance = 500000;
let portfolio = {}; // Format: { "AAPL": { qty: 5, avgPrice: 150, currentPrice: 150 } }
let activeStock = { ticker: "", price: 0 };
// --- Core Data Engines ---
// 1. Core Fetch utility targeting hidden Yahoo server endpoints
async function fetchYahooPrice(ticker) {
// 1. We use the standard format for allorigins
const proxyUrl = "https://api.allorigins.win/get?url=";
const yahooUrl = `https://query1.finance.yahoo.com/v8/finance/chart/${ticker}`;
try {
// 2. encodeURIComponent cleans up the characters so the proxy understands it cleanly
const response = await fetch(proxyUrl + encodeURIComponent(yahooUrl));
if (!response.ok) throw new Error("Network query failed");
const data = await response.json();
// 3. Since we used the /get endpoint, allorigins wraps the answer inside data.contents
const yahooData = JSON.parse(data.contents);
return yahooData.chart.result[0].meta.regularMarketPrice;
} catch (err) {
console.warn(`Fetch err for ${ticker}:`, err);
return null;
}
}
// 2. Search Handler Function
async function searchStock() {
const input = document.getElementById("search-input").value.trim().toUpperCase();
if (!input) return alert("Please enter a valid stock ticker symbol!");
const loader = document.getElementById("loading-text");
const card = document.getElementById("stock-display-card");
loader.style.display = "block";
card.style.display = "none";
const price = await fetchYahooPrice(input);
loader.style.display = "none";
if (price === null) {
alert("Verification Failure: Unable to fetch price data. Make sure your ticker is valid and your demo access is turned on at: https://cors-anywhere.herokuapp.com/corsdemo");
return;
}
activeStock.ticker = input;
activeStock.price = price;
document.getElementById("display-ticker").innerText = activeStock.ticker;
document.getElementById("display-price").innerText = `${activeStock.price.toLocaleString(undefined, {minimumFractionDigits: 2})} Coins`;
card.style.display = "block";
}
// 3. Trade Routing Execution: BUY
function executeBuy() {
const qty = parseInt(document.getElementById("trade-qty").value);
if (isNaN(qty) || qty <= 0) return alert("Please provide a real share quantity target.");
const totalCost = activeStock.price * qty;
if (totalCost > walletBalance) return alert("Transaction Aborted: Insufficient virtual cash pools!");
walletBalance -= totalCost;
if (portfolio[activeStock.ticker]) {
const existingQty = portfolio[activeStock.ticker].qty;
const currentTotalInvested = (existingQty * portfolio[activeStock.ticker].avgPrice) + totalCost;
portfolio[activeStock.ticker].qty += qty;
portfolio[activeStock.ticker].avgPrice = currentTotalInvested / portfolio[activeStock.ticker].qty;
portfolio[activeStock.ticker].currentPrice = activeStock.price; // match initial tracking price
} else {
portfolio[activeStock.ticker] = {
qty: qty,
avgPrice: activeStock.price,
currentPrice: activeStock.price
};
}
updateDashboard();
alert(`Asset Acquired: Purchased ${qty} shares of ${activeStock.ticker}!`);
}
// 4. Trade Routing Execution: SELL
function executeSell() {
const qty = parseInt(document.getElementById("trade-qty").value);
if (isNaN(qty) || qty <= 0) return alert("Please provide a real share quantity target.");
const ticker = activeStock.ticker;
if (!portfolio[ticker] || portfolio[ticker].qty < qty) {
return alert(`Transaction Aborted: Short position limits exceeded for ${ticker}!`);
}
const revenue = activeStock.price * qty;
walletBalance += revenue;
portfolio[ticker].qty -= qty;
if (portfolio[ticker].qty === 0) {
delete portfolio[ticker];
}
updateDashboard();
alert(`Asset Liquidated: Successfully sold ${qty} shares of ${ticker}!`);
}
// 5. Interface Synchronization and P&L Math Computation Engine
function updateDashboard() {
// Balance Rendering
document.getElementById("balance-display").innerText = `${walletBalance.toLocaleString(undefined, {minimumFractionDigits: 2})} Coins`;
const container = document.getElementById("portfolio-list");
container.innerHTML = "";
const assets = Object.keys(portfolio);
if (assets.length === 0) {
container.innerHTML = `<p class="empty-state">Your portfolio is currently empty.</p>`;
document.getElementById("total-pnl-display").innerText = "0.00 Coins";
document.getElementById("total-pnl-display").className = "";
return;
}
let globalPortfolioPnL = 0;
assets.forEach(symbol => {
const holding = portfolio[symbol];
// Math Processing Metrics
const totalCostBasis = holding.qty * holding.avgPrice;
const currentMarketValue = holding.qty * holding.currentPrice;
const individualStockPnL = currentMarketValue - totalCostBasis;
globalPortfolioPnL += individualStockPnL;
// Visual Layout Styling Modifiers based on profit margins
let pnlClass = "";
let rowIndicatorStyle = "";
if (individualStockPnL > 0) {
pnlClass = "profit-text";
rowIndicatorStyle = "profit";
} else if (individualStockPnL < 0) {
pnlClass = "loss-text";
rowIndicatorStyle = "loss";
}
const card = document.createElement("div");
card.className = `portfolio-item ${rowIndicatorStyle}`;
card.innerHTML = `
<div>
<h4>${symbol}</h4>
<p>Units: <strong>${holding.qty}</strong> | Avg Cost: ${holding.avgPrice.toFixed(2)}</p>
<p>Live Price: <span style="color:white; font-weight:500;">${holding.currentPrice.toFixed(2)}</span></p>
</div>
<div style="text-align: right;">
<p class="${pnlClass}" style="font-size:16px;">
${individualStockPnL >= 0 ? '+' : ''}${individualStockPnL.toLocaleString(undefined, {minimumFractionDigits: 2})}
</p>
<p style="font-size: 11px;">Equity Value: ${currentMarketValue.toLocaleString(undefined, {minimumFractionDigits: 2})}</p>
</div>
`;
container.appendChild(card);
});
// Update Top Master Total P&L Card Summary
const masterPnLDisplay = document.getElementById("total-pnl-display");
masterPnLDisplay.innerText = `${globalPortfolioPnL >= 0 ? '+' : ''}${globalPortfolioPnL.toLocaleString(undefined, {minimumFractionDigits: 2})} Coins`;
if (globalPortfolioPnL > 0) {
masterPnLDisplay.className = "profit-text";
} else if (globalPortfolioPnL < 0) {
masterPnLDisplay.className = "loss-text";
} else {
masterPnLDisplay.className = "";
}
}
// --- Background Real-Time Synchronization Engine ---
// Runs an asynchronous pricing update loop across all active assets every 4 seconds
setInterval(async function() {
const assetsToUpdate = Object.keys(portfolio);
if (assetsToUpdate.length === 0 && !activeStock.ticker) return;
const badge = document.getElementById("refresh-status");
badge.innerText = "Syncing Feeds...";
badge.style.backgroundColor = "#1e3a8a";
// 1. Update the portfolio assets prices
for (const symbol of assetsToUpdate) {
const freshPrice = await fetchYahooPrice(symbol);
if (freshPrice !== null) {
portfolio[symbol].currentPrice = freshPrice;
}
}
// 2. Also keep the active searched stock matching the background updates
if (activeStock.ticker) {
const freshActivePrice = await fetchYahooPrice(activeStock.ticker);
if (freshActivePrice !== null) {
activeStock.price = freshActivePrice;
document.getElementById("display-price").innerText = `${activeStock.price.toLocaleString(undefined, {minimumFractionDigits: 2})} Coins`;
}
}
// 3. Re-render display states
updateDashboard();
badge.innerText = "Live Tracking Active";
badge.style.backgroundColor = "#334155";
}, 9000); // 4-second interval loop balances live speed with server limits
</script>
</body>
</html>
Dhaval Visariya
AMFI Registered Distributor (ARN-327925) | Software Developer & Finance Enthusiast
Connect with me:
LinkedIn |
Telegram |
YouTube |
GitHub |
Medium
Disclaimer: I am an AMFI Registered Mutual Fund Distributor. Views expressed here are for educational purposes. Mutual Fund investments are subject to market risks, read all scheme related documents carefully.
📧 Contact: info.apollofinancialservices@gmail.com
Comments
Post a Comment