topsecrettraders commited on
Commit
2ff8359
·
verified ·
1 Parent(s): b69ee78

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +638 -594
app.py CHANGED
@@ -6,7 +6,7 @@ import re
6
  from datetime import timedelta
7
  from fastapi import FastAPI, Body
8
  from fastapi.responses import HTMLResponse, JSONResponse
9
- from typing import Optional, Set
10
 
11
  # ==========================================
12
  # 1. CONFIGURATION
@@ -48,53 +48,26 @@ def sb_rpc(func_name: str, params: dict = None):
48
  print(f"DB Error RPC ({func_name}): {e}")
49
  return []
50
 
51
- def extract_variables_from_formulas(series_list: list) -> Set[str]:
52
- """
53
- Parse formulas to extract required variables.
54
- Returns set of column names needed from SQL.
55
- Always includes 'price' as base.
56
- """
57
- required = {'price'} # Always need price
58
-
59
- variable_map = {
60
- '$P': 'price',
61
- '$V': 'volume',
62
- '$OI': 'oi',
63
- '$B': 'buy_qty',
64
- '$S': 'sell_qty',
65
- '$CB': 'cb',
66
- '$CS': 'cs',
67
- '$PB': 'pb',
68
- '$PS': 'ps'
69
- }
70
-
71
- for s in series_list:
72
- formula = s.get('formula', '')
73
- for var, col in variable_map.items():
74
- if var in formula:
75
- required.add(col)
76
-
77
- return required
78
-
79
- def detect_instrument_type(instrument: str) -> str:
80
- """
81
- Detect instrument type from name.
82
- Returns: 'futures', 'equity', 'call', 'put'
83
- """
84
  if not instrument:
85
- return 'unknown'
86
- instrument = instrument.upper()
87
-
88
- if 'FUT' in instrument:
89
- return 'futures'
90
- elif '-INDEX' in instrument or '-EQ' in instrument:
91
- return 'equity'
92
- elif instrument.endswith('CE'):
93
- return 'call'
94
  elif instrument.endswith('PE'):
95
- return 'put'
96
- else:
97
- return 'unknown'
 
 
 
 
 
 
98
 
99
  # ==========================================
100
  # 3. API ROUTES - METADATA
@@ -171,7 +144,7 @@ def get_instruments(date: str, root: str, expiry: str):
171
  return JSONResponse({"spot_fut": sorted(spot_fut), "options": sorted(options)})
172
 
173
  # ==========================================
174
- # 4. API ROUTES - DATA FETCHING (RPC DRIVEN WITH CHUNKING)
175
  # ==========================================
176
 
177
  @app.post("/api/fetch_series")
@@ -185,24 +158,118 @@ async def fetch_series(
185
  end_time: str = Body(...),
186
  mode: str = Body("normal"),
187
  atm_range: int = Body(5),
188
- series: list = Body(default=[]) # For variable extraction
189
  ):
190
  root = root.upper()
191
  t_start_dt = pd.to_datetime(f"{date} {start_time}:00")
192
  t_end_dt = pd.to_datetime(f"{date} {end_time}:59")
193
 
194
- # Extract required variables from formulas
195
- required_cols = extract_variables_from_formulas(series)
 
 
 
 
 
 
 
 
 
 
 
 
196
 
197
- if mode == "advanced":
198
- # 1. Fetch available instruments (Lightweight metadata fetch)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  inst_data = sb_rpc("get_instruments_for_expiry", {"p_date": date, "p_root": root, "p_expiry": expiry})
200
  instruments = [r['instrument_name'] for r in inst_data]
201
 
202
  if not instruments:
203
- return JSONResponse({"error": f"No data found for expiry {expiry}", "labels":[], "P":[], "breakdown": []})
204
 
205
- # 2. Intelligent Default Selection
206
  ref_instrument = instrument
207
  if not ref_instrument:
208
  spot_keys = [k for k in instruments if k.endswith('-INDEX') or k.endswith('-EQ')]
