| <!doctype html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <title>Order Entry</title> |
| <style> |
| body { font-family: Arial, sans-serif; margin: 20px; background: #f7f7f7; } |
| h1 { display: flex; align-items: center; gap: 15px; margin-bottom: 20px; } |
| h2 { margin: 5px 0 10px; font-size: 16px; } |
| |
| .panel { |
| background: #fff; |
| border-radius: 8px; |
| padding: 15px; |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
| margin-bottom: 20px; |
| } |
| .grid { display: grid; grid-template-columns: 1fr 1fr; grid-gap: 20px; } |
| .panel-inner { |
| background: #fff; |
| border-radius: 8px; |
| padding: 15px; |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
| display: flex; |
| flex-direction: column; |
| min-height: 350px; |
| } |
| |
| |
| .status { |
| display: inline-flex; |
| align-items: center; |
| gap: 6px; |
| padding: 4px 12px; |
| border-radius: 20px; |
| font-size: 12px; |
| font-weight: bold; |
| } |
| .status .dot { width: 10px; height: 10px; border-radius: 50%; } |
| .status.connected { background: #d4edda; color: #155724; } |
| .status.connected .dot { background: #28a745; } |
| .status.disconnected { background: #f8d7da; color: #721c24; } |
| .status.disconnected .dot { background: #dc3545; } |
| @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } |
| .status.connecting { background: #fff3cd; color: #856404; } |
| .status.connecting .dot { background: #ffc107; animation: pulse 1s infinite; } |
| |
| |
| .btn-reconnect { |
| padding: 4px 12px; |
| background: #ff9800; |
| color: #fff; |
| border: none; |
| border-radius: 20px; |
| cursor: pointer; |
| font-size: 12px; |
| font-weight: bold; |
| display: none; |
| } |
| .btn-reconnect:hover { background: #e68900; } |
| |
| |
| .order-grid { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 12px; |
| } |
| .form-group { } |
| .form-group.full { grid-column: 1 / -1; } |
| .form-group label { display: block; font-size: 12px; color: #666; margin-bottom: 4px; } |
| .form-group input, |
| .form-group select { |
| width: 100%; |
| padding: 8px; |
| border: 1px solid #ccc; |
| border-radius: 4px; |
| font-size: 13px; |
| box-sizing: border-box; |
| } |
| .btn-send { |
| width: 100%; |
| padding: 10px; |
| background: #2196F3; |
| color: #fff; |
| border: none; |
| border-radius: 4px; |
| cursor: pointer; |
| font-size: 14px; |
| font-weight: bold; |
| } |
| .btn-send:hover { background: #1976D2; } |
| |
| |
| #order-status { |
| padding: 8px 12px; |
| border-radius: 4px; |
| font-size: 13px; |
| margin-top: 10px; |
| display: none; |
| } |
| #order-status.success { background: #d4edda; color: #155724; display: block; } |
| #order-status.error { background: #f8d7da; color: #721c24; display: block; } |
| |
| |
| table { width: 100%; border-collapse: collapse; font-size: 13px; } |
| th, td { border: 1px solid #ccc; padding: 5px 8px; text-align: center; } |
| th { background: #f0f0f0; font-weight: bold; } |
| tbody tr:hover { background: #f5f5f5; } |
| .bid { color: #2e7d32; font-weight: bold; } |
| .ask { color: #c62828; font-weight: bold; } |
| |
| @keyframes highlight { from { background: #c8e6c9; } to { background: transparent; } } |
| .new-row { animation: highlight 2s ease-out; } |
| |
| .refresh-info { font-size: 11px; color: #999; text-align: right; margin-top: 6px; } |
| </style> |
| </head> |
| <body> |
|
|
| <h1> |
| Order Entry |
| <span id="status-badge" class="status connecting"> |
| <span class="dot"></span> |
| <span id="status-text">Connecting...</span> |
| </span> |
| <button id="reconnect-btn" class="btn-reconnect" onclick="reconnect()">Reconnect</button> |
| <a onclick="window.location.href='/'" style="margin-left:auto; padding:4px 14px; background:#6c757d; color:#fff; border-radius:20px; font-size:12px; font-weight:bold; text-decoration:none; cursor:pointer;">← Dashboard</a> |
| </h1> |
|
|
| |
| <div class="panel"> |
| <h2>Place Order</h2> |
| <form id="orderForm" onsubmit="sendOrder(event)"> |
| <div class="order-grid"> |
| <div class="form-group"> |
| <label>Order ID <span style="color:#bbb;">(optional)</span></label> |
| <input name="order_id" placeholder="Auto-generated"> |
| </div> |
| <div class="form-group"> |
| <label>Symbol</label> |
| <select id="symbol-select" name="symbol" onchange="loadBook()"> |
| <option value="">Loading...</option> |
| </select> |
| </div> |
| <div class="form-group"> |
| <label>Side</label> |
| <select name="type"> |
| <option value="buy">BUY</option> |
| <option value="sell">SELL</option> |
| </select> |
| </div> |
| <div class="form-group"> |
| <label>Quantity</label> |
| <input name="quantity" type="number" value="10"> |
| </div> |
| <div class="form-group full"> |
| <label>Price</label> |
| <input name="price" type="number" step="0.01" value="100.00"> |
| </div> |
| <div class="form-group full"> |
| <button type="submit" class="btn-send">Send Order</button> |
| </div> |
| </div> |
| </form> |
| <div id="order-status"></div> |
| </div> |
|
|
| |
| <div class="grid"> |
|
|
| <div class="panel-inner"> |
| <h2>Order Book</h2> |
| <div style="flex-grow:1; overflow-y:auto;"> |
| <table> |
| <thead> |
| <tr> |
| <th class="bid">Bid Qty</th> |
| <th class="bid">Bid Price</th> |
| <th class="ask">Ask Price</th> |
| <th class="ask">Ask Qty</th> |
| </tr> |
| </thead> |
| <tbody id="book-body"> |
| <tr><td colspan="4" style="color:#999;">Loading...</td></tr> |
| </tbody> |
| </table> |
| </div> |
| <div class="refresh-info" id="book-updated">--</div> |
| </div> |
|
|
| <div class="panel-inner"> |
| <h2>Trades <span id="trade-count" style="font-size:13px; color:#999; font-weight:normal;"></span></h2> |
| <div style="flex-grow:1; overflow-y:auto;"> |
| <table> |
| <thead> |
| <tr> |
| <th>Symbol</th> |
| <th>Qty</th> |
| <th>Price</th> |
| <th>Value</th> |
| <th>Time</th> |
| </tr> |
| </thead> |
| <tbody id="trades-body"> |
| <tr><td colspan="5" style="color:#999;">Loading...</td></tr> |
| </tbody> |
| </table> |
| </div> |
| <div class="refresh-info" id="trades-updated">--</div> |
| </div> |
|
|
| </div> |
|
|
| <script> |
| |
| const BASE = window.location.pathname === '/' ? '' : window.location.pathname.replace(/\/$/, ''); |
| |
| let pollInterval = null; |
| |
| function setStatus(cls, text) { |
| const badge = document.getElementById('status-badge'); |
| const btn = document.getElementById('reconnect-btn'); |
| badge.className = 'status ' + cls; |
| document.getElementById('status-text').textContent = text; |
| btn.style.display = (cls === 'disconnected') ? 'inline-block' : 'none'; |
| } |
| |
| async function loadSecurities() { |
| try { |
| const r = await fetch(BASE + '/securities'); |
| const symbols = await r.json(); |
| const sel = document.getElementById('symbol-select'); |
| sel.innerHTML = ''; |
| symbols.forEach(s => { |
| const opt = document.createElement('option'); |
| opt.value = s; |
| opt.textContent = s; |
| sel.appendChild(opt); |
| }); |
| } catch(e) { |
| console.warn('Could not load securities:', e); |
| } |
| } |
| |
| async function sendOrder(evt) { |
| evt.preventDefault(); |
| const form = document.getElementById('orderForm'); |
| const statusEl = document.getElementById('order-status'); |
| const data = { |
| order_id: form.order_id.value || Date.now().toString(), |
| symbol: form.symbol.value, |
| type: form.type.value, |
| quantity: parseInt(form.quantity.value, 10), |
| price: parseFloat(form.price.value), |
| timestamp: Date.now() / 1000 |
| }; |
| try { |
| const r = await fetch(BASE + '/order', { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify(data) |
| }); |
| await r.json(); |
| statusEl.className = 'success'; |
| statusEl.textContent = `Order sent: ${data.type.toUpperCase()} ${data.quantity} ${data.symbol} @ ${data.price.toFixed(2)}`; |
| setTimeout(() => { statusEl.style.display = 'none'; }, 4000); |
| loadBook(); |
| } catch(e) { |
| statusEl.className = 'error'; |
| statusEl.textContent = 'Error sending order: ' + e.message; |
| } |
| } |
| |
| function renderBook(b) { |
| const bids = (b.bids || []).sort((a, c) => (c.price || 0) - (a.price || 0)); |
| const asks = (b.asks || []).sort((a, c) => (a.price || 0) - (c.price || 0)); |
| const tbody = document.getElementById('book-body'); |
| tbody.innerHTML = ''; |
| const maxRows = Math.max(bids.length, asks.length); |
| if (maxRows === 0) { |
| tbody.innerHTML = '<tr><td colspan="4" style="color:#999; text-align:center;">No resting orders</td></tr>'; |
| return; |
| } |
| for (let i = 0; i < Math.min(maxRows, 20); i++) { |
| const bid = bids[i] || {}; |
| const ask = asks[i] || {}; |
| const row = document.createElement('tr'); |
| row.innerHTML = ` |
| <td class="bid">${bid.quantity != null ? bid.quantity : ''}</td> |
| <td class="bid">${bid.price != null ? Number(bid.price).toFixed(2) : ''}</td> |
| <td class="ask">${ask.price != null ? Number(ask.price).toFixed(2) : ''}</td> |
| <td class="ask">${ask.quantity != null ? ask.quantity : ''}</td> |
| `; |
| tbody.appendChild(row); |
| } |
| } |
| |
| let prevTradeCount = 0; |
| function renderTrades(trades) { |
| const tbody = document.getElementById('trades-body'); |
| const isNew = trades.length > prevTradeCount; |
| tbody.innerHTML = ''; |
| if (!trades || trades.length === 0) { |
| tbody.innerHTML = '<tr><td colspan="5" style="color:#999; text-align:center;">No trades yet</td></tr>'; |
| document.getElementById('trade-count').textContent = ''; |
| return; |
| } |
| for (const t of trades.slice(0, 50)) { |
| const qty = t.quantity || t.qty || 0; |
| const price = Number(t.price || 0); |
| const value = (qty * price).toFixed(2); |
| const ts = t.timestamp ? new Date(t.timestamp * 1000).toLocaleTimeString() : '-'; |
| const row = document.createElement('tr'); |
| if (isNew) row.className = 'new-row'; |
| row.innerHTML = ` |
| <td><strong>${t.symbol || '?'}</strong></td> |
| <td>${qty}</td> |
| <td>${price.toFixed(2)}</td> |
| <td style="color:#666;">${value}</td> |
| <td style="font-size:11px;">${ts}</td> |
| `; |
| tbody.appendChild(row); |
| } |
| document.getElementById('trade-count').textContent = `(${trades.length})`; |
| prevTradeCount = trades.length; |
| } |
| |
| async function loadBook() { |
| const now = new Date().toLocaleTimeString(); |
| const symbol = document.getElementById('symbol-select')?.value || ''; |
| try { |
| const r = await fetch(BASE + '/book?symbol=' + encodeURIComponent(symbol)); |
| const b = await r.json(); |
| renderBook(b); |
| document.getElementById('book-updated').textContent = 'Updated: ' + now; |
| setStatus('connected', 'Live'); |
| } catch(e) { |
| document.getElementById('book-body').innerHTML = |
| '<tr><td colspan="4" style="color:red; text-align:center;">Error loading book</td></tr>'; |
| setStatus('disconnected', 'Disconnected'); |
| } |
| try { |
| const r2 = await fetch(BASE + '/trades'); |
| const t = await r2.json(); |
| renderTrades(Array.isArray(t) ? t : (t.trades || [])); |
| document.getElementById('trades-updated').textContent = 'Updated: ' + now; |
| } catch(e) { |
| document.getElementById('trades-body').innerHTML = |
| '<tr><td colspan="5" style="color:red; text-align:center;">Error loading trades</td></tr>'; |
| } |
| } |
| |
| function reconnect() { |
| setStatus('connecting', 'Connecting...'); |
| loadBook(); |
| } |
| |
| async function init() { |
| await loadSecurities(); |
| await loadBook(); |
| pollInterval = setInterval(loadBook, 2000); |
| } |
| |
| window.onload = init; |
| </script> |
| </body> |
| </html> |
|
|