Update app.py
Browse files
app.py
CHANGED
|
@@ -28,18 +28,28 @@ app = FastAPI()
|
|
| 28 |
# 2. HELPER FUNCTIONS
|
| 29 |
# ==========================================
|
| 30 |
def sb_get(endpoint: str, params: dict = None):
|
| 31 |
-
"""Generic Supabase GET helper."""
|
| 32 |
try:
|
| 33 |
url = f"{SB_URL}/rest/v1{endpoint}"
|
| 34 |
r = requests.get(url, headers=HEADERS, params=params)
|
| 35 |
r.raise_for_status()
|
| 36 |
return r.json()
|
| 37 |
except Exception as e:
|
| 38 |
-
print(f"DB Error ({endpoint}): {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
return[]
|
| 40 |
|
| 41 |
# ==========================================
|
| 42 |
-
# 3. API ROUTES - METADATA
|
| 43 |
# ==========================================
|
| 44 |
|
| 45 |
@app.get("/")
|
|
@@ -48,93 +58,66 @@ def home():
|
|
| 48 |
|
| 49 |
@app.get("/api/dates")
|
| 50 |
def get_dates():
|
| 51 |
-
"""Fetch distinct dates."""
|
| 52 |
-
data =
|
| 53 |
-
dates =
|
| 54 |
-
|
| 55 |
-
if row.get('minute'):
|
| 56 |
-
dates.add(row['minute'].split('T')[0])
|
| 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
|
| 62 |
-
|
| 63 |
-
# Added 50000 limit to ensure we load ALL roots without Supabase 1000 row truncation
|
| 64 |
-
endpoint = f"/history_buckets_fyers?select=root&minute=gte.{start}&minute=lte.{end}&limit=50000"
|
| 65 |
if q:
|
| 66 |
-
|
| 67 |
|
| 68 |
-
data =
|
| 69 |
-
roots =
|
| 70 |
-
|
| 71 |
-
if row.get('root'):
|
| 72 |
-
roots.add(row['root'])
|
| 73 |
-
return JSONResponse(sorted(list(roots)))
|
| 74 |
|
| 75 |
@app.get("/api/auto_config")
|
| 76 |
def get_auto_config(date: str, root: str):
|
| 77 |
-
"""Smart helper for defaults
|
| 78 |
root = root.upper()
|
| 79 |
-
|
| 80 |
-
|
| 81 |
|
| 82 |
-
expiry_map = {}
|
| 83 |
-
for row in data:
|
| 84 |
-
exp = row.get('expiry')
|
| 85 |
-
if not exp: continue
|
| 86 |
-
if exp not in expiry_map:
|
| 87 |
-
keys = list(row.get('data', {}).keys())
|
| 88 |
-
expiry_map[exp] = keys
|
| 89 |
-
else:
|
| 90 |
-
existing = set(expiry_map[exp])
|
| 91 |
-
new_keys = row.get('data', {}).keys()
|
| 92 |
-
expiry_map[exp] = list(existing.union(new_keys))
|
| 93 |
-
|
| 94 |
-
all_expiries = sorted(expiry_map.keys())
|
| 95 |
valid_expiries =[e for e in all_expiries if e >= date]
|
| 96 |
if not valid_expiries:
|
| 97 |
valid_expiries = all_expiries
|
| 98 |
|
| 99 |
response = {"expiries": all_expiries, "current": None, "next": None}
|
| 100 |
|
| 101 |
-
def
|
|
|
|
|
|
|
| 102 |
for i in instruments:
|
| 103 |
if 'FUT' in i: return i
|
| 104 |
return instruments[0] if instruments else None
|
| 105 |
|
| 106 |
if len(valid_expiries) > 0:
|
| 107 |
exp_c = valid_expiries[0]
|
| 108 |
-
|
| 109 |
-
response["current"] = {"expiry": exp_c, "instrument": inst_c}
|
| 110 |
|
| 111 |
if len(valid_expiries) > 1:
|
| 112 |
exp_n = valid_expiries[1]
|
| 113 |
-
|
| 114 |
-
response["next"] = {"expiry": exp_n, "instrument": inst_n}
|
| 115 |
|
| 116 |
return JSONResponse(response)
|
| 117 |
|
| 118 |
@app.get("/api/expiries")
|
| 119 |
def get_expiries(date: str, root: str):
|
| 120 |
-
"""Fetch
|
| 121 |
root = root.upper()
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
exps = set(row['expiry'] for row in data if row.get('expiry'))
|
| 126 |
-
return JSONResponse(sorted(list(exps)))
|
| 127 |
|
| 128 |
@app.get("/api/instruments")
|
| 129 |
def get_instruments(date: str, root: str, expiry: str):
|
| 130 |
-
"""Fetch instruments
|
| 131 |
root = root.upper()
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
instruments = set()
|
| 135 |
-
for row in data:
|
| 136 |
-
keys = row.get('data', {}).keys()
|
| 137 |
-
instruments.update(keys)
|
| 138 |
|
| 139 |
spot_fut = []
|
| 140 |
options =[]
|
|
@@ -201,7 +184,7 @@ def fetch_series(
|
|
| 201 |
for i in range(6):
|
| 202 |
p_base = None
|
| 203 |
if base_key and base_key in data_dict:
|
| 204 |
-
p_arr = data_dict[base_key].get('p',
|
| 205 |
if i < len(p_arr) and p_arr[i] is not None:
|
| 206 |
p_base = float(p_arr[i])
|
| 207 |
|
|
@@ -255,10 +238,10 @@ def fetch_series(
|
|
| 255 |
else:
|
| 256 |
inst_data = data_dict.get(instrument)
|
| 257 |
if not inst_data: continue
|
| 258 |
-
p_arr = inst_data.get('p', []) or[]
|
| 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):
|
|
@@ -644,7 +627,7 @@ document.addEventListener('click', (e) => {
|
|
| 644 |
});
|
| 645 |
|
| 646 |
const CustomDD = {
|
| 647 |
-
data: { cfgRoot: [], cfgExp:
|
| 648 |
recent: { cfgRoot:[] }, // Persistent memory for symbols
|
| 649 |
|
| 650 |
init() {
|
|
@@ -668,10 +651,9 @@ const CustomDD = {
|
|
| 668 |
async open(id) {
|
| 669 |
document.querySelectorAll('.custom-dd').forEach(el => el.style.display = 'none');
|
| 670 |
const inp = document.getElementById(id);
|
| 671 |
-
inp.select();
|
| 672 |
|
| 673 |
-
//
|
| 674 |
-
// It ignores the current value (like "2026-03-24") so you don't have to backspace!
|
| 675 |
this.render(id, '');
|
| 676 |
|
| 677 |
document.getElementById(`dd-${id}`).style.display = 'block';
|
|
@@ -680,8 +662,12 @@ const CustomDD = {
|
|
| 680 |
async filter(id) {
|
| 681 |
const query = document.getElementById(id).value.toUpperCase();
|
| 682 |
|
| 683 |
-
//
|
| 684 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 685 |
|
| 686 |
this.render(id, query);
|
| 687 |
document.getElementById(`dd-${id}`).style.display = 'block';
|
|
@@ -693,14 +679,14 @@ const CustomDD = {
|
|
| 693 |
let html = '';
|
| 694 |
let currentGroup = null;
|
| 695 |
|
| 696 |
-
// Add Recent Symbols to the top of Root list
|
| 697 |
let pool = [...this.data[id]];
|
|
|
|
| 698 |
if(id === 'cfgRoot' && q === '') {
|
| 699 |
const recentItems = this.recent.cfgRoot.map(r => ({val: r, group: 'Recent Search'}));
|
| 700 |
pool =[...recentItems, ...pool];
|
| 701 |
}
|
| 702 |
|
| 703 |
-
const filtered = pool.filter((v, i, a) => a.findIndex(t => t.val === v.val) === i)
|
| 704 |
.filter(item => item.val.toLowerCase().includes(q));
|
| 705 |
|
| 706 |
if (filtered.length === 0) {
|
|
@@ -723,7 +709,6 @@ const CustomDD = {
|
|
| 723 |
document.getElementById(`dd-${id}`).style.display = 'none';
|
| 724 |
|
| 725 |
if(id === 'cfgRoot') {
|
| 726 |
-
// Save to recent
|
| 727 |
if(!this.recent.cfgRoot.includes(val)) {
|
| 728 |
this.recent.cfgRoot.unshift(val);
|
| 729 |
if(this.recent.cfgRoot.length > 10) this.recent.cfgRoot.pop();
|
|
@@ -1081,8 +1066,10 @@ const ChartLogic = {
|
|
| 1081 |
let c = s.color1;
|
| 1082 |
if(s.cond && s.cond !== 'none') {
|
| 1083 |
let thresh = parseFloat(s.thresh) || 0;
|
| 1084 |
-
|
| 1085 |
-
|
|
|
|
|
|
|
| 1086 |
}
|
| 1087 |
return { borderColor: c, backgroundColor: c };
|
| 1088 |
}
|
|
@@ -1114,7 +1101,7 @@ const Config = {
|
|
| 1114 |
|
| 1115 |
this.updateInfoTooltip(mode);
|
| 1116 |
this.updatePresetsDropdown();
|
| 1117 |
-
this.loadPreset("Model 1"); // Model 1
|
| 1118 |
},
|
| 1119 |
|
| 1120 |
updateInfoTooltip(mode) {
|
|
@@ -1177,7 +1164,7 @@ const Config = {
|
|
| 1177 |
|
| 1178 |
this.updatePresetsDropdown();
|
| 1179 |
|
| 1180 |
-
// Load
|
| 1181 |
CustomDD.clear('cfgRoot');
|
| 1182 |
const res = await fetch(`/api/roots?date=${App.state.date}`);
|
| 1183 |
const roots = await res.json();
|
|
@@ -1241,7 +1228,7 @@ const Config = {
|
|
| 1241 |
|
| 1242 |
if (!root) return;
|
| 1243 |
|
| 1244 |
-
// Fetches ALL available expiries for the root
|
| 1245 |
const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`);
|
| 1246 |
const exps = await res.json();
|
| 1247 |
CustomDD.set('cfgExp', exps);
|
|
@@ -1266,7 +1253,7 @@ const Config = {
|
|
| 1266 |
|
| 1267 |
if (!root || !exp) return;
|
| 1268 |
|
| 1269 |
-
// Fetch ALL instruments
|
| 1270 |
const res = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`);
|
| 1271 |
const data = await res.json();
|
| 1272 |
|
|
|
|
| 28 |
# 2. HELPER FUNCTIONS
|
| 29 |
# ==========================================
|
| 30 |
def sb_get(endpoint: str, params: dict = None):
|
|
|
|
| 31 |
try:
|
| 32 |
url = f"{SB_URL}/rest/v1{endpoint}"
|
| 33 |
r = requests.get(url, headers=HEADERS, params=params)
|
| 34 |
r.raise_for_status()
|
| 35 |
return r.json()
|
| 36 |
except Exception as e:
|
| 37 |
+
print(f"DB Error GET ({endpoint}): {e}")
|
| 38 |
+
return[]
|
| 39 |
+
|
| 40 |
+
def sb_rpc(func_name: str, params: dict = {}):
|
| 41 |
+
"""Executes a Supabase Postgres Function (RPC) for Zero Egress"""
|
| 42 |
+
try:
|
| 43 |
+
url = f"{SB_URL}/rest/v1/rpc/{func_name}"
|
| 44 |
+
r = requests.post(url, headers=HEADERS, json=params)
|
| 45 |
+
r.raise_for_status()
|
| 46 |
+
return r.json()
|
| 47 |
+
except Exception as e:
|
| 48 |
+
print(f"DB Error RPC ({func_name}): {e}")
|
| 49 |
return[]
|
| 50 |
|
| 51 |
# ==========================================
|
| 52 |
+
# 3. API ROUTES - METADATA (NOW USING RPC!)
|
| 53 |
# ==========================================
|
| 54 |
|
| 55 |
@app.get("/")
|
|
|
|
| 58 |
|
| 59 |
@app.get("/api/dates")
|
| 60 |
def get_dates():
|
| 61 |
+
"""Fetch last 30 distinct dates via RPC."""
|
| 62 |
+
data = sb_rpc("get_available_dates", {"p_limit": 30})
|
| 63 |
+
dates = [row['available_date'] for row in data]
|
| 64 |
+
return JSONResponse(dates)
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
@app.get("/api/roots")
|
| 67 |
def get_roots(date: str, q: Optional[str] = None):
|
| 68 |
+
"""Fetch roots via RPC. Zero egress!"""
|
| 69 |
+
params = {"p_date": date}
|
|
|
|
|
|
|
| 70 |
if q:
|
| 71 |
+
params["p_q"] = q.upper()
|
| 72 |
|
| 73 |
+
data = sb_rpc("get_roots_for_date", params)
|
| 74 |
+
roots = [row['root_name'] for row in data]
|
| 75 |
+
return JSONResponse(roots)
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
@app.get("/api/auto_config")
|
| 78 |
def get_auto_config(date: str, root: str):
|
| 79 |
+
"""Smart helper for defaults using RPCs."""
|
| 80 |
root = root.upper()
|
| 81 |
+
exps_data = sb_rpc("get_expiries_for_root", {"p_date": date, "p_root": root})
|
| 82 |
+
all_expiries = [row['expiry_date'] for row in exps_data if row['expiry_date'] != 'MARKET']
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
valid_expiries =[e for e in all_expiries if e >= date]
|
| 85 |
if not valid_expiries:
|
| 86 |
valid_expiries = all_expiries
|
| 87 |
|
| 88 |
response = {"expiries": all_expiries, "current": None, "next": None}
|
| 89 |
|
| 90 |
+
def find_fut_rpc(exp):
|
| 91 |
+
inst_data = sb_rpc("get_instruments_for_expiry", {"p_date": date, "p_root": root, "p_expiry": exp})
|
| 92 |
+
instruments = [r['instrument_name'] for r in inst_data]
|
| 93 |
for i in instruments:
|
| 94 |
if 'FUT' in i: return i
|
| 95 |
return instruments[0] if instruments else None
|
| 96 |
|
| 97 |
if len(valid_expiries) > 0:
|
| 98 |
exp_c = valid_expiries[0]
|
| 99 |
+
response["current"] = {"expiry": exp_c, "instrument": find_fut_rpc(exp_c)}
|
|
|
|
| 100 |
|
| 101 |
if len(valid_expiries) > 1:
|
| 102 |
exp_n = valid_expiries[1]
|
| 103 |
+
response["next"] = {"expiry": exp_n, "instrument": find_fut_rpc(exp_n)}
|
|
|
|
| 104 |
|
| 105 |
return JSONResponse(response)
|
| 106 |
|
| 107 |
@app.get("/api/expiries")
|
| 108 |
def get_expiries(date: str, root: str):
|
| 109 |
+
"""Fetch distinct expiries via RPC."""
|
| 110 |
root = root.upper()
|
| 111 |
+
data = sb_rpc("get_expiries_for_root", {"p_date": date, "p_root": root})
|
| 112 |
+
exps = [row['expiry_date'] for row in data]
|
| 113 |
+
return JSONResponse(exps)
|
|
|
|
|
|
|
| 114 |
|
| 115 |
@app.get("/api/instruments")
|
| 116 |
def get_instruments(date: str, root: str, expiry: str):
|
| 117 |
+
"""Fetch instruments via RPC."""
|
| 118 |
root = root.upper()
|
| 119 |
+
data = sb_rpc("get_instruments_for_expiry", {"p_date": date, "p_root": root, "p_expiry": expiry})
|
| 120 |
+
instruments = [row['instrument_name'] for row in data]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
spot_fut = []
|
| 123 |
options =[]
|
|
|
|
| 184 |
for i in range(6):
|
| 185 |
p_base = None
|
| 186 |
if base_key and base_key in data_dict:
|
| 187 |
+
p_arr = data_dict[base_key].get('p',[])
|
| 188 |
if i < len(p_arr) and p_arr[i] is not None:
|
| 189 |
p_base = float(p_arr[i])
|
| 190 |
|
|
|
|
| 238 |
else:
|
| 239 |
inst_data = data_dict.get(instrument)
|
| 240 |
if not inst_data: continue
|
| 241 |
+
p_arr = inst_data.get('p', []) or []
|
| 242 |
b_arr = inst_data.get('b', []) or[]
|
| 243 |
+
s_arr = inst_data.get('s', []) or[]
|
| 244 |
+
v_arr = inst_data.get('v', []) or []
|
| 245 |
o_arr = inst_data.get('o', []) or[]
|
| 246 |
|
| 247 |
for i in range(6):
|
|
|
|
| 627 |
});
|
| 628 |
|
| 629 |
const CustomDD = {
|
| 630 |
+
data: { cfgRoot: [], cfgExp:[], cfgInst:[] },
|
| 631 |
recent: { cfgRoot:[] }, // Persistent memory for symbols
|
| 632 |
|
| 633 |
init() {
|
|
|
|
| 651 |
async open(id) {
|
| 652 |
document.querySelectorAll('.custom-dd').forEach(el => el.style.display = 'none');
|
| 653 |
const inp = document.getElementById(id);
|
| 654 |
+
inp.select();
|
| 655 |
|
| 656 |
+
// Always show ALL items when opening
|
|
|
|
| 657 |
this.render(id, '');
|
| 658 |
|
| 659 |
document.getElementById(`dd-${id}`).style.display = 'block';
|
|
|
|
| 662 |
async filter(id) {
|
| 663 |
const query = document.getElementById(id).value.toUpperCase();
|
| 664 |
|
| 665 |
+
// Fetch from DB only when typing roots to save egress
|
| 666 |
+
if(id === 'cfgRoot' && query.length >= 2) {
|
| 667 |
+
const res = await fetch(`/api/roots?date=${App.state.date}&q=${query}`);
|
| 668 |
+
const results = await res.json();
|
| 669 |
+
this.set(id, results);
|
| 670 |
+
}
|
| 671 |
|
| 672 |
this.render(id, query);
|
| 673 |
document.getElementById(`dd-${id}`).style.display = 'block';
|
|
|
|
| 679 |
let html = '';
|
| 680 |
let currentGroup = null;
|
| 681 |
|
|
|
|
| 682 |
let pool = [...this.data[id]];
|
| 683 |
+
|
| 684 |
if(id === 'cfgRoot' && q === '') {
|
| 685 |
const recentItems = this.recent.cfgRoot.map(r => ({val: r, group: 'Recent Search'}));
|
| 686 |
pool =[...recentItems, ...pool];
|
| 687 |
}
|
| 688 |
|
| 689 |
+
const filtered = pool.filter((v, i, a) => a.findIndex(t => t.val === v.val) === i)
|
| 690 |
.filter(item => item.val.toLowerCase().includes(q));
|
| 691 |
|
| 692 |
if (filtered.length === 0) {
|
|
|
|
| 709 |
document.getElementById(`dd-${id}`).style.display = 'none';
|
| 710 |
|
| 711 |
if(id === 'cfgRoot') {
|
|
|
|
| 712 |
if(!this.recent.cfgRoot.includes(val)) {
|
| 713 |
this.recent.cfgRoot.unshift(val);
|
| 714 |
if(this.recent.cfgRoot.length > 10) this.recent.cfgRoot.pop();
|
|
|
|
| 1066 |
let c = s.color1;
|
| 1067 |
if(s.cond && s.cond !== 'none') {
|
| 1068 |
let thresh = parseFloat(s.thresh) || 0;
|
| 1069 |
+
let cTrue = s.color1;
|
| 1070 |
+
let cFalse = s.color2 || s.color1;
|
| 1071 |
+
if(s.cond === '>') c = v > thresh ? cTrue : cFalse;
|
| 1072 |
+
if(s.cond === '<') c = v < thresh ? cTrue : cFalse;
|
| 1073 |
}
|
| 1074 |
return { borderColor: c, backgroundColor: c };
|
| 1075 |
}
|
|
|
|
| 1101 |
|
| 1102 |
this.updateInfoTooltip(mode);
|
| 1103 |
this.updatePresetsDropdown();
|
| 1104 |
+
this.loadPreset("Model 1"); // Default to Model 1 always when switching modes
|
| 1105 |
},
|
| 1106 |
|
| 1107 |
updateInfoTooltip(mode) {
|
|
|
|
| 1164 |
|
| 1165 |
this.updatePresetsDropdown();
|
| 1166 |
|
| 1167 |
+
// Load roots logic (RPC)
|
| 1168 |
CustomDD.clear('cfgRoot');
|
| 1169 |
const res = await fetch(`/api/roots?date=${App.state.date}`);
|
| 1170 |
const roots = await res.json();
|
|
|
|
| 1228 |
|
| 1229 |
if (!root) return;
|
| 1230 |
|
| 1231 |
+
// Fetches ALL available expiries for the root via RPC
|
| 1232 |
const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`);
|
| 1233 |
const exps = await res.json();
|
| 1234 |
CustomDD.set('cfgExp', exps);
|
|
|
|
| 1253 |
|
| 1254 |
if (!root || !exp) return;
|
| 1255 |
|
| 1256 |
+
// Fetch ALL instruments via RPC
|
| 1257 |
const res = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`);
|
| 1258 |
const data = await res.json();
|
| 1259 |
|