Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -7,11 +7,9 @@ import time
|
|
| 7 |
import urllib.parse
|
| 8 |
import os
|
| 9 |
import threading
|
| 10 |
-
import
|
| 11 |
-
from fastapi import FastAPI, Request, Response, Cookie, BackgroundTasks, Query, WebSocket, WebSocketDisconnect
|
| 12 |
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
| 13 |
from typing import Optional, List, Dict
|
| 14 |
-
from fyers_apiv3.FyersWebsocket import data_ws
|
| 15 |
|
| 16 |
# ==========================================
|
| 17 |
# 1. CONFIGURATION
|
|
@@ -26,25 +24,17 @@ CACHE_FILE = "master_cache.json"
|
|
| 26 |
app = FastAPI()
|
| 27 |
|
| 28 |
# ==========================================
|
| 29 |
-
# 2.
|
| 30 |
# ==========================================
|
| 31 |
|
| 32 |
MASTER_DB = {}
|
| 33 |
SEARCH_INDEX = []
|
| 34 |
|
| 35 |
-
# LIVE MARKET DATA STORE (The "Scoreboard")
|
| 36 |
-
LIVE_DATA = {}
|
| 37 |
-
LIVE_DATA_LOCK = threading.Lock()
|
| 38 |
-
|
| 39 |
SYSTEM_STATUS = {
|
| 40 |
"ready": False,
|
| 41 |
-
"message": "Booting..."
|
| 42 |
-
"socket": False
|
| 43 |
}
|
| 44 |
-
|
| 45 |
-
# FYERS SOCKET OBJECT
|
| 46 |
-
FYERS_SOCKET = None
|
| 47 |
-
CURRENT_TOKEN = None
|
| 48 |
|
| 49 |
# Manual mapping for Index Spot symbols
|
| 50 |
INDEX_MAP = {
|
|
@@ -57,13 +47,8 @@ INDEX_MAP = {
|
|
| 57 |
"SENSEX50": "BSE:SENSEX50-INDEX"
|
| 58 |
}
|
| 59 |
|
| 60 |
-
# ==========================================
|
| 61 |
-
# 3. MASTER DATA LOADER
|
| 62 |
-
# ==========================================
|
| 63 |
-
|
| 64 |
-
UPDATE_LOCK = threading.Lock()
|
| 65 |
-
|
| 66 |
def get_root_symbol(desc: str, symbol: str) -> str:
|
|
|
|
| 67 |
try:
|
| 68 |
parts = desc.strip().split(' ')
|
| 69 |
root = parts[0].upper()
|
|
@@ -87,17 +72,29 @@ def update_master_db():
|
|
| 87 |
|
| 88 |
# 1. NSE F&O
|
| 89 |
url_nse = "https://public.fyers.in/sym_details/NSE_FO.csv"
|
| 90 |
-
df_nse = pd.read_csv(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
df_nse['Exch'] = 'NSE'
|
| 92 |
|
| 93 |
# 2. MCX F&O
|
| 94 |
url_mcx = "https://public.fyers.in/sym_details/MCX_COM.csv"
|
| 95 |
-
df_mcx = pd.read_csv(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
df_mcx['Exch'] = 'MCX'
|
| 97 |
|
| 98 |
# 3. BSE F&O
|
| 99 |
url_bse = "https://public.fyers.in/sym_details/BSE_FO.csv"
|
| 100 |
-
df_bse = pd.read_csv(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
df_bse['Exch'] = 'BSE'
|
| 102 |
|
| 103 |
SYSTEM_STATUS["message"] = "Processing Data..."
|
|
@@ -204,84 +201,7 @@ def startup_event():
|
|
| 204 |
thread.start()
|
| 205 |
|
| 206 |
# ==========================================
|
| 207 |
-
#
|
| 208 |
-
# ==========================================
|
| 209 |
-
|
| 210 |
-
def on_fyers_msg(msg):
|
| 211 |
-
global LIVE_DATA
|
| 212 |
-
# msg format: [{'symbol': 'NSE:SBIN-EQ', 'ltp': 500.0, ...}] or dict
|
| 213 |
-
# Fyers SDK sometimes returns list, sometimes dict
|
| 214 |
-
|
| 215 |
-
data_list = []
|
| 216 |
-
if isinstance(msg, list): data_list = msg
|
| 217 |
-
elif isinstance(msg, dict): data_list = [msg]
|
| 218 |
-
|
| 219 |
-
with LIVE_DATA_LOCK:
|
| 220 |
-
for tick in data_list:
|
| 221 |
-
if 'symbol' in tick:
|
| 222 |
-
sym = tick['symbol']
|
| 223 |
-
# Create or Update
|
| 224 |
-
if sym not in LIVE_DATA: LIVE_DATA[sym] = {}
|
| 225 |
-
|
| 226 |
-
# Update fields present in tick
|
| 227 |
-
if 'ltp' in tick: LIVE_DATA[sym]['ltp'] = tick['ltp']
|
| 228 |
-
if 'vol_traded_today' in tick: LIVE_DATA[sym]['vol'] = tick['vol_traded_today']
|
| 229 |
-
if 'oi' in tick: LIVE_DATA[sym]['oi'] = tick['oi']
|
| 230 |
-
if 'ch' in tick: LIVE_DATA[sym]['ch'] = tick['ch']
|
| 231 |
-
|
| 232 |
-
# Fyers sends BuyQty/SellQty differently sometimes
|
| 233 |
-
if 'totalbuyqty' in tick: LIVE_DATA[sym]['tbq'] = tick['totalbuyqty']
|
| 234 |
-
if 'totalsellqty' in tick: LIVE_DATA[sym]['tsq'] = tick['totalsellqty']
|
| 235 |
-
|
| 236 |
-
# Also handle the depth packets if enabled
|
| 237 |
-
if 'bid' in tick and 'ask' in tick:
|
| 238 |
-
LIVE_DATA[sym]['bid'] = tick['bid']
|
| 239 |
-
LIVE_DATA[sym]['ask'] = tick['ask']
|
| 240 |
-
|
| 241 |
-
def on_fyers_error(msg):
|
| 242 |
-
print(f"!!! FYERS SOCKET ERROR: {msg}")
|
| 243 |
-
|
| 244 |
-
def on_fyers_open():
|
| 245 |
-
print(">>> FYERS SOCKET CONNECTED")
|
| 246 |
-
SYSTEM_STATUS['socket'] = True
|
| 247 |
-
|
| 248 |
-
def on_fyers_close(msg):
|
| 249 |
-
print(">>> FYERS SOCKET CLOSED")
|
| 250 |
-
SYSTEM_STATUS['socket'] = False
|
| 251 |
-
|
| 252 |
-
def start_fyers_socket(token):
|
| 253 |
-
global FYERS_SOCKET
|
| 254 |
-
try:
|
| 255 |
-
# data_type: SymbolUpdate
|
| 256 |
-
FYERS_SOCKET = data_ws.FyersDataSocket(
|
| 257 |
-
access_token=token,
|
| 258 |
-
log_path="logs",
|
| 259 |
-
litemode=False, # We want OI and Volume
|
| 260 |
-
write_to_file=False,
|
| 261 |
-
reconnect=True,
|
| 262 |
-
on_connect=on_fyers_open,
|
| 263 |
-
on_close=on_fyers_close,
|
| 264 |
-
on_error=on_fyers_error,
|
| 265 |
-
on_message=on_fyers_msg
|
| 266 |
-
)
|
| 267 |
-
FYERS_SOCKET.connect()
|
| 268 |
-
except Exception as e:
|
| 269 |
-
print(f"!!! SOCKET START ERROR: {e}")
|
| 270 |
-
|
| 271 |
-
def subscribe_to_fyers(symbols):
|
| 272 |
-
global FYERS_SOCKET
|
| 273 |
-
if FYERS_SOCKET and symbols:
|
| 274 |
-
# Fyers requires max 50-100 symbols per call usually, batching recommended for massive lists
|
| 275 |
-
# But for UI usage, simple list is fine
|
| 276 |
-
try:
|
| 277 |
-
print(f">>> SUBSCRIBING TO {len(symbols)} SYMBOLS")
|
| 278 |
-
FYERS_SOCKET.subscribe(symbols=symbols, data_type="SymbolUpdate")
|
| 279 |
-
FYERS_SOCKET.subscribe(symbols=symbols, data_type="DepthUpdate") # Orderflow need
|
| 280 |
-
except Exception as e:
|
| 281 |
-
print(f"!!! SUBSCRIPTION ERROR: {e}")
|
| 282 |
-
|
| 283 |
-
# ==========================================
|
| 284 |
-
# 5. ROUTES
|
| 285 |
# ==========================================
|
| 286 |
|
| 287 |
@app.get("/")
|
|
@@ -302,15 +222,6 @@ def home(access_token: Optional[str] = Cookie(None)):
|
|
| 302 |
</div>
|
| 303 |
""")
|
| 304 |
|
| 305 |
-
# Start Socket if not running
|
| 306 |
-
global CURRENT_TOKEN
|
| 307 |
-
if not CURRENT_TOKEN:
|
| 308 |
-
CURRENT_TOKEN = access_token
|
| 309 |
-
# Start in thread
|
| 310 |
-
t = threading.Thread(target=start_fyers_socket, args=(access_token,))
|
| 311 |
-
t.daemon = True
|
| 312 |
-
t.start()
|
| 313 |
-
|
| 314 |
return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))
|
| 315 |
|
| 316 |
@app.get("/callback")
|
|
@@ -369,53 +280,7 @@ def force_refresh(background_tasks: BackgroundTasks):
|
|
| 369 |
return JSONResponse({"s": "ok"})
|
| 370 |
|
| 371 |
# ==========================================
|
| 372 |
-
#
|
| 373 |
-
# ==========================================
|
| 374 |
-
|
| 375 |
-
@app.websocket("/ws")
|
| 376 |
-
async def websocket_endpoint(websocket: WebSocket):
|
| 377 |
-
await websocket.accept()
|
| 378 |
-
active_symbols = set()
|
| 379 |
-
|
| 380 |
-
try:
|
| 381 |
-
while True:
|
| 382 |
-
# 1. Check for incoming messages (commands from frontend)
|
| 383 |
-
try:
|
| 384 |
-
# Use a timeout so we don't block the sending loop
|
| 385 |
-
data = await asyncio.wait_for(websocket.receive_text(), timeout=0.1)
|
| 386 |
-
msg = json.loads(data)
|
| 387 |
-
|
| 388 |
-
if msg.get('t') == 'sub':
|
| 389 |
-
# Subscribe command
|
| 390 |
-
syms = msg.get('s', [])
|
| 391 |
-
active_symbols.update(syms)
|
| 392 |
-
# Trigger Fyers Subscription
|
| 393 |
-
if syms: subscribe_to_fyers(list(active_symbols))
|
| 394 |
-
|
| 395 |
-
except asyncio.TimeoutError:
|
| 396 |
-
pass # No message from client, continue to send data
|
| 397 |
-
except Exception:
|
| 398 |
-
break # Client disconnected
|
| 399 |
-
|
| 400 |
-
# 2. Send Data Updates to Client
|
| 401 |
-
if active_symbols:
|
| 402 |
-
updates = {}
|
| 403 |
-
with LIVE_DATA_LOCK:
|
| 404 |
-
for s in active_symbols:
|
| 405 |
-
if s in LIVE_DATA:
|
| 406 |
-
updates[s] = LIVE_DATA[s]
|
| 407 |
-
|
| 408 |
-
if updates:
|
| 409 |
-
await websocket.send_text(json.dumps({"t": "d", "d": updates}))
|
| 410 |
-
|
| 411 |
-
# Throttle to avoid freezing browser (100ms updates)
|
| 412 |
-
await asyncio.sleep(0.1)
|
| 413 |
-
|
| 414 |
-
except WebSocketDisconnect:
|
| 415 |
-
print(">>> CLIENT DISCONNECTED")
|
| 416 |
-
|
| 417 |
-
# ==========================================
|
| 418 |
-
# 7. FRONTEND
|
| 419 |
# ==========================================
|
| 420 |
|
| 421 |
HTML_TEMPLATE = """
|
|
@@ -423,7 +288,7 @@ HTML_TEMPLATE = """
|
|
| 423 |
<html lang="en">
|
| 424 |
<head>
|
| 425 |
<meta charset="UTF-8">
|
| 426 |
-
<title>PRO CHAIN V3
|
| 427 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 428 |
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
|
| 429 |
<style>
|
|
@@ -705,12 +570,10 @@ HTML_TEMPLATE = """
|
|
| 705 |
let SPOT_PRICE = 0;
|
| 706 |
let FUT_SYMS = [];
|
| 707 |
let OPT_SYMS = [];
|
|
|
|
|
|
|
| 708 |
let SEARCH_TIMER = null;
|
| 709 |
let ATM_STRIKE = 0;
|
| 710 |
-
|
| 711 |
-
// WS State
|
| 712 |
-
let WS = null;
|
| 713 |
-
let WS_CACHE = {}; // Store latest values for smoothing
|
| 714 |
|
| 715 |
// Default Configurations
|
| 716 |
const DEFAULTS = {
|
|
@@ -731,7 +594,6 @@ HTML_TEMPLATE = """
|
|
| 731 |
|
| 732 |
applyConfig();
|
| 733 |
checkStatus();
|
| 734 |
-
initWebSocket(); // START THE SOCKET
|
| 735 |
|
| 736 |
const inp = document.getElementById('searchInp');
|
| 737 |
inp.addEventListener('input', (e) => {
|
|
@@ -742,55 +604,6 @@ HTML_TEMPLATE = """
|
|
| 742 |
if(!e.target.closest('.search-box')) document.getElementById('searchRes').style.display = 'none';
|
| 743 |
});
|
| 744 |
};
|
| 745 |
-
|
| 746 |
-
// ===================================
|
| 747 |
-
// WEBSOCKET LOGIC (THE HEART)
|
| 748 |
-
// ===================================
|
| 749 |
-
function initWebSocket() {
|
| 750 |
-
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 751 |
-
WS = new WebSocket(`${proto}//${location.host}/ws`);
|
| 752 |
-
|
| 753 |
-
WS.onopen = () => {
|
| 754 |
-
document.getElementById('statusDot').classList.add('active');
|
| 755 |
-
document.getElementById('statusDot').classList.remove('error');
|
| 756 |
-
// Re-subscribe if we have active symbols
|
| 757 |
-
if(OPT_SYMS.length > 0) requestSubscription();
|
| 758 |
-
};
|
| 759 |
-
|
| 760 |
-
WS.onmessage = (evt) => {
|
| 761 |
-
const msg = JSON.parse(evt.data);
|
| 762 |
-
if(msg.t === 'd') {
|
| 763 |
-
updateUI(msg.d); // LIVE UPDATES
|
| 764 |
-
}
|
| 765 |
-
};
|
| 766 |
-
|
| 767 |
-
WS.onclose = () => {
|
| 768 |
-
document.getElementById('statusDot').classList.remove('active');
|
| 769 |
-
document.getElementById('statusDot').classList.add('error');
|
| 770 |
-
setTimeout(initWebSocket, 2000); // Auto Reconnect
|
| 771 |
-
};
|
| 772 |
-
}
|
| 773 |
-
|
| 774 |
-
function requestSubscription() {
|
| 775 |
-
if(!WS || WS.readyState !== 1) return;
|
| 776 |
-
|
| 777 |
-
// Calculate symbols needed
|
| 778 |
-
let symbolsToFetch = [SPOT_SYM, ...FUT_SYMS.map(x => x.s)];
|
| 779 |
-
|
| 780 |
-
// Get Visible Range + Buffer
|
| 781 |
-
const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
|
| 782 |
-
|
| 783 |
-
if(kList.length > 0) {
|
| 784 |
-
// Basic Logic: Subscribe to ALL loaded strikes for simplicity in V1
|
| 785 |
-
// Optimization can be added later (only visible)
|
| 786 |
-
OPT_SYMS.forEach(o => symbolsToFetch.push(o.s));
|
| 787 |
-
}
|
| 788 |
-
|
| 789 |
-
const uniqueSyms = [...new Set(symbolsToFetch)].filter(s => s && s.length > 2);
|
| 790 |
-
if(uniqueSyms.length > 0) {
|
| 791 |
-
WS.send(JSON.stringify({ t: 'sub', s: uniqueSyms }));
|
| 792 |
-
}
|
| 793 |
-
}
|
| 794 |
|
| 795 |
// ===================================
|
| 796 |
// CONFIGURATION LOGIC
|
|
@@ -822,13 +635,14 @@ HTML_TEMPLATE = """
|
|
| 822 |
|
| 823 |
function setFormula(val) {
|
| 824 |
const input = document.getElementById('confForm');
|
|
|
|
| 825 |
let f = val;
|
| 826 |
if(EDITING_COL === 'left') {
|
| 827 |
f = f.replace(/S/g, 'CS').replace(/B/g, 'CB');
|
| 828 |
} else if(EDITING_COL === 'right') {
|
| 829 |
f = f.replace(/S/g, 'PS').replace(/B/g, 'PB');
|
| 830 |
} else {
|
| 831 |
-
f = f.replace(/S/g, 'PS').replace(/B/g, 'CB');
|
| 832 |
}
|
| 833 |
input.value = f;
|
| 834 |
}
|
|
@@ -848,7 +662,7 @@ HTML_TEMPLATE = """
|
|
| 848 |
|
| 849 |
applyConfig();
|
| 850 |
closeModal('confModal');
|
| 851 |
-
//
|
| 852 |
}
|
| 853 |
|
| 854 |
function saveSideConfig() {
|
|
@@ -863,6 +677,7 @@ HTML_TEMPLATE = """
|
|
| 863 |
|
| 864 |
applyConfig();
|
| 865 |
closeModal('sideConfModal');
|
|
|
|
| 866 |
}
|
| 867 |
|
| 868 |
function resetConfig() {
|
|
@@ -871,6 +686,7 @@ HTML_TEMPLATE = """
|
|
| 871 |
localStorage.setItem('tst_chain_config', JSON.stringify(CONFIG));
|
| 872 |
applyConfig();
|
| 873 |
closeModal('confModal');
|
|
|
|
| 874 |
}
|
| 875 |
}
|
| 876 |
|
|
@@ -880,13 +696,20 @@ HTML_TEMPLATE = """
|
|
| 880 |
// MATH ENGINE
|
| 881 |
// ===================================
|
| 882 |
function evaluateFormula(formula, vars) {
|
|
|
|
| 883 |
try {
|
| 884 |
let exp = formula;
|
|
|
|
|
|
|
| 885 |
exp = exp.replace(/CS/g, vars.CS || 0);
|
| 886 |
exp = exp.replace(/CB/g, vars.CB || 0);
|
| 887 |
exp = exp.replace(/PS/g, vars.PS || 0);
|
| 888 |
exp = exp.replace(/PB/g, vars.PB || 0);
|
|
|
|
|
|
|
| 889 |
if(/[^0-9+\-*/(). ]/.test(exp)) return 0;
|
|
|
|
|
|
|
| 890 |
return new Function('return ' + exp)();
|
| 891 |
} catch(e) {
|
| 892 |
return 0;
|
|
@@ -948,8 +771,7 @@ HTML_TEMPLATE = """
|
|
| 948 |
CURRENT_ROOT = root;
|
| 949 |
CHAIN_DATA = data.items;
|
| 950 |
SPOT_SYM = data.spot;
|
| 951 |
-
SPOT_PRICE = 0;
|
| 952 |
-
WS_CACHE = {}; // Clear old data
|
| 953 |
|
| 954 |
FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
|
| 955 |
|
|
@@ -1026,111 +848,148 @@ HTML_TEMPLATE = """
|
|
| 1026 |
</div>
|
| 1027 |
`).join('');
|
| 1028 |
|
| 1029 |
-
window.scrolled = false;
|
| 1030 |
-
|
| 1031 |
}
|
| 1032 |
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
for(let k in newData) {
|
| 1039 |
-
WS_CACHE[k] = { ...WS_CACHE[k], ...newData[k] };
|
| 1040 |
-
}
|
| 1041 |
|
| 1042 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1043 |
|
| 1044 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1045 |
if(data[SPOT_SYM] && typeof data[SPOT_SYM].ltp === 'number') {
|
| 1046 |
SPOT_PRICE = data[SPOT_SYM].ltp;
|
| 1047 |
document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
|
| 1048 |
}
|
| 1049 |
|
| 1050 |
-
//
|
| 1051 |
const futHTML = FUT_SYMS.map(f => {
|
| 1052 |
const d = data[f.s];
|
| 1053 |
if(!d || typeof d.ltp !== 'number') return '';
|
| 1054 |
const dt = new Date(f.e*1000).toLocaleDateString('en-GB', {day:'numeric', month:'short'});
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
const tbq = d.tbq || d.vol || 1; // Fallback
|
| 1058 |
-
const tsq = d.tsq || d.vol || 1;
|
| 1059 |
-
const tot = tbq + tsq;
|
| 1060 |
-
const bPct = (tbq/tot)*100;
|
| 1061 |
-
const ch = d.ch || 0;
|
| 1062 |
-
|
| 1063 |
return `
|
| 1064 |
<div class="fut-card">
|
| 1065 |
<div style="display:flex; justify-content:space-between; font-weight:700;">
|
| 1066 |
-
<span>${dt} FUT</span> <span style="color:${ch>=0?'var(--green)':'var(--red)'}">${d.ltp}</span>
|
| 1067 |
</div>
|
| 1068 |
<div class="meter-bg">
|
| 1069 |
<div style="width:${bPct}%; background:var(--green)"></div>
|
| 1070 |
<div style="width:${100-bPct}%; background:var(--red)"></div>
|
| 1071 |
</div>
|
| 1072 |
<div style="display:flex; justify-content:space-between; font-size:10px; color:#666; margin-top:4px;">
|
| 1073 |
-
<span>B: ${fmt(
|
| 1074 |
</div>
|
| 1075 |
</div>`;
|
| 1076 |
}).join('');
|
| 1077 |
if(futHTML) document.getElementById('futBody').innerHTML = futHTML;
|
| 1078 |
|
| 1079 |
-
//
|
| 1080 |
const rowData = {};
|
| 1081 |
|
|
|
|
| 1082 |
for(const item of OPT_SYMS) {
|
| 1083 |
const d = data[item.s];
|
| 1084 |
if(!d) continue;
|
| 1085 |
|
| 1086 |
if(!rowData[item.k]) rowData[item.k] = { CS:0, CB:0, PS:0, PB:0 };
|
| 1087 |
|
| 1088 |
-
// Fyers logic: We use OI (Open Interest) or Volume based on availability
|
| 1089 |
-
// Often 'totalsellqty' is not sent in basic feed, we use OI for sellers proxy
|
| 1090 |
-
// But user wants OI specifically
|
| 1091 |
-
const oi = d.oi || 0;
|
| 1092 |
-
const vol = d.vol || 0;
|
| 1093 |
-
|
| 1094 |
-
// Simplified Logic for Demo:
|
| 1095 |
-
// In reality, OI is just a single number. We can't split OI into Buyer/Seller perfectly without Orderflow.
|
| 1096 |
-
// PRO TRICK: We use the 'Total Sell Qty' if available (Depth) else we use OI.
|
| 1097 |
-
// Since we enabled Depth in backend, let's try to use Bid/Ask sums if available, or just OI.
|
| 1098 |
-
|
| 1099 |
-
let sellPressure = d.tsq || (oi / 2); // Approximate if depth missing
|
| 1100 |
-
let buyPressure = d.tbq || (oi / 2);
|
| 1101 |
-
|
| 1102 |
if(item.t === 'CE') {
|
| 1103 |
-
rowData[item.k].CS =
|
| 1104 |
-
rowData[item.k].CB =
|
| 1105 |
}
|
| 1106 |
if(item.t === 'PE') {
|
| 1107 |
-
rowData[item.k].PS =
|
| 1108 |
-
rowData[item.k].PB =
|
| 1109 |
}
|
| 1110 |
}
|
| 1111 |
|
| 1112 |
-
//
|
| 1113 |
-
const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
|
| 1114 |
-
if(kList.length > 0 && SPOT_PRICE > 0) {
|
| 1115 |
-
let selectedK = kList.find(k => k >= SPOT_PRICE);
|
| 1116 |
-
if (selectedK === undefined) selectedK = kList[kList.length - 1];
|
| 1117 |
-
|
| 1118 |
-
ATM_STRIKE = selectedK;
|
| 1119 |
-
document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
|
| 1120 |
-
const atmRow = document.getElementById(`row-${selectedK}`);
|
| 1121 |
-
if(atmRow) {
|
| 1122 |
-
atmRow.classList.add('atm');
|
| 1123 |
-
if(!window.scrolled) {
|
| 1124 |
-
atmRow.scrollIntoView({block:'center', behavior:'auto'});
|
| 1125 |
-
window.scrolled = true;
|
| 1126 |
-
}
|
| 1127 |
-
}
|
| 1128 |
-
}
|
| 1129 |
-
|
| 1130 |
-
// 5. Render Chain Values
|
| 1131 |
let maxMidVal = 0;
|
| 1132 |
const processedRows = {};
|
| 1133 |
|
|
|
|
| 1134 |
for(const k in rowData) {
|
| 1135 |
const vars = rowData[k];
|
| 1136 |
processedRows[k] = {
|
|
@@ -1143,6 +1002,7 @@ HTML_TEMPLATE = """
|
|
| 1143 |
|
| 1144 |
for(const k in processedRows) {
|
| 1145 |
const r = processedRows[k];
|
|
|
|
| 1146 |
const lEl = document.getElementById(`colL-${k}`);
|
| 1147 |
if(lEl) {
|
| 1148 |
lEl.querySelector('.col-val').innerText = fmt(r.left);
|
|
@@ -1170,19 +1030,19 @@ HTML_TEMPLATE = """
|
|
| 1170 |
}
|
| 1171 |
}
|
| 1172 |
|
| 1173 |
-
//
|
| 1174 |
if(ATM_STRIKE > 0) {
|
| 1175 |
let totalFlow = 0;
|
| 1176 |
-
const
|
| 1177 |
-
const atmIdx =
|
| 1178 |
const sideRange = CONFIG.side.range;
|
| 1179 |
|
| 1180 |
if(atmIdx !== -1) {
|
| 1181 |
const start = Math.max(0, atmIdx - sideRange);
|
| 1182 |
-
const end = Math.min(
|
| 1183 |
|
| 1184 |
for(let i=start; i<end; i++) {
|
| 1185 |
-
const k =
|
| 1186 |
if(rowData[k]) {
|
| 1187 |
totalFlow += evaluateFormula(CONFIG.side.formula, rowData[k]);
|
| 1188 |
}
|
|
@@ -1194,8 +1054,14 @@ HTML_TEMPLATE = """
|
|
| 1194 |
|
| 1195 |
nfEl.innerText = (totalFlow > 0 ? "+" : "") + fmt(totalFlow);
|
| 1196 |
nfEl.style.color = totalFlow >= 0 ? 'var(--green)' : 'var(--red)';
|
| 1197 |
-
|
| 1198 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1199 |
}
|
| 1200 |
}
|
| 1201 |
|
|
|
|
| 7 |
import urllib.parse
|
| 8 |
import os
|
| 9 |
import threading
|
| 10 |
+
from fastapi import FastAPI, Request, Response, Cookie, BackgroundTasks, Query
|
|
|
|
| 11 |
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
| 12 |
from typing import Optional, List, Dict
|
|
|
|
| 13 |
|
| 14 |
# ==========================================
|
| 15 |
# 1. CONFIGURATION
|
|
|
|
| 24 |
app = FastAPI()
|
| 25 |
|
| 26 |
# ==========================================
|
| 27 |
+
# 2. ROBUST DATA LOADER (THREAD SAFE)
|
| 28 |
# ==========================================
|
| 29 |
|
| 30 |
MASTER_DB = {}
|
| 31 |
SEARCH_INDEX = []
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
SYSTEM_STATUS = {
|
| 34 |
"ready": False,
|
| 35 |
+
"message": "Booting..."
|
|
|
|
| 36 |
}
|
| 37 |
+
UPDATE_LOCK = threading.Lock()
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
# Manual mapping for Index Spot symbols
|
| 40 |
INDEX_MAP = {
|
|
|
|
| 47 |
"SENSEX50": "BSE:SENSEX50-INDEX"
|
| 48 |
}
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
def get_root_symbol(desc: str, symbol: str) -> str:
|
| 51 |
+
"""Extracts root symbol (e.g. 'RELIANCE', 'SENSEX') from description."""
|
| 52 |
try:
|
| 53 |
parts = desc.strip().split(' ')
|
| 54 |
root = parts[0].upper()
|
|
|
|
| 72 |
|
| 73 |
# 1. NSE F&O
|
| 74 |
url_nse = "https://public.fyers.in/sym_details/NSE_FO.csv"
|
| 75 |
+
df_nse = pd.read_csv(
|
| 76 |
+
url_nse, usecols=[0, 1, 8, 9, 13],
|
| 77 |
+
names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'],
|
| 78 |
+
header=0, on_bad_lines='skip', storage_options=headers
|
| 79 |
+
)
|
| 80 |
df_nse['Exch'] = 'NSE'
|
| 81 |
|
| 82 |
# 2. MCX F&O
|
| 83 |
url_mcx = "https://public.fyers.in/sym_details/MCX_COM.csv"
|
| 84 |
+
df_mcx = pd.read_csv(
|
| 85 |
+
url_mcx, usecols=[0, 1, 8, 9, 13],
|
| 86 |
+
names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'],
|
| 87 |
+
header=0, on_bad_lines='skip', storage_options=headers
|
| 88 |
+
)
|
| 89 |
df_mcx['Exch'] = 'MCX'
|
| 90 |
|
| 91 |
# 3. BSE F&O
|
| 92 |
url_bse = "https://public.fyers.in/sym_details/BSE_FO.csv"
|
| 93 |
+
df_bse = pd.read_csv(
|
| 94 |
+
url_bse, usecols=[0, 1, 8, 9, 13],
|
| 95 |
+
names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'],
|
| 96 |
+
header=0, on_bad_lines='skip', storage_options=headers
|
| 97 |
+
)
|
| 98 |
df_bse['Exch'] = 'BSE'
|
| 99 |
|
| 100 |
SYSTEM_STATUS["message"] = "Processing Data..."
|
|
|
|
| 201 |
thread.start()
|
| 202 |
|
| 203 |
# ==========================================
|
| 204 |
+
# 3. ROUTES
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
# ==========================================
|
| 206 |
|
| 207 |
@app.get("/")
|
|
|
|
| 222 |
</div>
|
| 223 |
""")
|
| 224 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))
|
| 226 |
|
| 227 |
@app.get("/callback")
|
|
|
|
| 280 |
return JSONResponse({"s": "ok"})
|
| 281 |
|
| 282 |
# ==========================================
|
| 283 |
+
# 4. FRONTEND
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
# ==========================================
|
| 285 |
|
| 286 |
HTML_TEMPLATE = """
|
|
|
|
| 288 |
<html lang="en">
|
| 289 |
<head>
|
| 290 |
<meta charset="UTF-8">
|
| 291 |
+
<title>PRO CHAIN V3</title>
|
| 292 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 293 |
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
|
| 294 |
<style>
|
|
|
|
| 570 |
let SPOT_PRICE = 0;
|
| 571 |
let FUT_SYMS = [];
|
| 572 |
let OPT_SYMS = [];
|
| 573 |
+
let POLLER = null;
|
| 574 |
+
let IS_POLLING = false;
|
| 575 |
let SEARCH_TIMER = null;
|
| 576 |
let ATM_STRIKE = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 577 |
|
| 578 |
// Default Configurations
|
| 579 |
const DEFAULTS = {
|
|
|
|
| 594 |
|
| 595 |
applyConfig();
|
| 596 |
checkStatus();
|
|
|
|
| 597 |
|
| 598 |
const inp = document.getElementById('searchInp');
|
| 599 |
inp.addEventListener('input', (e) => {
|
|
|
|
| 604 |
if(!e.target.closest('.search-box')) document.getElementById('searchRes').style.display = 'none';
|
| 605 |
});
|
| 606 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 607 |
|
| 608 |
// ===================================
|
| 609 |
// CONFIGURATION LOGIC
|
|
|
|
| 635 |
|
| 636 |
function setFormula(val) {
|
| 637 |
const input = document.getElementById('confForm');
|
| 638 |
+
// If editing Left, defaults use Call variables; Right uses Put variables
|
| 639 |
let f = val;
|
| 640 |
if(EDITING_COL === 'left') {
|
| 641 |
f = f.replace(/S/g, 'CS').replace(/B/g, 'CB');
|
| 642 |
} else if(EDITING_COL === 'right') {
|
| 643 |
f = f.replace(/S/g, 'PS').replace(/B/g, 'PB');
|
| 644 |
} else {
|
| 645 |
+
f = f.replace(/S/g, 'PS').replace(/B/g, 'CB'); // Middle default logic usually mix
|
| 646 |
}
|
| 647 |
input.value = f;
|
| 648 |
}
|
|
|
|
| 662 |
|
| 663 |
applyConfig();
|
| 664 |
closeModal('confModal');
|
| 665 |
+
pollData(); // Force refresh
|
| 666 |
}
|
| 667 |
|
| 668 |
function saveSideConfig() {
|
|
|
|
| 677 |
|
| 678 |
applyConfig();
|
| 679 |
closeModal('sideConfModal');
|
| 680 |
+
pollData();
|
| 681 |
}
|
| 682 |
|
| 683 |
function resetConfig() {
|
|
|
|
| 686 |
localStorage.setItem('tst_chain_config', JSON.stringify(CONFIG));
|
| 687 |
applyConfig();
|
| 688 |
closeModal('confModal');
|
| 689 |
+
pollData();
|
| 690 |
}
|
| 691 |
}
|
| 692 |
|
|
|
|
| 696 |
// MATH ENGINE
|
| 697 |
// ===================================
|
| 698 |
function evaluateFormula(formula, vars) {
|
| 699 |
+
// vars: { CS, CB, PS, PB }
|
| 700 |
try {
|
| 701 |
let exp = formula;
|
| 702 |
+
// Replace vars with values. We sort by length desc to avoid replacing 'CS' inside 'CSS' if that existed.
|
| 703 |
+
// But here we have 2-char vars.
|
| 704 |
exp = exp.replace(/CS/g, vars.CS || 0);
|
| 705 |
exp = exp.replace(/CB/g, vars.CB || 0);
|
| 706 |
exp = exp.replace(/PS/g, vars.PS || 0);
|
| 707 |
exp = exp.replace(/PB/g, vars.PB || 0);
|
| 708 |
+
|
| 709 |
+
// Safety check: only allow numbers and math ops
|
| 710 |
if(/[^0-9+\-*/(). ]/.test(exp)) return 0;
|
| 711 |
+
|
| 712 |
+
// Evaluate
|
| 713 |
return new Function('return ' + exp)();
|
| 714 |
} catch(e) {
|
| 715 |
return 0;
|
|
|
|
| 771 |
CURRENT_ROOT = root;
|
| 772 |
CHAIN_DATA = data.items;
|
| 773 |
SPOT_SYM = data.spot;
|
| 774 |
+
SPOT_PRICE = 0; // Reset price on new load
|
|
|
|
| 775 |
|
| 776 |
FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
|
| 777 |
|
|
|
|
| 848 |
</div>
|
| 849 |
`).join('');
|
| 850 |
|
| 851 |
+
window.scrolled = false; // Reset scroll state
|
| 852 |
+
restartPoller();
|
| 853 |
}
|
| 854 |
|
| 855 |
+
function restartPoller() {
|
| 856 |
+
if(POLLER) clearInterval(POLLER);
|
| 857 |
+
POLLER = setInterval(pollData, 1500);
|
| 858 |
+
pollData();
|
| 859 |
+
}
|
|
|
|
|
|
|
|
|
|
| 860 |
|
| 861 |
+
async function pollData() {
|
| 862 |
+
if(IS_POLLING || !CURRENT_ROOT) return;
|
| 863 |
+
IS_POLLING = true;
|
| 864 |
+
|
| 865 |
+
try {
|
| 866 |
+
let symbolsToFetch = [SPOT_SYM, ...FUT_SYMS.map(x => x.s)];
|
| 867 |
+
const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
|
| 868 |
+
|
| 869 |
+
if(kList.length > 0) {
|
| 870 |
+
// LOGIC UPDATE: Select Next Strike just above live price
|
| 871 |
+
// If price is 0 (first load), fallback to middle temporarily
|
| 872 |
+
const refPrice = SPOT_PRICE > 0 ? SPOT_PRICE : kList[Math.floor(kList.length/2)];
|
| 873 |
+
|
| 874 |
+
// Find first strike >= refPrice (Next Strike)
|
| 875 |
+
let selectedK = kList.find(k => k >= refPrice);
|
| 876 |
+
|
| 877 |
+
// Edge Case: If price is higher than all strikes, take the last one
|
| 878 |
+
if (selectedK === undefined) selectedK = kList[kList.length - 1];
|
| 879 |
|
| 880 |
+
ATM_STRIKE = selectedK;
|
| 881 |
+
const atmIdx = kList.indexOf(selectedK);
|
| 882 |
+
|
| 883 |
+
document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
|
| 884 |
+
const atmRow = document.getElementById(`row-${selectedK}`);
|
| 885 |
+
|
| 886 |
+
if(atmRow) {
|
| 887 |
+
atmRow.classList.add('atm');
|
| 888 |
+
// SCROLL UPDATE: Only scroll if we have a valid LIVE price
|
| 889 |
+
if(!window.scrolled && SPOT_PRICE > 0) {
|
| 890 |
+
atmRow.scrollIntoView({block:'center', behavior:'auto'});
|
| 891 |
+
window.scrolled = true;
|
| 892 |
+
}
|
| 893 |
+
}
|
| 894 |
+
|
| 895 |
+
// Range settings for Side Panel
|
| 896 |
+
const sideRange = CONFIG.side.range;
|
| 897 |
+
|
| 898 |
+
// Visible Range (Fixed)
|
| 899 |
+
const start = Math.max(0, atmIdx - 20);
|
| 900 |
+
const end = Math.min(kList.length, atmIdx + 20);
|
| 901 |
+
const viewKs = kList.slice(start, end);
|
| 902 |
+
|
| 903 |
+
// Side Calculation Range
|
| 904 |
+
const rangeStart = Math.max(0, atmIdx - sideRange);
|
| 905 |
+
const rangeEnd = Math.min(kList.length, atmIdx + sideRange + 1);
|
| 906 |
+
const calcKs = kList.slice(rangeStart, rangeEnd);
|
| 907 |
+
|
| 908 |
+
const allNeededKs = new Set([...viewKs, ...calcKs]);
|
| 909 |
+
|
| 910 |
+
OPT_SYMS.forEach(o => {
|
| 911 |
+
if(allNeededKs.has(o.k)) symbolsToFetch.push(o.s);
|
| 912 |
+
});
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
const uniqueSyms = [...new Set(symbolsToFetch)].filter(s => s && s.length > 2);
|
| 916 |
+
if(uniqueSyms.length === 0) return;
|
| 917 |
+
|
| 918 |
+
const url = `https://api-t1.fyers.in/data/depth?symbol=${uniqueSyms.join(',')}&ohlcv_flag=1`;
|
| 919 |
+
const res = await fetch(url, { headers: { 'Authorization': AUTH_TOKEN }});
|
| 920 |
+
|
| 921 |
+
if(res.status === 401) { location.href = '/logout'; return; }
|
| 922 |
+
|
| 923 |
+
const json = await res.json();
|
| 924 |
+
if(json.s === 'ok') {
|
| 925 |
+
updateUI(json.d);
|
| 926 |
+
const dot = document.getElementById('statusDot');
|
| 927 |
+
dot.classList.add('active');
|
| 928 |
+
dot.classList.remove('error');
|
| 929 |
+
}
|
| 930 |
+
} catch(e) {
|
| 931 |
+
console.error(e);
|
| 932 |
+
const dot = document.getElementById('statusDot');
|
| 933 |
+
dot.classList.remove('active');
|
| 934 |
+
dot.classList.add('error');
|
| 935 |
+
}
|
| 936 |
+
finally { IS_POLLING = false; }
|
| 937 |
+
}
|
| 938 |
+
|
| 939 |
+
function updateUI(data) {
|
| 940 |
if(data[SPOT_SYM] && typeof data[SPOT_SYM].ltp === 'number') {
|
| 941 |
SPOT_PRICE = data[SPOT_SYM].ltp;
|
| 942 |
document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
|
| 943 |
}
|
| 944 |
|
| 945 |
+
// Futures
|
| 946 |
const futHTML = FUT_SYMS.map(f => {
|
| 947 |
const d = data[f.s];
|
| 948 |
if(!d || typeof d.ltp !== 'number') return '';
|
| 949 |
const dt = new Date(f.e*1000).toLocaleDateString('en-GB', {day:'numeric', month:'short'});
|
| 950 |
+
const tot = (d.totalbuyqty + d.totalsellqty) || 1;
|
| 951 |
+
const bPct = (d.totalbuyqty/tot)*100;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 952 |
return `
|
| 953 |
<div class="fut-card">
|
| 954 |
<div style="display:flex; justify-content:space-between; font-weight:700;">
|
| 955 |
+
<span>${dt} FUT</span> <span style="color:${d.ch>=0?'var(--green)':'var(--red)'}">${d.ltp}</span>
|
| 956 |
</div>
|
| 957 |
<div class="meter-bg">
|
| 958 |
<div style="width:${bPct}%; background:var(--green)"></div>
|
| 959 |
<div style="width:${100-bPct}%; background:var(--red)"></div>
|
| 960 |
</div>
|
| 961 |
<div style="display:flex; justify-content:space-between; font-size:10px; color:#666; margin-top:4px;">
|
| 962 |
+
<span>B: ${fmt(d.totalbuyqty)}</span> <span>S: ${fmt(d.totalsellqty)}</span>
|
| 963 |
</div>
|
| 964 |
</div>`;
|
| 965 |
}).join('');
|
| 966 |
if(futHTML) document.getElementById('futBody').innerHTML = futHTML;
|
| 967 |
|
| 968 |
+
// Process Options
|
| 969 |
const rowData = {};
|
| 970 |
|
| 971 |
+
// Populate raw data first
|
| 972 |
for(const item of OPT_SYMS) {
|
| 973 |
const d = data[item.s];
|
| 974 |
if(!d) continue;
|
| 975 |
|
| 976 |
if(!rowData[item.k]) rowData[item.k] = { CS:0, CB:0, PS:0, PB:0 };
|
| 977 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 978 |
if(item.t === 'CE') {
|
| 979 |
+
rowData[item.k].CS = d.totalsellqty;
|
| 980 |
+
rowData[item.k].CB = d.totalbuyqty;
|
| 981 |
}
|
| 982 |
if(item.t === 'PE') {
|
| 983 |
+
rowData[item.k].PS = d.totalsellqty;
|
| 984 |
+
rowData[item.k].PB = d.totalbuyqty;
|
| 985 |
}
|
| 986 |
}
|
| 987 |
|
| 988 |
+
// --- RENDER CHAIN ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 989 |
let maxMidVal = 0;
|
| 990 |
const processedRows = {};
|
| 991 |
|
| 992 |
+
// Calculate Formula values first
|
| 993 |
for(const k in rowData) {
|
| 994 |
const vars = rowData[k];
|
| 995 |
processedRows[k] = {
|
|
|
|
| 1002 |
|
| 1003 |
for(const k in processedRows) {
|
| 1004 |
const r = processedRows[k];
|
| 1005 |
+
|
| 1006 |
const lEl = document.getElementById(`colL-${k}`);
|
| 1007 |
if(lEl) {
|
| 1008 |
lEl.querySelector('.col-val').innerText = fmt(r.left);
|
|
|
|
| 1030 |
}
|
| 1031 |
}
|
| 1032 |
|
| 1033 |
+
// --- SIDEBAR AGGREGATION ---
|
| 1034 |
if(ATM_STRIKE > 0) {
|
| 1035 |
let totalFlow = 0;
|
| 1036 |
+
const kList = Object.keys(rowData).map(parseFloat).sort((a,b)=>a-b);
|
| 1037 |
+
const atmIdx = kList.indexOf(ATM_STRIKE);
|
| 1038 |
const sideRange = CONFIG.side.range;
|
| 1039 |
|
| 1040 |
if(atmIdx !== -1) {
|
| 1041 |
const start = Math.max(0, atmIdx - sideRange);
|
| 1042 |
+
const end = Math.min(kList.length, atmIdx + sideRange + 1);
|
| 1043 |
|
| 1044 |
for(let i=start; i<end; i++) {
|
| 1045 |
+
const k = kList[i];
|
| 1046 |
if(rowData[k]) {
|
| 1047 |
totalFlow += evaluateFormula(CONFIG.side.formula, rowData[k]);
|
| 1048 |
}
|
|
|
|
| 1054 |
|
| 1055 |
nfEl.innerText = (totalFlow > 0 ? "+" : "") + fmt(totalFlow);
|
| 1056 |
nfEl.style.color = totalFlow >= 0 ? 'var(--green)' : 'var(--red)';
|
| 1057 |
+
|
| 1058 |
+
if(totalFlow > 0) {
|
| 1059 |
+
nfSig.innerText = "POSITIVE FLOW";
|
| 1060 |
+
nfSig.style.color = "var(--green)";
|
| 1061 |
+
} else {
|
| 1062 |
+
nfSig.innerText = "NEGATIVE FLOW";
|
| 1063 |
+
nfSig.style.color = "var(--red)";
|
| 1064 |
+
}
|
| 1065 |
}
|
| 1066 |
}
|
| 1067 |
|