| | <!doctype html> |
| | <html lang="en"> |
| | <head> |
| | <meta charset="utf-8" /> |
| | <meta name="viewport" content="width=device-width,initial-scale=1" /> |
| | <title>MPTrading Admin</title> |
| | <style> |
| | :root{ |
| | --bg:#0b1020; |
| | --panel:#111a33; |
| | --panel2:#0f1730; |
| | --text:#e7eaf3; |
| | --muted:#aab2d5; |
| | --line:#233055; |
| | --green:#22c55e; |
| | --red:#ef4444; |
| | --blue:#60a5fa; |
| | } |
| | *{ box-sizing:border-box; } |
| | body{ |
| | margin:0; |
| | font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif; |
| | background:var(--bg); |
| | color:var(--text); |
| | } |
| | header{ |
| | height:52px; |
| | display:flex; |
| | align-items:center; |
| | justify-content:space-between; |
| | padding:0 14px; |
| | border-bottom:1px solid var(--line); |
| | background:rgba(0,0,0,0.15); |
| | gap:12px; |
| | } |
| | header .title{ |
| | font-weight:700; |
| | letter-spacing:0.3px; |
| | white-space:nowrap; |
| | } |
| | .small{ font-size:12px; color:var(--muted); } |
| | code{ color:#c7d2fe; } |
| | main{ |
| | padding:12px; |
| | display:grid; |
| | grid-template-columns: 1.2fr 1fr; |
| | gap:12px; |
| | height: calc(100vh - 52px); |
| | } |
| | @media (max-width: 1000px){ |
| | main{ grid-template-columns: 1fr; height:auto; } |
| | } |
| | .card{ |
| | background:var(--panel); |
| | border:1px solid var(--line); |
| | border-radius:10px; |
| | overflow:hidden; |
| | display:flex; |
| | flex-direction:column; |
| | min-height:0; |
| | } |
| | .card h2{ |
| | margin:0; |
| | padding:10px 12px; |
| | font-size:13px; |
| | color:var(--muted); |
| | text-transform:uppercase; |
| | letter-spacing:0.08em; |
| | border-bottom:1px solid var(--line); |
| | background:var(--panel2); |
| | } |
| | .content{ |
| | padding:12px; |
| | overflow:auto; |
| | min-height:0; |
| | } |
| | label{ |
| | display:block; |
| | font-size:12px; |
| | color:var(--muted); |
| | margin:10px 0 6px; |
| | } |
| | input, textarea, button{ |
| | font:inherit; |
| | border-radius:10px; |
| | border:1px solid var(--line); |
| | background:#0c1430; |
| | color:var(--text); |
| | padding:10px 12px; |
| | } |
| | textarea{ |
| | width:100%; |
| | min-height:260px; |
| | resize:vertical; |
| | font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; |
| | font-size:12px; |
| | line-height:1.35; |
| | } |
| | input{ width:100%; } |
| | .row{ display:grid; grid-template-columns: 1fr 1fr; gap:10px; } |
| | .row3{ display:grid; grid-template-columns: 1fr 1fr 1fr; gap:10px; } |
| | .actions{ |
| | display:flex; |
| | gap:10px; |
| | flex-wrap:wrap; |
| | margin-top:10px; |
| | align-items:center; |
| | } |
| | button{ cursor:pointer; } |
| | button.primary:hover{ background:rgba(96,165,250,0.12); } |
| | button.danger{ border-color:rgba(239,68,68,0.5); } |
| | button.danger:hover{ background:rgba(239,68,68,0.12); } |
| | table{ |
| | width:100%; |
| | border-collapse:collapse; |
| | font-size:13px; |
| | } |
| | th, td{ |
| | text-align:left; |
| | padding:8px 6px; |
| | border-bottom:1px solid rgba(35,48,85,0.6); |
| | vertical-align:top; |
| | } |
| | th{ color:var(--muted); font-weight:600; } |
| | td.mono{ |
| | font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; |
| | } |
| | .ok{ color:var(--green); } |
| | .err{ color:var(--red); } |
| | .hint{ |
| | font-size:12px; |
| | color:var(--muted); |
| | margin-top:6px; |
| | line-height:1.35; |
| | } |
| | .pill{ |
| | display:inline-block; |
| | padding:2px 8px; |
| | border-radius:999px; |
| | border:1px solid var(--line); |
| | font-size:12px; |
| | color:var(--muted); |
| | background:rgba(255,255,255,0.02); |
| | } |
| | </style> |
| | </head> |
| | <body> |
| | <header> |
| | <div class="title">MPTrading Admin</div> |
| | <div class="small"> |
| | Endpoints: |
| | <code>/admin/state</code>, <code>/admin/load_scenario</code>, <code>/admin/add_event</code>, <code>/admin/clear_events</code> |
| | </div> |
| | </header> |
| |
|
| | <main> |
| | |
| | <section class="card"> |
| | <h2>Scenario loader</h2> |
| | <div class="content"> |
| | <label>Admin token (sent as <code>X-ADMIN-TOKEN</code>)</label> |
| | <input id="token" type="password" placeholder="Enter ADMIN_TOKEN" autocomplete="off" /> |
| |
|
| | <div class="row"> |
| | <div> |
| | <label>Market length override (optional)</label> |
| | <input id="marketLength" type="number" min="600" step="1" placeholder="Leave blank = auto" /> |
| | <div class="hint"> |
| | Blank = auto-size to at least <span class="pill">600</span> and at least the highest scenario day + 1. |
| | Set a number to force a specific length (minimum 600). |
| | </div> |
| | </div> |
| | <div> |
| | <label>Server state</label> |
| | <div class="small"> |
| | Day: <span class="mono" id="curDay">--</span><br/> |
| | Vol: <span class="mono" id="curVol">--</span><br/> |
| | Market length: <span class="mono" id="curMktLen">--</span><br/> |
| | Tick rate: <span class="mono" id="curTickRate">--</span> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | <label>Scenario JSON</label> |
| | <textarea id="scenario" spellcheck="false"></textarea> |
| |
|
| | <div class="actions"> |
| | <button class="primary" id="loadBtn">Load scenario</button> |
| | <button class="danger" id="clearBtn">Clear events</button> |
| | <button id="refreshBtn">Refresh state</button> |
| | <span id="msg" class="small"></span> |
| | </div> |
| |
|
| | <div class="small" style="margin-top:12px;"> |
| | Scenario format: |
| | <pre class="small" style="white-space:pre-wrap;margin:8px 0 0;color:var(--muted);"> |
| | { |
| | "name": "FOMC week", |
| | "startDay": 0, |
| | "basePrice": 100.0, |
| | "defaultVolatility": 0.8, |
| | "events": [ |
| | {"day": 20, "shockPct": 5.0, "volatility": 1.4, "news": "Rumor of rate cut"} |
| | ] |
| | } |
| | </pre> |
| | </div> |
| | </div> |
| | </section> |
| |
|
| | |
| | <section class="card"> |
| | <h2>Add event schedule</h2> |
| | <div class="content"> |
| | <div class="row3"> |
| | <div> |
| | <label>Offset ticks ahead</label> |
| | <input id="offset" type="number" value="10" min="0" step="1" /> |
| | </div> |
| | <div> |
| | <label>Shock % (e.g. 5 or -3)</label> |
| | <input id="shockPct" type="number" value="5" step="0.1" /> |
| | </div> |
| | <div> |
| | <label>Volatility (optional)</label> |
| | <input id="vol" type="number" placeholder="leave blank" step="0.1" /> |
| | </div> |
| | </div> |
| |
|
| | <label>News (optional)</label> |
| | <input id="news" type="text" placeholder="Headline to broadcast at that tick" /> |
| |
|
| | <div class="actions"> |
| | <button class="primary" id="addBtn">Add event</button> |
| | </div> |
| |
|
| | <div style="height:12px;"></div> |
| |
|
| | <table> |
| | <thead> |
| | <tr> |
| | <th>Day</th> |
| | <th>Shock</th> |
| | <th>Vol</th> |
| | <th>News</th> |
| | </tr> |
| | </thead> |
| | <tbody id="eventsBody"> |
| | <tr><td colspan="4" class="small">No events loaded.</td></tr> |
| | </tbody> |
| | </table> |
| | </div> |
| | </section> |
| | </main> |
| |
|
| | <script> |
| | const id = (x) => document.getElementById(x); |
| | |
| | const token = id("token"); |
| | const scenario = id("scenario"); |
| | const marketLength = id("marketLength"); |
| | |
| | const curDay = id("curDay"); |
| | const curVol = id("curVol"); |
| | const curMktLen = id("curMktLen"); |
| | const curTickRate = id("curTickRate"); |
| | |
| | const msg = id("msg"); |
| | const eventsBody = id("eventsBody"); |
| | |
| | const loadBtn = id("loadBtn"); |
| | const clearBtn = id("clearBtn"); |
| | const refreshBtn = id("refreshBtn"); |
| | const addBtn = id("addBtn"); |
| | |
| | const offset = id("offset"); |
| | const shockPct = id("shockPct"); |
| | const vol = id("vol"); |
| | const news = id("news"); |
| | |
| | function setMsg(text, ok = true){ |
| | msg.textContent = text; |
| | msg.className = ok ? "small ok" : "small err"; |
| | } |
| | |
| | function headers(){ |
| | const t = token.value; |
| | return { |
| | "Content-Type": "application/json", |
| | "X-ADMIN-TOKEN": t |
| | }; |
| | } |
| | |
| | async function apiGet(path){ |
| | const r = await fetch(path, { method: "GET", headers: headers() }); |
| | const txt = await r.text(); |
| | let data = null; |
| | try { data = JSON.parse(txt); } catch { data = { raw: txt }; } |
| | if (!r.ok) throw new Error((data && data.detail) ? data.detail : ("HTTP " + r.status)); |
| | return data; |
| | } |
| | |
| | async function apiPost(path, body){ |
| | const r = await fetch(path, { method: "POST", headers: headers(), body: JSON.stringify(body) }); |
| | const txt = await r.text(); |
| | let data = null; |
| | try { data = JSON.parse(txt); } catch { data = { raw: txt }; } |
| | if (!r.ok) throw new Error((data && data.detail) ? data.detail : ("HTTP " + r.status)); |
| | return data; |
| | } |
| | |
| | function escapeHtml(s){ |
| | return String(s).replace(/[&<>"']/g, (c) => ({ |
| | "&":"&","<":"<",">":">",'"':""","'":"'" |
| | }[c])); |
| | } |
| | |
| | function renderEvents(events){ |
| | const tb = eventsBody; |
| | tb.innerHTML = ""; |
| | if (!events || !events.length){ |
| | tb.innerHTML = '<tr><td colspan="4" class="small">No events loaded.</td></tr>'; |
| | return; |
| | } |
| | for (const e of events){ |
| | const tr = document.createElement("tr"); |
| | const shock = Number(e.shockPct || 0).toFixed(2); |
| | const v = (e.volatility === null || e.volatility === undefined) ? "" : Number(e.volatility).toFixed(2); |
| | tr.innerHTML = ` |
| | <td class="mono">${Number(e.day)}</td> |
| | <td class="mono">${shock}</td> |
| | <td class="mono">${v}</td> |
| | <td>${escapeHtml(e.news || "")}</td> |
| | `; |
| | tb.appendChild(tr); |
| | } |
| | } |
| | |
| | async function refresh(){ |
| | const st = await apiGet("/admin/state"); |
| | curDay.textContent = st.day; |
| | curVol.textContent = st.currentVolatility; |
| | curMktLen.textContent = st.marketLength; |
| | curTickRate.textContent = st.tickRate; |
| | renderEvents(st.events); |
| | } |
| | |
| | |
| | scenario.value = JSON.stringify({ |
| | name: "Example scenario", |
| | startDay: 0, |
| | basePrice: 100.0, |
| | defaultVolatility: 0.8, |
| | events: [ |
| | { day: 20, shockPct: 5.0, volatility: 1.4, news: "Rumor of rate cut" }, |
| | { day: 30, shockPct: -3.0, volatility: 2.0, news: "Unexpected hike" } |
| | ] |
| | }, null, 2); |
| | |
| | refreshBtn.addEventListener("click", async () => { |
| | try{ |
| | await refresh(); |
| | setMsg("State refreshed."); |
| | }catch(e){ |
| | setMsg(e.message, false); |
| | } |
| | }); |
| | |
| | clearBtn.addEventListener("click", async () => { |
| | try{ |
| | await apiPost("/admin/clear_events", {}); |
| | await refresh(); |
| | setMsg("Events cleared."); |
| | }catch(e){ |
| | setMsg(e.message, false); |
| | } |
| | }); |
| | |
| | loadBtn.addEventListener("click", async () => { |
| | try{ |
| | const obj = JSON.parse(scenario.value); |
| | |
| | const ml = marketLength.value.trim(); |
| | if (ml){ |
| | obj.marketLength = Number(ml); |
| | } else { |
| | delete obj.marketLength; |
| | } |
| | |
| | const res = await apiPost("/admin/load_scenario", obj); |
| | await refresh(); |
| | setMsg(`Scenario loaded. startDay=${res.startDay}, eventsLoaded=${res.eventsLoaded}, marketLength=${res.marketLength} (${res.marketLengthMode})`); |
| | }catch(e){ |
| | setMsg(e.message, false); |
| | } |
| | }); |
| | |
| | addBtn.addEventListener("click", async () => { |
| | try{ |
| | const off = Number(offset.value); |
| | const sh = Number(shockPct.value); |
| | |
| | const volRaw = vol.value.trim(); |
| | const volatility = volRaw ? Number(volRaw) : null; |
| | |
| | const n = news.value.trim(); |
| | |
| | const body = { offset: off, shockPct: sh }; |
| | if (volatility !== null && Number.isFinite(volatility)) body.volatility = volatility; |
| | if (n) body.news = n; |
| | |
| | await apiPost("/admin/add_event", body); |
| | await refresh(); |
| | setMsg("Event added."); |
| | }catch(e){ |
| | setMsg(e.message, false); |
| | } |
| | }); |
| | |
| | |
| | refresh().catch(() => {}); |
| | </script> |
| | </body> |
| | </html> |
| |
|