topsecrettraders commited on
Commit
be8c727
·
verified ·
1 Parent(s): 53011c5

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +464 -0
  2. requirements.txt +7 -0
app.py ADDED
@@ -0,0 +1,464 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import requests
3
+ import uvicorn
4
+ import hashlib
5
+ import json
6
+ import io
7
+ import time
8
+ from fastapi import FastAPI, Request, Response, Cookie
9
+ from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
10
+ from fastapi.middleware.cors import CORSMiddleware
11
+ from typing import Optional
12
+
13
+ # ==========================================
14
+ # 1. CONFIGURATION (EDIT THESE)
15
+ # ==========================================
16
+ CLIENT_ID = "J4PNT5QI22-100" # Format: XPxxxxx-100
17
+ SECRET_KEY = "ABRSWC1PMR"
18
+ # IMPORTANT: Change this to your Space's URL + /callback
19
+ # Example: https://huggingface-username-space-name.hf.space/callback
20
+ REDIRECT_URI = "https://your-space-url.hf.space/callback"
21
+
22
+ # Global Cache
23
+ MASTER_DATA = []
24
+ LAST_UPDATE = 0
25
+
26
+ app = FastAPI()
27
+
28
+ # ==========================================
29
+ # 2. PYTHON BACKEND (Optimized Data Loader)
30
+ # ==========================================
31
+
32
+ def load_nifty_master():
33
+ """
34
+ Downloads the large CSV, filters ONLY NIFTY 50 and NIFTY FUTS using Pandas.
35
+ This runs in seconds and stays in RAM.
36
+ """
37
+ global MASTER_DATA, LAST_UPDATE
38
+
39
+ print(">>> DOWNLOADING MASTER DATA...")
40
+ try:
41
+ url = "https://public.fyers.in/sym_details/NSE_FO.csv"
42
+ # Read CSV directly into Pandas
43
+ df = pd.read_csv(url, usecols=[0, 1, 8, 9, 13], names=['RecType','Desc','Exch','Expiry','Symbol','Unknown'], header=0)
44
+
45
+ # 1. FILTER: Symbol must contain 'NIFTY' but NOT 'BANK', 'FIN', 'MID'
46
+ # We only want pure NIFTY (NIFTY 50)
47
+ mask = (
48
+ df['Symbol'].str.contains('NIFTY', na=False) &
49
+ ~df['Symbol'].str.contains('BANKNIFTY', na=False) &
50
+ ~df['Symbol'].str.contains('FINNIFTY', na=False) &
51
+ ~df['Symbol'].str.contains('MIDCPNIFTY', na=False)
52
+ )
53
+ df = df[mask]
54
+
55
+ results = []
56
+
57
+ # Add Spot manually
58
+ results.append({"s": "NSE:NIFTY50-INDEX", "e": 0, "k": 0})
59
+
60
+ # Process Rows
61
+ for _, row in df.iterrows():
62
+ sym = row['Symbol']
63
+ desc = str(row['Desc'])
64
+ exp = int(row['Expiry'])
65
+
66
+ # Extract Strike
67
+ strike = 0.0
68
+ if "CE" in sym or "PE" in sym:
69
+ # Desc format usually: "NIFTY 06 Jan 20500 CE"
70
+ parts = desc.strip().split(' ')
71
+ # Try getting the 2nd to last item
72
+ if len(parts) >= 2:
73
+ val = parts[-2]
74
+ if val.replace('.','',1).isdigit():
75
+ strike = float(val)
76
+
77
+ results.append({
78
+ "s": sym,
79
+ "e": exp,
80
+ "k": strike
81
+ })
82
+
83
+ MASTER_DATA = results
84
+ LAST_UPDATE = time.time()
85
+ print(f">>> LOADED {len(results)} NIFTY SYMBOLS.")
86
+ return True
87
+ except Exception as e:
88
+ print(f"Error loading master: {e}")
89
+ return False
90
+
91
+ # Initialize on startup
92
+ @app.on_event("startup")
93
+ def startup_event():
94
+ load_nifty_master()
95
+
96
+ # ==========================================
97
+ # 3. ROUTES
98
+ # ==========================================
99
+
100
+ @app.get("/")
101
+ def home(access_token: Optional[str] = Cookie(None)):
102
+ if not access_token:
103
+ # Generate Auth URL
104
+ state = "init"
105
+ auth_url = (
106
+ f"https://api-t1.fyers.in/api/v3/generate-authcode?"
107
+ f"client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&response_type=code&state={state}"
108
+ )
109
+ return HTMLResponse(f"""
110
+ <div style="height:100vh; display:flex; flex-direction:column; justify-content:center; align-items:center; font-family:sans-serif;">
111
+ <h1>FYERS NIFTY CHAIN (PYTHON)</h1>
112
+ <p>Please update REDIRECT_URI in app.py to match this HuggingFace Space URL.</p>
113
+ <a href="{auth_url}" style="background:#2962ff; color:white; padding:15px 30px; text-decoration:none; border-radius:5px; font-weight:bold;">LOGIN WITH FYERS</a>
114
+ </div>
115
+ """)
116
+
117
+ # If logged in, serve the App
118
+ return HTMLResponse(HTML_TEMPLATE)
119
+
120
+ @app.get("/callback")
121
+ def callback(code: str):
122
+ # Validate Auth Code
123
+ app_id_hash = hashlib.sha256(f"{CLIENT_ID}:{SECRET_KEY}".encode()).hexdigest()
124
+
125
+ payload = {
126
+ "grant_type": "authorization_code",
127
+ "appIdHash": app_id_hash,
128
+ "code": code
129
+ }
130
+
131
+ resp = requests.post("https://api-t1.fyers.in/api/v3/validate-authcode", json=payload)
132
+ data = resp.json()
133
+
134
+ if "access_token" in data:
135
+ full_token = f"{CLIENT_ID}:{data['access_token']}"
136
+ response = RedirectResponse(url="/")
137
+ response.set_cookie(key="access_token", value=full_token, httponly=True)
138
+ return response
139
+ else:
140
+ return JSONResponse(content={"error": "Login Failed", "details": data})
141
+
142
+ @app.get("/logout")
143
+ def logout():
144
+ response = RedirectResponse(url="/")
145
+ response.delete_cookie("access_token")
146
+ return response
147
+
148
+ @app.get("/api/init")
149
+ def get_master_data(force: bool = False):
150
+ global MASTER_DATA
151
+ if force or not MASTER_DATA:
152
+ load_nifty_master()
153
+ return JSONResponse(content=MASTER_DATA)
154
+
155
+ @app.get("/api/depth")
156
+ def get_market_depth(symbols: str, access_token: Optional[str] = Cookie(None)):
157
+ if not access_token:
158
+ return JSONResponse(content={"s": "error", "message": "No Token"}, status_code=401)
159
+
160
+ url = f"https://api-t1.fyers.in/data/depth?symbol={symbols}&ohlcv_flag=1"
161
+ headers = {"Authorization": access_token}
162
+
163
+ resp = requests.get(url, headers=headers)
164
+ return JSONResponse(content=resp.json())
165
+
166
+
167
+ # ==========================================
168
+ # 4. FRONTEND (Single Page Application)
169
+ # ==========================================
170
+
171
+ HTML_TEMPLATE = """
172
+ <!DOCTYPE html>
173
+ <html lang="en">
174
+ <head>
175
+ <meta charset="UTF-8">
176
+ <title>NIFTY PYTHON CHAIN</title>
177
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
178
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
179
+ <style>
180
+ :root {
181
+ --bg: #ffffff; --panel: #f4f6f8; --border: #e0e0e0;
182
+ --text-main: #1a1a1a; --text-sub: #555555;
183
+ --accent: #2962ff; --green: #00c853; --red: #d50000;
184
+ --green-soft: rgba(0, 200, 83, 0.3); --red-soft: rgba(213, 0, 0, 0.3);
185
+ --atm-bg: rgba(41, 98, 255, 0.1);
186
+ }
187
+ * { box-sizing: border-box; }
188
+ body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
189
+
190
+ .top-nav { padding: 0 20px; background: #fff; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; height: 60px; }
191
+ .btn { background: #fff; border: 1px solid var(--border); padding: 8px 16px; font-size: 12px; border-radius: 6px; cursor: pointer; font-weight: 700; display: flex; gap: 8px; align-items: center; }
192
+ .btn:hover { border-color: var(--accent); color: var(--accent); }
193
+
194
+ .grid-layout { display: flex; height: calc(100vh - 60px); }
195
+ .chain-panel { flex: 1; display: flex; flex-direction: column; position: relative; }
196
+ .chain-header { display: grid; grid-template-columns: 1fr 180px 1fr; padding: 10px 0; background: #f8f9fa; border-bottom: 1px solid var(--border); font-size: 11px; font-weight: 700; text-align: center; color: var(--text-sub); }
197
+ .chain-body { flex: 1; overflow-y: auto; background: #fff; }
198
+
199
+ .row { display: grid; grid-template-columns: 1fr 180px 1fr; border-bottom: 1px solid #f0f0f0; height: 45px; align-items: center; font-family: 'JetBrains Mono'; font-size: 12px; }
200
+ .row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
201
+
202
+ .mid-col { position: relative; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; border-left: 1px solid var(--border); border-right: 1px solid var(--border); }
203
+ .visual-bar { position: absolute; top: 6px; bottom: 6px; z-index: 0; border-radius: 4px; transition: 0.3s; opacity: 0.8; }
204
+ .strike-txt { z-index: 2; font-weight: 800; font-size: 14px; }
205
+ .delta-txt { z-index: 2; font-size: 10px; font-weight: 600; background: rgba(255,255,255,0.7); padding: 0 4px; border-radius: 3px; }
206
+
207
+ .side-col { padding: 0 15px; display: flex; justify-content: space-between; color: #666; font-size: 11px; }
208
+
209
+ .fut-panel { width: 300px; background: var(--panel); border-left: 1px solid var(--border); display: flex; flex-direction: column; }
210
+ .fut-card { padding: 15px; background: #fff; border-bottom: 1px solid var(--border); }
211
+ .meter-bg { height: 6px; background: #eee; border-radius: 3px; display: flex; overflow: hidden; margin-top: 5px; }
212
+
213
+ /* Modal */
214
+ .modal { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(255,255,255,0.9); z-index: 1000; display: none; justify-content: center; align-items: center; }
215
+ .modal-box { background: #fff; border: 1px solid #ccc; width: 400px; border-radius: 8px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); padding: 20px; }
216
+ .modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; max-height: 50vh; overflow-y: auto; }
217
+ .exp-btn { padding: 10px; text-align: center; border: 1px solid #eee; border-radius: 4px; cursor: pointer; }
218
+ .exp-btn:hover { background: #f0f7ff; border-color: var(--accent); }
219
+ .exp-btn.active { background: var(--accent); color: white; }
220
+
221
+ .loader { position: absolute; top:0; left:0; width:100%; height:100%; background:rgba(255,255,255,0.95); display:flex; justify-content:center; align-items:center; z-index: 2000; font-weight: 700; color: var(--accent); }
222
+ </style>
223
+ </head>
224
+ <body>
225
+
226
+ <div class="loader" id="loader">INITIALIZING PYTHON DB...</div>
227
+
228
+ <!-- MODAL -->
229
+ <div class="modal" id="expModal">
230
+ <div class="modal-box">
231
+ <div style="display:flex; justify-content:space-between; font-weight:700;">
232
+ <span>SELECT EXPIRY</span>
233
+ <span style="cursor:pointer" onclick="document.getElementById('expModal').style.display='none'">✕</span>
234
+ </div>
235
+ <div class="modal-grid" id="expGrid"></div>
236
+ </div>
237
+ </div>
238
+
239
+ <div class="top-nav">
240
+ <div style="font-weight:800; font-size:18px; color:var(--accent);">NIFTY 50 (PYTHON)</div>
241
+
242
+ <div style="display:flex; gap:20px; align-items:center;">
243
+ <div style="text-align:right;">
244
+ <div style="font-size:10px; font-weight:700; color:#888;">SPOT</div>
245
+ <div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:16px;">0.00</div>
246
+ </div>
247
+ <button class="btn" onclick="openExpModal()">
248
+ <span style="color:#888">EXP:</span> <span id="selExpTxt">SELECT</span>
249
+ </button>
250
+ <button class="btn" onclick="location.href='/logout'" style="border-color:var(--red); color:var(--red);">LOGOUT</button>
251
+ <div id="statusDot" style="width:10px; height:10px; background:#ccc; border-radius:50%;"></div>
252
+ </div>
253
+ </div>
254
+
255
+ <div class="grid-layout">
256
+ <div class="chain-panel">
257
+ <div class="chain-header">
258
+ <div>CALL SELLERS (OI)</div>
259
+ <div>NET FLOW (PUT S - CALL S)</div>
260
+ <div>PUT SELLERS (OI)</div>
261
+ </div>
262
+ <div class="chain-body" id="chainBody"></div>
263
+ </div>
264
+
265
+ <div class="fut-panel">
266
+ <div style="padding:15px; font-weight:700; border-bottom:1px solid var(--border); background:#fff;">FUTURES</div>
267
+ <div id="futBody"></div>
268
+ </div>
269
+ </div>
270
+
271
+ <script>
272
+ let DB = [];
273
+ let SPOT_SYM = "NSE:NIFTY50-INDEX";
274
+ let ACTIVE_EXP = 0;
275
+ let SPOT_PRICE = 0;
276
+ let POLLER = null;
277
+ let FUT_SYMS = [];
278
+
279
+ window.onload = async () => {
280
+ try {
281
+ // Load from Python Backend
282
+ const res = await fetch('/api/init');
283
+ DB = await res.json();
284
+ document.getElementById('loader').style.display = 'none';
285
+
286
+ if(DB.length === 0) { alert("DB Empty. Check Python Logs."); return; }
287
+
288
+ // Setup Futures
289
+ FUT_SYMS = DB.filter(i => i.s.startsWith('NSE:NIFTY') && i.s.endsWith('FUT') && /\\d/.test(i.s))
290
+ .sort((a,b) => a.e - b.e).slice(0,3);
291
+
292
+ openExpModal();
293
+ } catch(e) {
294
+ document.getElementById('loader').innerText = "ERROR LOADING DATA";
295
+ }
296
+ };
297
+
298
+ function openExpModal() {
299
+ const grid = document.getElementById('expGrid');
300
+ grid.innerHTML = '';
301
+ const ops = DB.filter(i => i.s.endsWith('CE') || i.s.endsWith('PE'));
302
+ const exps = [...new Set(ops.map(i => i.e))].sort((a,b) => a-b);
303
+ const today = Math.floor(Date.now()/1000) - 86400;
304
+
305
+ exps.forEach(ts => {
306
+ if(ts < today) return;
307
+ const d = new Date(ts * 1000);
308
+ const dStr = d.toLocaleDateString('en-GB', {day:'2-digit', month:'short'});
309
+ const div = document.createElement('div');
310
+ div.className = `exp-btn ${ts === ACTIVE_EXP ? 'active' : ''}`;
311
+ div.innerText = dStr;
312
+ div.onclick = () => {
313
+ ACTIVE_EXP = ts;
314
+ document.getElementById('selExpTxt').innerText = dStr;
315
+ document.getElementById('expModal').style.display = 'none';
316
+ buildChain();
317
+ }
318
+ grid.appendChild(div);
319
+ });
320
+ document.getElementById('expModal').style.display = 'flex';
321
+ }
322
+
323
+ function buildChain() {
324
+ const body = document.getElementById('chainBody');
325
+ body.innerHTML = '';
326
+
327
+ const relevant = DB.filter(i => i.e === ACTIVE_EXP && (i.s.endsWith('CE') || i.s.endsWith('PE')));
328
+ const strikes = [...new Set(relevant.map(i => i.k))].sort((a,b) => a-b);
329
+
330
+ body.innerHTML = strikes.map(k => `
331
+ <div class="row" id="row-${k}" data-k="${k}">
332
+ <div class="side-col" id="ce-${k}"><span>-</span><span>-</span></div>
333
+ <div class="mid-col">
334
+ <div class="visual-bar" id="bar-${k}"></div>
335
+ <span class="strike-txt">${k}</span>
336
+ <span class="delta-txt" id="val-${k}">-</span>
337
+ </div>
338
+ <div class="side-col" id="pe-${k}"><span>-</span><span>-</span></div>
339
+ </div>
340
+ `).join('');
341
+
342
+ if(POLLER) clearInterval(POLLER);
343
+ POLLER = setInterval(poll, 2000);
344
+ poll();
345
+ }
346
+
347
+ async function poll() {
348
+ let syms = [SPOT_SYM, ...FUT_SYMS.map(f => f.s)];
349
+
350
+ // Optimize visible rows
351
+ if(SPOT_PRICE > 0) {
352
+ const rows = Array.from(document.querySelectorAll('.row'));
353
+ let closest = rows[0], min = 999999;
354
+ rows.forEach(r => {
355
+ const diff = Math.abs(SPOT_PRICE - parseFloat(r.dataset.k));
356
+ if(diff < min) { min = diff; closest = r; }
357
+ });
358
+ document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
359
+ closest.classList.add('atm');
360
+
361
+ if(!window.scrolled) { closest.scrollIntoView({block:'center'}); window.scrolled = true; }
362
+
363
+ const idx = rows.indexOf(closest);
364
+ const viewRows = rows.slice(Math.max(0, idx-15), Math.min(rows.length, idx+15));
365
+ const viewKs = viewRows.map(r => parseFloat(r.dataset.k));
366
+
367
+ DB.forEach(i => {
368
+ if(i.e === ACTIVE_EXP && viewKs.includes(i.k)) syms.push(i.s);
369
+ });
370
+ }
371
+
372
+ try {
373
+ const res = await fetch(`/api/depth?symbols=${encodeURIComponent([...new Set(syms)].join(','))}`);
374
+ const json = await res.json();
375
+ if(json.s === 'ok') {
376
+ document.getElementById('statusDot').style.background = 'var(--green)';
377
+ updateUI(json.d);
378
+ }
379
+ } catch(e) { document.getElementById('statusDot').style.background = 'var(--red)'; }
380
+ }
381
+
382
+ function updateUI(data) {
383
+ if(data[SPOT_SYM]) {
384
+ SPOT_PRICE = data[SPOT_SYM].ltp;
385
+ document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
386
+ }
387
+
388
+ // Futures
389
+ document.getElementById('futBody').innerHTML = FUT_SYMS.map(obj => {
390
+ const d = data[obj.s];
391
+ if(!d) return '';
392
+ const tot = (d.totalbuyqty + d.totalsellqty) || 1;
393
+ const dt = new Date(obj.e * 1000).toLocaleDateString('en-GB', {day:'numeric', month:'short'});
394
+ return `
395
+ <div class="fut-card">
396
+ <div style="display:flex; justify-content:space-between; font-weight:700;">
397
+ <span>${dt} FUT</span> <span style="color:${d.ch>=0?'var(--green)':'var(--red)'}">${d.ltp}</span>
398
+ </div>
399
+ <div class="meter-bg">
400
+ <div style="width:${(d.totalbuyqty/tot)*100}%; background:var(--green)"></div>
401
+ <div style="width:${(d.totalsellqty/tot)*100}%; background:var(--red)"></div>
402
+ </div>
403
+ <div style="display:flex; justify-content:space-between; font-size:10px; color:#666; margin-top:2px;">
404
+ <span>B: ${fmt(d.totalbuyqty)}</span> <span>S: ${fmt(d.totalsellqty)}</span>
405
+ </div>
406
+ </div>`;
407
+ }).join('');
408
+
409
+ // Chain
410
+ const rowData = {};
411
+ for(const [sym, d] of Object.entries(data)) {
412
+ const meta = DB.find(x => x.s === sym);
413
+ if(!meta || meta.e !== ACTIVE_EXP) continue;
414
+ if(!rowData[meta.k]) rowData[meta.k] = { ce:0, pe:0 };
415
+ if(sym.endsWith('CE')) rowData[meta.k].ce = d.totalsellqty;
416
+ if(sym.endsWith('PE')) rowData[meta.k].pe = d.totalsellqty;
417
+ }
418
+
419
+ let maxD = 0;
420
+ for(const k in rowData) maxD = Math.max(maxD, Math.abs(rowData[k].pe - rowData[k].ce));
421
+
422
+ for(const k in rowData) {
423
+ const r = rowData[k];
424
+ const delta = r.pe - r.ce; // Put Sellers - Call Sellers
425
+
426
+ const ceDiv = document.getElementById(`ce-${k}`);
427
+ const peDiv = document.getElementById(`pe-${k}`);
428
+ if(ceDiv) ceDiv.innerHTML = `<span>S: <strong>${fmt(r.ce)}</strong></span>`;
429
+ if(peDiv) peDiv.innerHTML = `<span>S: <strong>${fmt(r.pe)}</strong></span>`;
430
+
431
+ const bar = document.getElementById(`bar-${k}`);
432
+ const val = document.getElementById(`val-${k}`);
433
+
434
+ if(bar) {
435
+ const pct = maxD > 0 ? (Math.abs(delta)/maxD)*100 : 0;
436
+ if(delta >= 0) {
437
+ bar.style.background = 'var(--green-soft)';
438
+ bar.style.left = '50%';
439
+ bar.style.width = (pct/2) + '%';
440
+ val.style.color = 'var(--green)';
441
+ val.innerText = "+" + fmt(delta);
442
+ } else {
443
+ bar.style.background = 'var(--red-soft)';
444
+ bar.style.left = (50 - (pct/2)) + '%';
445
+ bar.style.width = (pct/2) + '%';
446
+ val.style.color = 'var(--red)';
447
+ val.innerText = fmt(delta);
448
+ }
449
+ }
450
+ }
451
+ }
452
+
453
+ function fmt(n) {
454
+ if(n>=100000) return (n/100000).toFixed(2)+'L';
455
+ if(n>=1000) return (n/1000).toFixed(1)+'k';
456
+ return n;
457
+ }
458
+ </script>
459
+ </body>
460
+ </html>
461
+ """
462
+
463
+ if __name__ == "__main__":
464
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ requests
4
+ pandas
5
+ httpx
6
+ jinja2
7
+ python-multipart