topsecrettraders commited on
Commit
1dba94a
·
verified ·
1 Parent(s): 303b4e5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +77 -46
app.py CHANGED
@@ -5,7 +5,7 @@ import json
5
  import asyncio
6
  import re
7
  from datetime import datetime, timedelta
8
- from fastapi import FastAPI, Body, HTTPException
9
  from fastapi.responses import HTMLResponse, JSONResponse
10
  from fastapi.staticfiles import StaticFiles
11
  from typing import Optional, List, Dict, Any
@@ -57,10 +57,14 @@ def get_dates():
57
  return JSONResponse(sorted(list(dates), reverse=True))
58
 
59
  @app.get("/api/roots")
60
- def get_roots(date: str):
61
- """Fetch ALL roots available for a specific date."""
62
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
63
- data = sb_get(f"/history_buckets_fyers?select=root&minute=gte.{start}&minute=lte.{end}&limit=50000")
 
 
 
 
64
  roots = set()
65
  for row in data:
66
  if row.get('root'):
@@ -72,7 +76,7 @@ def get_auto_config(date: str, root: str):
72
  """Smart helper for defaults (Current/Next expiry)."""
73
  root = root.upper()
74
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
75
- data = sb_get(f"/history_buckets_fyers?select=expiry,data&root=eq.{root}&expiry=neq.MARKET&minute=gte.{start}&minute=lte.{end}&limit=200")
76
 
77
  expiry_map = {}
78
  for row in data:
@@ -112,10 +116,11 @@ def get_auto_config(date: str, root: str):
112
 
113
  @app.get("/api/expiries")
114
  def get_expiries(date: str, root: str):
115
- """Fetch ALL distinct expiries."""
116
  root = root.upper()
117
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
118
- data = sb_get(f"/history_buckets_fyers?select=expiry&root=eq.{root}&minute=gte.{start}&minute=lte.{end}&limit=50000")
 
119
  exps = set(row['expiry'] for row in data if row.get('expiry'))
120
  return JSONResponse(sorted(list(exps)))
121
 
@@ -124,7 +129,7 @@ def get_instruments(date: str, root: str, expiry: str):
124
  """Fetch instruments and segregate into spot/future vs options."""
125
  root = root.upper()
126
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
127
- data = sb_get(f"/history_buckets_fyers?select=data&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{start}&minute=lte.{end}&limit=100")
128
  instruments = set()
129
  for row in data:
130
  keys = row.get('data', {}).keys()
@@ -156,11 +161,6 @@ def fetch_series(
156
  mode: str = Body("normal"),
157
  atm_range: int = Body(5)
158
  ):
159
- """
160
- Fetches 10s data.
161
- - Normal mode uses Postgres JSON filtering.
162
- - Advanced mode fetches minute 'data' dict to calculate ATM strikes dynamically.
163
- """
164
  root = root.upper()
165
  t_start = f"{date}T{start_time}:00"
166
  t_end = f"{date}T{end_time}:59"
