topsecrettraders commited on
Commit
25ee2a8
·
verified ·
1 Parent(s): 7e82e76

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +83 -50
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 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
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
- t_start = f"{date} {start_time}:00"
143
- t_end = f"{date} {end_time}:59"
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 (Spot > Future > First Available)
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 = [k for k in instruments if k.endswith('-INDEX') or k.endswith('-EQ')]
159
- fut_keys = [k for k in instruments if 'FUT' in k]
160
 
161
  if spot_keys:
162
  ref_instrument = spot_keys[0]
163
  elif fut_keys:
164
  ref_instrument = fut_keys[0]
165
  else:
166
- # Fallback: Find first key that is NOT an option
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
- # 3. Call Postgres (Logic is now 100% inside DB)
171
- # We pass empty dict for p_strike_map as it is handled in SQL now
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 (One specific instrument)
185
  if not instrument:
186
  return JSONResponse({"error": "No Instrument Selected.", "labels": [], "P":[]})
187
 
188
- params = {
 
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
- if not records or (isinstance(records, dict) and records.get("error")):
198
- # Handle case where Time range exists but maybe reference instrument had no ticks
199
- return JSONResponse({"error": "No valid data found in range.", "labels":[], "P":[]})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
- df = pd.DataFrame(records)
202
- if df.empty:
203
- return JSONResponse({"error": "Dataframe Empty", "labels": [], "P":[]})
204
 
205
- # Rename columns to match standard
 
 
 
 
 
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 Logic
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", # Vol should be sum for aggregations usually
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 (VISIBLE IN BOTH MODES NOW) -->
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)