Update app.py
Browse files
app.py
CHANGED
|
@@ -1,14 +1,11 @@
|
|
| 1 |
import pandas as pd
|
| 2 |
import requests
|
| 3 |
import uvicorn
|
| 4 |
-
import json
|
| 5 |
import asyncio
|
| 6 |
-
import
|
| 7 |
-
from
|
| 8 |
-
from fastapi import FastAPI, Body, HTTPException, Query
|
| 9 |
from fastapi.responses import HTMLResponse, JSONResponse
|
| 10 |
-
from
|
| 11 |
-
from typing import Optional, List, Dict, Any
|
| 12 |
|
| 13 |
# ==========================================
|
| 14 |
# 1. CONFIGURATION
|
|
@@ -37,8 +34,10 @@ def sb_get(endpoint: str, params: dict = None):
|
|
| 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)
|
|
@@ -76,7 +75,7 @@ def get_auto_config(date: str, root: str):
|
|
| 76 |
exps_data = sb_rpc("get_expiries_for_root", {"p_date": date, "p_root": root})
|
| 77 |
all_expiries = [row['expiry_date'] for row in exps_data if row['expiry_date'] != 'MARKET']
|
| 78 |
|
| 79 |
-
valid_expiries =[e for e in all_expiries if e >= date]
|
| 80 |
if not valid_expiries:
|
| 81 |
valid_expiries = all_expiries
|
| 82 |
|
|
@@ -123,11 +122,11 @@ def get_instruments(date: str, root: str, expiry: str):
|
|
| 123 |
return JSONResponse({"spot_fut": sorted(spot_fut), "options": sorted(options)})
|
| 124 |
|
| 125 |
# ==========================================
|
| 126 |
-
# 4. API ROUTES - DATA FETCHING (RPC DRIVEN)
|
| 127 |
# ==========================================
|
| 128 |
|
| 129 |
@app.post("/api/fetch_series")
|
| 130 |
-
def fetch_series(
|
| 131 |
date: str = Body(...),
|
| 132 |
root: str = Body(...),
|
| 133 |
expiry: str = Body(...),
|
|
@@ -139,8 +138,8 @@ def fetch_series(
|
|
| 139 |
atm_range: int = Body(5)
|
| 140 |
):
|
| 141 |
root = root.upper()
|
| 142 |
-
|
| 143 |
-
|
| 144 |
|
| 145 |
if mode == "advanced":
|
| 146 |
# 1. Fetch available instruments (Lightweight metadata fetch)
|
|
@@ -150,59 +149,83 @@ def fetch_series(
|
|
| 150 |
if not instruments:
|
| 151 |
return JSONResponse({"error": f"No data found for expiry {expiry}", "labels":[], "P":[]})
|
| 152 |
|
| 153 |
-
# 2. Intelligent Default Selection
|
| 154 |
ref_instrument = instrument
|
| 155 |
-
|
| 156 |
-
# If user left instrument blank, find the best reference (Index or Future)
|
| 157 |
if not ref_instrument:
|
| 158 |
-
spot_keys =
|
| 159 |
-
fut_keys =
|
| 160 |
|
| 161 |
if spot_keys:
|
| 162 |
ref_instrument = spot_keys[0]
|
| 163 |
elif fut_keys:
|
| 164 |
ref_instrument = fut_keys[0]
|
| 165 |
else:
|
| 166 |
-
|
| 167 |
-
others = [k for k in instruments if not k.endswith('CE') and not k.endswith('PE')]
|
| 168 |
ref_instrument = others[0] if others else instruments[0]
|
| 169 |
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
params = {
|
| 173 |
"p_root": root,
|
| 174 |
"p_expiry": expiry,
|
| 175 |
-
"p_start_time": t_start,
|
| 176 |
-
"p_end_time": t_end,
|
| 177 |
"p_ref_instrument": ref_instrument,
|
| 178 |
"p_atm_range": atm_range,
|
| 179 |
-
"p_strike_map": {}
|
| 180 |
}
|
| 181 |
-
records = sb_rpc("get_advanced_chart_data", params)
|
| 182 |
|
| 183 |
else:
|
| 184 |
-
# Normal fetching
|
| 185 |
if not instrument:
|
| 186 |
return JSONResponse({"error": "No Instrument Selected.", "labels": [], "P":[]})
|
| 187 |
|
| 188 |
-
|
|
|
|
| 189 |
"p_root": root,
|
| 190 |
"p_expiry": expiry,
|
| 191 |
-
"p_start_time": t_start,
|
| 192 |
-
"p_end_time": t_end,
|
| 193 |
"p_instrument": instrument
|
| 194 |
}
|
| 195 |
-
records = sb_rpc("get_normal_chart_data", params)
|
| 196 |
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
return JSONResponse({"error": "Dataframe Empty", "labels": [], "P":[]})
|
| 204 |
|
| 205 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
df.rename(columns={
|
| 207 |
"tick_ts": "ts",
|
| 208 |
"b_qty": "buy_qty",
|
|
@@ -211,9 +234,13 @@ def fetch_series(
|
|
| 211 |
}, inplace=True)
|
| 212 |
|
| 213 |
df['ts'] = pd.to_datetime(df['ts'])
|
|
|
|
|
|
|
|
|
|
| 214 |
df.set_index('ts', inplace=True)
|
|
|
|
| 215 |
|
| 216 |
-
# Resample
|
| 217 |
tf_map = {
|
| 218 |
"1min": "1min", "3min": "3min", "5min": "5min",
|
| 219 |
"15min": "15min", "30min": "30min", "1hour": "1h"
|
|
@@ -221,7 +248,7 @@ def fetch_series(
|
|
| 221 |
panda_tf = tf_map.get(timeframe, "1min")
|
| 222 |
|
| 223 |
agg_dict = {
|
| 224 |
-
"price": "last", "volume": "sum", "oi": "last",
|
| 225 |
"buy_qty": "mean", "sell_qty": "mean",
|
| 226 |
"cb": "mean", "cs": "mean", "pb": "mean", "ps": "mean"
|
| 227 |
}
|
|
@@ -234,14 +261,14 @@ def fetch_series(
|
|
| 234 |
response_data = {
|
| 235 |
"labels": resampled.index.strftime('%H:%M').tolist(),
|
| 236 |
"P": resampled['price'].tolist() if 'price' in resampled else [],
|
| 237 |
-
"B": resampled['buy_qty'].tolist() if 'buy_qty' in resampled else
|
| 238 |
-
"S": resampled['sell_qty'].tolist() if 'sell_qty' in resampled else
|
| 239 |
"V": resampled['volume'].tolist() if 'volume' in resampled else [],
|
| 240 |
"OI": resampled['oi'].tolist() if 'oi' in resampled else [],
|
| 241 |
"CB": resampled['cb'].tolist() if 'cb' in resampled else [],
|
| 242 |
"CS": resampled['cs'].tolist() if 'cs' in resampled else [],
|
| 243 |
"PB": resampled['pb'].tolist() if 'pb' in resampled else [],
|
| 244 |
-
"PS": resampled['ps'].tolist() if 'ps' in resampled else
|
| 245 |
}
|
| 246 |
|
| 247 |
return JSONResponse(response_data)
|
|
@@ -499,7 +526,7 @@ HTML_TEMPLATE = """
|
|
| 499 |
</div>
|
| 500 |
</div>
|
| 501 |
|
| 502 |
-
<!-- EXPLICIT INSTRUMENT SELECTION
|
| 503 |
<div class="form-section" id="cfgInstrumentContainer">
|
| 504 |
<label id="lblInst">Instrument</label>
|
| 505 |
<div class="input-wrapper">
|
|
@@ -627,7 +654,7 @@ const CustomDD = {
|
|
| 627 |
let html = '';
|
| 628 |
let currentGroup = null;
|
| 629 |
|
| 630 |
-
let pool =[...this.data[id]];
|
| 631 |
|
| 632 |
if(id === 'cfgRoot' && q === '') {
|
| 633 |
const recentItems = this.recent.cfgRoot.map(r => ({val: r, group: 'Recent Search'}));
|
|
@@ -930,10 +957,10 @@ const ChartLogic = {
|
|
| 930 |
|
| 931 |
// Frontend JS evaluates the line using the variables passed from the backend
|
| 932 |
let f = s.formula
|
| 933 |
-
.replace(/\$CB/g, CB).replace(/\$CS/g, CS)
|
| 934 |
-
.replace(/\$PB/g, PB).replace(/\$PS/g, PS)
|
| 935 |
-
.replace(/\$OI/g, OI).replace(/\$P/g, P)
|
| 936 |
-
.replace(/\$V/g, V).replace(/\$B/g, B).replace(/\$S/g, S);
|
| 937 |
|
| 938 |
try { return new Function('return '+f)(); } catch{ return null; }
|
| 939 |
});
|
|
@@ -1324,10 +1351,16 @@ const Config = {
|
|
| 1324 |
}
|
| 1325 |
};
|
| 1326 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1327 |
window.onload = () => App.init();
|
| 1328 |
</script>
|
| 1329 |
</body>
|
| 1330 |
-
</html>
|
|
|
|
| 1331 |
|
| 1332 |
if __name__ == "__main__":
|
| 1333 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
| 1 |
import pandas as pd
|
| 2 |
import requests
|
| 3 |
import uvicorn
|
|
|
|
| 4 |
import asyncio
|
| 5 |
+
from datetime import timedelta
|
| 6 |
+
from fastapi import FastAPI, Body
|
|
|
|
| 7 |
from fastapi.responses import HTMLResponse, JSONResponse
|
| 8 |
+
from typing import Optional
|
|
|
|
| 9 |
|
| 10 |
# ==========================================
|
| 11 |
# 1. CONFIGURATION
|
|
|
|
| 34 |
print(f"DB Error GET ({endpoint}): {e}")
|
| 35 |
return[]
|
| 36 |
|
| 37 |
+
def sb_rpc(func_name: str, params: dict = None):
|
| 38 |
"""Executes a Supabase Postgres Function (RPC) for Zero Egress"""
|
| 39 |
+
if params is None:
|
| 40 |
+
params = {}
|
| 41 |
try:
|
| 42 |
url = f"{SB_URL}/rest/v1/rpc/{func_name}"
|
| 43 |
r = requests.post(url, headers=HEADERS, json=params)
|
|
|
|
| 75 |
exps_data = sb_rpc("get_expiries_for_root", {"p_date": date, "p_root": root})
|
| 76 |
all_expiries = [row['expiry_date'] for row in exps_data if row['expiry_date'] != 'MARKET']
|
| 77 |
|
| 78 |
+
valid_expiries = [e for e in all_expiries if e >= date]
|
| 79 |
if not valid_expiries:
|
| 80 |
valid_expiries = all_expiries
|
| 81 |
|
|
|
|
| 122 |
return JSONResponse({"spot_fut": sorted(spot_fut), "options": sorted(options)})
|
| 123 |
|
| 124 |
# ==========================================
|
| 125 |
+
# 4. API ROUTES - DATA FETCHING (RPC DRIVEN WITH CHUNKING)
|
| 126 |
# ==========================================
|
| 127 |
|
| 128 |
@app.post("/api/fetch_series")
|
| 129 |
+
async def fetch_series(
|
| 130 |
date: str = Body(...),
|
| 131 |
root: str = Body(...),
|
| 132 |
expiry: str = Body(...),
|
|
|
|
| 138 |
atm_range: int = Body(5)
|
| 139 |
):
|
| 140 |
root = root.upper()
|
| 141 |
+
t_start_dt = pd.to_datetime(f"{date} {start_time}:00")
|
| 142 |
+
t_end_dt = pd.to_datetime(f"{date} {end_time}:59")
|
| 143 |
|
| 144 |
if mode == "advanced":
|
| 145 |
# 1. Fetch available instruments (Lightweight metadata fetch)
|
|
|
|
| 149 |
if not instruments:
|
| 150 |
return JSONResponse({"error": f"No data found for expiry {expiry}", "labels":[], "P":[]})
|
| 151 |
|
| 152 |
+
# 2. Intelligent Default Selection
|
| 153 |
ref_instrument = instrument
|
|
|
|
|
|
|
| 154 |
if not ref_instrument:
|
| 155 |
+
spot_keys =[k for k in instruments if k.endswith('-INDEX') or k.endswith('-EQ')]
|
| 156 |
+
fut_keys =[k for k in instruments if 'FUT' in k]
|
| 157 |
|
| 158 |
if spot_keys:
|
| 159 |
ref_instrument = spot_keys[0]
|
| 160 |
elif fut_keys:
|
| 161 |
ref_instrument = fut_keys[0]
|
| 162 |
else:
|
| 163 |
+
others =[k for k in instruments if not k.endswith('CE') and not k.endswith('PE')]
|
|
|
|
| 164 |
ref_instrument = others[0] if others else instruments[0]
|
| 165 |
|
| 166 |
+
rpc_name = "get_advanced_chart_data"
|
| 167 |
+
base_params = {
|
|
|
|
| 168 |
"p_root": root,
|
| 169 |
"p_expiry": expiry,
|
|
|
|
|
|
|
| 170 |
"p_ref_instrument": ref_instrument,
|
| 171 |
"p_atm_range": atm_range,
|
| 172 |
+
"p_strike_map": {} # Sent but ignored by SQL safely
|
| 173 |
}
|
|
|
|
| 174 |
|
| 175 |
else:
|
| 176 |
+
# Normal fetching
|
| 177 |
if not instrument:
|
| 178 |
return JSONResponse({"error": "No Instrument Selected.", "labels": [], "P":[]})
|
| 179 |
|
| 180 |
+
rpc_name = "get_normal_chart_data"
|
| 181 |
+
base_params = {
|
| 182 |
"p_root": root,
|
| 183 |
"p_expiry": expiry,
|
|
|
|
|
|
|
| 184 |
"p_instrument": instrument
|
| 185 |
}
|
|
|
|
| 186 |
|
| 187 |
+
# =========================================================
|
| 188 |
+
# CHUNKING LOGIC: Bypass Supabase DB/Timeout Limits
|
| 189 |
+
# Fetches 2-hour chunks concurrently!
|
| 190 |
+
# =========================================================
|
| 191 |
+
async def fetch_chunk(start_dt, end_dt):
|
| 192 |
+
params = base_params.copy()
|
| 193 |
+
params["p_start_time"] = start_dt.strftime("%Y-%m-%d %H:%M:%S")
|
| 194 |
+
params["p_end_time"] = end_dt.strftime("%Y-%m-%d %H:%M:%S")
|
| 195 |
+
|
| 196 |
+
loop = asyncio.get_event_loop()
|
| 197 |
+
# Execute synchronous requests.post in a background thread to allow parallel execution
|
| 198 |
+
return await loop.run_in_executor(None, sb_rpc, rpc_name, params)
|
| 199 |
+
|
| 200 |
+
tasks =[]
|
| 201 |
+
curr = t_start_dt
|
| 202 |
+
chunk_hours = 2 # Safe size for Postgres processing limit
|
| 203 |
+
|
| 204 |
+
while curr <= t_end_dt:
|
| 205 |
+
nxt = curr + timedelta(hours=chunk_hours) - timedelta(seconds=1)
|
| 206 |
+
if nxt > t_end_dt:
|
| 207 |
+
nxt = t_end_dt
|
| 208 |
+
|
| 209 |
+
tasks.append(fetch_chunk(curr, nxt))
|
| 210 |
+
curr += timedelta(hours=chunk_hours)
|
| 211 |
+
|
| 212 |
+
# Execute all chunks in parallel
|
| 213 |
+
chunk_results = await asyncio.gather(*tasks)
|
| 214 |
+
|
| 215 |
+
all_records =[]
|
| 216 |
+
for res in chunk_results:
|
| 217 |
+
if isinstance(res, list):
|
| 218 |
+
all_records.extend(res)
|
| 219 |
|
| 220 |
+
if not all_records:
|
| 221 |
+
return JSONResponse({"error": "No valid data found in range.", "labels":[], "P":[]})
|
|
|
|
| 222 |
|
| 223 |
+
# =========================================================
|
| 224 |
+
# DATA PROCESSING & AGGREGATION
|
| 225 |
+
# =========================================================
|
| 226 |
+
df = pd.DataFrame(all_records)
|
| 227 |
+
|
| 228 |
+
# Standardize output
|
| 229 |
df.rename(columns={
|
| 230 |
"tick_ts": "ts",
|
| 231 |
"b_qty": "buy_qty",
|
|
|
|
| 234 |
}, inplace=True)
|
| 235 |
|
| 236 |
df['ts'] = pd.to_datetime(df['ts'])
|
| 237 |
+
|
| 238 |
+
# Deduplicate boundaries just in case overlapping seconds occurred
|
| 239 |
+
df.drop_duplicates(subset=['ts'], inplace=True)
|
| 240 |
df.set_index('ts', inplace=True)
|
| 241 |
+
df.sort_index(inplace=True)
|
| 242 |
|
| 243 |
+
# Resample Timeframes accurately in Pandas
|
| 244 |
tf_map = {
|
| 245 |
"1min": "1min", "3min": "3min", "5min": "5min",
|
| 246 |
"15min": "15min", "30min": "30min", "1hour": "1h"
|
|
|
|
| 248 |
panda_tf = tf_map.get(timeframe, "1min")
|
| 249 |
|
| 250 |
agg_dict = {
|
| 251 |
+
"price": "last", "volume": "sum", "oi": "last",
|
| 252 |
"buy_qty": "mean", "sell_qty": "mean",
|
| 253 |
"cb": "mean", "cs": "mean", "pb": "mean", "ps": "mean"
|
| 254 |
}
|
|
|
|
| 261 |
response_data = {
|
| 262 |
"labels": resampled.index.strftime('%H:%M').tolist(),
|
| 263 |
"P": resampled['price'].tolist() if 'price' in resampled else [],
|
| 264 |
+
"B": resampled['buy_qty'].tolist() if 'buy_qty' in resampled else[],
|
| 265 |
+
"S": resampled['sell_qty'].tolist() if 'sell_qty' in resampled else[],
|
| 266 |
"V": resampled['volume'].tolist() if 'volume' in resampled else [],
|
| 267 |
"OI": resampled['oi'].tolist() if 'oi' in resampled else [],
|
| 268 |
"CB": resampled['cb'].tolist() if 'cb' in resampled else [],
|
| 269 |
"CS": resampled['cs'].tolist() if 'cs' in resampled else [],
|
| 270 |
"PB": resampled['pb'].tolist() if 'pb' in resampled else [],
|
| 271 |
+
"PS": resampled['ps'].tolist() if 'ps' in resampled else[]
|
| 272 |
}
|
| 273 |
|
| 274 |
return JSONResponse(response_data)
|
|
|
|
| 526 |
</div>
|
| 527 |
</div>
|
| 528 |
|
| 529 |
+
<!-- EXPLICIT INSTRUMENT SELECTION -->
|
| 530 |
<div class="form-section" id="cfgInstrumentContainer">
|
| 531 |
<label id="lblInst">Instrument</label>
|
| 532 |
<div class="input-wrapper">
|
|
|
|
| 654 |
let html = '';
|
| 655 |
let currentGroup = null;
|
| 656 |
|
| 657 |
+
let pool = [...this.data[id]];
|
| 658 |
|
| 659 |
if(id === 'cfgRoot' && q === '') {
|
| 660 |
const recentItems = this.recent.cfgRoot.map(r => ({val: r, group: 'Recent Search'}));
|
|
|
|
| 957 |
|
| 958 |
// Frontend JS evaluates the line using the variables passed from the backend
|
| 959 |
let f = s.formula
|
| 960 |
+
.replace(/\$CB/g, str(CB)).replace(/\$CS/g, str(CS))
|
| 961 |
+
.replace(/\$PB/g, str(PB)).replace(/\$PS/g, str(PS))
|
| 962 |
+
.replace(/\$OI/g, str(OI)).replace(/\$P/g, str(P))
|
| 963 |
+
.replace(/\$V/g, str(V)).replace(/\$B/g, str(B)).replace(/\$S/g, str(S));
|
| 964 |
|
| 965 |
try { return new Function('return '+f)(); } catch{ return null; }
|
| 966 |
});
|
|
|
|
| 1351 |
}
|
| 1352 |
};
|
| 1353 |
|
| 1354 |
+
// Helper inside JS for string replacement in formulas
|
| 1355 |
+
function str(val) {
|
| 1356 |
+
return val !== null && val !== undefined ? val.toString() : "0";
|
| 1357 |
+
}
|
| 1358 |
+
|
| 1359 |
window.onload = () => App.init();
|
| 1360 |
</script>
|
| 1361 |
</body>
|
| 1362 |
+
</html>
|
| 1363 |
+
"""
|
| 1364 |
|
| 1365 |
if __name__ == "__main__":
|
| 1366 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|