@@ -197,7 +197,6 @@ def fetch_series(
197
 
198
  sorted_strikes = sorted(list(unique_strikes))
199
 
200
- # PER TICK DYNAMIC ATM CALCULATION
201
  for i in range(6):
202
  p_base = None
203
  if base_key and base_key in data_dict:
@@ -224,7 +223,7 @@ def fetch_series(
224
  k_data = data_dict[k]
225
  bv = k_data.get('b', [0]*6)
226
  sv = k_data.get('s', [0]*6)
227
- vv = k_data.get('v',[0]*6)
228
  ov = k_data.get('o', [0]*6)
229
 
230
  b_val = float(bv[i]) if i < len(bv) and bv[i] else 0.0
@@ -259,7 +258,7 @@ def fetch_series(
259
  b_arr = inst_data.get('b',[]) or[]
260
  s_arr = inst_data.get('s', []) or[]
261
  v_arr = inst_data.get('v', []) or[]
262
- o_arr = inst_data.get('o',[]) or[]
263
 
264
  for i in range(6):
265
  if i < len(p_arr) and p_arr[i] is not None:
@@ -287,14 +286,12 @@ def fetch_series(
287
  }
288
  panda_tf = tf_map.get(timeframe, "1min")
289
 
290
- # Advanced Aggregation: Last for price/vol/oi, Mean for dynamic orderbook qtys
291
  agg_dict = {
292
  "price": "last", "volume": "last", "oi": "last",
293
  "buy_qty": "mean", "sell_qty": "mean",
294
  "cb": "mean", "cs": "mean", "pb": "mean", "ps": "mean"
295
  }
296
 
297
- # Smart gap filling to prevent chart distortion
298
  resampled = df.resample(panda_tf).agg(agg_dict).ffill().fillna(0)
299
 
300
  response_data = {
@@ -647,11 +644,18 @@ document.addEventListener('click', (e) => {
647
 
648
  const CustomDD = {
649
  data: { cfgRoot: [], cfgExp: [], cfgInst:[] },
 
650
 
 
 
 
 
 
651
  clear(id) {
652
  this.data[id] =[];
653
  document.getElementById(id).value = '';
654
  },
 
655
  set(id, items, groupName = null) {
656
  if (!groupName) {
657
  this.data[id] = items.map(i => ({val: i, group: null}));
@@ -659,24 +663,50 @@ const CustomDD = {
659
  this.data[id].push(...items.map(i => ({val: i, group: groupName})));
660
  }
661
  },
662
- open(id) {
 
663
  document.querySelectorAll('.custom-dd').forEach(el => el.style.display = 'none');
664
  const inp = document.getElementById(id);
665
  inp.select();
666
- this.render(id, ''); // Empty string ignores typed text and shows all options!
 
 
 
 
 
 
667
  document.getElementById(`dd-${id}`).style.display = 'block';
668
  },
669
- filter(id) {
670
- this.render(id, document.getElementById(id).value);
 
 
 
 
 
 
 
 
 
 
671
  document.getElementById(`dd-${id}`).style.display = 'block';
672
  },
 
673
  render(id, query) {
674
  const el = document.getElementById(`dd-${id}`);
675
  const q = query.toLowerCase();
676
  let html = '';
677
  let currentGroup = null;
678
 
679
- const filtered = this.data[id].filter(item => item.val.toLowerCase().includes(q));
 
 
 
 
 
 
 
 
680
 
681
  if (filtered.length === 0) {
682
  el.innerHTML = '<div class="custom-dd-opt" style="color:var(--text-secondary); cursor:default;">No matches found</div>';
@@ -692,27 +722,31 @@ const CustomDD = {
692
  });
693
  el.innerHTML = html;
694
  },
 
695
  pick(id, val) {
696
  document.getElementById(id).value = val;
697
  document.getElementById(`dd-${id}`).style.display = 'none';
698
- if(id === 'cfgRoot') Config.onRootChange();
 
 
 
 
 
 
 
 
 
699
  if(id === 'cfgExp') Config.onExpChange();
700
  }
701
  };
702
- // ----------------------------------------
703
 
704
  const App = {
705
  state: { date: null, mode: 'LIVE', columns: 3, layout: [[],[],[]], charts: {}, refreshRate: 15 },
706
  presets: {
707
- "Standard (Normal Mode)":[
708
- { formula: '$P', color1: '#2f81f7', cond: 'none', label: 'Price', axis: 'left' },
709
- { formula: '$B', color1: '#238636', cond: 'none', label: 'Buy', axis: 'right' },
710
- { formula: '$S', color1: '#da3633', cond: 'none', label: 'Sell', axis: 'right' }
711
- ],
712
  "Model 1":[
713
  { formula: '$P', color1: '#2f81f7', cond: 'none', label: 'Price', axis: 'left' },
714
- { formula: '$CB + $PS', color1: '#238636', cond: 'none', label: 'Buy Prs', axis: 'right' },
715
- { formula: '$CS + $PB', color1: '#da3633', cond: 'none', label: 'Sell Prs', axis: 'right' }
716
  ],
717
  "Model 2":[
718
  { formula: '$P', color1: '#2f81f7', cond: 'none', label: 'Price', axis: 'left' },
@@ -721,6 +755,7 @@ const App = {
721
  },
722
 
723
  async init() {
 
724
  await this.loadDates();
725
  const savedPresets = localStorage.getItem('dc_pro_v3_presets');
726
  if(savedPresets) {
@@ -978,7 +1013,6 @@ const ChartLogic = {
978
  borderWidth: 1.5, pointRadius: 0, hitRadius: 10, tension: 0.1, yAxisID: s.axis==='left'?'y':'y1'
979
  };
980
 
981
- // Dynamic Segment Coloring using Chart.js Segment
982
  if(s.cond && s.cond !== 'none') {
983
  let thresh = parseFloat(s.thresh) || 0;
984
  let cTrue = s.color1;
@@ -986,16 +1020,15 @@ const ChartLogic = {
986
 
987
  dataset.segment = {
988
  borderColor: ctx => {
989
- let v = ctx.p1.parsed.y; // next point value determines segment color
990
  if(s.cond === '>') return v > thresh ? cTrue : cFalse;
991
  if(s.cond === '<') return v < thresh ? cTrue : cFalse;
992
  return cTrue;
993
  }
994
  };
995
- dataset.borderColor = cTrue; // fallback
996
  dataset.backgroundColor = cTrue;
997
 
998
- // Dynamically calculate the hover color of each point to match the line segment color exactly
999
  const hoverColorCallback = (ctx) => {
1000
  let v = ctx.raw;
1001
  if (v === undefined || v === null) return cTrue;
@@ -1053,10 +1086,8 @@ const ChartLogic = {
1053
  let c = s.color1;
1054
  if(s.cond && s.cond !== 'none') {
1055
  let thresh = parseFloat(s.thresh) || 0;
1056
- let cTrue = s.color1;
1057
- let cFalse = s.color2 || s.color1;
1058
- if(s.cond === '>') c = v > thresh ? cTrue : cFalse;
1059
- if(s.cond === '<') c = v < thresh ? cTrue : cFalse;
1060
  }
1061
  return { borderColor: c, backgroundColor: c };
1062
  }
@@ -1088,7 +1119,7 @@ const Config = {
1088
 
1089
  this.updateInfoTooltip(mode);
1090
  this.updatePresetsDropdown();
1091
- this.loadPreset(mode === 'advanced' ? "Model 1" : "Standard (Normal Mode)");
1092
  },
1093
 
1094
  updateInfoTooltip(mode) {
@@ -1130,7 +1161,7 @@ const Config = {
1130
 
1131
  deletePreset() {
1132
  const name = document.getElementById('cfgPresetsDropdown').value;
1133
- if(name.startsWith('Model 1') || name.startsWith('Model 2') || name.startsWith('Standard')) return alert("Cannot delete default models.");
1134
  if(confirm(`Delete model '${name}'?`)) {
1135
  delete App.presets[name];
1136
  localStorage.setItem('dc_pro_v3_presets', JSON.stringify(App.presets));
@@ -1151,6 +1182,7 @@ const Config = {
1151
 
1152
  this.updatePresetsDropdown();
1153
 
 
1154
  CustomDD.clear('cfgRoot');
1155
  const res = await fetch(`/api/roots?date=${App.state.date}`);
1156
  const roots = await res.json();
@@ -1164,7 +1196,6 @@ const Config = {
1164
  this.currentMode = cfg.chartMode || 'normal';
1165
  document.getElementById('cfgAtmRange').value = cfg.atmRange || 5;
1166
 
1167
- // Setup initial UI states without forcing preset reset if series exist
1168
  const btns = document.querySelectorAll('#cfgModeToggle .seg-btn');
1169
  btns[0].classList.toggle('active', this.currentMode === 'normal');
1170
  btns[1].classList.toggle('active', this.currentMode === 'advanced');
@@ -1173,11 +1204,10 @@ const Config = {
1173
 
1174
  this.updateInfoTooltip(this.currentMode);
1175
 
1176
- // Override defaults if there is already a saved config
1177
  if (cfg.series && cfg.series.length > 0) {
1178
  this.renderSeries(cfg.series);
1179
  } else {
1180
- this.loadPreset(this.currentMode === 'advanced' ? "Model 1" : "Standard (Normal Mode)");
1181
  }
1182
 
1183
  document.getElementById('cfgRoot').value = cfg.root || '';
@@ -1201,7 +1231,7 @@ const Config = {
1201
  document.getElementById('cfgRefresh').value = App.state.refreshRate || 15;
1202
 
1203
  this.updatePresetsDropdown();
1204
- this.loadPreset("Standard (Normal Mode)");
1205
  },
1206
 
1207
  close() { document.getElementById('configModal').style.display = 'none'; },
@@ -1216,6 +1246,7 @@ const Config = {
1216
 
1217
  if (!root) return;
1218
 
 
1219
  const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`);
1220
  const exps = await res.json();
1221
  CustomDD.set('cfgExp', exps);
 
5
  import asyncio
6
  import re
7
  from datetime import datetime, timedelta
8
+ from fastapi import FastAPI, Body, HTTPException, Query
9
  from fastapi.responses import HTMLResponse, JSONResponse
10
  from fastapi.staticfiles import StaticFiles
11
  from typing import Optional, List, Dict, Any
 
57
  return JSONResponse(sorted(list(dates), reverse=True))
58
 
59
  @app.get("/api/roots")
60
+ def get_roots(date: str, q: Optional[str] = None):
61
+ """Fetch roots. If q is provided, search specifically for that string."""
62
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
63
+ endpoint = f"/history_buckets_fyers?select=root&minute=gte.{start}&minute=lte.{end}"
64
+ if q:
65
+ endpoint += f"&root=ilike.*{q.upper()}*"
66
+
67
+ data = sb_get(endpoint)
68
  roots = set()
69
  for row in data:
70
  if row.get('root'):
 
76
  """Smart helper for defaults (Current/Next expiry)."""
77
  root = root.upper()
78
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
79
+ data = sb_get(f"/history_buckets_fyers?select=expiry,data&root=eq.{root}&expiry=neq.MARKET&minute=gte.{start}&minute=lte.{end}&limit=50")
80
 
81
  expiry_map = {}
82
  for row in data:
 
116
 
117
  @app.get("/api/expiries")
118
  def get_expiries(date: str, root: str):
119
+ """Fetch LITERALLY all distinct expiries for a specific root."""
120
  root = root.upper()
121
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
122
+ # Note: Removed small limit to ensure all expiries are caught
123
+ data = sb_get(f"/history_buckets_fyers?select=expiry&root=eq.{root}&minute=gte.{start}&minute=lte.{end}")
124
  exps = set(row['expiry'] for row in data if row.get('expiry'))
125
  return JSONResponse(sorted(list(exps)))
126
 
 
129
  """Fetch instruments and segregate into spot/future vs options."""
130
  root = root.upper()
131
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
132
+ data = sb_get(f"/history_buckets_fyers?select=data&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{start}&minute=lte.{end}&limit=50")
133
  instruments = set()
134
  for row in data:
135
  keys = row.get('data', {}).keys()
 
161
  mode: str = Body("normal"),
162
  atm_range: int = Body(5)
163
  ):
 
 
 
 
 
164
  root = root.upper()
165
  t_start = f"{date}T{start_time}:00"
166
  t_end = f"{date}T{end_time}:59"
 
197
 
198
  sorted_strikes = sorted(list(unique_strikes))
199
 
 
200
  for i in range(6):
201
  p_base = None
202
  if base_key and base_key in data_dict:
 
223
  k_data = data_dict[k]
224
  bv = k_data.get('b', [0]*6)
225
  sv = k_data.get('s', [0]*6)
226
+ vv = k_data.get('v', [0]*6)
227
  ov = k_data.get('o', [0]*6)
228
 
229
  b_val = float(bv[i]) if i < len(bv) and bv[i] else 0.0
 
258
  b_arr = inst_data.get('b',[]) or[]
259
  s_arr = inst_data.get('s', []) or[]
260
  v_arr = inst_data.get('v', []) or[]
261
+ o_arr = inst_data.get('o', []) or[]
262
 
263
  for i in range(6):
264
  if i < len(p_arr) and p_arr[i] is not None:
 
286
  }
287
  panda_tf = tf_map.get(timeframe, "1min")
288
 
 
289
  agg_dict = {
290
  "price": "last", "volume": "last", "oi": "last",
291
  "buy_qty": "mean", "sell_qty": "mean",
292
  "cb": "mean", "cs": "mean", "pb": "mean", "ps": "mean"
293
  }
294
 
 
295
  resampled = df.resample(panda_tf).agg(agg_dict).ffill().fillna(0)
296
 
297
  response_data = {
 
644
 
645
  const CustomDD = {
646
  data: { cfgRoot: [], cfgExp: [], cfgInst:[] },
647
+ recent: { cfgRoot: [] }, // Persistent memory for symbols
648
 
649
+ init() {
650
+ const saved = localStorage.getItem('dc_recent_symbols');
651
+ if(saved) this.recent.cfgRoot = JSON.parse(saved);
652
+ },
653
+
654
  clear(id) {
655
  this.data[id] =[];
656
  document.getElementById(id).value = '';
657
  },
658
+
659
  set(id, items, groupName = null) {
660
  if (!groupName) {
661
  this.data[id] = items.map(i => ({val: i, group: null}));
 
663
  this.data[id].push(...items.map(i => ({val: i, group: groupName})));
664
  }
665
  },
666
+
667
+ async open(id) {
668
  document.querySelectorAll('.custom-dd').forEach(el => el.style.display = 'none');
669
  const inp = document.getElementById(id);
670
  inp.select();
671
+
672
+ if(id === 'cfgRoot' && inp.value.length === 0) {
673
+ // If empty root box, show popular/recent symbols first
674
+ this.render(id, '');
675
+ } else {
676
+ this.render(id, inp.value);
677
+ }
678
  document.getElementById(`dd-${id}`).style.display = 'block';
679
  },
680
+
681
+ async filter(id) {
682
+ const query = document.getElementById(id).value.toUpperCase();
683
+
684
+ // Targeted backend search for symbols to minimize egress
685
+ if(id === 'cfgRoot' && query.length >= 2) {
686
+ const res = await fetch(`/api/roots?date=${App.state.date}&q=${query}`);
687
+ const results = await res.json();
688
+ this.set(id, results);
689
+ }
690
+
691
+ this.render(id, query);
692
  document.getElementById(`dd-${id}`).style.display = 'block';
693
  },
694
+
695
  render(id, query) {
696
  const el = document.getElementById(`dd-${id}`);
697
  const q = query.toLowerCase();
698
  let html = '';
699
  let currentGroup = null;
700
 
701
+ // Add Recent Symbols to the top of Root list
702
+ let pool = [...this.data[id]];
703
+ if(id === 'cfgRoot' && q === '') {
704
+ const recentItems = this.recent.cfgRoot.map(r => ({val: r, group: 'Recent Search'}));
705
+ pool = [...recentItems, ...pool];
706
+ }
707
+
708
+ const filtered = pool.filter((v, i, a) => a.findIndex(t => t.val === v.val) === i) // Unique
709
+ .filter(item => item.val.toLowerCase().includes(q));
710
 
711
  if (filtered.length === 0) {
712
  el.innerHTML = '<div class="custom-dd-opt" style="color:var(--text-secondary); cursor:default;">No matches found</div>';
 
722
  });
723
  el.innerHTML = html;
724
  },
725
+
726
  pick(id, val) {
727
  document.getElementById(id).value = val;
728
  document.getElementById(`dd-${id}`).style.display = 'none';
729
+
730
+ if(id === 'cfgRoot') {
731
+ // Save to recent
732
+ if(!this.recent.cfgRoot.includes(val)) {
733
+ this.recent.cfgRoot.unshift(val);
734
+ if(this.recent.cfgRoot.length > 10) this.recent.cfgRoot.pop();
735
+ localStorage.setItem('dc_recent_symbols', JSON.stringify(this.recent.cfgRoot));
736
+ }
737
+ Config.onRootChange();
738
+ }
739
  if(id === 'cfgExp') Config.onExpChange();
740
  }
741
  };
 
742
 
743
  const App = {
744
  state: { date: null, mode: 'LIVE', columns: 3, layout: [[],[],[]], charts: {}, refreshRate: 15 },
745
  presets: {
 
 
 
 
 
746
  "Model 1":[
747
  { formula: '$P', color1: '#2f81f7', cond: 'none', label: 'Price', axis: 'left' },
748
+ { formula: '$CB + $PS', color1: '#238636', cond: 'none', label: 'Buy', axis: 'right' },
749
+ { formula: '$CS + $PB', color1: '#da3633', cond: 'none', label: 'Sell', axis: 'right' }
750
  ],
751
  "Model 2":[
752
  { formula: '$P', color1: '#2f81f7', cond: 'none', label: 'Price', axis: 'left' },
 
755
  },
756
 
757
  async init() {
758
+ CustomDD.init();
759
  await this.loadDates();
760
  const savedPresets = localStorage.getItem('dc_pro_v3_presets');
761
  if(savedPresets) {
 
1013
  borderWidth: 1.5, pointRadius: 0, hitRadius: 10, tension: 0.1, yAxisID: s.axis==='left'?'y':'y1'
1014
  };
1015
 
 
1016
  if(s.cond && s.cond !== 'none') {
1017
  let thresh = parseFloat(s.thresh) || 0;
1018
  let cTrue = s.color1;
 
1020
 
1021
  dataset.segment = {
1022
  borderColor: ctx => {
1023
+ let v = ctx.p1.parsed.y;
1024
  if(s.cond === '>') return v > thresh ? cTrue : cFalse;
1025
  if(s.cond === '<') return v < thresh ? cTrue : cFalse;
1026
  return cTrue;
1027
  }
1028
  };
1029
+ dataset.borderColor = cTrue;
1030
  dataset.backgroundColor = cTrue;
1031
 
 
1032
  const hoverColorCallback = (ctx) => {
1033
  let v = ctx.raw;
1034
  if (v === undefined || v === null) return cTrue;
 
1086
  let c = s.color1;
1087
  if(s.cond && s.cond !== 'none') {
1088
  let thresh = parseFloat(s.thresh) || 0;
1089
+ if(s.cond === '>') c = v > thresh ? s.color1 : s.color2;
1090
+ if(s.cond === '<') c = v < thresh ? s.color1 : s.color2;
 
 
1091
  }
1092
  return { borderColor: c, backgroundColor: c };
1093
  }
 
1119
 
1120
  this.updateInfoTooltip(mode);
1121
  this.updatePresetsDropdown();
1122
+ this.loadPreset("Model 1"); // Model 1 is now the default
1123
  },
1124
 
1125
  updateInfoTooltip(mode) {
 
1161
 
1162
  deletePreset() {
1163
  const name = document.getElementById('cfgPresetsDropdown').value;
1164
+ if(name.startsWith('Model 1') || name.startsWith('Model 2')) return alert("Cannot delete default models.");
1165
  if(confirm(`Delete model '${name}'?`)) {
1166
  delete App.presets[name];
1167
  localStorage.setItem('dc_pro_v3_presets', JSON.stringify(App.presets));
 
1182
 
1183
  this.updatePresetsDropdown();
1184
 
1185
+ // Load existing roots once
1186
  CustomDD.clear('cfgRoot');
1187
  const res = await fetch(`/api/roots?date=${App.state.date}`);
1188
  const roots = await res.json();
 
1196
  this.currentMode = cfg.chartMode || 'normal';
1197
  document.getElementById('cfgAtmRange').value = cfg.atmRange || 5;
1198
 
 
1199
  const btns = document.querySelectorAll('#cfgModeToggle .seg-btn');
1200
  btns[0].classList.toggle('active', this.currentMode === 'normal');
1201
  btns[1].classList.toggle('active', this.currentMode === 'advanced');
 
1204
 
1205
  this.updateInfoTooltip(this.currentMode);
1206
 
 
1207
  if (cfg.series && cfg.series.length > 0) {
1208
  this.renderSeries(cfg.series);
1209
  } else {
1210
+ this.loadPreset("Model 1");
1211
  }
1212
 
1213
  document.getElementById('cfgRoot').value = cfg.root || '';
 
1231
  document.getElementById('cfgRefresh').value = App.state.refreshRate || 15;
1232
 
1233
  this.updatePresetsDropdown();
1234
+ this.loadPreset("Model 1");
1235
  },
1236
 
1237
  close() { document.getElementById('configModal').style.display = 'none'; },
 
1246
 
1247
  if (!root) return;
1248
 
1249
+ // Fetches ALL available expiries for the root
1250
  const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`);
1251
  const exps = await res.json();
1252
  CustomDD.set('cfgExp', exps);