@@ -215,152 +282,253 @@ async def fetch_series(
215
  else:
216
  others = [k for k in instruments if not k.endswith('CE') and not k.endswith('PE')]
217
  ref_instrument = others[0] if others else instruments[0]
218
-
219
- rpc_name = "get_advanced_chart_data"
220
- base_params = {
221
- "p_root": root,
222
- "p_expiry": expiry,
223
- "p_ref_instrument": ref_instrument,
224
- "p_atm_range": atm_range,
225
- "p_strike_map": {}
226
- }
227
-
228
- else:
229
- # Normal fetching
230
- if not instrument:
231
- return JSONResponse({"error": "No Instrument Selected.", "labels": [], "P":[], "breakdown": []})
232
-
233
- rpc_name = "get_normal_chart_data"
234
- base_params = {
235
- "p_root": root,
236
- "p_expiry": expiry,
237
- "p_instrument": instrument
238
- }
239
-
240
- # =========================================================
241
- # CHUNKING LOGIC: Bypass Supabase DB/Timeout Limits
242
- # ADVANCED MODE: Use 30 min chunks (faster for heavy query)
243
- # NORMAL MODE: Use 2 hour chunks
244
- # =========================================================
245
- async def fetch_chunk(start_dt, end_dt):
246
- params = base_params.copy()
247
- params["p_start_time"] = start_dt.strftime("%Y-%m-%d %H:%M:%S")
248
- params["p_end_time"] = end_dt.strftime("%Y-%m-%d %H:%M:%S")
249
 
250
- loop = asyncio.get_event_loop()
251
- return await loop.run_in_executor(None, sb_rpc, rpc_name, params)
252
-
253
- tasks = []
254
- curr = t_start_dt
255
-
256
- # CRITICAL FIX: Smaller chunks for advanced mode to avoid RPC timeout
257
- chunk_hours = 0.5 if mode == "advanced" else 2 # 30 min for advanced, 2 hours for normal
258
-
259
- while curr <= t_end_dt:
260
- if mode == "advanced":
261
- # 30 minute chunks for advanced mode
262
- nxt = curr + timedelta(minutes=30) - timedelta(seconds=1)
263
- else:
264
- # 2 hour chunks for normal mode
265
- nxt = curr + timedelta(hours=chunk_hours) - timedelta(seconds=1)
266
-
267
- if nxt > t_end_dt:
268
- nxt = t_end_dt
269
 
270
- tasks.append(fetch_chunk(curr, nxt))
271
-
272
- if mode == "advanced":
273
- curr += timedelta(minutes=30)
274
- else:
 
 
 
 
275
  curr += timedelta(hours=chunk_hours)
276
 
277
- # Execute all chunks in parallel
278
- chunk_results = await asyncio.gather(*tasks)
279
-
280
- all_records = []
281
- for res in chunk_results:
282
- if isinstance(res, list):
283
- all_records.extend(res)
284
 
285
- if not all_records:
286
- return JSONResponse({"error": "No valid data found in range.", "labels":[], "P":[], "breakdown": []})
287
 
288
- # =========================================================
289
- # DATA PROCESSING & AGGREGATION
290
- # =========================================================
291
- df = pd.DataFrame(all_records)
 
 
 
292
 
293
- # Standardize output
294
- df.rename(columns={
295
- "tick_ts": "ts",
296
- "b_qty": "buy_qty",
297
- "s_qty": "sell_qty",
298
- "vol": "volume"
299
- }, inplace=True)
300
 
301
- df['ts'] = pd.to_datetime(df['ts'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
 
303
- # Deduplicate boundaries
304
- df.drop_duplicates(subset=['ts'], inplace=True)
305
- df.set_index('ts', inplace=True)
306
- df.sort_index(inplace=True)
307
 
308
- # Resample Timeframes accurately in Pandas
309
  tf_map = {
310
  "1min": "1min", "3min": "3min", "5min": "5min",
311
  "15min": "15min", "30min": "30min", "1hour": "1h"
312
  }
313
  panda_tf = tf_map.get(timeframe, "1min")
314
 
315
- # CORRECTED: Use LAST value for all quantity columns (not mean)
316
  agg_dict = {
317
  "price": "last",
318
- "volume": "sum",
319
  "oi": "last",
320
- "buy_qty": "last", # Changed from mean to last
321
- "sell_qty": "last", # Changed from mean to last
322
- "cb": "last", # Changed from mean to last
323
- "cs": "last", # Changed from mean to last
324
- "pb": "last", # Changed from mean to last
325
- "ps": "last" # Changed from mean to last
326
  }
327
 
328
- # Only aggregate columns that actually exist
329
  existing_cols = {k: v for k, v in agg_dict.items() if k in df.columns}
330
-
331
  resampled = df.resample(panda_tf).agg(existing_cols).ffill().fillna(0)
332
 
333
- # Prepare breakdown data for inspection
334
  breakdown_data = []
335
- if 'breakdown' in df.columns:
336
- # Take last breakdown for each resampled period
337
- breakdown_series = df['breakdown'].resample(panda_tf).last()
338
- for idx, bd in breakdown_series.items():
339
- if bd and isinstance(bd, list):
340
- breakdown_data.append({
341
- "time": idx.strftime('%H:%M'),
342
- "breakdown": bd
343
- })
344
 
345
- response_data = {
346
  "labels": resampled.index.strftime('%H:%M').tolist(),
347
  "P": resampled['price'].tolist() if 'price' in resampled else [],
348
- "B": resampled['buy_qty'].tolist() if 'buy_qty' in resampled else [],
349
- "S": resampled['sell_qty'].tolist() if 'sell_qty' in resampled else [],
350
- "V": resampled['volume'].tolist() if 'volume' in resampled else [],
351
  "OI": resampled['oi'].tolist() if 'oi' in resampled else [],
352
  "CB": resampled['cb'].tolist() if 'cb' in resampled else [],
353
  "CS": resampled['cs'].tolist() if 'cs' in resampled else [],
354
  "PB": resampled['pb'].tolist() if 'pb' in resampled else [],
355
  "PS": resampled['ps'].tolist() if 'ps' in resampled else [],
356
  "breakdown": breakdown_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
 
359
- return JSONResponse(response_data)
 
 
 
 
 
 
 
 
 
 
 
 
360
 
361
 
362
  # ==========================================
363
- # 5. FRONTEND TEMPLATE
364
  # ==========================================
365
 
366
  HTML_TEMPLATE = """
@@ -373,7 +541,6 @@ HTML_TEMPLATE = """
373
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
374
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
375
  <style>
376
- /* --- THEME --- */
377
  :root {
378
  --bg-body: #0d1117;
379
  --bg-header: #161b22;
@@ -400,7 +567,6 @@ HTML_TEMPLATE = """
400
  ::-webkit-scrollbar-track { background: var(--bg-body); }
401
  ::-webkit-scrollbar-thumb { background: var(--bg-panel); border: 2px solid var(--bg-body); border-radius: 6px; }
402
 
403
- /* HEADER */
404
  .top-bar {
405
  height: 52px; background: var(--bg-header); border-bottom: 1px solid var(--border);
406
  display: flex; align-items: center; justify-content: space-between; padding: 0 16px; flex-shrink: 0;
@@ -424,7 +590,6 @@ HTML_TEMPLATE = """
424
  .layout-btn { width: 30px; height: 26px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; }
425
  .layout-btn.active { background: var(--bg-panel); color: var(--accent); box-shadow: 0 1px 2px rgba(0,0,0,0.2); }
426
 
427
- /* GRID */
428
  .main-container { flex: 1; display: flex; overflow-y: auto; overflow-x: hidden; }
429
  .column { flex: 1; display: flex; flex-direction: column; border-right: 1px solid var(--border); min-width: 0; }
430
  .column:last-child { border-right: none; }
@@ -435,7 +600,6 @@ HTML_TEMPLATE = """
435
  }
436
  .add-chart-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
437
 
438
- /* CARD */
439
  .chart-card { background: var(--bg-card); border-bottom: 1px solid var(--border); display: flex; flex-direction: column; position: relative; }
440
  .card-header {
441
  height: 38px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between;
@@ -458,7 +622,6 @@ HTML_TEMPLATE = """
458
  .card-body { flex: 1; position: relative; width: 100%; height: 100%; overflow: hidden; display: flex; flex-direction: column; }
459
  .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-secondary); background: radial-gradient(circle, var(--bg-panel) 0%, var(--bg-card) 100%); text-align: center; padding: 20px;}
460
 
461
- /* MODAL */
462
  .modal-backdrop {
463
  position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7);
464
  z-index: 100; display: none; justify-content: center; align-items: center;
@@ -475,7 +638,6 @@ HTML_TEMPLATE = """
475
 
476
  .form-section label { font-size: 11px; text-transform: uppercase; color: var(--text-secondary); font-weight: 600; margin-bottom: 6px; display: block; }
477
 
478
- /* CUSTOM DROPDOWN UI */
479
  .input-wrapper { position: relative; width: 100%; }
480
  .input-arrow { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); pointer-events: none; color: var(--text-secondary); font-size: 10px; }
481
  .custom-dd {
@@ -488,10 +650,8 @@ HTML_TEMPLATE = """
488
  .custom-dd-opt:hover { background: var(--bg-input); color: var(--accent); }
489
  .custom-dd-group { padding: 6px 12px; font-size: 10px; font-weight: 700; color: var(--text-secondary); background: var(--bg-body); text-transform: uppercase; }
490
 
491
- /* SERIES ROW STRUCTURE */
492
  .series-row {
493
  background: var(--bg-panel); padding: 12px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 10px;
494
- box-shadow: inset 0 1px 0 rgba(255,255,255,0.02);
495
  }
496
  .series-row-top { display: flex; gap: 10px; margin-bottom: 10px; align-items: center; }
497
  .series-row-bottom { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
@@ -517,7 +677,6 @@ HTML_TEMPLATE = """
517
  padding: 4px 10px; border-radius: 6px; border: 1px dashed var(--border); flex: 1;
518
  }
519
 
520
- /* UI ENHANCEMENTS */
521
  .segmented-control {
522
  display: flex; background: var(--bg-body); border: 1px solid var(--border);
523
  border-radius: 6px; padding: 2px; overflow: hidden; margin-bottom: 6px;
@@ -537,19 +696,12 @@ HTML_TEMPLATE = """
537
  }
538
  .info-icon:hover { color: var(--text-primary); border-color: var(--accent); }
539
 
540
- /* INFO MODAL TABLE */
541
  .info-table { width: 100%; font-family: var(--font-mono); font-size: 11px; border-collapse: collapse; margin-top: 10px; }
542
  .info-table th { text-align: left; color: var(--text-secondary); border-bottom: 1px solid var(--border); padding: 8px; font-weight: 600; }
543
  .info-table td { padding: 8px; border-bottom: 1px solid var(--border); color: var(--text-primary); }
544
  .info-table tr:hover { background: rgba(47, 129, 247, 0.05); }
545
- .info-table .result-row { background: rgba(47, 129, 247, 0.1); font-weight: 700; }
546
  .info-table .positive { color: var(--success); }
547
  .info-table .negative { color: var(--danger); }
548
-
549
- .label-group {
550
- display: flex; gap: 8px; align-items: center;
551
- }
552
- .label-group span { font-size: 11px; color: var(--text-secondary); white-space: nowrap; }
553
  </style>
554
  </head>
555
  <body>
@@ -589,17 +741,13 @@ HTML_TEMPLATE = """
589
  <div class="modal-body">
590
  <div id="cfgLoading" style="display:none; text-align:center; color:var(--text-secondary);">Loading...</div>
591
  <div id="cfgContent">
592
-
593
- <!-- Global Settings Mode -->
594
  <div id="cfgGlobalOnly" style="display:none;">
595
  <div class="form-section">
596
  <label>Refresh Rate (Seconds)</label>
597
  <input type="number" id="cfgRefresh" value="15" min="5">
598
  </div>
599
- <div style="height:1px; background:var(--border); margin: 16px 0;"></div>
600
  </div>
601
 
602
- <!-- Single Chart Config Mode -->
603
  <div id="cfgChartOnly">
604
  <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
605
  <div class="form-section">
@@ -628,7 +776,6 @@ HTML_TEMPLATE = """
628
  </div>
629
  </div>
630
 
631
- <!-- EXPLICIT INSTRUMENT SELECTION -->
632
  <div class="form-section" id="cfgInstrumentContainer">
633
  <label id="lblInst">Instrument</label>
634
  <div class="input-wrapper">
@@ -637,16 +784,14 @@ HTML_TEMPLATE = """
637
  <div id="dd-cfgInst" class="custom-dd"></div>
638
  </div>
639
  <div id="advInstHint" style="display:none; font-size:10px; color:var(--text-secondary); margin-top:4px; line-height: 1.4;">
640
- * For Advanced Mode, select the base Spot or Future here. We will use this price to calculate the ATM options.
641
  </div>
642
  </div>
643
 
644
- <!-- Advanced Mode Fields -->
645
  <div id="cfgAdvancedFields" style="display:none;">
646
  <div class="form-section" style="margin-top:12px;">
647
  <label style="color:var(--accent);">ATM (+/-) Strike Range</label>
648
  <input type="number" id="cfgAtmRange" value="5" min="1" max="50" style="width:100%;">
649
- <div style="font-size:10px; color:var(--text-secondary); margin-top:4px; line-height: 1.4;">Calculates aggregate pressure dynamically across ATM plus/minus this many strikes.</div>
650
  </div>
651
  </div>
652
 
@@ -670,20 +815,17 @@ HTML_TEMPLATE = """
670
  </div>
671
  </div>
672
  </div>
673
- <div style="height:1px; background:var(--border); margin: 12px 0 8px 0;"></div>
674
  </div>
675
 
676
- <!-- PRESET MANAGER -->
677
- <div class="form-section" style="background:var(--bg-panel); padding:10px; border-radius:6px; border:1px solid var(--border); margin-bottom: 12px;">
678
  <label>Quick Configurations</label>
679
  <div style="display:flex; gap:8px;">
680
  <select id="cfgPresetsDropdown" style="flex:1;" onchange="Config.loadPreset()"></select>
681
- <button class="primary" onclick="Config.savePreset()" title="Save Current Lines as Configuration">Save</button>
682
- <button class="danger" onclick="Config.deletePreset()" title="Delete Selected Model" style="width: 32px; padding:0;">✕</button>
683
  </div>
684
  </div>
685
 
686
- <!-- Common Series Config -->
687
  <div class="form-section">
688
  <div style="display:flex; align-items:center; margin-bottom: 6px;">
689
  <label style="margin:0;">Plot Series</label>
@@ -701,7 +843,7 @@ HTML_TEMPLATE = """
701
  </div>
702
  </div>
703
 
704
- <!-- INSPECTION/BREAKDOWN MODAL -->
705
  <div class="modal-backdrop" id="infoModal">
706
  <div class="modal modal-lg">
707
  <div class="modal-head">
@@ -734,7 +876,6 @@ HTML_TEMPLATE = """
734
  </div>
735
 
736
  <script>
737
- // --- CUSTOM SEARCHABLE DROPDOWN LOGIC ---
738
  document.addEventListener('click', (e) => {
739
  if (!e.target.closest('.input-wrapper')) {
740
  document.querySelectorAll('.custom-dd').forEach(el => el.style.display = 'none');
@@ -791,7 +932,7 @@ const CustomDD = {
791
  let pool = [...this.data[id]];
792
 
793
  if(id === 'cfgRoot' && q === '') {
794
- const recentItems = this.recent.cfgRoot.map(r => ({val: r, group: 'Recent Search'}));
795
  pool = [...recentItems, ...pool];
796
  }
797
 
@@ -799,7 +940,7 @@ const CustomDD = {
799
  .filter(item => item.val.toLowerCase().includes(q));
800
 
801
  if (filtered.length === 0) {
802
- el.innerHTML = '<div class="custom-dd-opt" style="color:var(--text-secondary); cursor:default;">No matches found</div>';
803
  return;
804
  }
805
 
@@ -832,20 +973,16 @@ const CustomDD = {
832
  const App = {
833
  state: { date: null, mode: 'LIVE', columns: 3, layout: [[],[],[]], charts: {}, refreshRate: 15 },
834
 
835
- // PRESETS - Organized by Mode and Instrument Type
836
  presets: {
837
- // Normal Mode - Futures/Equity/Spot
838
  "Basic Flow": [
839
  { formula: '$P', label: 'Price', color1: '#2f81f7', axis: 'left', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
840
- { formula: '$B', label: 'Buy Qty', color1: '#238636', axis: 'right', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
841
- { formula: '$S', label: 'Sell Qty', color1: '#da3633', axis: 'right', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' }
842
  ],
843
- "Net Flow (Futures)": [
844
  { formula: '$P', label: 'Price', color1: '#2f81f7', axis: 'left', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
845
  { formula: '$B - $S', label: 'Net Flow', color1: '#238636', axis: 'right', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BUY', labelFalse: 'SELL' }
846
  ],
847
-
848
- // Normal Mode - Options
849
  "Call Flow": [
850
  { formula: '$P', label: 'Price', color1: '#2f81f7', axis: 'left', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
851
  { formula: '$CB', label: 'Call Buy', color1: '#238636', axis: 'right', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
@@ -865,23 +1002,9 @@ const App = {
865
  { formula: '$P', label: 'Price', color1: '#2f81f7', axis: 'left', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
866
  { formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', color1: '#238636', axis: 'right', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
867
  ],
868
-
869
- // Advanced Mode
870
- "Advanced - Price & Flows": [
871
- { formula: '$P', label: 'Spot Price', color1: '#2f81f7', axis: 'left', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
872
- { formula: '$CB + $PS', label: 'Bull Side', color1: '#238636', axis: 'right', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
873
- { formula: '$PB + $CS', label: 'Bear Side', color1: '#da3633', axis: 'right', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' }
874
- ],
875
  "Advanced - Net Flow": [
876
- { formula: '$P', label: 'Spot Price', color1: '#2f81f7', axis: 'left', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
877
  { formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', color1: '#238636', axis: 'right', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
878
- ],
879
- "Advanced - Components": [
880
- { formula: '$P', label: 'Spot Price', color1: '#2f81f7', axis: 'left', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
881
- { formula: '$CB', label: 'Call Buy', color1: '#238636', axis: 'right', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
882
- { formula: '$CS', label: 'Call Sell', color1: '#3fb950', axis: 'right', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
883
- { formula: '$PB', label: 'Put Buy', color1: '#da3633', axis: 'right', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
884
- { formula: '$PS', label: 'Put Sell', color1: '#f85149', axis: 'right', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' }
885
  ]
886
  },
887
 
@@ -943,14 +1066,9 @@ const App = {
943
  save() {
944
  const cleanCharts = {};
945
  for(let id in this.state.charts) cleanCharts[id] = { config: this.state.charts[id].config };
946
-
947
  localStorage.setItem('dc_pro_v4_final', JSON.stringify({
948
- date: this.state.date,
949
- mode: this.state.mode,
950
- columns: this.state.columns,
951
- layout: this.state.layout,
952
- refreshRate: this.state.refreshRate,
953
- charts: cleanCharts
954
  }));
955
  },
956
 
@@ -972,7 +1090,6 @@ const App = {
972
 
973
  renderGrid() {
974
  document.querySelectorAll('.layout-btn').forEach((b, i) => b.classList.toggle('active', (i+1) === this.state.columns));
975
-
976
  const container = document.getElementById('gridRoot');
977
  container.innerHTML = '';
978
 
@@ -987,11 +1104,9 @@ const App = {
987
  this.state.charts[id].instance.destroy();
988
  this.state.charts[id].instance = null;
989
  }
990
-
991
  const card = this.createCardDOM(id);
992
  if (count > 0 && count <= 3) card.style.height = `calc(${viewH / count}px - 1px)`;
993
  else card.style.height = '300px';
994
-
995
  col.appendChild(card);
996
  setTimeout(() => ChartLogic.plot(id), 0);
997
  });
@@ -1021,19 +1136,14 @@ const App = {
1021
  <span>${isCfg ? cfg.root : 'New Chart'}</span>
1022
  ${isCfg ? `${advancedLabel} <span class="card-meta">${cfg.expiry}</span>` : ''}
1023
  </div>
1024
- <div class="card-tools">
1025
- <button class="tool-btn info-btn" id="info-btn-${id}" onclick="ChartLogic.showBreakdown('${id}')" style="display:none;">i</button>
1026
  <button class="tool-btn" onclick="Config.open('${id}')">⚙</button>
1027
  <button class="tool-btn" style="color:var(--danger)" onclick="App.removeChart('${id}')">✕</button>
1028
  </div>
1029
  </div>
1030
  <div class="card-body" id="body-${id}">
1031
- ${!isCfg ? `
1032
- <div class="empty-state">
1033
- <div style="font-size:24px; opacity:0.3;">📊</div>
1034
- <button style="background:transparent; border:1px solid var(--border); padding:5px 10px; border-radius:4px; color:var(--text-secondary); margin-top:8px;" onclick="Config.open('${id}')">Configure</button>
1035
- </div>
1036
- ` : `<canvas id="canvas-${id}"></canvas>`}
1037
  </div>
1038
  `;
1039
  return div;
@@ -1050,12 +1160,6 @@ const App = {
1050
 
1051
  onDateChange() {
1052
  this.state.date = document.getElementById('globalDate').value;
1053
- const today = new Date().toISOString().split('T')[0];
1054
- if(this.state.date === today && this.state.mode !== 'LIVE') {
1055
- this.state.mode = 'LIVE';
1056
- document.getElementById('globalMode').value = 'LIVE';
1057
- this.updateModeUI();
1058
- }
1059
  this.save();
1060
  this.refreshAll();
1061
  },
@@ -1076,13 +1180,55 @@ const App = {
1076
  startLoop() {
1077
  const loop = () => {
1078
  if(this.state.mode === 'LIVE') Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id, true));
1079
- const rate = (this.state.refreshRate || 15) * 1000;
1080
- setTimeout(loop, rate);
1081
  };
1082
  loop();
1083
  }
1084
  };
1085
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1086
  const ChartLogic = {
1087
  async plot(id, isUpdate=false) {
1088
  const chart = App.state.charts[id];
@@ -1116,454 +1262,352 @@ const ChartLogic = {
1116
  if(data.error || !data.labels || !data.labels.length) {
1117
  if(App.state.mode !== 'LIVE') {
1118
  const b = document.getElementById(`body-${id}`);
1119
- if(b) b.innerHTML = `<div class="empty-state" style="opacity:0.8"><span style="font-size:20px;">⚠️</span><br/>${data.error || "No Data Found"}</div>`;
1120
  }
1121
  if(dot) dot.classList.remove('retry');
1122
  return;
1123
  }
1124
-
1125
- // Store data for breakdown modal
1126
- chart.chartData = data;
1127
 
1128
- // Show info button if we have breakdown data
1129
- const infoBtn = document.getElementById(`info-btn-${id}`);
1130
- if(infoBtn && data.breakdown && data.breakdown.length > 0) {
1131
- infoBtn.style.display = 'flex';
1132
- }
1133
 
1134
  const body = document.getElementById(`body-${id}`);
1135
  if(body && !body.querySelector('canvas')) body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
1136
 
1137
  if(dot) dot.className = `status-dot ${App.state.mode==='LIVE'?'live':'hist'}`;
 
 
 
1138
 
1139
  const datasets = cfg.series.map(s => {
1140
  const vals = data.labels.map((_, i) => {
1141
- const P=data.P[i]||0, V=data.V[i]||0, OI=data.OI[i]||0, B=data.B[i]||0, S=data.S[i]||0;
1142
- const CB=data.CB[i]||0, CS=data.CS[i]||0, PB=data.PB[i]||0, PS=data.PS[i]||0;
 
 
1143
 
1144
- // CRITICAL FIX: Use String() not str() - JavaScript doesn't have str()
1145
  let f = s.formula
1146
- .replace(/\$CB/g, String(CB)).replace(/\$CS/g, String(CS))
1147
- .replace(/\$PB/g, String(PB)).replace(/\$PS/g, String(PS))
1148
- .replace(/\$OI/g, String(OI)).replace(/\$P/g, String(P))
1149
- .replace(/\$V/g, String(V)).replace(/\$B/g, String(B)).replace(/\$S/g, String(S));
1150
 
1151
  try { return new Function('return '+f)(); } catch{ return null; }
1152
  });
1153
 
1154
  let dataset = {
1155
  label: s.label, data: vals,
1156
- borderWidth: 1.5, pointRadius: 0, hitRadius: 10, tension: 0.1, yAxisID: s.axis==='left'?'y':'y1'
 
 
 
 
1157
  };
1158
 
1159
  if(s.cond && s.cond !== 'none') {
1160
- let thresh = parseFloat(s.thresh) || 0;
1161
- let cTrue = s.color1;
1162
- let cFalse = s.color2 || s.color1;
1163
-
1164
  dataset.segment = {
1165
  borderColor: ctx => {
1166
  let v = ctx.p1.parsed.y;
1167
- if(s.cond === '>') return v > thresh ? cTrue : cFalse;
1168
- if(s.cond === '<') return v < thresh ? cTrue : cFalse;
1169
- return cTrue;
1170
  }
1171
  };
1172
- dataset.borderColor = cTrue;
1173
- dataset.backgroundColor = cTrue;
1174
-
1175
- const hoverColorCallback = (ctx) => {
1176
- let v = ctx.raw;
1177
- if (v === undefined || v === null) return cTrue;
1178
- if(s.cond === '>') return v > thresh ? cTrue : cFalse;
1179
- if(s.cond === '<') return v < thresh ? cTrue : cFalse;
1180
- return cTrue;
1181
- };
1182
- dataset.pointHoverBackgroundColor = hoverColorCallback;
1183
- dataset.pointHoverBorderColor = hoverColorCallback;
1184
-
1185
  } else {
1186
  dataset.borderColor = s.color1;
1187
  dataset.backgroundColor = s.color1;
1188
  }
 
1189
  return dataset;
1190
  });
1191
 
1192
- const ctx = document.getElementById(`canvas-${id}`).getContext('2d');
1193
-
1194
- if(chart.instance) chart.instance.destroy();
1195
-
1196
- chart.instance = new Chart(ctx, {
1197
- type: 'line',
1198
- data: { labels: data.labels, datasets: datasets },
1199
- options: {
1200
- responsive: true,
1201
- maintainAspectRatio: false,
1202
- interaction: { mode: 'index', intersect: false },
1203
- plugins: {
1204
- legend: {
1205
- labels: {
1206
- color: '#e6edf3',
1207
- font: { size: 11, family: 'Inter' },
1208
- boxWidth: 12,
1209
- usePointStyle: true
1210
- },
1211
- position: 'top'
1212
- },
1213
- tooltip: {
1214
- backgroundColor: 'rgba(13, 17, 23, 0.95)',
1215
- titleColor: '#e6edf3',
1216
- bodyColor: '#8b949e',
1217
- borderColor: '#30363d',
1218
- borderWidth: 1,
1219
- padding: 10,
1220
- titleFont: { family: 'JetBrains Mono', size: 12 },
1221
- bodyFont: { family: 'Inter', size: 11 }
1222
- }
1223
- },
1224
- scales: {
1225
- x: {
1226
- grid: { color: '#21262d', drawBorder: false },
1227
- ticks: { color: '#8b949e', font: { family: 'JetBrains Mono', size: 10 }, maxTicksLimit: 8 }
1228
- },
1229
- y: {
1230
- type: 'linear',
1231
- display: true,
1232
- position: 'left',
1233
- grid: { color: '#21262d', drawBorder: false },
1234
- ticks: { color: '#8b949e', font: { family: 'JetBrains Mono', size: 10 } }
1235
- },
1236
- y1: {
1237
- type: 'linear',
1238
- display: true,
1239
- position: 'right',
1240
- grid: { drawOnChartArea: false },
1241
- ticks: { color: '#8b949e', font: { family: 'JetBrains Mono', size: 10 } }
1242
- }
1243
- }
1244
- }
1245
- });
1246
- } catch(e) {
1247
- console.error(e);
1248
- if(dot) dot.classList.remove('retry');
1249
- }
1250
  },
1251
 
1252
- showBreakdown(id) {
1253
- const chart = App.state.charts[id];
1254
- if(!chart || !chart.chartData || !chart.chartData.breakdown) return;
1255
-
1256
- const data = chart.chartData;
1257
- const cfg = chart.config;
1258
-
1259
- // Get the first series formula for display
1260
- const formula = cfg.series && cfg.series[0] ? cfg.series[0].formula : 'N/A';
1261
- document.getElementById('infoFormula').innerText = formula;
1262
 
1263
- const tbody = document.getElementById('infoTableBody');
1264
- tbody.innerHTML = '';
1265
-
1266
- data.breakdown.forEach((item, idx) => {
1267
- const bd = item.breakdown;
1268
- if(!bd || !Array.isArray(bd)) return;
1269
-
1270
- // Calculate aggregates
1271
- let cbTotal = 0, csTotal = 0, pbTotal = 0, psTotal = 0;
1272
- bd.forEach(strike => {
1273
- cbTotal += strike.b || 0;
1274
- csTotal += strike.s || 0;
1275
- });
1276
-
1277
- // For display, we'll show the sums
1278
- const cbPlusPs = cbTotal + psTotal;
1279
- const pbPlusCs = pbTotal + csTotal;
1280
- const netFlow = cbPlusPs - pbPlusCs;
1281
-
1282
- const row = document.createElement('tr');
1283
- row.innerHTML = `
1284
- <td>${item.time}</td>
1285
- <td>${cbTotal.toFixed(0)}</td>
1286
- <td>${csTotal.toFixed(0)}</td>
1287
- <td>${pbTotal.toFixed(0)}</td>
1288
- <td>${psTotal.toFixed(0)}</td>
1289
- <td>${cbPlusPs.toFixed(0)}</td>
1290
- <td>${pbPlusCs.toFixed(0)}</td>
1291
- <td class="${netFlow >= 0 ? 'positive' : 'negative'}">${netFlow.toFixed(0)}</td>
1292
- `;
1293
- tbody.appendChild(row);
 
 
 
 
 
 
1294
  });
1295
-
1296
- document.getElementById('infoModal').style.display = 'flex';
1297
  }
1298
  };
1299
 
1300
  const Config = {
1301
- editingId: null,
1302
- isGlobal: false,
1303
- currentMode: 'normal',
 
 
 
 
 
 
 
 
1304
 
1305
- open(id) {
1306
- this.editingId = id;
1307
- this.isGlobal = false;
1308
- const cfg = App.state.charts[id].config;
1309
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1310
  document.getElementById('cfgTitle').innerText = 'Configure Chart';
 
 
1311
  document.getElementById('cfgGlobalOnly').style.display = 'none';
1312
  document.getElementById('cfgChartOnly').style.display = 'block';
1313
- document.getElementById('configModal').style.display = 'flex';
1314
-
1315
- // Reset fields
1316
- document.getElementById('cfgRoot').value = cfg.root || '';
1317
- document.getElementById('cfgExp').value = cfg.expiry || '';
1318
- document.getElementById('cfgInst').value = cfg.instrument || '';
1319
- document.getElementById('cfgTF').value = cfg.timeframe || '5min';
1320
  document.getElementById('cfgStart').value = cfg.start || '09:15';
1321
  document.getElementById('cfgEnd').value = cfg.end || '15:30';
1322
- document.getElementById('cfgAtmRange').value = cfg.atmRange || 5;
1323
-
1324
- // Set mode
1325
  this.currentMode = cfg.chartMode || 'normal';
1326
- this.updateModeUI();
1327
-
1328
- // Load series
1329
- this.renderSeries(cfg.series || []);
1330
-
1331
- // Load presets dropdown
1332
- this.loadPresetsDropdown();
1333
 
1334
- // Load instruments if root and expiry set
1335
- if(cfg.root && cfg.expiry) {
1336
- this.loadInstruments(cfg.root, cfg.expiry);
1337
- }
 
 
 
 
 
 
 
 
 
 
 
 
1338
  },
1339
 
1340
  openDefaults() {
1341
  this.isGlobal = true;
1342
- this.editingId = null;
1343
  document.getElementById('cfgTitle').innerText = 'Settings';
 
 
1344
  document.getElementById('cfgGlobalOnly').style.display = 'block';
1345
  document.getElementById('cfgChartOnly').style.display = 'none';
1346
- document.getElementById('cfgRefresh').value = App.state.refreshRate;
1347
- document.getElementById('configModal').style.display = 'flex';
1348
  },
1349
 
1350
- close() {
1351
- document.getElementById('configModal').style.display = 'none';
1352
- },
1353
 
1354
- setMode(mode) {
1355
- this.currentMode = mode;
1356
- this.updateModeUI();
1357
- },
 
 
 
1358
 
1359
- updateModeUI() {
1360
- const btns = document.querySelectorAll('#cfgModeToggle .seg-btn');
1361
- btns.forEach(b => b.classList.remove('active'));
1362
- btns[this.currentMode === 'normal' ? 0 : 1].classList.add('active');
1363
-
1364
- const advFields = document.getElementById('cfgAdvancedFields');
1365
- const instHint = document.getElementById('advInstHint');
1366
- const lblInst = document.getElementById('lblInst');
1367
 
1368
- if(this.currentMode === 'advanced') {
1369
- advFields.style.display = 'block';
1370
- instHint.style.display = 'block';
1371
- lblInst.innerText = 'Reference Instrument (Spot/Future)';
1372
- } else {
1373
- advFields.style.display = 'none';
1374
- instHint.style.display = 'none';
1375
- lblInst.innerText = 'Instrument';
 
1376
  }
1377
  },
1378
 
1379
- async onRootChange() {
1380
- const root = document.getElementById('cfgRoot').value;
1381
- if(!root) return;
1382
-
1383
- // Load expiries
1384
- const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`);
1385
- const exps = await res.json();
1386
-
1387
- CustomDD.set('cfgExp', exps);
1388
- CustomDD.render('cfgExp', '');
1389
- },
1390
 
1391
- async onExpChange() {
1392
- const root = document.getElementById('cfgRoot').value;
1393
- const expiry = document.getElementById('cfgExp').value;
1394
- if(!root || !expiry) return;
1395
-
1396
- await this.loadInstruments(root, expiry);
1397
  },
1398
 
1399
- async loadInstruments(root, expiry) {
1400
- const res = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${expiry}`);
1401
- const data = await res.json();
1402
-
1403
- CustomDD.data.cfgInst = [];
1404
- CustomDD.set('cfgInst', data.spot_fut, 'Spot/Futures');
1405
- CustomDD.set('cfgInst', data.options, 'Options');
1406
- CustomDD.render('cfgInst', '');
1407
- },
1408
 
1409
- renderSeries(series) {
1410
- const container = document.getElementById('cfgSeries');
1411
- container.innerHTML = '';
1412
-
1413
- series.forEach((s, idx) => {
1414
- const row = document.createElement('div');
1415
- row.className = 'series-row';
1416
- row.innerHTML = `
1417
  <div class="series-row-top">
1418
- <input type="text" class="s-form" placeholder="Formula (e.g. $CB + $PS)" value="${s.formula || ''}">
1419
- <input type="text" class="s-lbl" placeholder="Label" value="${s.label || ''}" style="width:120px;">
1420
- <input type="color" class="color-input" value="${s.color1 || '#2f81f7'}">
1421
- <button class="tool-btn" onclick="Config.removeSeriesRow(this)" style="color:var(--danger)">✕</button>
1422
  </div>
1423
  <div class="series-row-bottom">
1424
- <select class="s-axis">
1425
- <option value="left" ${s.axis==='left'?'selected':''}>Left Axis</option>
1426
- <option value="right" ${s.axis==='right'?'selected':''}>Right Axis</option>
 
 
1427
  </select>
1428
- <div class="s-cond-group">
1429
- <select class="s-cond">
1430
- <option value="none" ${s.cond==='none'?'selected':''}>No Cond</option>
1431
- <option value=">" ${s.cond==='>'?'selected':''}>></option>
1432
- <option value="<" ${s.cond==='<'?'selected':''}><</option>
1433
- </select>
1434
- <input type="text" class="s-thresh" placeholder="Threshold" value="${s.thresh || '0'}" style="width:70px;">
1435
- <input type="color" class="color-input" value="${s.color2 || '#ffffff'}" title="False Color">
1436
  </div>
1437
- </div>
1438
- `;
1439
- container.appendChild(row);
1440
  });
1441
  },
1442
 
1443
  addSeriesRow() {
1444
- const container = document.getElementById('cfgSeries');
1445
- const row = document.createElement('div');
1446
- row.className = 'series-row';
1447
- row.innerHTML = `
1448
  <div class="series-row-top">
1449
- <input type="text" class="s-form" placeholder="Formula (e.g. $CB + $PS)">
1450
- <input type="text" class="s-lbl" placeholder="Label" style="width:120px;">
1451
- <input type="color" class="color-input" value="#2f81f7">
1452
- <button class="tool-btn" onclick="Config.removeSeriesRow(this)" style="color:var(--danger)">✕</button>
1453
  </div>
1454
  <div class="series-row-bottom">
1455
- <select class="s-axis">
1456
- <option value="left">Left Axis</option>
1457
- <option value="right" selected>Right Axis</option>
 
 
1458
  </select>
1459
- <div class="s-cond-group">
1460
- <select class="s-cond">
1461
- <option value="none">No Cond</option>
1462
- <option value=">">></option>
1463
- <option value="<"><</option>
1464
- </select>
1465
- <input type="text" class="s-thresh" placeholder="Threshold" value="0" style="width:70px;">
1466
- <input type="color" class="color-input" value="#ffffff" title="False Color">
1467
  </div>
1468
- </div>
1469
- `;
1470
- container.appendChild(row);
1471
- },
1472
-
1473
- removeSeriesRow(btn) {
1474
- btn.closest('.series-row').remove();
1475
- },
1476
-
1477
- loadPresetsDropdown() {
1478
- const sel = document.getElementById('cfgPresetsDropdown');
1479
- sel.innerHTML = '<option value="">-- Select Preset --</option>' +
1480
- Object.keys(App.presets).map(p => `<option value="${p}">${p}</option>`).join('');
1481
- },
1482
-
1483
- loadPreset() {
1484
- const name = document.getElementById('cfgPresetsDropdown').value;
1485
- if(!name || !App.presets[name]) return;
1486
- this.renderSeries(App.presets[name]);
1487
- },
1488
-
1489
- savePreset() {
1490
- const name = prompt('Enter preset name:');
1491
- if(!name) return;
1492
-
1493
- const series = this.getSeriesFromDOM();
1494
- App.presets[name] = series;
1495
-
1496
- // Save to localStorage
1497
- localStorage.setItem('dc_pro_v4_presets', JSON.stringify(App.presets));
1498
- this.loadPresetsDropdown();
1499
- alert('Preset saved!');
1500
  },
1501
 
1502
- deletePreset() {
1503
- const name = document.getElementById('cfgPresetsDropdown').value;
1504
- if(!name || !App.presets[name]) return;
1505
- if(!confirm(`Delete preset "${name}"?`)) return;
1506
-
1507
- delete App.presets[name];
1508
- localStorage.setItem('dc_pro_v4_presets', JSON.stringify(App.presets));
1509
- this.loadPresetsDropdown();
1510
- },
1511
-
1512
- getSeriesFromDOM() {
1513
- const rows = document.querySelectorAll('#cfgSeries .series-row');
1514
- return Array.from(rows).map(row => ({
1515
- formula: row.querySelector('.s-form').value,
1516
- label: row.querySelector('.s-lbl').value,
1517
- color1: row.querySelector('.color-input').value,
1518
- axis: row.querySelector('.s-axis').value,
1519
- cond: row.querySelector('.s-cond').value,
1520
- thresh: row.querySelector('.s-thresh').value,
1521
- color2: row.querySelectorAll('.color-input')[1]?.value || '#ffffff'
1522
  }));
1523
  },
1524
 
1525
  saveChartConfig() {
1526
- if(this.isGlobal) {
1527
  App.state.refreshRate = parseInt(document.getElementById('cfgRefresh').value) || 15;
1528
  App.save();
1529
- this.close();
1530
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
1531
  }
1532
-
1533
- const cfg = {
1534
- root: document.getElementById('cfgRoot').value.toUpperCase(),
1535
- expiry: document.getElementById('cfgExp').value,
1536
- instrument: document.getElementById('cfgInst').value,
1537
- timeframe: document.getElementById('cfgTF').value,
1538
- start: document.getElementById('cfgStart').value,
1539
- end: document.getElementById('cfgEnd').value,
1540
- chartMode: this.currentMode,
1541
- atmRange: parseInt(document.getElementById('cfgAtmRange').value) || 5,
1542
- series: this.getSeriesFromDOM()
1543
- };
1544
-
1545
- App.state.charts[this.editingId].config = cfg;
1546
- App.save();
1547
  this.close();
1548
- App.renderGrid();
1549
  }
1550
  };
1551
 
1552
- // Info icon tooltip
1553
- document.getElementById('infoIcon').addEventListener('click', () => {
1554
- alert(`Formula Variables:
1555
- $P = Price
1556
- $V = Volume
1557
- $OI = Open Interest
1558
- $B = Buy Qty
1559
- $S = Sell Qty
1560
- $CB = Call Buy
1561
- $CS = Call Sell
1562
- $PB = Put Buy
1563
- $PS = Put Sell
1564
-
1565
- Example: $CB + $PS - ($PB + $CS)`);
1566
- });
1567
 
1568
  window.onload = () => App.init();
1569
  </script>
@@ -1572,4 +1616,4 @@ window.onload = () => App.init();
1572
  """
1573
 
1574
  if __name__ == "__main__":
1575
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
6
  from datetime import timedelta
7
  from fastapi import FastAPI, Body
8
  from fastapi.responses import HTMLResponse, JSONResponse
9
+ from typing import Optional, Set, Dict, List
10
 
11
  # ==========================================
12
  # 1. CONFIGURATION
 
48
  print(f"DB Error RPC ({func_name}): {e}")
49
  return []
50
 
51
+ def is_option(instrument: str) -> bool:
52
+ """Check if instrument is an option (CE or PE)"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  if not instrument:
54
+ return False
55
+ return instrument.upper().endswith('CE') or instrument.upper().endswith('PE')
56
+
57
+ def get_pair_instrument(instrument: str) -> str:
58
+ """Get the pair instrument (CE->PE, PE->CE)"""
59
+ if instrument.endswith('CE'):
60
+ return instrument.replace('CE', 'PE')
 
 
61
  elif instrument.endswith('PE'):
62
+ return instrument.replace('PE', 'CE')
63
+ return None
64
+
65
+ def extract_strike(instrument: str) -> float:
66
+ """Extract strike price from instrument name"""
67
+ match = re.search(r'(\d+(?:\.\d+)?)(?:CE|PE)$', instrument)
68
+ if match:
69
+ return float(match.group(1))
70
+ return 0.0
71
 
72
  # ==========================================
73
  # 3. API ROUTES - METADATA
 
144
  return JSONResponse({"spot_fut": sorted(spot_fut), "options": sorted(options)})
145
 
146
  # ==========================================
147
+ # 4. API ROUTES - DATA FETCHING
148
  # ==========================================
149
 
150
  @app.post("/api/fetch_series")
 
158
  end_time: str = Body(...),
159
  mode: str = Body("normal"),
160
  atm_range: int = Body(5),
161
+ series: list = Body(default=[])
162
  ):
163
  root = root.upper()
164
  t_start_dt = pd.to_datetime(f"{date} {start_time}:00")
165
  t_end_dt = pd.to_datetime(f"{date} {end_time}:59")
166
 
167
+ # =========================================================
168
+ # CHUNKING LOGIC: Split into 2-hour chunks for fast RPC
169
+ # =========================================================
170
+ async def fetch_chunk_normal(start_dt, end_dt, inst):
171
+ """Fetch chunk for normal mode - single instrument"""
172
+ params = {
173
+ "p_root": root,
174
+ "p_expiry": expiry,
175
+ "p_start_time": start_dt.strftime("%Y-%m-%d %H:%M:%S"),
176
+ "p_end_time": end_dt.strftime("%Y-%m-%d %H:%M:%S"),
177
+ "p_instrument": inst
178
+ }
179
+ loop = asyncio.get_event_loop()
180
+ return await loop.run_in_executor(None, sb_rpc, "get_normal_chart_data", params)
181
 
182
+ async def fetch_chunk_advanced(start_dt, end_dt, ref_inst):
183
+ """Fetch chunk for advanced mode - returns raw options data"""
184
+ params = {
185
+ "p_root": root,
186
+ "p_expiry": expiry,
187
+ "p_start_time": start_dt.strftime("%Y-%m-%d %H:%M:%S"),
188
+ "p_end_time": end_dt.strftime("%Y-%m-%d %H:%M:%S"),
189
+ "p_ref_instrument": ref_inst,
190
+ "p_atm_range": atm_range
191
+ }
192
+ loop = asyncio.get_event_loop()
193
+ return await loop.run_in_executor(None, sb_rpc, "get_advanced_chart_data", params)
194
+
195
+ # =========================================================
196
+ # NORMAL MODE
197
+ # =========================================================
198
+ if mode == "normal":
199
+ if not instrument:
200
+ return JSONResponse({"error": "No Instrument Selected.", "labels": [], "P": []})
201
+
202
+ all_records = []
203
+
204
+ # Check if it's an option - need to fetch both CE and PE
205
+ if is_option(instrument):
206
+ primary_inst = instrument
207
+ pair_inst = get_pair_instrument(instrument)
208
+
209
+ # Fetch both instruments
210
+ curr = t_start_dt
211
+ chunk_hours = 2
212
+
213
+ while curr <= t_end_dt:
214
+ nxt = curr + timedelta(hours=chunk_hours) - timedelta(seconds=1)
215
+ if nxt > t_end_dt:
216
+ nxt = t_end_dt
217
+
218
+ # Fetch primary and pair in parallel
219
+ tasks = [
220
+ fetch_chunk_normal(curr, nxt, primary_inst),
221
+ fetch_chunk_normal(curr, nxt, pair_inst) if pair_inst else None
222
+ ]
223
+ tasks = [t for t in tasks if t is not None]
224
+
225
+ results = await asyncio.gather(*tasks)
226
+
227
+ # Process primary instrument
228
+ if results[0] and isinstance(results[0], list):
229
+ for rec in results[0]:
230
+ rec['is_primary'] = True
231
+ all_records.append(rec)
232
+
233
+ # Process pair instrument (for CB/CS/PB/PS)
234
+ if len(results) > 1 and results[1] and isinstance(results[1], list):
235
+ for rec in results[1]:
236
+ rec['is_pair'] = True
237
+ all_records.append(rec)
238
+
239
+ curr += timedelta(hours=chunk_hours)
240
+ else:
241
+ # Non-option (Futures/Equity) - single fetch
242
+ curr = t_start_dt
243
+ chunk_hours = 2
244
+
245
+ while curr <= t_end_dt:
246
+ nxt = curr + timedelta(hours=chunk_hours) - timedelta(seconds=1)
247
+ if nxt > t_end_dt:
248
+ nxt = t_end_dt
249
+
250
+ result = await fetch_chunk_normal(curr, nxt, instrument)
251
+ if result and isinstance(result, list):
252
+ all_records.extend(result)
253
+
254
+ curr += timedelta(hours=chunk_hours)
255
+
256
+ if not all_records:
257
+ return JSONResponse({"error": "No valid data found in range.", "labels": [], "P": []})
258
+
259
+ # Process normal mode data
260
+ return process_normal_data(all_records, timeframe, instrument, is_option(instrument))
261
+
262
+ # =========================================================
263
+ # ADVANCED MODE
264
+ # =========================================================
265
+ else:
266
+ # Get reference instrument
267
  inst_data = sb_rpc("get_instruments_for_expiry", {"p_date": date, "p_root": root, "p_expiry": expiry})
268
  instruments = [r['instrument_name'] for r in inst_data]
269
 
270
  if not instruments:
271
+ return JSONResponse({"error": f"No data found for expiry {expiry}", "labels": [], "P": []})
272
 
 
273
  ref_instrument = instrument
274
  if not ref_instrument:
275
  spot_keys = [k for k in instruments if k.endswith('-INDEX') or k.endswith('-EQ')]
 
282
  else:
283
  others = [k for k in instruments if not k.endswith('CE') and not k.endswith('PE')]
284
  ref_instrument = others[0] if others else instruments[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
+ # Fetch all chunks
287
+ all_records = []
288
+ curr = t_start_dt
289
+ chunk_hours = 2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
+ while curr <= t_end_dt:
292
+ nxt = curr + timedelta(hours=chunk_hours) - timedelta(seconds=1)
293
+ if nxt > t_end_dt:
294
+ nxt = t_end_dt
295
+
296
+ result = await fetch_chunk_advanced(curr, nxt, ref_instrument)
297
+ if result and isinstance(result, list):
298
+ all_records.extend(result)
299
+
300
  curr += timedelta(hours=chunk_hours)
301
 
302
+ if not all_records:
303
+ return JSONResponse({"error": "No valid data found in range.", "labels": [], "P": []})
304
+
305
+ # Process advanced mode data - aggregate options based on ATM
306
+ return process_advanced_data(all_records, timeframe, atm_range)
 
 
307
 
 
 
308
 
309
+ def process_normal_data(records: list, timeframe: str, instrument: str, is_opt: bool) -> JSONResponse:
310
+ """Process normal mode data - handle CE/PE pairing for options"""
311
+
312
+ df = pd.DataFrame(records)
313
+
314
+ if df.empty:
315
+ return JSONResponse({"error": "No data to process.", "labels": [], "P": []})
316
 
317
+ df['tick_ts'] = pd.to_datetime(df['tick_ts'])
318
+ df = df.sort_values('tick_ts')
 
 
 
 
 
319
 
320
+ if is_opt:
321
+ # For options, merge primary and pair data
322
+ primary_df = df[df.get('is_primary', False) == True].copy()
323
+ pair_df = df[df.get('is_pair', False) == True].copy()
324
+
325
+ if not primary_df.empty:
326
+ primary_df = primary_df.set_index('tick_ts')
327
+
328
+ # Rename for primary (the selected instrument)
329
+ if instrument.endswith('CE'):
330
+ # Primary is CE, Pair is PE
331
+ primary_df = primary_df.rename(columns={
332
+ 'b_qty': 'cb', 's_qty': 'cs', 'vol': 'vol', 'oi': 'oi'
333
+ })
334
+ primary_df['pb'] = 0
335
+ primary_df['ps'] = 0
336
+ else:
337
+ # Primary is PE, Pair is CE
338
+ primary_df = primary_df.rename(columns={
339
+ 'b_qty': 'pb', 's_qty': 'ps', 'vol': 'vol', 'oi': 'oi'
340
+ })
341
+ primary_df['cb'] = 0
342
+ primary_df['cs'] = 0
343
+
344
+ if not pair_df.empty:
345
+ pair_df = pair_df.set_index('tick_ts')
346
+ if instrument.endswith('CE'):
347
+ # Pair is PE
348
+ primary_df['pb'] = pair_df['b_qty'].reindex(primary_df.index, method='ffill').fillna(0)
349
+ primary_df['ps'] = pair_df['s_qty'].reindex(primary_df.index, method='ffill').fillna(0)
350
+ else:
351
+ # Pair is CE
352
+ primary_df['cb'] = pair_df['b_qty'].reindex(primary_df.index, method='ffill').fillna(0)
353
+ primary_df['cs'] = pair_df['s_qty'].reindex(primary_df.index, method='ffill').fillna(0)
354
+
355
+ df = primary_df.reset_index()
356
+ else:
357
+ # Fallback - just use all records
358
+ df = df.set_index('tick_ts')
359
+ df['cb'] = 0
360
+ df['cs'] = 0
361
+ df['pb'] = 0
362
+ df['ps'] = 0
363
+ df = df.reset_index()
364
+ else:
365
+ # For non-options, just use the data as-is
366
+ df['cb'] = 0
367
+ df['cs'] = 0
368
+ df['pb'] = 0
369
+ df['ps'] = 0
370
 
371
+ df = df.set_index('tick_ts')
372
+ df = df.sort_index()
 
 
373
 
374
+ # Resample
375
  tf_map = {
376
  "1min": "1min", "3min": "3min", "5min": "5min",
377
  "15min": "15min", "30min": "30min", "1hour": "1h"
378
  }
379
  panda_tf = tf_map.get(timeframe, "1min")
380
 
 
381
  agg_dict = {
382
  "price": "last",
383
+ "vol": "sum",
384
  "oi": "last",
385
+ "b_qty": "last",
386
+ "s_qty": "last",
387
+ "cb": "last",
388
+ "cs": "last",
389
+ "pb": "last",
390
+ "ps": "last"
391
  }
392
 
 
393
  existing_cols = {k: v for k, v in agg_dict.items() if k in df.columns}
 
394
  resampled = df.resample(panda_tf).agg(existing_cols).ffill().fillna(0)
395
 
396
+ # Build breakdown for inspection
397
  breakdown_data = []
398
+ for idx in resampled.index:
399
+ breakdown_data.append({
400
+ "time": idx.strftime('%H:%M'),
401
+ "cb": resampled.loc[idx, 'cb'] if 'cb' in resampled else 0,
402
+ "cs": resampled.loc[idx, 'cs'] if 'cs' in resampled else 0,
403
+ "pb": resampled.loc[idx, 'pb'] if 'pb' in resampled else 0,
404
+ "ps": resampled.loc[idx, 'ps'] if 'ps' in resampled else 0,
405
+ })
 
406
 
407
+ return JSONResponse({
408
  "labels": resampled.index.strftime('%H:%M').tolist(),
409
  "P": resampled['price'].tolist() if 'price' in resampled else [],
410
+ "B": resampled['b_qty'].tolist() if 'b_qty' in resampled else [],
411
+ "S": resampled['s_qty'].tolist() if 's_qty' in resampled else [],
412
+ "V": resampled['vol'].tolist() if 'vol' in resampled else [],
413
  "OI": resampled['oi'].tolist() if 'oi' in resampled else [],
414
  "CB": resampled['cb'].tolist() if 'cb' in resampled else [],
415
  "CS": resampled['cs'].tolist() if 'cs' in resampled else [],
416
  "PB": resampled['pb'].tolist() if 'pb' in resampled else [],
417
  "PS": resampled['ps'].tolist() if 'ps' in resampled else [],
418
  "breakdown": breakdown_data
419
+ })
420
+
421
+
422
+ def process_advanced_data(records: list, timeframe: str, atm_range: int) -> JSONResponse:
423
+ """Process advanced mode data - aggregate options within ATM ± range"""
424
+
425
+ df = pd.DataFrame(records)
426
+
427
+ if df.empty:
428
+ return JSONResponse({"error": "No data to process.", "labels": [], "P": []})
429
+
430
+ df['tick_ts'] = pd.to_datetime(df['tick_ts'])
431
+ df = df.sort_values('tick_ts')
432
+
433
+ # Forward fill ref_price within each tick
434
+ df['ref_price'] = df.groupby('tick_ts')['ref_price'].transform(lambda x: x.ffill().bfill())
435
+
436
+ # Get unique timestamps and process each
437
+ results = []
438
+ for ts, group in df.groupby('tick_ts'):
439
+ ref_price = group['ref_price'].iloc[0]
440
+ ref_b = group['ref_b'].iloc[0]
441
+ ref_s = group['ref_s'].iloc[0]
442
+
443
+ if ref_price <= 0:
444
+ continue
445
+
446
+ # Filter options within ATM ± range
447
+ options = group[group['strike_price'] > 0].copy()
448
+ if options.empty:
449
+ continue
450
+
451
+ # Calculate ATM distance
452
+ options['atm_dist'] = abs(options['strike_price'] - ref_price)
453
+ options['atm_rank'] = options['atm_dist'].rank(method='dense')
454
+
455
+ # Filter within range
456
+ options_in_range = options[options['atm_rank'] <= (atm_range + 1)]
457
+
458
+ if options_in_range.empty:
459
+ continue
460
+
461
+ # Aggregate
462
+ cb = options_in_range[options_in_range['opt_type'] == 'CE']['b_qty'].sum()
463
+ cs = options_in_range[options_in_range['opt_type'] == 'CE']['s_qty'].sum()
464
+ pb = options_in_range[options_in_range['opt_type'] == 'PE']['b_qty'].sum()
465
+ ps = options_in_range[options_in_range['opt_type'] == 'PE']['s_qty'].sum()
466
+
467
+ results.append({
468
+ 'tick_ts': ts,
469
+ 'price': ref_price,
470
+ 'b_qty': ref_b,
471
+ 's_qty': ref_s,
472
+ 'cb': cb,
473
+ 'cs': cs,
474
+ 'pb': pb,
475
+ 'ps': ps
476
+ })
477
+
478
+ if not results:
479
+ return JSONResponse({"error": "No options data in range.", "labels": [], "P": []})
480
+
481
+ result_df = pd.DataFrame(results)
482
+ result_df = result_df.set_index('tick_ts')
483
+ result_df = result_df.sort_index()
484
+
485
+ # Resample
486
+ tf_map = {
487
+ "1min": "1min", "3min": "3min", "5min": "5min",
488
+ "15min": "15min", "30min": "30min", "1hour": "1h"
489
  }
490
+ panda_tf = tf_map.get(timeframe, "1min")
491
+
492
+ agg_dict = {
493
+ "price": "last",
494
+ "b_qty": "last",
495
+ "s_qty": "last",
496
+ "cb": "last",
497
+ "cs": "last",
498
+ "pb": "last",
499
+ "ps": "last"
500
+ }
501
+
502
+ resampled = result_df.resample(panda_tf).agg(agg_dict).ffill().fillna(0)
503
+
504
+ # Build breakdown
505
+ breakdown_data = []
506
+ for idx in resampled.index:
507
+ breakdown_data.append({
508
+ "time": idx.strftime('%H:%M'),
509
+ "cb": float(resampled.loc[idx, 'cb']),
510
+ "cs": float(resampled.loc[idx, 'cs']),
511
+ "pb": float(resampled.loc[idx, 'pb']),
512
+ "ps": float(resampled.loc[idx, 'ps']),
513
+ })
514
 
515
+ return JSONResponse({
516
+ "labels": resampled.index.strftime('%H:%M').tolist(),
517
+ "P": resampled['price'].tolist(),
518
+ "B": resampled['b_qty'].tolist(),
519
+ "S": resampled['s_qty'].tolist(),
520
+ "V": [],
521
+ "OI": [],
522
+ "CB": resampled['cb'].tolist(),
523
+ "CS": resampled['cs'].tolist(),
524
+ "PB": resampled['pb'].tolist(),
525
+ "PS": resampled['ps'].tolist(),
526
+ "breakdown": breakdown_data
527
+ })
528
 
529
 
530
  # ==========================================
531
+ # 5. FRONTEND TEMPLATE (same as before)
532
  # ==========================================
533
 
534
  HTML_TEMPLATE = """
 
541
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
542
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
543
  <style>
 
544
  :root {
545
  --bg-body: #0d1117;
546
  --bg-header: #161b22;
 
567
  ::-webkit-scrollbar-track { background: var(--bg-body); }
568
  ::-webkit-scrollbar-thumb { background: var(--bg-panel); border: 2px solid var(--bg-body); border-radius: 6px; }
569
 
 
570
  .top-bar {
571
  height: 52px; background: var(--bg-header); border-bottom: 1px solid var(--border);
572
  display: flex; align-items: center; justify-content: space-between; padding: 0 16px; flex-shrink: 0;
 
590
  .layout-btn { width: 30px; height: 26px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; }
591
  .layout-btn.active { background: var(--bg-panel); color: var(--accent); box-shadow: 0 1px 2px rgba(0,0,0,0.2); }
592
 
 
593
  .main-container { flex: 1; display: flex; overflow-y: auto; overflow-x: hidden; }
594
  .column { flex: 1; display: flex; flex-direction: column; border-right: 1px solid var(--border); min-width: 0; }
595
  .column:last-child { border-right: none; }
 
600
  }
601
  .add-chart-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
602
 
 
603
  .chart-card { background: var(--bg-card); border-bottom: 1px solid var(--border); display: flex; flex-direction: column; position: relative; }
604
  .card-header {
605
  height: 38px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between;
 
622
  .card-body { flex: 1; position: relative; width: 100%; height: 100%; overflow: hidden; display: flex; flex-direction: column; }
623
  .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-secondary); background: radial-gradient(circle, var(--bg-panel) 0%, var(--bg-card) 100%); text-align: center; padding: 20px;}
624
 
 
625
  .modal-backdrop {
626
  position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7);
627
  z-index: 100; display: none; justify-content: center; align-items: center;
 
638
 
639
  .form-section label { font-size: 11px; text-transform: uppercase; color: var(--text-secondary); font-weight: 600; margin-bottom: 6px; display: block; }
640
 
 
641
  .input-wrapper { position: relative; width: 100%; }
642
  .input-arrow { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); pointer-events: none; color: var(--text-secondary); font-size: 10px; }
643
  .custom-dd {
 
650
  .custom-dd-opt:hover { background: var(--bg-input); color: var(--accent); }
651
  .custom-dd-group { padding: 6px 12px; font-size: 10px; font-weight: 700; color: var(--text-secondary); background: var(--bg-body); text-transform: uppercase; }
652
 
 
653
  .series-row {
654
  background: var(--bg-panel); padding: 12px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 10px;
 
655
  }
656
  .series-row-top { display: flex; gap: 10px; margin-bottom: 10px; align-items: center; }
657
  .series-row-bottom { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
 
677
  padding: 4px 10px; border-radius: 6px; border: 1px dashed var(--border); flex: 1;
678
  }
679
 
 
680
  .segmented-control {
681
  display: flex; background: var(--bg-body); border: 1px solid var(--border);
682
  border-radius: 6px; padding: 2px; overflow: hidden; margin-bottom: 6px;
 
696
  }
697
  .info-icon:hover { color: var(--text-primary); border-color: var(--accent); }
698
 
 
699
  .info-table { width: 100%; font-family: var(--font-mono); font-size: 11px; border-collapse: collapse; margin-top: 10px; }
700
  .info-table th { text-align: left; color: var(--text-secondary); border-bottom: 1px solid var(--border); padding: 8px; font-weight: 600; }
701
  .info-table td { padding: 8px; border-bottom: 1px solid var(--border); color: var(--text-primary); }
702
  .info-table tr:hover { background: rgba(47, 129, 247, 0.05); }
 
703
  .info-table .positive { color: var(--success); }
704
  .info-table .negative { color: var(--danger); }
 
 
 
 
 
705
  </style>
706
  </head>
707
  <body>
 
741
  <div class="modal-body">
742
  <div id="cfgLoading" style="display:none; text-align:center; color:var(--text-secondary);">Loading...</div>
743
  <div id="cfgContent">
 
 
744
  <div id="cfgGlobalOnly" style="display:none;">
745
  <div class="form-section">
746
  <label>Refresh Rate (Seconds)</label>
747
  <input type="number" id="cfgRefresh" value="15" min="5">
748
  </div>
 
749
  </div>
750
 
 
751
  <div id="cfgChartOnly">
752
  <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
753
  <div class="form-section">
 
776
  </div>
777
  </div>
778
 
 
779
  <div class="form-section" id="cfgInstrumentContainer">
780
  <label id="lblInst">Instrument</label>
781
  <div class="input-wrapper">
 
784
  <div id="dd-cfgInst" class="custom-dd"></div>
785
  </div>
786
  <div id="advInstHint" style="display:none; font-size:10px; color:var(--text-secondary); margin-top:4px; line-height: 1.4;">
787
+ * For Advanced Mode, select the base Spot or Future here for ATM calculation.
788
  </div>
789
  </div>
790
 
 
791
  <div id="cfgAdvancedFields" style="display:none;">
792
  <div class="form-section" style="margin-top:12px;">
793
  <label style="color:var(--accent);">ATM (+/-) Strike Range</label>
794
  <input type="number" id="cfgAtmRange" value="5" min="1" max="50" style="width:100%;">
 
795
  </div>
796
  </div>
797
 
 
815
  </div>
816
  </div>
817
  </div>
 
818
  </div>
819
 
820
+ <div class="form-section" style="background:var(--bg-panel); padding:10px; border-radius:6px; border:1px solid var(--border); margin-top:12px;">
 
821
  <label>Quick Configurations</label>
822
  <div style="display:flex; gap:8px;">
823
  <select id="cfgPresetsDropdown" style="flex:1;" onchange="Config.loadPreset()"></select>
824
+ <button class="primary" onclick="Config.savePreset()">Save</button>
825
+ <button class="danger" onclick="Config.deletePreset()" style="width: 32px; padding:0;">✕</button>
826
  </div>
827
  </div>
828
 
 
829
  <div class="form-section">
830
  <div style="display:flex; align-items:center; margin-bottom: 6px;">
831
  <label style="margin:0;">Plot Series</label>
 
843
  </div>
844
  </div>
845
 
846
+ <!-- INSPECTION MODAL -->
847
  <div class="modal-backdrop" id="infoModal">
848
  <div class="modal modal-lg">
849
  <div class="modal-head">
 
876
  </div>
877
 
878
  <script>
 
879
  document.addEventListener('click', (e) => {
880
  if (!e.target.closest('.input-wrapper')) {
881
  document.querySelectorAll('.custom-dd').forEach(el => el.style.display = 'none');
 
932
  let pool = [...this.data[id]];
933
 
934
  if(id === 'cfgRoot' && q === '') {
935
+ const recentItems = this.recent.cfgRoot.map(r => ({val: r, group: 'Recent'}));
936
  pool = [...recentItems, ...pool];
937
  }
938
 
 
940
  .filter(item => item.val.toLowerCase().includes(q));
941
 
942
  if (filtered.length === 0) {
943
+ el.innerHTML = '<div class="custom-dd-opt" style="color:var(--text-secondary); cursor:default;">No matches</div>';
944
  return;
945
  }
946
 
 
973
  const App = {
974
  state: { date: null, mode: 'LIVE', columns: 3, layout: [[],[],[]], charts: {}, refreshRate: 15 },
975
 
 
976
  presets: {
 
977
  "Basic Flow": [
978
  { formula: '$P', label: 'Price', color1: '#2f81f7', axis: 'left', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
979
+ { formula: '$B', label: 'Buy', color1: '#238636', axis: 'right', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
980
+ { formula: '$S', label: 'Sell', color1: '#da3633', axis: 'right', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' }
981
  ],
982
+ "Net Flow": [
983
  { formula: '$P', label: 'Price', color1: '#2f81f7', axis: 'left', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
984
  { formula: '$B - $S', label: 'Net Flow', color1: '#238636', axis: 'right', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BUY', labelFalse: 'SELL' }
985
  ],
 
 
986
  "Call Flow": [
987
  { formula: '$P', label: 'Price', color1: '#2f81f7', axis: 'left', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
988
  { formula: '$CB', label: 'Call Buy', color1: '#238636', axis: 'right', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
 
1002
  { formula: '$P', label: 'Price', color1: '#2f81f7', axis: 'left', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
1003
  { formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', color1: '#238636', axis: 'right', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
1004
  ],
 
 
 
 
 
 
 
1005
  "Advanced - Net Flow": [
1006
+ { formula: '$P', label: 'Spot', color1: '#2f81f7', axis: 'left', cond: 'none', thresh: '0', color2: '#ffffff', labelTrue: '', labelFalse: '' },
1007
  { formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', color1: '#238636', axis: 'right', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
 
 
 
 
 
 
 
1008
  ]
1009
  },
1010
 
 
1066
  save() {
1067
  const cleanCharts = {};
1068
  for(let id in this.state.charts) cleanCharts[id] = { config: this.state.charts[id].config };
 
1069
  localStorage.setItem('dc_pro_v4_final', JSON.stringify({
1070
+ date: this.state.date, mode: this.state.mode, columns: this.state.columns,
1071
+ layout: this.state.layout, refreshRate: this.state.refreshRate, charts: cleanCharts
 
 
 
 
1072
  }));
1073
  },
1074
 
 
1090
 
1091
  renderGrid() {
1092
  document.querySelectorAll('.layout-btn').forEach((b, i) => b.classList.toggle('active', (i+1) === this.state.columns));
 
1093
  const container = document.getElementById('gridRoot');
1094
  container.innerHTML = '';
1095
 
 
1104
  this.state.charts[id].instance.destroy();
1105
  this.state.charts[id].instance = null;
1106
  }
 
1107
  const card = this.createCardDOM(id);
1108
  if (count > 0 && count <= 3) card.style.height = `calc(${viewH / count}px - 1px)`;
1109
  else card.style.height = '300px';
 
1110
  col.appendChild(card);
1111
  setTimeout(() => ChartLogic.plot(id), 0);
1112
  });
 
1136
  <span>${isCfg ? cfg.root : 'New Chart'}</span>
1137
  ${isCfg ? `${advancedLabel} <span class="card-meta">${cfg.expiry}</span>` : ''}
1138
  </div>
1139
+ <div class="card-tools" style="display: flex; gap: 4px;">
1140
+ <button class="tool-btn info-btn" id="infoBtn-${id}" onclick="InspectionModal.open('${id}')" title="Breakdown" style="display: ${isCfg ? 'flex' : 'none'};">i</button>
1141
  <button class="tool-btn" onclick="Config.open('${id}')">⚙</button>
1142
  <button class="tool-btn" style="color:var(--danger)" onclick="App.removeChart('${id}')">✕</button>
1143
  </div>
1144
  </div>
1145
  <div class="card-body" id="body-${id}">
1146
+ ${!isCfg ? `<div class="empty-state"><div style="font-size:24px; opacity:0.3;">📊</div><button style="background:transparent; border:1px solid var(--border); padding:5px 10px; border-radius:4px; color:var(--text-secondary); margin-top:8px;" onclick="Config.open('${id}')">Configure</button></div>` : `<canvas id="canvas-${id}"></canvas>`}
 
 
 
 
 
1147
  </div>
1148
  `;
1149
  return div;
 
1160
 
1161
  onDateChange() {
1162
  this.state.date = document.getElementById('globalDate').value;
 
 
 
 
 
 
1163
  this.save();
1164
  this.refreshAll();
1165
  },
 
1180
  startLoop() {
1181
  const loop = () => {
1182
  if(this.state.mode === 'LIVE') Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id, true));
1183
+ setTimeout(loop, (this.state.refreshRate || 15) * 1000);
 
1184
  };
1185
  loop();
1186
  }
1187
  };
1188
 
1189
+ const InspectionModal = {
1190
+ open(chartId) {
1191
+ const chart = App.state.charts[chartId];
1192
+ if(!chart || !chart.chartData) return;
1193
+
1194
+ const data = chart.chartData;
1195
+ const seriesConfig = chart.config.series;
1196
+
1197
+ const netFlowSeries = seriesConfig.find(s => s.formula.includes('CB') || s.formula.includes('PS'));
1198
+ document.getElementById('infoFormula').innerText = netFlowSeries ? netFlowSeries.formula : 'N/A';
1199
+
1200
+ let html = '';
1201
+ for(let i = 0; i < data.labels.length; i++) {
1202
+ const cb = data.CB[i] || 0;
1203
+ const cs = data.CS[i] || 0;
1204
+ const pb = data.PB[i] || 0;
1205
+ const ps = data.PS[i] || 0;
1206
+ const bullSide = cb + ps;
1207
+ const bearSide = pb + cs;
1208
+ const netFlow = bullSide - bearSide;
1209
+
1210
+ html += `<tr>
1211
+ <td>${data.labels[i]}</td>
1212
+ <td>${formatNum(cb)}</td>
1213
+ <td>${formatNum(cs)}</td>
1214
+ <td>${formatNum(pb)}</td>
1215
+ <td>${formatNum(ps)}</td>
1216
+ <td>${formatNum(bullSide)}</td>
1217
+ <td>${formatNum(bearSide)}</td>
1218
+ <td class="${netFlow>=0?'positive':'negative'}">${formatNum(netFlow)} (${netFlow>=0?'BULLISH':'BEARISH'})</td>
1219
+ </tr>`;
1220
+ }
1221
+ document.getElementById('infoTableBody').innerHTML = html;
1222
+ document.getElementById('infoModal').style.display = 'flex';
1223
+ }
1224
+ };
1225
+
1226
+ function formatNum(n) {
1227
+ if(Math.abs(n) >= 1000000) return (n/1000000).toFixed(2) + 'M';
1228
+ if(Math.abs(n) >= 1000) return (n/1000).toFixed(1) + 'K';
1229
+ return n.toFixed(0);
1230
+ }
1231
+
1232
  const ChartLogic = {
1233
  async plot(id, isUpdate=false) {
1234
  const chart = App.state.charts[id];
 
1262
  if(data.error || !data.labels || !data.labels.length) {
1263
  if(App.state.mode !== 'LIVE') {
1264
  const b = document.getElementById(`body-${id}`);
1265
+ if(b) b.innerHTML = `<div class="empty-state" style="opacity:0.8"><span style="font-size:20px;">⚠️</span><br/>${data.error || "No Data"}</div>`;
1266
  }
1267
  if(dot) dot.classList.remove('retry');
1268
  return;
1269
  }
 
 
 
1270
 
1271
+ App.state.charts[id].chartData = data;
 
 
 
 
1272
 
1273
  const body = document.getElementById(`body-${id}`);
1274
  if(body && !body.querySelector('canvas')) body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
1275
 
1276
  if(dot) dot.className = `status-dot ${App.state.mode==='LIVE'?'live':'hist'}`;
1277
+
1278
+ const infoBtn = document.getElementById(`infoBtn-${id}`);
1279
+ if(infoBtn) infoBtn.style.display = 'flex';
1280
 
1281
  const datasets = cfg.series.map(s => {
1282
  const vals = data.labels.map((_, i) => {
1283
+ const P = data.P[i] || 0, V = data.V[i] || 0, OI = data.OI[i] || 0;
1284
+ const B = data.B[i] || 0, S = data.S[i] || 0;
1285
+ const CB = data.CB[i] || 0, CS = data.CS[i] || 0;
1286
+ const PB = data.PB[i] || 0, PS = data.PS[i] || 0;
1287
 
 
1288
  let f = s.formula
1289
+ .replace(/\\$CB/g, str(CB)).replace(/\\$CS/g, str(CS))
1290
+ .replace(/\\$PB/g, str(PB)).replace(/\\$PS/g, str(PS))
1291
+ .replace(/\\$OI/g, str(OI)).replace(/\\$P/g, str(P))
1292
+ .replace(/\\$V/g, str(V)).replace(/\\$B/g, str(B)).replace(/\\$S/g, str(S));
1293
 
1294
  try { return new Function('return '+f)(); } catch{ return null; }
1295
  });
1296
 
1297
  let dataset = {
1298
  label: s.label, data: vals,
1299
+ borderWidth: 1.5, pointRadius: 0, hitRadius: 10, tension: 0.1,
1300
+ yAxisID: s.axis==='left'?'y':'y1',
1301
+ labelTrue: s.labelTrue || '', labelFalse: s.labelFalse || '',
1302
+ condition: s.cond, threshold: parseFloat(s.thresh) || 0,
1303
+ colorTrue: s.color1, colorFalse: s.color2 || s.color1
1304
  };
1305
 
1306
  if(s.cond && s.cond !== 'none') {
 
 
 
 
1307
  dataset.segment = {
1308
  borderColor: ctx => {
1309
  let v = ctx.p1.parsed.y;
1310
+ if(s.cond === '>') return v > dataset.threshold ? dataset.colorTrue : dataset.colorFalse;
1311
+ if(s.cond === '<') return v < dataset.threshold ? dataset.colorTrue : dataset.colorFalse;
1312
+ return dataset.colorTrue;
1313
  }
1314
  };
1315
+ dataset.borderColor = dataset.colorTrue;
1316
+ dataset.backgroundColor = dataset.colorTrue;
 
 
 
 
 
 
 
 
 
 
 
1317
  } else {
1318
  dataset.borderColor = s.color1;
1319
  dataset.backgroundColor = s.color1;
1320
  }
1321
+
1322
  return dataset;
1323
  });
1324
 
1325
+ this.render(id, data.labels, datasets, cfg.series);
1326
+
1327
+ } catch(e) { console.error(e); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1328
  },
1329
 
1330
+ render(id, labels, datasets, seriesConfig) {
1331
+ const ctx = document.getElementById(`canvas-${id}`);
1332
+ if(!ctx) return;
 
 
 
 
 
 
 
1333
 
1334
+ if(App.state.charts[id].instance) {
1335
+ App.state.charts[id].instance.destroy();
1336
+ App.state.charts[id].instance = null;
1337
+ }
1338
+
1339
+ const hasRight = datasets.some(d => d.yAxisID === 'y1');
1340
+ App.state.charts[id].instance = new Chart(ctx.getContext('2d'), {
1341
+ type: 'line',
1342
+ data: { labels, datasets },
1343
+ options: {
1344
+ responsive: true, maintainAspectRatio: false, animation: false,
1345
+ interaction: { mode: 'index', intersect: false },
1346
+ plugins: {
1347
+ legend: { display: false },
1348
+ tooltip: {
1349
+ backgroundColor: 'rgba(22, 27, 34, 0.95)', titleColor:'#e6edf3', bodyColor:'#e6edf3',
1350
+ callbacks: {
1351
+ label: function(context) {
1352
+ let s = seriesConfig[context.datasetIndex];
1353
+ let v = context.parsed.y;
1354
+ let label = s.label;
1355
+ if(s.cond && s.cond !== 'none') {
1356
+ let thresh = parseFloat(s.thresh) || 0;
1357
+ if(s.cond === '>') label = v > thresh ? (s.labelTrue || s.label) : (s.labelFalse || s.label);
1358
+ if(s.cond === '<') label = v < thresh ? (s.labelTrue || s.label) : (s.labelFalse || s.label);
1359
+ }
1360
+ return `${label}: ${formatNum(v)}`;
1361
+ }
1362
+ }
1363
+ }
1364
+ },
1365
+ scales: {
1366
+ x: { grid:{color:'#30363d'}, ticks:{color:'#8b949e', maxTicksLimit:8, font:{size:10}} },
1367
+ y: { type:'linear', display:true, position:'left', grid:{color:'#30363d'}, ticks:{color:'#8b949e'} },
1368
+ y1: { type:'linear', display:hasRight, position:'right', grid:{display:false}, ticks:{color:'#8b949e'} }
1369
+ }
1370
+ }
1371
  });
 
 
1372
  }
1373
  };
1374
 
1375
  const Config = {
1376
+ tid: null, isGlobal: false, currentMode: 'normal',
1377
+
1378
+ setMode(mode) {
1379
+ this.currentMode = mode;
1380
+ document.querySelectorAll('#cfgModeToggle .seg-btn').forEach((b, i) => b.classList.toggle('active', (i===0) === (mode==='normal')));
1381
+ document.getElementById('cfgAdvancedFields').style.display = mode === 'advanced' ? 'block' : 'none';
1382
+ document.getElementById('lblInst').innerText = mode === 'normal' ? 'Instrument' : 'Reference Spot/Fut';
1383
+ document.getElementById('advInstHint').style.display = mode === 'advanced' ? 'block' : 'none';
1384
+ this.updatePresetsDropdown();
1385
+ this.loadPreset(mode === 'advanced' ? "Advanced - Net Flow" : "Overall Net Flow");
1386
+ },
1387
 
1388
+ updatePresetsDropdown() {
1389
+ const sel = document.getElementById('cfgPresetsDropdown');
1390
+ const mode = this.currentMode;
1391
+ let presetNames = Object.keys(App.presets).filter(k => mode === 'advanced' ? k.startsWith('Advanced') : !k.startsWith('Advanced'));
1392
+ sel.innerHTML = presetNames.map(k => `<option value="${k}">${k}</option>`).join('');
1393
+ },
1394
+
1395
+ loadPreset(name) {
1396
+ name = name || document.getElementById('cfgPresetsDropdown').value;
1397
+ if(name && App.presets[name]) {
1398
+ document.getElementById('cfgPresetsDropdown').value = name;
1399
+ this.renderSeries(App.presets[name]);
1400
+ }
1401
+ },
1402
+
1403
+ savePreset() {
1404
+ const name = prompt("Enter name:", "Custom Model");
1405
+ if(!name) return;
1406
+ App.presets[name] = this.extractSeriesFromDOM();
1407
+ localStorage.setItem('dc_pro_v4_presets', JSON.stringify(App.presets));
1408
+ this.updatePresetsDropdown();
1409
+ },
1410
+
1411
+ deletePreset() {
1412
+ const name = document.getElementById('cfgPresetsDropdown').value;
1413
+ if(name.startsWith('Basic')||name.startsWith('Net Flow')||name.startsWith('Call')||name.startsWith('Put')||name.startsWith('Aggregated')||name.startsWith('Overall')||name.startsWith('Advanced')) return alert("Cannot delete default models.");
1414
+ if(confirm(`Delete '${name}'?`)) {
1415
+ delete App.presets[name];
1416
+ localStorage.setItem('dc_pro_v4_presets', JSON.stringify(App.presets));
1417
+ this.updatePresetsDropdown();
1418
+ }
1419
+ },
1420
+
1421
+ async open(id) {
1422
+ this.tid = id; this.isGlobal = false;
1423
+ document.getElementById('configModal').style.display = 'flex';
1424
  document.getElementById('cfgTitle').innerText = 'Configure Chart';
1425
+ document.getElementById('cfgLoading').style.display = 'block';
1426
+ document.getElementById('cfgContent').style.display = 'none';
1427
  document.getElementById('cfgGlobalOnly').style.display = 'none';
1428
  document.getElementById('cfgChartOnly').style.display = 'block';
1429
+
1430
+ CustomDD.clear('cfgRoot');
1431
+ const res = await fetch(`/api/roots?date=${App.state.date}`);
1432
+ CustomDD.set('cfgRoot', await res.json());
1433
+
1434
+ const cfg = App.state.charts[id].config;
1435
+ document.getElementById('cfgTF').value = cfg.timeframe || '1min';
1436
  document.getElementById('cfgStart').value = cfg.start || '09:15';
1437
  document.getElementById('cfgEnd').value = cfg.end || '15:30';
 
 
 
1438
  this.currentMode = cfg.chartMode || 'normal';
1439
+ document.getElementById('cfgAtmRange').value = cfg.atmRange || 5;
 
 
 
 
 
 
1440
 
1441
+ document.querySelectorAll('#cfgModeToggle .seg-btn').forEach((b, i) => b.classList.toggle('active', (i===0) === (this.currentMode==='normal')));
1442
+ document.getElementById('cfgAdvancedFields').style.display = this.currentMode === 'advanced' ? 'block' : 'none';
1443
+ document.getElementById('lblInst').innerText = this.currentMode === 'normal' ? 'Instrument' : 'Reference Spot/Fut';
1444
+ document.getElementById('advInstHint').style.display = this.currentMode === 'advanced' ? 'block' : 'none';
1445
+
1446
+ this.updatePresetsDropdown();
1447
+ if (cfg.series && cfg.series.length > 0) this.renderSeries(cfg.series);
1448
+ else this.loadPreset(this.currentMode === 'advanced' ? "Advanced - Net Flow" : "Overall Net Flow");
1449
+
1450
+ document.getElementById('cfgRoot').value = cfg.root || '';
1451
+ document.getElementById('cfgExp').value = cfg.expiry || '';
1452
+ document.getElementById('cfgInst').value = cfg.instrument || '';
1453
+
1454
+ if(cfg.root) await this.onRootChange(cfg.expiry, cfg.instrument);
1455
+ document.getElementById('cfgLoading').style.display = 'none';
1456
+ document.getElementById('cfgContent').style.display = 'block';
1457
  },
1458
 
1459
  openDefaults() {
1460
  this.isGlobal = true;
1461
+ document.getElementById('configModal').style.display = 'flex';
1462
  document.getElementById('cfgTitle').innerText = 'Settings';
1463
+ document.getElementById('cfgLoading').style.display = 'none';
1464
+ document.getElementById('cfgContent').style.display = 'block';
1465
  document.getElementById('cfgGlobalOnly').style.display = 'block';
1466
  document.getElementById('cfgChartOnly').style.display = 'none';
1467
+ document.getElementById('cfgRefresh').value = App.state.refreshRate || 15;
1468
+ this.updatePresetsDropdown();
1469
  },
1470
 
1471
+ close() { document.getElementById('configModal').style.display = 'none'; },
 
 
1472
 
1473
+ async onRootChange(preExp, preInst) {
1474
+ const root = document.getElementById('cfgRoot').value.toUpperCase();
1475
+ CustomDD.clear('cfgExp');
1476
+ CustomDD.clear('cfgInst');
1477
+ if (!preExp) document.getElementById('cfgExp').value = '';
1478
+ if (!preInst) document.getElementById('cfgInst').value = '';
1479
+ if (!root) return;
1480
 
1481
+ const exps = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`).then(r=>r.json());
1482
+ CustomDD.set('cfgExp', exps);
 
 
 
 
 
 
1483
 
1484
+ if(preExp && exps.includes(preExp)) {
1485
+ document.getElementById('cfgExp').value = preExp;
1486
+ await this.onExpChange(preInst);
1487
+ } else if (exps.length > 0) {
1488
+ const auto = await fetch(`/api/auto_config?date=${App.state.date}&root=${root}`).then(r=>r.json());
1489
+ if(auto.current) {
1490
+ document.getElementById('cfgExp').value = auto.current.expiry;
1491
+ await this.onExpChange(auto.current.instrument);
1492
+ }
1493
  }
1494
  },
1495
 
1496
+ async onExpChange(preInst) {
1497
+ const root = document.getElementById('cfgRoot').value.toUpperCase();
1498
+ const exp = document.getElementById('cfgExp').value.toUpperCase();
1499
+ CustomDD.clear('cfgInst');
1500
+ if (!root || !exp) return;
 
 
 
 
 
 
1501
 
1502
+ const data = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`).then(r=>r.json());
1503
+ if (data.spot_fut.length) CustomDD.set('cfgInst', data.spot_fut, 'Futures & Equity');
1504
+ if (data.options.length) CustomDD.set('cfgInst', data.options, 'Options');
1505
+ if(preInst) document.getElementById('cfgInst').value = preInst;
 
 
1506
  },
1507
 
1508
+ toggleCond(sel) { sel.nextElementSibling.style.display = sel.value === 'none' ? 'none' : 'flex'; },
 
 
 
 
 
 
 
 
1509
 
1510
+ renderSeries(list) {
1511
+ const c = document.getElementById('cfgSeries'); c.innerHTML = '';
1512
+ list.forEach(s => {
1513
+ const r = document.createElement('div'); r.className = 'series-row';
1514
+ const showCond = s.cond && s.cond !== 'none';
1515
+ r.innerHTML = `
 
 
1516
  <div class="series-row-top">
1517
+ <input type="text" value="${s.formula}" class="s-form" placeholder="Formula" style="flex:1;">
1518
+ <input type="text" value="${s.label}" class="s-lbl" placeholder="Label" style="width:100px;">
1519
+ <select class="s-axis" style="width:60px;"><option value="left" ${s.axis==='left'?'selected':''}>Left</option><option value="right" ${s.axis==='right'?'selected':''}>Right</option></select>
1520
+ <button class="tool-btn" style="color:var(--danger); width:32px; background:rgba(218,54,51,0.1);" onclick="this.parentElement.parentElement.remove()">✕</button>
1521
  </div>
1522
  <div class="series-row-bottom">
1523
+ <input type="color" value="${s.color1}" class="color-input s-color1" title="Color">
1524
+ <select class="s-cond" style="width:80px;" onchange="Config.toggleCond(this)">
1525
+ <option value="none" ${!s.cond||s.cond==='none'?'selected':''}>Solid</option>
1526
+ <option value=">" ${s.cond==='>'?'selected':''}>If &gt;</option>
1527
+ <option value="<" ${s.cond==='<'?'selected':''}>If &lt;</option>
1528
  </select>
1529
+ <div class="s-cond-group" style="display:${showCond?'flex':'none'}; flex-wrap:wrap;">
1530
+ <input type="number" class="s-thresh" value="${s.thresh||0}" style="width:70px;">
1531
+ <span style="color:var(--text-secondary); font-size:11px;">True:</span>
1532
+ <input type="color" value="${s.color1||'#238636'}" class="color-input s-color-true">
1533
+ <input type="text" class="s-label-true" value="${s.labelTrue||''}" placeholder="Label" style="width:70px;">
1534
+ <span style="color:var(--text-secondary); font-size:11px;">False:</span>
1535
+ <input type="color" value="${s.color2||'#da3633'}" class="color-input s-color-false">
1536
+ <input type="text" class="s-label-false" value="${s.labelFalse||''}" placeholder="Label" style="width:70px;">
1537
  </div>
1538
+ </div>`;
1539
+ c.appendChild(r);
 
1540
  });
1541
  },
1542
 
1543
  addSeriesRow() {
1544
+ const r = document.createElement('div'); r.className = 'series-row';
1545
+ r.innerHTML = `
 
 
1546
  <div class="series-row-top">
1547
+ <input type="text" value="$P" class="s-form" placeholder="Formula" style="flex:1;">
1548
+ <input type="text" value="New" class="s-lbl" placeholder="Label" style="width:100px;">
1549
+ <select class="s-axis" style="width:60px;"><option value="left">Left</option><option value="right">Right</option></select>
1550
+ <button class="tool-btn" style="color:var(--danger); width:32px; background:rgba(218,54,51,0.1);" onclick="this.parentElement.parentElement.remove()">✕</button>
1551
  </div>
1552
  <div class="series-row-bottom">
1553
+ <input type="color" value="#2f81f7" class="color-input s-color1">
1554
+ <select class="s-cond" style="width:80px;" onchange="Config.toggleCond(this)">
1555
+ <option value="none" selected>Solid</option>
1556
+ <option value=">">If &gt;</option>
1557
+ <option value="<">If &lt;</option>
1558
  </select>
1559
+ <div class="s-cond-group" style="display:none; flex-wrap:wrap;">
1560
+ <input type="number" class="s-thresh" value="0" style="width:70px;">
1561
+ <span style="color:var(--text-secondary); font-size:11px;">True:</span>
1562
+ <input type="color" value="#238636" class="color-input s-color-true">
1563
+ <input type="text" class="s-label-true" value="" placeholder="Label" style="width:70px;">
1564
+ <span style="color:var(--text-secondary); font-size:11px;">False:</span>
1565
+ <input type="color" value="#da3633" class="color-input s-color-false">
1566
+ <input type="text" class="s-label-false" value="" placeholder="Label" style="width:70px;">
1567
  </div>
1568
+ </div>`;
1569
+ document.getElementById('cfgSeries').appendChild(r);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1570
  },
1571
 
1572
+ extractSeriesFromDOM() {
1573
+ return Array.from(document.querySelectorAll('#cfgSeries .series-row')).map(r => ({
1574
+ formula: r.querySelector('.s-form').value,
1575
+ label: r.querySelector('.s-lbl').value,
1576
+ axis: r.querySelector('.s-axis').value,
1577
+ color1: r.querySelector('.s-color-true') ? r.querySelector('.s-color-true').value : r.querySelector('.s-color1').value,
1578
+ cond: r.querySelector('.s-cond').value,
1579
+ thresh: r.querySelector('.s-thresh') ? r.querySelector('.s-thresh').value : '0',
1580
+ color2: r.querySelector('.s-color-false') ? r.querySelector('.s-color-false').value : '#ffffff',
1581
+ labelTrue: r.querySelector('.s-label-true') ? r.querySelector('.s-label-true').value : '',
1582
+ labelFalse: r.querySelector('.s-label-false') ? r.querySelector('.s-label-false').value : ''
 
 
 
 
 
 
 
 
 
1583
  }));
1584
  },
1585
 
1586
  saveChartConfig() {
1587
+ if(this.isGlobal) {
1588
  App.state.refreshRate = parseInt(document.getElementById('cfgRefresh').value) || 15;
1589
  App.save();
1590
+ } else {
1591
+ const root = document.getElementById('cfgRoot').value.toUpperCase();
1592
+ const exp = document.getElementById('cfgExp').value.toUpperCase();
1593
+ const inst = document.getElementById('cfgInst').value.toUpperCase();
1594
+ if(!root || !exp) return alert("Incomplete Details.");
1595
+ App.state.charts[this.tid].config = {
1596
+ chartMode: this.currentMode, atmRange: parseInt(document.getElementById('cfgAtmRange').value) || 5,
1597
+ root, expiry: exp, instrument: inst,
1598
+ timeframe: document.getElementById('cfgTF').value,
1599
+ start: document.getElementById('cfgStart').value, end: document.getElementById('cfgEnd').value,
1600
+ series: this.extractSeriesFromDOM()
1601
+ };
1602
+ App.save();
1603
+ App.renderGrid();
1604
+ ChartLogic.plot(this.tid);
1605
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1606
  this.close();
 
1607
  }
1608
  };
1609
 
1610
+ function str(val) { return val != null ? val.toString() : "0"; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1611
 
1612
  window.onload = () => App.init();
1613
  </script>
 
1616
  """
1617
 
1618
  if __name__ == "__main__":
1619
+ uvicorn.run(app, host="0.0.0.0", port=7860)