| import uvicorn |
| from fastapi import FastAPI |
| from fastapi.responses import HTMLResponse |
|
|
| |
| |
| |
| HOST = "0.0.0.0" |
| PORT = 7860 |
|
|
| app = FastAPI() |
|
|
| @app.get("/") |
| def home(): |
| return HTMLResponse(HTML_TEMPLATE) |
|
|
| |
| |
| |
|
|
| HTML_TEMPLATE = """ |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <title>DERIBIT PRO CHAIN</title> |
| <meta name="viewport" content="width=1400"> |
| <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet"> |
| <style> |
| :root { |
| --bg: #ffffff; --panel: #f4f6f8; --border: #e0e0e0; |
| --text-main: #1a1a1a; --text-sub: #555555; |
| --accent: #6c4af5; --green: #00c853; --red: #d50000; |
| --green-soft: rgba(0, 200, 83, 0.27); --red-soft: rgba(213, 0, 0, 0.18); |
| --atm-bg: rgba(108, 74, 245, 0.08); |
| } |
| * { box-sizing: border-box; } |
| body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; } |
| |
| /* TOP NAV */ |
| .top-nav { padding: 0 20px; background: #fff; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; height: 60px; z-index: 10; box-shadow: 0 2px 5px rgba(0,0,0,0.03); } |
| |
| .brand { font-family: 'JetBrains Mono'; font-weight: 800; font-size: 18px; color: var(--text-main); } |
| .brand span { color: var(--accent); } |
| |
| .controls { display: flex; gap: 15px; align-items: center; } |
| |
| /* INPUTS & SELECTS */ |
| select, input { |
| background: #f9f9f9; color: var(--text-main); border: 1px solid var(--border); |
| padding: 8px 12px; border-radius: 6px; font-weight: bold; outline:none; |
| font-family:'JetBrains Mono'; font-size: 13px; transition: 0.2s; |
| } |
| select:focus, input:focus { border-color: var(--accent); background: #fff; } |
| input { width: 100px; text-align: center; } |
| |
| .spot-box { text-align: right; } |
| .spot-val { font-size: 18px; font-weight: 800; color: var(--text-main); font-family: 'JetBrains Mono'; } |
| .spot-lbl { font-size: 10px; color: #aaa; font-weight: 700; letter-spacing: 1px; } |
| |
| /* LAYOUT */ |
| .grid-layout { display: flex; height: calc(100vh - 60px); } |
| |
| /* CHAIN PANEL */ |
| .chain-panel { flex: 1; display: flex; flex-direction: column; position: relative; } |
| |
| /* 5-Column Header: CallData | MathLeft | Strike | MathRight | PutData */ |
| .chain-header { |
| display: grid; |
| grid-template-columns: 0.6fr 1fr 60px 1fr 0.6fr; |
| background: #f8f9fa; border-bottom: 1px solid var(--border); |
| font-size: 11px; font-weight: 800; text-align: center; color: var(--text-sub); |
| height: 40px; align-items: center; |
| } |
| .header-cell { display: flex; align-items: center; justify-content: center; gap: 5px; cursor: pointer; height: 100%; border-right: 1px solid var(--border); } |
| .header-cell:last-child { border-right: none; } |
| .header-icon { opacity: 0.3; transition: 0.2s; } |
| .header-icon:hover { opacity: 1; color: var(--accent); } |
| |
| .chain-body { flex: 1; overflow-y: auto; background: #fff; scroll-behavior: smooth; } |
| |
| /* ROWS */ |
| .row { |
| display: grid; |
| grid-template-columns: 0.6fr 1fr 60px 1fr 0.6fr; |
| height: 45px; align-items: center; border-bottom: 1px solid #f0f0f0; |
| font-family: 'JetBrains Mono'; font-size: 12px; |
| } |
| .row:hover { background: #fafafa; } |
| .row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); } |
| |
| /* MATH COLUMNS WITH BARS */ |
| .graph-col { position: relative; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; border-right: 1px solid var(--border); cursor: crosshair; overflow: hidden; } |
| .visual-bar { position: absolute; top: 8px; bottom: 8px; z-index: 0; border-radius: 4px; transition: width 0.3s, left 0.3s; opacity: 0.8; } |
| .math-val { z-index: 2; font-weight: 800; font-size: 13px; color: var(--text-main); } |
| |
| /* RAW DATA COLUMNS (Small) */ |
| .raw-col { display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 10px; color: #888; border-right: 1px solid var(--border); height: 100%; line-height: 1.2; } |
| .raw-ltp { font-weight: bold; color: var(--text-main); font-size: 11px; } |
| .raw-sub { display: flex; gap: 6px; } |
| .txt-g { color: var(--green); } |
| .txt-r { color: var(--red); } |
| |
| .strike-box { background: #f8f9fa; height: 100%; display: flex; align-items: center; justify-content: center; font-weight: 800; border-right: 1px solid var(--border); } |
| |
| /* SIDEBAR */ |
| .fut-panel { width: 340px; background: var(--panel); border-left: 1px solid var(--border); display: flex; flex-direction: column; overflow-y: auto; } |
| |
| .panel-section { padding: 15px; border-bottom: 1px solid var(--border); background: #fff; margin-bottom: 10px; } |
| .panel-title { font-size: 11px; font-weight: 800; color: #888; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; display:flex; justify-content:space-between; align-items:center; } |
| |
| /* AGGREGATE CARDS */ |
| .nf-sec { margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid #eee; } |
| .nf-sec:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom: none; } |
| .nf-title { font-size: 10px; font-weight: 800; color: #888; text-transform: uppercase; margin-bottom: 8px; text-align: left; } |
| |
| .aggr-row { display: flex; gap: 5px; } |
| .aggr-item { flex: 1; background: #fafafa; border: 1px solid #eee; border-radius: 6px; padding: 8px; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; overflow: hidden; } |
| |
| .aggr-val { font-size: 14px; font-family: 'JetBrains Mono'; font-weight: 800; z-index: 2; position: relative; } |
| .aggr-lbl { font-size: 9px; font-weight: 700; color: #aaa; margin-top: 2px; z-index: 2; position: relative; } |
| |
| /* Mini Bar inside Aggregate Card */ |
| .mini-bar { position: absolute; bottom: 0; left: 0; height: 3px; width: 0%; transition: width 0.3s; } |
| |
| /* FUTURES CARDS */ |
| .fut-card { padding: 12px; background: #fff; border: 1px solid #eee; border-radius: 8px; margin-bottom: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.02); } |
| .meter-bg { height: 6px; background: #eee; border-radius: 3px; display: flex; overflow: hidden; margin-top: 8px; } |
| |
| /* MODALS */ |
| .modal { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.5); z-index: 1000; display: none; justify-content: center; align-items: center; backdrop-filter: blur(2px); } |
| .modal-box { background: #fff; width: 420px; border-radius: 12px; box-shadow: 0 20px 50px rgba(0,0,0,0.2); padding: 25px; display:flex; flex-direction:column; animation: popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); } |
| @keyframes popIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } } |
| |
| .inp-group { margin-top:15px; } |
| .inp-label { font-size:11px; font-weight:800; color:#888; margin-bottom:5px; display:block; } |
| .modal-inp { width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; font-family:'JetBrains Mono'; font-weight:bold; font-size:14px; } |
| .modal-action { background:var(--accent); color:white; border:none; padding:10px 20px; border-radius:6px; font-weight:bold; cursor:pointer; width:100%; margin-top:20px; } |
| .modal-action.sec { background:#eee; color:#333; margin-top:10px; } |
| |
| /* LOADER */ |
| .loader-ov { position: fixed; top:0; left:0; width:100%; height:100%; background:#fff; display:flex; flex-direction:column; justify-content:center; align-items:center; z-index: 9999; } |
| .spinner { width: 30px; height: 30px; border: 3px solid #f3f3f3; border-top: 3px solid var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 15px; } |
| @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } |
| |
| /* SCROLLBAR */ |
| ::-webkit-scrollbar { width: 6px; height: 6px; } |
| ::-webkit-scrollbar-track { background: transparent; } |
| ::-webkit-scrollbar-thumb { background: #ddd; border-radius: 3px; } |
| ::-webkit-scrollbar-thumb:hover { background: #bbb; } |
| |
| .pending { opacity: 0.4; } |
| </style> |
| </head> |
| <body> |
| |
| <!-- LOADER --> |
| <div class="loader-ov" id="loader"> |
| <div class="spinner"></div> |
| <div id="loaderMsg" style="font-weight:700; color:var(--text-main); font-size:14px; letter-spacing:1px;">CONNECTING TO DERIBIT...</div> |
| </div> |
| |
| <!-- CONFIG MODAL --> |
| <div class="modal" id="confModal"> |
| <div class="modal-box"> |
| <div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;"> |
| <span id="confTitle">COLUMN SETTINGS</span> |
| <span style="cursor:pointer; color:#999;" onclick="closeModal('confModal')">✕</span> |
| </div> |
| <div class="inp-group"> |
| <span class="inp-label">DISPLAY TITLE</span> |
| <input type="text" id="confName" class="modal-inp"> |
| </div> |
| <div class="inp-group"> |
| <span class="inp-label">CUSTOM FORMULA (CS, CB, PS, PB)</span> |
| <input type="text" id="confForm" class="modal-inp"> |
| </div> |
| <button class="modal-action" onclick="saveConfig()">SAVE CONFIGURATION</button> |
| <button class="modal-action sec" onclick="resetConfig()">RESET TO DEFAULTS</button> |
| </div> |
| </div> |
| |
| <!-- SIDEBAR CONFIG MODAL --> |
| <div class="modal" id="sideConfModal"> |
| <div class="modal-box"> |
| <div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;"> |
| <span>SIDEBAR CONFIG</span> |
| <span style="cursor:pointer; color:#999;" onclick="closeModal('sideConfModal')">✕</span> |
| </div> |
| <div style="font-size:11px; color:#888; margin-bottom:10px;"> |
| Set ranges for ATM aggregation (e.g. 3, 7, 13). |
| </div> |
| <div id="sideConfList"></div> |
| <button class="modal-action" onclick="saveSideConfig()">SAVE SETTINGS</button> |
| <button class="modal-action sec" onclick="resetSideConfig()">RESET DEFAULTS</button> |
| </div> |
| </div> |
| |
| <!-- TOP NAV --> |
| <div class="top-nav"> |
| <div class="brand">DERIBIT<span>PRO</span></div> |
| |
| <div class="controls"> |
| <select id="symSel" onchange="changeSymbol()"> |
| <option value="BTC">BTC</option> |
| <option value="ETH">ETH</option> |
| </select> |
| |
| <select id="expSel" onchange="renderTable()"></select> |
| |
| <div style="display:flex; align-items:center; gap:8px;"> |
| <span style="font-size:10px; font-weight:800; color:#888;">DEPTH</span> |
| <input type="number" id="depthInp" value="10000" onchange="restartScanner()"> |
| </div> |
| |
| <div style="display:flex; align-items:center; gap:8px;"> |
| <span style="font-size:10px; font-weight:800; color:#888;">ATM ±</span> |
| <input type="number" id="rangeInp" value="20" onchange="renderTable()"> |
| </div> |
| </div> |
| |
| <div class="spot-box"> |
| <div class="spot-val" id="spotVal">$0.00</div> |
| <div class="spot-lbl">INDEX PRICE</div> |
| </div> |
| </div> |
| |
| <div class="grid-layout"> |
| <!-- CHAIN PANEL --> |
| <div class="chain-panel"> |
| <div class="chain-header"> |
| <div class="header-cell">CALL DATA</div> |
| <div class="header-cell" onclick="openConfig('left')"> |
| <span id="headL_txt">NET FLOW</span> |
| <svg class="header-icon" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg> |
| </div> |
| <div class="header-cell">STRIKE</div> |
| <div class="header-cell" onclick="openConfig('right')"> |
| <span id="headR_txt">OVERALL FLOW</span> |
| <svg class="header-icon" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg> |
| </div> |
| <div class="header-cell">PUT DATA</div> |
| </div> |
| <div class="chain-body" id="chainBody"></div> |
| </div> |
| |
| <!-- SIDEBAR --> |
| <div class="fut-panel"> |
| <div class="panel-section"> |
| <div class="panel-title"> |
| <span>AGGREGATE FLOW</span> |
| <svg onclick="openSideConfig()" style="cursor:pointer; opacity:0.5;" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg> |
| </div> |
| <div id="sideContainer"></div> |
| </div> |
| <div class="panel-section"> |
| <div class="panel-title">FUTURES CONTRACTS</div> |
| <div id="futBody"></div> |
| </div> |
| </div> |
| </div> |
| |
| <script> |
| // ========================================== |
| // STATE & CONFIG |
| // ========================================== |
| const DEFAULTS = { |
| left: { name: "NET FLOW", formula: "PS - CS" }, |
| right: { name: "OVERALL FLOW", formula: "CB + PS - (PB + CS)" }, |
| side: [ |
| { id: 1, name: "NET FLOW", formula: "PS - CS", ranges: [3, 7, 13] }, |
| { id: 2, name: "OVERALL", formula: "CB + PS - (PB + CS)", ranges: [3, 7, 13] } |
| ] |
| }; |
| |
| let CONFIG = JSON.parse(localStorage.getItem('deribit_chain_config')) || JSON.parse(JSON.stringify(DEFAULTS)); |
| |
| let MASTER_DATA = { opts: [], futs: [] }; |
| let CURRENT_DATA = {}; // { instrument_name: { buys: 0, sells: 0, ltp: 0 } } |
| let SCAN_QUEUE = []; |
| let SCAN_ACTIVE = false; |
| let SPOT_PRICE = 0; |
| let EDIT_SIDE = null; |
| let CURRENT_SYM = "BTC"; |
| |
| // ========================================== |
| // INITIALIZATION |
| // ========================================== |
| window.onload = () => { |
| applyConfig(); |
| initApp(); |
| }; |
| |
| async function initApp() { |
| document.getElementById('loader').style.display = 'flex'; |
| CURRENT_SYM = document.getElementById('symSel').value; |
| |
| try { |
| // 1. Fetch Instruments |
| const [optRes, futRes] = await Promise.all([ |
| fetch(`https://www.deribit.com/api/v2/public/get_instruments?currency=${CURRENT_SYM}&kind=option&expired=false`), |
| fetch(`https://www.deribit.com/api/v2/public/get_instruments?currency=${CURRENT_SYM}&kind=future&expired=false`) |
| ]); |
| |
| const optJson = await optRes.json(); |
| const futJson = await futRes.json(); |
| |
| // 2. Process Options |
| MASTER_DATA.opts = optJson.result.map(i => ({ |
| s: i.instrument_name, |
| k: i.strike, |
| t: i.option_type === "call" ? "CE" : "PE", |
| e: i.expiration_timestamp / 1000 |
| })).sort((a,b) => (a.e - b.e) || (a.k - b.k)); |
| |
| // 3. Process Futures |
| MASTER_DATA.futs = futJson.result.map(i => ({ |
| s: i.instrument_name, |
| e: i.expiration_timestamp / 1000, |
| d: i.expiration_timestamp > 9000000000 ? "PERP" : new Date(i.expiration_timestamp).toLocaleDateString('en-GB', {day:'numeric', month:'short'}) |
| })).sort((a,b) => a.e - b.e); |
| |
| // 4. Setup Expiries |
| const now = Date.now()/1000; |
| const exps = [...new Set(MASTER_DATA.opts.map(o => o.e))].filter(e => e > now).sort((a,b)=>a-b); |
| |
| const expSel = document.getElementById('expSel'); |
| expSel.innerHTML = exps.map(e => { |
| const d = new Date(e*1000).toLocaleDateString('en-GB', {day:'numeric', month:'short'}).toUpperCase(); |
| return `<option value="${e}">${d}</option>`; |
| }).join(''); |
| |
| // 5. Initial Spot & Render |
| updateSpot(); |
| renderTable(); |
| renderFutures(); |
| renderSidebar(); |
| |
| // 6. Start Scanner |
| if(!SCAN_ACTIVE) startScanner(); |
| document.getElementById('loader').style.display = 'none'; |
| |
| } catch(e) { |
| console.error(e); |
| alert("Connection Failed. Check Console."); |
| } |
| } |
| |
| function changeSymbol() { |
| // FLICKER FIX: Clear everything immediately |
| document.getElementById('chainBody').innerHTML = ''; |
| document.getElementById('futBody').innerHTML = ''; |
| document.getElementById('spotVal').innerText = '...'; |
| SCAN_QUEUE = []; |
| CURRENT_DATA = {}; |
| SPOT_PRICE = 0; |
| |
| initApp(); |
| } |
| |
| async function updateSpot() { |
| try { |
| const res = await fetch(`https://www.deribit.com/api/v2/public/get_index_price?index_name=${CURRENT_SYM.toLowerCase()}_usd`); |
| const json = await res.json(); |
| if(json.result) { |
| SPOT_PRICE = json.result.index_price; |
| document.getElementById('spotVal').innerText = `$${SPOT_PRICE.toFixed(2)}`; |
| highlightATM(); |
| } |
| } catch(e) {} |
| setTimeout(updateSpot, 2000); |
| } |
| |
| // ========================================== |
| // RENDERING |
| // ========================================== |
| function renderTable() { |
| const exp = parseFloat(document.getElementById('expSel').value); |
| if(!exp) return; |
| |
| // Reset Queue for new expiry |
| SCAN_QUEUE = []; |
| // We don't clear CURRENT_DATA here to allow cache hits, but we rebuild UI |
| |
| const rel = MASTER_DATA.opts.filter(o => o.e === exp); |
| const strikes = [...new Set(rel.map(o => o.k))].sort((a,b)=>a-b); |
| |
| // Find ATM and filter range |
| let atmIdx = 0; |
| let minDiff = Infinity; |
| if(SPOT_PRICE > 0) { |
| strikes.forEach((k, idx) => { |
| const diff = Math.abs(k - SPOT_PRICE); |
| if(diff < minDiff) { minDiff = diff; atmIdx = idx; } |
| }); |
| } else { |
| atmIdx = Math.floor(strikes.length / 2); |
| } |
| |
| const range = parseInt(document.getElementById('rangeInp').value) || 20; |
| const start = Math.max(0, atmIdx - range); |
| const end = Math.min(strikes.length, atmIdx + range + 1); |
| const viewStrikes = strikes.slice(start, end); |
| |
| let html = ""; |
| viewStrikes.forEach(k => { |
| const ce = rel.find(o => o.k === k && o.t === "CE"); |
| const pe = rel.find(o => o.k === k && o.t === "PE"); |
| const cSym = ce ? ce.s : ""; |
| const pSym = pe ? pe.s : ""; |
| |
| if(cSym) SCAN_QUEUE.push(cSym); |
| if(pSym) SCAN_QUEUE.push(pSym); |
| |
| html += ` |
| <div class="row" id="row-${k}" data-k="${k}" data-c="${cSym}" data-p="${pSym}"> |
| <!-- CALL DATA (Small) --> |
| <div class="raw-col"> |
| <span class="raw-ltp pending" id="cltp-${cSym}">-</span> |
| <div class="raw-sub"> |
| <span class="txt-g pending" id="cb-${cSym}">-</span> |
| <span class="txt-r pending" id="cs-${cSym}">-</span> |
| </div> |
| </div> |
| |
| <!-- MATH LEFT --> |
| <div class="graph-col"> |
| <div class="visual-bar" id="barL-${k}"></div> |
| <span class="math-val" id="valL-${k}">-</span> |
| </div> |
| |
| <!-- STRIKE --> |
| <div class="strike-box">${k}</div> |
| |
| <!-- MATH RIGHT --> |
| <div class="graph-col"> |
| <div class="visual-bar" id="barR-${k}"></div> |
| <span class="math-val" id="valR-${k}">-</span> |
| </div> |
| |
| <!-- PUT DATA (Small) --> |
| <div class="raw-col"> |
| <span class="raw-ltp pending" id="pltp-${pSym}">-</span> |
| <div class="raw-sub"> |
| <span class="txt-g pending" id="pb-${pSym}">-</span> |
| <span class="txt-r pending" id="ps-${pSym}">-</span> |
| </div> |
| </div> |
| </div>`; |
| }); |
| |
| document.getElementById('chainBody').innerHTML = html; |
| highlightATM(); |
| |
| // Re-add Futures to queue so they keep updating |
| MASTER_DATA.futs.forEach(f => SCAN_QUEUE.push(f.s)); |
| } |
| |
| function renderFutures() { |
| const list = document.getElementById('futBody'); |
| list.innerHTML = MASTER_DATA.futs.map(f => { |
| SCAN_QUEUE.push(f.s); |
| return ` |
| <div class="fut-card"> |
| <div style="display:flex; justify-content:space-between; font-weight:700; font-size:12px;"> |
| <span>${f.d}</span> <span id="fltp-${f.s}" class="pending">...</span> |
| </div> |
| <div class="meter-bg"> |
| <div style="width:50%; background:var(--green)" id="fbar-b-${f.s}"></div> |
| <div style="width:50%; background:var(--red)" id="fbar-s-${f.s}"></div> |
| </div> |
| <div style="display:flex; justify-content:space-between; font-size:10px; color:#666; margin-top:4px;"> |
| <span>B: <span id="fb-${f.s}">-</span></span> |
| <span>S: <span id="fs-${f.s}">-</span></span> |
| </div> |
| </div>`; |
| }).join(''); |
| } |
| |
| function renderSidebar() { |
| const con = document.getElementById('sideContainer'); |
| con.innerHTML = CONFIG.side.map((item, idx) => { |
| const boxes = item.ranges.map((r, rIdx) => ` |
| <div class="aggr-item"> |
| <span class="aggr-val" id="sv-${idx}-${rIdx}">0</span> |
| <span class="aggr-lbl">ATM ±${r}</span> |
| <div class="mini-bar" id="sb-${idx}-${rIdx}"></div> |
| </div> |
| `).join(''); |
| |
| return ` |
| <div class="nf-sec"> |
| <div class="nf-title">${item.name}</div> |
| <div class="aggr-row">${boxes}</div> |
| </div>`; |
| }).join(''); |
| } |
| |
| function highlightATM() { |
| if(!SPOT_PRICE) return; |
| document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm')); |
| |
| let minDiff = Infinity; |
| let atmRow = null; |
| |
| document.querySelectorAll('.row').forEach(row => { |
| const k = parseFloat(row.dataset.k); |
| const diff = Math.abs(k - SPOT_PRICE); |
| if(diff < minDiff) { minDiff = diff; atmRow = row; } |
| }); |
| |
| if(atmRow) { |
| atmRow.classList.add('atm'); |
| if(!window.scrolled) { |
| atmRow.scrollIntoView({block:'center', behavior:'auto'}); |
| window.scrolled = true; |
| } |
| } |
| } |
| |
| // ========================================== |
| // SCANNER ENGINE |
| // ========================================== |
| async function startScanner() { |
| SCAN_ACTIVE = true; |
| const BATCH_SIZE = 8; |
| |
| while(true) { |
| if(SCAN_QUEUE.length === 0) { |
| await new Promise(r => setTimeout(r, 500)); |
| continue; |
| } |
| |
| const batch = []; |
| for(let i=0; i<BATCH_SIZE; i++) { |
| const item = SCAN_QUEUE.shift(); |
| if(item) { |
| batch.push(item); |
| SCAN_QUEUE.push(item); // Rotate |
| } |
| } |
| |
| await Promise.all(batch.map(fetchInstrumentData)); |
| await new Promise(r => setTimeout(r, 250)); |
| } |
| } |
| |
| function restartScanner() { |
| // Just triggers config reload in next fetch cycle |
| } |
| |
| async function fetchInstrumentData(inst) { |
| if(!inst) return; |
| const depth = document.getElementById('depthInp').value; |
| const url = `https://www.deribit.com/api/v2/public/get_order_book?instrument_name=${inst}&depth=${depth}`; |
| |
| try { |
| const res = await fetch(url); |
| const json = await res.json(); |
| |
| if(json.result) { |
| const ob = json.result; |
| const buys = ob.bids.reduce((a,b) => a + b[1], 0); |
| const sells = ob.asks.reduce((a,b) => a + b[1], 0); |
| const ltp = ob.mark_price; |
| |
| CURRENT_DATA[inst] = { buys, sells, ltp }; |
| updateDOM(inst, buys, sells, ltp); |
| |
| // If option, update math |
| if(inst.endsWith('-C') || inst.endsWith('-P')) { |
| const parts = inst.split('-'); |
| const k = parts[2]; |
| updateRowMath(k); |
| updateAggregates(); |
| } |
| } |
| } catch(e) {} |
| } |
| |
| // ========================================== |
| // UI UPDATES & MATH |
| // ========================================== |
| function updateDOM(inst, b, s, ltp) { |
| const fmtQ = (q) => q >= 1000 ? (q/1000).toFixed(1)+'k' : q.toFixed(1); |
| const fmtP = (p) => { |
| if(inst.includes('-C') || inst.includes('-P')) return (p * SPOT_PRICE).toFixed(2); |
| return p.toLocaleString(); |
| }; |
| |
| // Update Small Data Columns |
| const elB = document.getElementById(`cb-${inst}`) || document.getElementById(`pb-${inst}`) || document.getElementById(`fb-${inst}`); |
| const elS = document.getElementById(`cs-${inst}`) || document.getElementById(`ps-${inst}`) || document.getElementById(`fs-${inst}`); |
| const elP = document.getElementById(`cltp-${inst}`) || document.getElementById(`pltp-${inst}`) || document.getElementById(`fltp-${inst}`); |
| |
| if(elB) { elB.innerText = fmtQ(b); elB.classList.remove('pending'); } |
| if(elS) { elS.innerText = fmtQ(s); elS.classList.remove('pending'); } |
| if(elP) { elP.innerText = fmtP(ltp); elP.classList.remove('pending'); } |
| |
| // Update Futures Bars |
| if(document.getElementById(`fbar-b-${inst}`)) { |
| const tot = b + s || 1; |
| document.getElementById(`fbar-b-${inst}`).style.width = ((b/tot)*100) + "%"; |
| document.getElementById(`fbar-s-${inst}`).style.width = ((s/tot)*100) + "%"; |
| } |
| } |
| |
| function updateRowMath(k) { |
| const row = document.getElementById(`row-${k}`); |
| if(!row) return; |
| |
| const cSym = row.dataset.c; |
| const pSym = row.dataset.p; |
| |
| const cData = CURRENT_DATA[cSym] || { buys:0, sells:0, ltp:0 }; |
| const pData = CURRENT_DATA[pSym] || { buys:0, sells:0, ltp:0 }; |
| |
| const vars = { |
| CS: cData.sells, CB: cData.buys, |
| PS: pData.sells, PB: pData.buys |
| }; |
| |
| const valL = evalFormula(CONFIG.left.formula, vars); |
| const valR = evalFormula(CONFIG.right.formula, vars); |
| |
| // Update Left |
| const elValL = document.getElementById(`valL-${k}`); |
| const elBarL = document.getElementById(`barL-${k}`); |
| elValL.innerText = fmtNum(valL); |
| elValL.style.color = valL >= 0 ? 'var(--green)' : 'var(--red)'; |
| updateBar(elBarL, valL); |
| |
| // Update Right |
| const elValR = document.getElementById(`valR-${k}`); |
| const elBarR = document.getElementById(`barR-${k}`); |
| elValR.innerText = fmtNum(valR); |
| elValR.style.color = valR >= 0 ? 'var(--green)' : 'var(--red)'; |
| updateBar(elBarR, valR); |
| } |
| |
| function updateBar(el, val) { |
| const MAX = 50000; // Scaling factor |
| const pct = Math.min(100, (Math.abs(val) / MAX) * 100); |
| el.style.width = pct + "%"; |
| el.style.left = val < 0 ? (50 - pct/2) + "%" : "0%"; // Basic left align for now, or use color |
| // Actually app.py style is left aligned, color indicates sign |
| el.style.left = "0%"; |
| el.style.background = val >= 0 ? "var(--green-soft)" : "var(--red-soft)"; |
| } |
| |
| function updateAggregates() { |
| if(!SPOT_PRICE) return; |
| |
| // Get all visible strikes and sort |
| const strikes = Object.keys(CURRENT_DATA).map(k => { |
| if(k.includes('-C') || k.includes('-P')) return parseFloat(k.split('-')[2]); |
| return null; |
| }).filter(x => x).sort((a,b)=>a-b); |
| |
| // Unique strikes |
| const uStrikes = [...new Set(strikes)]; |
| const lowers = uStrikes.filter(k => k < SPOT_PRICE); |
| const uppers = uStrikes.filter(k => k >= SPOT_PRICE); |
| |
| CONFIG.side.forEach((item, idx) => { |
| item.ranges.forEach((rng, rIdx) => { |
| const el = document.getElementById(`sv-${idx}-${rIdx}`); |
| const bar = document.getElementById(`sb-${idx}-${rIdx}`); |
| |
| if(el) { |
| const selLow = lowers.slice(-rng); |
| const selHigh = uppers.slice(0, rng); |
| const targetStrikes = [...selLow, ...selHigh]; |
| |
| let total = 0; |
| targetStrikes.forEach(k => { |
| // Find syms |
| const row = document.getElementById(`row-${k}`); |
| if(row) { |
| const cData = CURRENT_DATA[row.dataset.c] || { buys:0, sells:0 }; |
| const pData = CURRENT_DATA[row.dataset.p] || { buys:0, sells:0 }; |
| const vars = { CS: cData.sells, CB: cData.buys, PS: pData.sells, PB: pData.buys }; |
| total += evalFormula(item.formula, vars); |
| } |
| }); |
| |
| el.innerText = (total > 0 ? "+" : "") + fmtNum(total); |
| el.style.color = total >= 0 ? 'var(--green)' : 'var(--red)'; |
| |
| // Mini Bar |
| const MAX_AGG = 200000; |
| const pct = Math.min(100, (Math.abs(total)/MAX_AGG)*100); |
| bar.style.width = pct + "%"; |
| bar.style.background = total >= 0 ? "var(--green)" : "var(--red)"; |
| } |
| }); |
| }); |
| } |
| |
| // ========================================== |
| // HELPERS & MODALS |
| // ========================================== |
| function evalFormula(formula, v) { |
| try { |
| let exp = formula.toUpperCase(); |
| exp = exp.replace(/CS/g, v.CS).replace(/CB/g, v.CB) |
| .replace(/PS/g, v.PS).replace(/PB/g, v.PB); |
| return eval(exp) || 0; |
| } catch(e) { return 0; } |
| } |
| |
| function fmtNum(n) { |
| const abs = Math.abs(n); |
| if(abs >= 1000000) return (n/1000000).toFixed(2) + 'M'; |
| if(abs >= 1000) return (n/1000).toFixed(1) + 'k'; |
| return n.toFixed(0); |
| } |
| |
| function applyConfig() { |
| document.getElementById('headL_txt').innerText = CONFIG.left.name; |
| document.getElementById('headR_txt').innerText = CONFIG.right.name; |
| } |
| |
| function openConfig(side) { |
| EDIT_SIDE = side; |
| document.getElementById('confTitle').innerText = (side === 'left' ? "LEFT" : "RIGHT") + " COLUMN"; |
| document.getElementById('confName').value = CONFIG[side].name; |
| document.getElementById('confForm').value = CONFIG[side].formula; |
| document.getElementById('confModal').style.display = 'flex'; |
| } |
| |
| function saveConfig() { |
| const name = document.getElementById('confName').value; |
| const form = document.getElementById('confForm').value; |
| if(name && form) { |
| CONFIG[EDIT_SIDE] = { name, formula: form }; |
| localStorage.setItem('deribit_chain_config', JSON.stringify(CONFIG)); |
| applyConfig(); |
| renderTable(); // Re-calc |
| } |
| closeModal('confModal'); |
| } |
| |
| function openSideConfig() { |
| const list = document.getElementById('sideConfList'); |
| list.innerHTML = CONFIG.side.map((item, i) => ` |
| <div style="margin-bottom:10px; border-bottom:1px solid #eee; padding-bottom:10px;"> |
| <div style="font-weight:bold; font-size:12px;">CARD ${i+1}</div> |
| <input type="text" value="${item.name}" class="modal-inp" style="margin-top:5px;" onchange="CONFIG.side[${i}].name=this.value"> |
| <input type="text" value="${item.formula}" class="modal-inp" style="margin-top:5px;" onchange="CONFIG.side[${i}].formula=this.value"> |
| <input type="text" value="${item.ranges.join(',')}" class="modal-inp" style="margin-top:5px;" onchange="CONFIG.side[${i}].ranges=this.value.split(',').map(Number)"> |
| </div> |
| `).join(''); |
| document.getElementById('sideConfModal').style.display = 'flex'; |
| } |
| |
| function saveSideConfig() { |
| localStorage.setItem('deribit_chain_config', JSON.stringify(CONFIG)); |
| renderSidebar(); |
| closeModal('sideConfModal'); |
| } |
| |
| function resetSideConfig() { |
| CONFIG.side = JSON.parse(JSON.stringify(DEFAULTS.side)); |
| saveSideConfig(); |
| } |
| |
| function resetConfig() { |
| CONFIG = JSON.parse(JSON.stringify(DEFAULTS)); |
| saveConfig(); |
| saveSideConfig(); |
| } |
| |
| function closeModal(id) { |
| document.getElementById(id).style.display = 'none'; |
| } |
| </script> |
| </body> |
| </html> |
| """ |
|
|
| if __name__ == "__main__": |
| uvicorn.run(app, host=HOST, port=PORT) |