topsecrettraders's picture
Update app.py
a7dcb34 verified
Raw
History Blame Contribute Delete
36.1 kB
import uvicorn
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
# ==========================================
# 1. SERVER CONFIG (STATIC HOST ONLY)
# ==========================================
HOST = "0.0.0.0"
PORT = 7860
app = FastAPI()
@app.get("/")
def home():
return HTMLResponse(HTML_TEMPLATE)
# ==========================================
# 2. FRONTEND CODE
# ==========================================
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)