Subham9126 commited on
Commit
54ada51
Β·
verified Β·
1 Parent(s): 93d6a6b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +954 -274
app.py CHANGED
@@ -1,290 +1,970 @@
1
- # @title Function To Get PreOpenDetails
2
- # Install required packages if not installed
3
- # !pip install aiohttp nest-asyncio pandas gradio pytz requests
4
-
5
  import asyncio
6
- import aiohttp
7
  import json
8
- from typing import List, Dict, Optional, Union
9
- import gradio as gr
10
- import pandas as pd
11
  import time
12
- import threading
13
- import requests
 
14
  import pytz
15
- from datetime import datetime
16
- from requests.adapters import HTTPAdapter
17
- from urllib3.util.retry import Retry
18
-
19
- import nest_asyncio
20
- nest_asyncio.apply()
21
-
22
- # ==============================================================================
23
- # 1. TELEGRAM CONFIGURATION
24
- # ==============================================================================
25
- BOT_TOKEN = "8220537137:AAGtz1bBsHzMhbxtzMWNLmFbGPHIErcys9o"
26
- CHAT_ID = "1342204098"
27
-
28
- # ==============================================================================
29
- # 2. YOUR PROVIDED FUNCTIONS (UNCHANGED)
30
- # ==============================================================================
31
-
32
- class NSEAsyncAPI:
33
- # ... (Your NSEAsyncAPI class code remains exactly the same) ...
34
- def __init__(self, debug: bool = False):
35
- self.debug = debug
36
- self.base_url = "https://www.nseindia.com"
37
- self.headers = {
38
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
39
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
40
- 'Accept-Language': 'en-US,en;q=0.9',
41
- 'Accept-Encoding': 'gzip, deflate, br',
42
- }
43
 
44
- async def _initialize_session(self, session: aiohttp.ClientSession, symbol: str) -> bool:
45
- """Initialize session by visiting the quote page to get cookies"""
46
- try:
47
- init_url = f"{self.base_url}/get-quotes/equity?symbol={symbol}&series=EQ"
48
- if self.debug: print(f"Initializing session for {symbol}: {init_url}")
49
- async with session.get(init_url, headers=self.headers, timeout=15) as response:
50
- if response.status == 200:
51
- if self.debug: print(f"βœ… Session initialized for {symbol}.")
52
- return True
53
- else:
54
- if self.debug: print(f"❌ Failed to initialize session for {symbol}: {response.status}")
55
- return False
56
- except Exception as e:
57
- if self.debug: print(f"❌ Session initialization error for {symbol}: {e}")
58
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- async def _get_quote_data(self, session: aiohttp.ClientSession, symbol: str) -> Optional[Dict]:
61
- """Get quote data for a single symbol"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  try:
63
- api_url = f"{self.base_url}/api/quote-equity?symbol={symbol}&series=EQ"
64
- api_headers = self.headers.copy()
65
- api_headers['Accept'] = 'application/json'
66
- if self.debug: print(f"Fetching data for {symbol}: {api_url}")
67
- async with session.get(api_url, headers=api_headers, timeout=15) as response:
68
- if response.status == 200:
69
- data = await response.json()
70
- if self.debug: print(f"βœ… Data received for {symbol}")
71
- return data
72
- else:
73
- if self.debug: print(f"❌ API request failed for {symbol}: {response.status}")
74
- return None
75
  except Exception as e:
76
- if self.debug: print(f"❌ API request error for {symbol}: {e}")
77
- return None
78
-
79
- async def _fetch_single_symbol(self, symbol: str) -> Dict:
80
- """Fetch data for a single symbol with session initialization"""
81
- timeout = aiohttp.ClientTimeout(total=30)
82
- async with aiohttp.ClientSession(timeout=timeout, cookie_jar=aiohttp.CookieJar()) as session:
83
- init_success = await self._initialize_session(session, symbol)
84
- if not init_success:
85
- return {"symbol": symbol, "data": None, "error": "Failed to initialize session"}
86
- data = await self._get_quote_data(session, symbol)
87
- if data:
88
- return {"symbol": symbol, "data": data, "error": None}
89
- else:
90
- return {"symbol": symbol, "data": None, "error": "Failed to fetch data"}
91
-
92
- async def get_quotes_async(self, symbols: List[str]) -> List[Dict]:
93
- """Asynchronously fetch quotes for multiple symbols"""
94
- if self.debug: print(f"Starting async fetch for {len(symbols)} symbols: {symbols}")
95
- tasks = [self._fetch_single_symbol(symbol) for symbol in symbols]
96
- results = await asyncio.gather(*tasks, return_exceptions=True)
97
- processed_results = []
98
- for i, result in enumerate(results):
99
  if isinstance(result, Exception):
100
- processed_results.append({"symbol": symbols[i], "data": None, "error": str(result)})
 
 
 
 
 
 
 
 
 
101
  else:
102
- processed_results.append(result)
103
- if self.debug:
104
- success_count = sum(1 for r in processed_results if r["data"] is not None)
105
- print(f"Completed: {success_count}/{len(symbols)} successful")
106
- return processed_results
107
-
108
- def get_quote(self, symbol: str) -> Optional[Dict]:
109
- """Get quote for a single symbol (Jupyter-friendly)"""
110
- async def _fetch():
111
- result = await self._fetch_single_symbol(symbol)
112
- return result["data"]
113
- return asyncio.run(_fetch())
114
-
115
- def get_quotes(self, symbols: Union[str, List[str]]) -> Union[Dict, List[Dict]]:
116
- """Get quotes for single symbol or multiple symbols (Jupyter-friendly)"""
117
- if isinstance(symbols, str): return self.get_quote(symbols)
118
- results = asyncio.run(self.get_quotes_async(symbols))
119
- return [{"symbol": r["symbol"], "data": r["data"], "error": r["error"]} for r in results]
120
-
121
- def get_nse_quotes(symbols: List[str], debug: bool = False) -> List[Dict]:
122
- api = NSEAsyncAPI(debug=debug)
123
- return api.get_quotes(symbols)
124
-
125
- def predict_preopen_sentiment(obj):
126
- def _extract_data_and_symbol(item):
127
- if isinstance(item, dict) and "data" in item:
128
- symbol = item.get("symbol") or item.get("data", {}).get("metadata", {}).get("symbol") or "UNKNOWN"
129
- data = item["data"]
130
- return data, symbol
131
- if isinstance(item, dict):
132
- symbol = item.get("metadata", {}).get("symbol", "UNKNOWN")
133
- return item, symbol
134
- return {}, "UNKNOWN"
135
-
136
- def analyze_single(item):
137
- data, symbol = _extract_data_and_symbol(item)
138
- pre = data.get("preOpenMarket", {}) if data else {}
139
- result = { "symbol": symbol, "dominant": "Unknown", "TotalBuyOrder": 0, "TotalSellOrder": 0, "Multiplier": 0.0, "PrevClose": 0.0, "IEP": 0.0 }
140
- if not pre: return result
141
- buy_qty, sell_qty = pre.get("totalBuyQuantity", 0), pre.get("totalSellQuantity", 0)
142
- result.update({"TotalBuyOrder": buy_qty, "TotalSellOrder": sell_qty, "PrevClose": pre.get("prevClose", 0.0), "IEP": pre.get("IEP", 0.0)})
143
- dominant, multiplier = "Balanced", 0.0
144
- if buy_qty > sell_qty:
145
- dominant = "Demand"
146
- if sell_qty > 0: multiplier = (buy_qty / sell_qty)
147
- elif sell_qty > buy_qty:
148
- dominant = "Supply"
149
- if buy_qty > 0: multiplier = (-sell_qty / buy_qty)
150
- result.update({"dominant": dominant, "Multiplier": round(multiplier, 2)})
151
- return result
152
-
153
- return [analyze_single(item) for item in obj] if isinstance(obj, list) else analyze_single(obj)
154
-
155
- # ==============================================================================
156
- # 3. TELEGRAM SENDER FUNCTION
157
- # ==============================================================================
158
- def send_telegram_message(text: str):
159
- """Send a text message to the configured Telegram chat with retry logic."""
160
- session = requests.Session()
161
- retry_strategy = Retry(
162
- total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504]
163
  )
164
- adapter = HTTPAdapter(max_retries=retry_strategy)
165
- session.mount("https://", adapter)
166
- api_url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
167
- payload = {"chat_id": CHAT_ID, "text": text, "parse_mode": "Markdown"}
 
 
 
 
 
 
 
 
 
 
 
168
  try:
169
- response = session.post(api_url, data=payload, timeout=10)
170
- response.raise_for_status()
171
- print("Telegram message sent successfully.")
172
- except requests.exceptions.RequestException as e:
173
- print(f"Failed to send Telegram message after several retries: {e}")
174
-
175
- # ==============================================================================
176
- # 4. APPLICATION LOGIC
177
- # ==============================================================================
178
- ALL_SYMBOLS = ["360ONE", "3MINDIA", "ABB", "ACC", "AIAENG", "APLAPOLLO", "AUBANK", "AARTIIND", "AAVAS", "ABBOTTINDIA", "ADANIENT", "ADANIGREEN", "ADANIPORTS", "ADANIPOWER", "ADANITOTAL", "AWL", "ABCAPITAL", "ABFRL", "AEGISCHEM", "AETHER", "AFFLE", "AJANTPHARM", "APLLTD", "ALKEM", "ALKYLAMINE", "ALLCARGO", "ALOKINDS", "AMARAJABAT", "AMBER", "AMBUJACEM", "ANANDRATHI", "ANGELONE", "ANURAS", "APARINDS", "APOLLOHOSP", "APOLLOTYRE", "APTUS", "ARCHEAN", "ASAHIINDIA", "ASHOKLEY", "ASIANPAINT", "ASTERDM", "ASTRAZEN", "ASTRAL", "ATUL", "AUROPHARMA", "AVANTIFEED", "DMART", "AXISBANK", "BEML", "BLS", "BSE", "BAJAJ-AUTO", "BAJFINANCE", "BAJAJFINSV", "BAJAJHLDNG", "BALAMINES", "BALKRISIND", "BALRAMCHIN", "BANDHANBNK", "BATAINDIA", "BAYERCROP", "BERGEPAINT", "BDL", "BEL", "BHARATFORG", "BHEL", "BPCL", "BHARTIARTL", "BIKAJI", "BIOCON", "BIRLACORPN", "BSOFT", "BLUEDART", "BLUESTARCO", "BBTC", "BORORENEW", "BOSCHLTD", "BRIGADE", "BRITANNIA", "MAPMYINDIA", "CCL", "CESC", "CGPOWER", "CIEINDIA", "CRISIL", "CSBBANK", "CAMPUS", "CANFINHOME", "CAPLIPOINT", "CGCL", "CARBORUNIV", "CASTROLIND", "CEATLTD", "CDSL", "CENTRALBK", "CENTURYPLY", "CERA", "CHALET", "CHAMBLFERT", "CHEMPLASTS", "CHOLAFIN", "CHOLAHLDNG", "CIPLA", "CLEAN", "COALINDIA", "COCHINSHIP", "COFORGE", "COLPAL", "CONCOR", "COROMANDEL", "CREDITACC", "CROMPTON", "CUB", "CUMMINSIND", "CYIENT", "DCMSHRIRAM", "DABUR", "DALBHARAT", "DEEPAKNTR", "DELHIVERY", "DEVYANI", "DHANI", "DBL", "DIVISLAB", "DIXON", "LALPATHLAB", "DRREDDY", "ECLERX", "EIDPARRY", "EIHOTEL", "EQUITASBNK", "ERIS", "ESCORTS", "EXIDEIND", "FSL", "FACT", "FDC", "FEDERALBNK", "FINEORG", "FINCABLES", "FINPIPE", "FLUOROCHEM", "FORTIS", "GAEL", "GAIL", "GALAXYSURF", "GARFIBRES", "GESHIP", "GICRE", "GLAND", "GLAXO", "GLENMARK", "GMDCLTD", "GNFC", "GODFRYPHLP", "GODREJCP", "GODREJIND", "GODREJPROP", "GRANULES", "GRAPHITE", "GRASIM", "GESHIP", "GRINDWELL", "GUJALKALI", "GUJGASLTD", "GSFC", "GSPL", "HAVELLS", "HCLTECH", "HDFCAMC", "HDFCBANK", "HDFCLIFE", "HFCL", "HLEGLAS", "HAPPSTMNDS", "HAL", "HEMIPROP", "HEROMOTOCO", "HINDALCO", "HINDCOPPER", "HINDPETRO", "HINDUNILVR", "HINDZINC", "POWERINDIA", "HOMEFIRST", "HONAUT", "HUDCO", "IBULHSGFIN", "ICICIBANK", "ICICIGI", "ICICIPRULI", "IDBI", "IDFCFIRSTB", "IDFC", "IFBIND", "INDIACEM", "IBREALEST", "INDIAMART", "INDIANB", "IEX", "INDHOTEL", "INDIGO", "IOB", "IOC", "IRCTC", "IRB", "IRCON", "IREDA", "ITC", "ITDC", "ITI", "INDUSINDBK", "NAUKRI", "INFY", "INGERRAND", "INTELLECT", "IOB", "IPCALAB", "JBCHEPHARM", "JKCEMENT", "JKLAKSHMI", "JKPAPER", "JMFINANCIL", "JSWENERGY", "JSWSTEEL", "JAGRAN", "JAICORPLTD", "JSL", "JINDALSTEL", "JIOFIN", "JUBLFOOD", "JUBLINGT", "JUBLPHARMA", "JUSTDIAL", "JYOTHYLAB", "KPRMILL", "KEI", "KNRCON", "KPITTECH", "KRBL", "KAJARIACER", "KALPATPOWR", "KALYANKJIL", "KANSAINER", "KARURVYSYA", "KEC", "KIMS", "KOTAKBANK", "L&TFH", "LTTS", "LICHSGFIN", "LAURUSLABS", "LAXMIMACH", "LTIM", "LT", "LICI", "LINDEINDIA", "LUPIN", "LUXIND", "LXCHEM", "MMTC", "MOIL", "MRF", "MTARTECH", "MGL", "MAHABANK", "M&M", "MAHINDCIE", "M&MFIN", "MAHLIFE", "MANAPPURAM", "MRPL", "MARICO", "MARUTI", "MASTEK", "MAXHEALTH", "MAZFAB", "MEDANTA", "METROBRAND", "METROPOLIS", "MFSL", "MOTILALOFS", "MPHASIS", "MUMBAIAIRPORT", "MUTHOOTFIN", "NATCOPHARM", "NBCC", "NCC", "NHPC", "NLCINDIA", "NMDC", "NTPC", "NATIONALUM", "NAVINFLUOR", "NAZARA", "NESTLEIND", "NETWORK18", "NEWGEN", "NH", "NIACL", "NILKAMAL", "NOCIL", "NUVAMA", "OBEROIRLTY", "ONGC", "OIL", "PAYTM", "OFSS", "ORIENTELEC", "PCBL", "PIIND", "PNB", "PNBHOUSING", "PNCINFRA", "PFC", "PAGEIND", "PATANJALI", "PERSISTENT", "PETRONET", "PFIZER", "PHOENIXLTD", "PIDILITIND", "PEL", "POLYMED", "POLYCAB", "POONAWALLA", "POWERGRID", "PRAJIND", "PRESTIGE", "PRINCEPIPE", "PRSMJOHNSN", "PRUDENT", "PSYCHE", "PUJAPNP", "QUESS", "RBLBANK", "RECLTD", "RVNL", "RAILTEL", "RITES", "RADICO", "RAIN", "RAJESHEXPO", "RALLIS", "RCF", "RATNAMANI", "RAYMOND", "REDINGTON", "RELAXO", "RELIANCE", "RINFRA", "RENUKA", "RHIM", "RKFORG", "ROYALCHID", "SAIL", "SBIN", "SJVN", "SKFINDIA", "SRF", "SANOFI", "SAPPHIRE", "SAREGAMA", "SCHAEFFLER", "SENATE", "SEQUENT", "SFL", "SHOPERSTOP", "SHREECEM", "SHRIRAMFIN", "SIEMENS", "SIGNATURE", "SILV", "SOBHA", "SOLARINDS", "SONACOMS", "SONATSOFTW", "SPARC", "SPICEJET", "STARHEALTH", "SBILIFE", "SWSOLAR", "STEELXIND", "STERTOOLS", "STLTECH", "SUDARSCHEM", "SUMICHEM", "SUMIT", "SUNDARMFIN", "SUNDRMFAST", "SUNPHARMA", "SUNTV", "SUPRAJIT", "SUPREMEIND", "SUVENPHAR", "SYMPHONY", "SYNGENE", "TCIEXP", "TCNSBRANDS", "TTKPRESTIG", "TV18BRDCST", "TVSMOTOR", "TANLA", "TATACHEM", "TATACOMM", "TATACONSUM", "TATAELXSI", "TATAINVEST", "TATAMTRDVR", "TATAMOTORS", "TATAPOWER", "TATASTEEL", "TCS", "TECHM", "TEJASNET", "THERMAX", "TIMKEN", "TITAN", "TORNTPHARM", "TORNTPOWER", "TRENT", "TRIDENT", "TRIVENI", "TRU", "UBL", "UCOBANK", "ULTRACEMCO", "UNIONBANK", "UPL", "UTIAMC", "VAIBHAVGBL", "VGUARD", "VARROC", "VBL", "VEDL", "VENKEYS", "VIJAYA", "VOLTAS", "WELCORP", "WELSPUNIND", "WESTLIFE", "WHIRLPOOL", "WIPRO", "WOCKPHARMA", "YESBANK", "ZEEL", "ZENSARTECH", "ZFCVINDIA", "ZOMATO", "ZYDUSLIFE"]
179
-
180
- def create_batches(data: list, batch_size: int):
181
- for i in range(0, len(data), batch_size): yield data[i:i + batch_size]
182
-
183
- def analyze_symbols(symbols: List[str]):
184
- if not symbols: return pd.DataFrame()
185
- all_quotes = []
186
- symbol_batches = list(create_batches(symbols, 75))
187
- print(f"Processing {len(symbols)} symbols in {len(symbol_batches)} batches.")
188
- for i, batch in enumerate(symbol_batches):
189
- print(f"Fetching batch {i+1}/{len(symbol_batches)}...")
190
- quotes = get_nse_quotes(batch, debug=False)
191
- all_quotes.extend(quotes)
192
- print("All data fetched. Analyzing sentiment...")
193
- analysis_results = predict_preopen_sentiment(all_quotes)
194
- return pd.DataFrame(analysis_results)
195
-
196
- def manual_pull_handler(selected_symbols: List[str], select_all: bool):
197
- symbols_to_process = ALL_SYMBOLS if select_all else selected_symbols
198
- if not symbols_to_process:
199
- return pd.DataFrame(), "Please select symbols or check 'Select All'."
200
- df = analyze_symbols(symbols_to_process)
201
- return df, f"Successfully analyzed {len(df)} symbols."
202
-
203
- # ==============================================================================
204
- # 5. SCHEDULING LOGIC
205
- # ==============================================================================
206
- def scheduled_job():
207
- print("\n" + "="*50)
208
- print("RUNNING SCHEDULED JOB at 09:10 AM IST...")
209
- df = analyze_symbols(ALL_SYMBOLS)
210
- print("SCHEDULED JOB FINISHED. Data:")
211
- print(df)
212
-
213
- if df is not None and not df.empty:
214
- significant_movers = df[df['Multiplier'].abs() >= 15].copy()
215
- significant_movers.sort_values(by='Multiplier', ascending=False, inplace=True)
216
- if not significant_movers.empty:
217
- print(f"Found {len(significant_movers)} significant symbols. Formatting for Telegram.")
218
- message_header = "πŸš€ *Daily Pre-Open Movers (|Multiplier| >= 15)*\n"
219
- all_cards = []
220
- for i, row in enumerate(significant_movers.itertuples()):
221
- dom_text = { "Demand": "🟒 **Dominant: Demand**", "Supply": "πŸ”΄ **Dominant: Supply**" }.get(row.dominant, "βšͺ️ **Dominant: Balanced**")
222
- mult_emoji = "πŸ“ˆ" if row.Multiplier > 0 else "πŸ“‰"
223
- card = f"""*-----------------------------------*
224
- {i+1}️⃣ ***{row.symbol}***
225
- {dom_text}
226
- πŸ’° Prev Close: `{row.PrevClose:.2f}`
227
- βš–οΈ Open Price (IEP): `{row.IEP:.2f}`
228
- πŸ›’ Buy Qty: `{row.TotalBuyOrder:,}`
229
- πŸ›‘ Sell Qty: `{row.TotalSellOrder:,}`
230
- {mult_emoji} Multiplier: `{row.Multiplier:.2f}`"""
231
- all_cards.append(card)
232
- message = message_header + "\n".join(all_cards)
233
- send_telegram_message(message)
234
  else:
235
- print("No symbols found with |Multiplier| >= 15 today.")
236
- else:
237
- print("Analysis returned no data. Skipping Telegram notification.")
238
- print("="*50 + "\n")
239
-
240
- def run_scheduler():
241
- """Checks the time in IST every minute and runs the job at 09:10."""
242
- target_tz = pytz.timezone("Asia/Kolkata")
243
- target_hour, target_minute = 22, 29
244
- job_has_run_today = False
245
- print(f"Scheduler started. Will run job daily at {target_hour:02d}:{target_minute:02d} IST.")
246
-
247
- while True:
248
- now_ist = datetime.now(target_tz)
249
- if now_ist.hour == target_hour and now_ist.minute == target_minute and not job_has_run_today:
250
- scheduled_job()
251
- job_has_run_today = True
252
- if now_ist.hour == 0 and now_ist.minute == 0:
253
- job_has_run_today = False
254
- time.sleep(60)
255
-
256
- # ==============================================================================
257
- # 6. GRADIO USER INTERFACE (WITH BUTTON MOVED TO TOP)
258
- # ==============================================================================
259
- with gr.Blocks(title="NSE Pre-Open Market Analyzer") as demo:
260
- gr.Markdown("# NSE Pre-Open Market Sentiment Analyzer")
261
- gr.Markdown("Select symbols manually or choose 'Select All' to analyze pre-open market data. The data is also fetched automatically every day at 09:10 AM IST.")
262
- with gr.Row():
263
- with gr.Column(scale=1):
264
-
265
- # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! #
266
- # BUTTON IS NOW THE FIRST ELEMENT IN THIS COLUMN #
267
- # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! #
268
- manual_pull_button = gr.Button("Manual Pull Data", variant="primary")
269
-
270
- select_all_checkbox = gr.Checkbox(label="Select All Symbols", value=False)
271
- symbol_selector = gr.CheckboxGroup(choices=ALL_SYMBOLS, label="Select Symbols", interactive=True)
272
-
273
- def update_symbol_selector(select_all):
274
- return gr.CheckboxGroup(value=ALL_SYMBOLS, interactive=False) if select_all else gr.CheckboxGroup(value=[], interactive=True)
275
-
276
- select_all_checkbox.change(fn=update_symbol_selector, inputs=select_all_checkbox, outputs=symbol_selector)
277
-
278
- status_textbox = gr.Textbox(label="Status", interactive=False)
279
 
280
- with gr.Column(scale=3):
281
- output_dataframe = gr.DataFrame(label="Analysis Results")
282
-
283
- manual_pull_button.click(fn=manual_pull_handler, inputs=[symbol_selector, select_all_checkbox], outputs=[output_dataframe, status_textbox])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
 
285
  if __name__ == "__main__":
286
- scheduler_thread = threading.Thread(target=run_scheduler)
287
- scheduler_thread.daemon = True
288
- scheduler_thread.start()
289
- print("Starting Gradio application...")
290
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
 
 
 
2
  import asyncio
 
3
  import json
4
+ import re
5
+ import logging
 
6
  import time
7
+ import atexit
8
+ from typing import List, Dict, Optional, Union
9
+ from datetime import datetime, time as dt_time
10
  import pytz
11
+ import tzlocal
12
+ import aiohttp
13
+ from fastapi import FastAPI, HTTPException
14
+ from pydantic import BaseModel, validator
15
+ import uvicorn
16
+ import threading
17
+ import weakref
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
+ # Configure enhanced logging with timestamps
20
+ logging.basicConfig(
21
+ level=logging.INFO,
22
+ format='%(asctime)s.%(msecs)03d - %(name)s - %(levelname)s - %(message)s',
23
+ datefmt='%Y-%m-%d %H:%M:%S'
24
+ )
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # --- Enhanced Session Management ---
28
+ class SessionManager:
29
+ """
30
+ Thread-safe session manager with graceful rotation to prevent race conditions.
31
+ """
32
+
33
+ def __init__(self):
34
+ self._session: aiohttp.ClientSession = None
35
+ self._session_lock = asyncio.Lock() # Lock is created once and tied to the event loop.
36
+ self._creation_time = None
37
+ self._request_count = 0
38
+ self.max_session_age = 300 # 5 minutes
39
+ self.max_requests_per_session = 1000
40
+
41
+ async def _graceful_close(self, session_to_close: aiohttp.ClientSession, delay: int = 5):
42
+ """Waits for a delay before closing a stale session to allow in-flight requests to complete."""
43
+ if session_to_close and not session_to_close.closed:
44
+ logger.info(f"⏳ Waiting {delay}s before closing stale session (ID: {id(session_to_close)})...")
45
+ await asyncio.sleep(delay)
46
+ # **SYNTAX ERROR FIX**: Added the required colon ':' after 'try'
47
+ try:
48
+ logger.info(f"🧹 Gracefully closing stale session (ID: {id(session_to_close)}).")
49
+ await session_to_close.close()
50
+ except Exception as e:
51
+ logger.warning(f"⚠️ Error during graceful close of stale session: {e}")
52
 
53
+ async def get_session(self) -> aiohttp.ClientSession:
54
+ """
55
+ Get or create a session with graceful rotation to prevent race conditions.
56
+ """
57
+ async with self._session_lock:
58
+ now = time.time()
59
+
60
+ # Use >= for precision on max requests.
61
+ needs_renewal = (
62
+ self._session is None or
63
+ self._session.closed or
64
+ (self._creation_time and now - self._creation_time > self.max_session_age) or
65
+ self._request_count >= self.max_requests_per_session
66
+ )
67
+
68
+ if needs_renewal:
69
+ old_session = self._session
70
+
71
+ # *** RACE CONDITION FIX: Schedule the old session's closure instead of awaiting it. ***
72
+ if old_session and not old_session.closed:
73
+ logger.info(f"πŸ”„ Scheduling closure of old session after {self._request_count} requests.")
74
+ loop = asyncio.get_running_loop()
75
+ loop.create_task(self._graceful_close(old_session))
76
+
77
+ # --- Create the new session immediately ---
78
+ try:
79
+ connector = aiohttp.TCPConnector(
80
+ limit=100, limit_per_host=50, ttl_dns_cache=300,
81
+ use_dns_cache=True, keepalive_timeout=30,
82
+ enable_cleanup_closed=True
83
+ )
84
+ timeout = aiohttp.ClientTimeout(total=60, connect=10, sock_read=30)
85
+ headers = {
86
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
87
+ 'Accept': 'application/json', 'Accept-Encoding': 'gzip, deflate',
88
+ 'Connection': 'keep-alive', 'Cache-Control': 'no-cache'
89
+ }
90
+
91
+ self._session = aiohttp.ClientSession(
92
+ connector=connector, timeout=timeout, headers=headers
93
+ )
94
+
95
+ self._creation_time = now
96
+ self._request_count = 0
97
+ logger.info(f"πŸš€ Created new session at {datetime.fromtimestamp(now)}")
98
+
99
+ except Exception as e:
100
+ logger.error(f"Failed to create new session: {e}")
101
+ raise
102
+
103
+ return self._session
104
+
105
+ def increment_request_count(self):
106
+ """Increment the request count. Called after a session is successfully retrieved."""
107
+ self._request_count += 1
108
+
109
+ async def close(self):
110
+ """Clean shutdown of the current active session."""
111
+ if self._session and not self._session.closed:
112
+ try:
113
+ logger.info(f"πŸ”’ Closing active session after {self._request_count} requests.")
114
+ await self._session.close()
115
+ await asyncio.sleep(0.1) # Short delay for cleanup
116
+ except Exception as e:
117
+ logger.warning(f"Error during final session cleanup: {e}")
118
+ self._session = None
119
+
120
+ # Global session manager per event loop to handle multiple threads/loops
121
+ _session_managers = weakref.WeakKeyDictionary()
122
+
123
+ def get_session_manager():
124
+ """Get or create session manager for current event loop."""
125
+ try:
126
+ loop = asyncio.get_running_loop()
127
+ if loop not in _session_managers:
128
+ _session_managers[loop] = SessionManager()
129
+ logger.debug(f"πŸ“ Created new SessionManager for event loop {id(loop)}")
130
+ return _session_managers[loop]
131
+ except RuntimeError:
132
+ # No event loop running
133
+ logger.warning("⚠️ No event loop running, cannot get session manager.")
134
+ return None
135
+ # --- Enhanced Data Models ---
136
+
137
+ class StockDataRequest(BaseModel):
138
+ tickers: List[str]
139
+ start_date: str
140
+ end_date: str
141
+ interval: int = 15
142
+ timezone: str = "Asia/Kolkata"
143
+ batch_size: int = 50
144
+ batch_delay: float = 0.5
145
+ max_concurrent: int = 50
146
+
147
+ @validator('tickers')
148
+ def validate_tickers(cls, v):
149
+ if not v:
150
+ raise ValueError("Tickers list cannot be empty")
151
+ return [ticker.strip().upper() for ticker in v]
152
+
153
+ @validator('interval')
154
+ def validate_interval(cls, v):
155
+ if v <= 0:
156
+ raise ValueError("Interval must be positive")
157
+ return v
158
+
159
+ @validator('batch_size')
160
+ def validate_batch_size(cls, v):
161
+ if v <= 0 or v > 100:
162
+ raise ValueError("Batch size must be between 1 and 100")
163
+ return v
164
+
165
+ @validator('max_concurrent')
166
+ def validate_max_concurrent(cls, v):
167
+ if v <= 0 or v > 100:
168
+ raise ValueError("Max concurrent must be between 1 and 100")
169
+ return v
170
+
171
+ # --- Utility Functions (same as before) ---
172
+
173
+ class DateTimeValidationError(Exception):
174
+ pass
175
+
176
+ def validate_datetime_format(dt_str: str) -> datetime:
177
+ """Validate date in strict 'YYYY-MM-DD' format."""
178
+ date_pattern = re.compile(r'^\d{4}-\d{2}-\d{2}$')
179
+ if not date_pattern.match(dt_str):
180
+ raise DateTimeValidationError(
181
+ f"Invalid date format: '{dt_str}'. Expected 'YYYY-MM-DD'"
182
+ )
183
+ try:
184
+ parsed_date = datetime.strptime(dt_str, '%Y-%m-%d')
185
+ today = datetime.now().date()
186
+ if parsed_date.date() > today:
187
+ raise DateTimeValidationError(
188
+ f"Future date provided: '{dt_str}'. Please provide a past or current date."
189
+ )
190
+ return parsed_date
191
+ except ValueError as e:
192
+ raise DateTimeValidationError(
193
+ f"Invalid date value: '{dt_str}'. Please provide a valid calendar date."
194
+ ) from e
195
+
196
+ def _resolve_timezone(timezone: Optional[str]) -> pytz.BaseTzInfo:
197
+ """Resolve timezone string to pytz timezone object."""
198
+ try:
199
+ if timezone:
200
+ return pytz.timezone(timezone)
201
+ else:
202
+ return tzlocal.get_localzone()
203
+ except pytz.exceptions.UnknownTimeZoneError:
204
+ logger.warning(f"Unknown timezone '{timezone}', falling back to Asia/Kolkata")
205
+ return pytz.timezone('Asia/Kolkata')
206
+
207
+ def convert_to_unixtimestamp(date_time_str: str, timezone: Optional[str] = None) -> int:
208
+ """Convert 'YYYY-MM-DD HH:MM' string to Unix ms timestamp."""
209
+ dt = datetime.strptime(date_time_str, '%Y-%m-%d %H:%M')
210
+ target_tz = _resolve_timezone(timezone)
211
+ try:
212
+ if dt.tzinfo is None:
213
+ localized_dt = target_tz.localize(dt)
214
+ else:
215
+ localized_dt = dt.astimezone(target_tz)
216
+ return int(localized_dt.timestamp() * 1000)
217
+ except Exception as e:
218
+ logger.error(f"Error converting datetime to timestamp: {e}")
219
+ raise
220
+
221
+ def get_time_range_in_unix_ms(start_date_str: str, end_date_str: str, timezone: str = 'Asia/Kolkata') -> Dict[str, int]:
222
+ """Convert start/end date into full-day unix ms timestamps."""
223
+ start_date = validate_datetime_format(start_date_str)
224
+ end_date = validate_datetime_format(end_date_str)
225
+
226
+ if start_date > end_date:
227
+ raise DateTimeValidationError(
228
+ f"Start date '{start_date_str}' cannot be after end date '{end_date_str}'"
229
+ )
230
+
231
+ start_datetime = datetime.combine(start_date, dt_time.min)
232
+ end_datetime = datetime.combine(end_date, dt_time(23, 59))
233
+
234
+ start_ts = convert_to_unixtimestamp(start_datetime.strftime('%Y-%m-%d %H:%M'), timezone)
235
+ end_ts = convert_to_unixtimestamp(end_datetime.strftime('%Y-%m-%d %H:%M'), timezone)
236
+
237
+ return {"start_timestamp_ms": start_ts, "end_timestamp_ms": end_ts}
238
+
239
+
240
+
241
+ # --- Optimized API Functions ---
242
+
243
+ HIST_URL = "https://groww.in/v1/api/charting_service/v2/chart/exchange/NSE/segment/CASH"
244
+
245
+ async def call_price_api_optimized(
246
+ ticker: str,
247
+ start: int,
248
+ end: int,
249
+ interval: int,
250
+ timeout: int = 30,
251
+ request_id: str = "unknown"
252
+ ) -> Dict:
253
+ """Optimized API call with proper session handling and error recovery."""
254
+ start_time = time.time()
255
+ url = f"{HIST_URL}/{ticker}"
256
+ params = {
257
+ "startTimeInMillis": start,
258
+ "endTimeInMillis": end,
259
+ "intervalInMinutes": interval
260
+ }
261
+
262
+ try:
263
+ # Get session manager for current event loop
264
+ session_mgr = get_session_manager()
265
+ if session_mgr is None:
266
+ raise RuntimeError("No event loop available")
267
+
268
+ session = await session_mgr.get_session()
269
+ session_mgr.increment_request_count()
270
+
271
+ # Log request start
272
+ logger.debug(f"πŸš€ [{request_id}] Starting request for {ticker}")
273
+
274
+ async with session.get(url, params=params) as response:
275
+ response_time = time.time() - start_time
276
+
277
+ if response.status == 200:
278
+ json_data = await response.json()
279
+ logger.debug(f"βœ… [{request_id}] {ticker}: {response_time*1000:.1f}ms - SUCCESS")
280
+ return {
281
+ "ticker": ticker,
282
+ "data": json_data,
283
+ "error": None,
284
+ "status": "success",
285
+ "response_time_ms": round(response_time * 1000, 1),
286
+ "request_id": request_id
287
+ }
288
+ else:
289
+ logger.warning(f"❌ [{request_id}] {ticker}: {response_time*1000:.1f}ms - HTTP {response.status}")
290
+ return {
291
+ "ticker": ticker,
292
+ "data": None,
293
+ "error": f"HTTP {response.status}: {response.reason}",
294
+ "status": "failed",
295
+ "response_time_ms": round(response_time * 1000, 1),
296
+ "request_id": request_id
297
+ }
298
+
299
+ except asyncio.CancelledError:
300
+ response_time = time.time() - start_time
301
+ logger.error(f"🚫 [{request_id}] {ticker}: {response_time*1000:.1f}ms - CANCELLED")
302
+ raise # Re-raise cancellation
303
+
304
+ except asyncio.TimeoutError:
305
+ response_time = time.time() - start_time
306
+ logger.error(f"⏰ [{request_id}] {ticker}: {response_time*1000:.1f}ms - TIMEOUT")
307
+ return {
308
+ "ticker": ticker,
309
+ "data": None,
310
+ "error": "Request timeout",
311
+ "status": "timeout",
312
+ "response_time_ms": round(response_time * 1000, 1),
313
+ "request_id": request_id
314
+ }
315
+
316
+ except Exception as e:
317
+ response_time = time.time() - start_time
318
+ error_msg = str(e)
319
+
320
+ # Handle specific error types
321
+ if "Event loop is closed" in error_msg:
322
+ logger.error(f"πŸ’₯ [{request_id}] {ticker}: EVENT LOOP CLOSED - attempting recovery")
323
+ # Try to create a new session manager
324
+ try:
325
+ session_mgr = SessionManager() # Create fresh instance
326
+ session = await session_mgr.get_session()
327
+ # Retry the request once
328
+ async with session.get(url, params=params) as response:
329
+ if response.status == 200:
330
+ json_data = await response.json()
331
+ response_time = time.time() - start_time
332
+ logger.info(f"πŸ”„ [{request_id}] {ticker}: {response_time*1000:.1f}ms - RECOVERED")
333
+ return {
334
+ "ticker": ticker,
335
+ "data": json_data,
336
+ "error": None,
337
+ "status": "success",
338
+ "response_time_ms": round(response_time * 1000, 1),
339
+ "request_id": request_id
340
+ }
341
+ except Exception as retry_e:
342
+ logger.error(f"πŸ’₯ [{request_id}] {ticker}: Recovery failed: {retry_e}")
343
+
344
+ logger.error(f"πŸ’₯ [{request_id}] {ticker}: {response_time*1000:.1f}ms - ERROR: {error_msg}")
345
+ return {
346
+ "ticker": ticker,
347
+ "data": None,
348
+ "error": error_msg,
349
+ "status": "error",
350
+ "response_time_ms": round(response_time * 1000, 1),
351
+ "request_id": request_id
352
+ }
353
+
354
+ async def fetch_stock_data_batch_optimized(
355
+ tickers: List[str],
356
+ start_time: int,
357
+ end_time: int,
358
+ interval: int,
359
+ batch_size: int = 50,
360
+ batch_delay: float = 0.5,
361
+ max_concurrent: int = 50
362
+ ) -> List[Dict]:
363
+ """Highly optimized batch processing with proper event loop and session handling."""
364
+
365
+ overall_start = time.time()
366
+ request_id = f"batch_{int(time.time())}"
367
+
368
+ # Split tickers into batches
369
+ ticker_batches = [tickers[i:i + batch_size] for i in range(0, len(tickers), batch_size)]
370
+ all_results = []
371
+
372
+ logger.info(f"🎯 [{request_id}] Starting batch processing: {len(tickers)} tickers in {len(ticker_batches)} batches of {batch_size}")
373
+ logger.info(f"πŸ“Š [{request_id}] Config: max_concurrent={max_concurrent}, batch_delay={batch_delay}s")
374
+
375
+ # Get or create session manager for this event loop
376
+ session_mgr = get_session_manager()
377
+ if session_mgr is None:
378
+ raise RuntimeError("No event loop available for batch processing")
379
+
380
+ # Pre-warm session
381
+ logger.info(f"πŸ”₯ [{request_id}] Pre-warming session...")
382
+ try:
383
+ await session_mgr.get_session()
384
+ logger.info(f"βœ… [{request_id}] Session pre-warmed successfully")
385
+ except Exception as e:
386
+ logger.error(f"❌ [{request_id}] Session pre-warming failed: {e}")
387
+ raise
388
+
389
+ for batch_idx, ticker_batch in enumerate(ticker_batches):
390
+ batch_start = time.time()
391
+ batch_request_id = f"{request_id}_b{batch_idx+1}"
392
+
393
+ logger.info(f"πŸš€ [{batch_request_id}] Processing batch {batch_idx + 1}/{len(ticker_batches)} with {len(ticker_batch)} tickers")
394
+
395
+ # Create semaphore to limit concurrent requests within batch
396
+ semaphore = asyncio.Semaphore(max_concurrent)
397
+
398
+ async def bounded_fetch(ticker, idx):
399
+ async with semaphore:
400
+ tick_request_id = f"{batch_request_id}_t{idx+1}"
401
+ try:
402
+ return await call_price_api_optimized(
403
+ ticker, start_time, end_time, interval, 30, tick_request_id
404
+ )
405
+ except Exception as e:
406
+ logger.error(f"πŸ’₯ [{tick_request_id}] Bounded fetch error for {ticker}: {e}")
407
+ return {
408
+ "ticker": ticker,
409
+ "data": None,
410
+ "error": f"Bounded fetch error: {str(e)}",
411
+ "status": "error",
412
+ "response_time_ms": 0,
413
+ "request_id": tick_request_id
414
+ }
415
+
416
+ # Process current batch with error handling
417
  try:
418
+ tasks = [bounded_fetch(ticker, idx) for idx, ticker in enumerate(ticker_batch)]
419
+ batch_results = await asyncio.gather(*tasks, return_exceptions=True)
 
 
 
 
 
 
 
 
 
 
420
  except Exception as e:
421
+ logger.error(f"πŸ’₯ [{batch_request_id}] Batch gather failed: {e}")
422
+ # Create error results for entire batch
423
+ batch_results = [Exception(f"Batch gather failed: {e}") for _ in ticker_batch]
424
+
425
+ # Process results
426
+ processed_batch_results = []
427
+ successful_in_batch = 0
428
+
429
+ for i, result in enumerate(batch_results):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  if isinstance(result, Exception):
431
+ logger.error(f"πŸ’₯ [{batch_request_id}] Exception for {ticker_batch[i]}: {str(result)}")
432
+ processed_batch_results.append({
433
+ "ticker": ticker_batch[i],
434
+ "data": None,
435
+ "error": str(result),
436
+ "status": "exception",
437
+ "batch": batch_idx + 1,
438
+ "response_time_ms": 0,
439
+ "request_id": f"{batch_request_id}_t{i+1}"
440
+ })
441
  else:
442
+ result["batch"] = batch_idx + 1
443
+ processed_batch_results.append(result)
444
+ if result.get("status") == "success":
445
+ successful_in_batch += 1
446
+
447
+ all_results.extend(processed_batch_results)
448
+
449
+ batch_duration = time.time() - batch_start
450
+ avg_response_time = sum(r.get("response_time_ms", 0) for r in processed_batch_results) / len(processed_batch_results)
451
+
452
+ logger.info(f"βœ… [{batch_request_id}] Completed in {batch_duration:.2f}s | Success: {successful_in_batch}/{len(ticker_batch)} | Avg: {avg_response_time:.1f}ms")
453
+
454
+ # Add delay between batches (except for the last batch)
455
+ if batch_idx < len(ticker_batches) - 1 and batch_delay > 0:
456
+ logger.info(f"⏳ [{batch_request_id}] Waiting {batch_delay}s before next batch...")
457
+ await asyncio.sleep(batch_delay)
458
+
459
+ overall_duration = time.time() - overall_start
460
+ total_successful = len([r for r in all_results if r.get('status') == 'success'])
461
+ success_rate = (total_successful / len(tickers)) * 100 if len(tickers) > 0 else 0
462
+
463
+ logger.info(f"🏁 [{request_id}] COMPLETED: {overall_duration:.2f}s total | {total_successful}/{len(tickers)} successful ({success_rate:.1f}%)")
464
+ logger.info(f"πŸ“ˆ [{request_id}] Performance: {len(tickers)/overall_duration:.1f} tickers/sec")
465
+
466
+ return all_results
467
+
468
+ # --- Core Processing Function ---
469
+
470
+ def process_stock_request(
471
+ tickers: Union[str, List[str]],
472
+ start_date: str,
473
+ end_date: str,
474
+ interval: int = 15,
475
+ timezone: str = "Asia/Kolkata",
476
+ batch_size: int = 50,
477
+ batch_delay: float = 0.5,
478
+ max_concurrent: int = 50
479
+ ) -> Dict:
480
+ """Legacy function - now delegates to the enhanced loop-safe version."""
481
+ logger.info("πŸ”„ Using legacy process_stock_request, delegating to enhanced version")
482
+ return process_stock_request_with_new_loop(
483
+ tickers, start_date, end_date, interval, timezone,
484
+ batch_size, batch_delay, max_concurrent
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  )
486
+
487
+ # --- FastAPI Application ---
488
+
489
+ def safe_json_serialize(obj):
490
+ """Safely serialize any object to JSON string."""
491
+ def default_serializer(o):
492
+ if isinstance(o, (datetime, dt_time)):
493
+ return o.isoformat()
494
+ elif hasattr(o, '__dict__'):
495
+ return o.__dict__
496
+ elif hasattr(o, 'to_dict'):
497
+ return o.to_dict()
498
+ else:
499
+ return str(o)
500
+
501
  try:
502
+ return json.dumps(obj, indent=2, default=default_serializer, ensure_ascii=False)
503
+ except Exception:
504
+ return json.dumps(str(obj), indent=2)
505
+
506
+ api_app = FastAPI(title="Optimized Groww Stock Data API", version="2.0.0")
507
+
508
+ @api_app.post("/fetch-stock-data")
509
+ async def fetch_stock_data_endpoint(request: StockDataRequest):
510
+ """Optimized API endpoint with enhanced performance monitoring."""
511
+ try:
512
+ # Use the new loop-safe version for API calls
513
+ result = process_stock_request_with_new_loop(
514
+ request.tickers,
515
+ request.start_date,
516
+ request.end_date,
517
+ request.interval,
518
+ request.timezone,
519
+ request.batch_size,
520
+ request.batch_delay,
521
+ request.max_concurrent
522
+ )
523
+
524
+ result["timestamp"] = datetime.now().isoformat()
525
+ serializable_result = safe_json_serialize(result)
526
+ return json.loads(serializable_result)
527
+
528
+ except Exception as e:
529
+ logger.error(f"API endpoint error: {e}")
530
+ return {
531
+ "success": False,
532
+ "data": None,
533
+ "error": str(e),
534
+ "timestamp": datetime.now().isoformat(),
535
+ "processing_summary": {
536
+ "total_tickers": 0,
537
+ "successful": 0,
538
+ "failed": 0,
539
+ "success_rate": "0%",
540
+ "total_duration_seconds": 0,
541
+ "batch_processing_used": True
542
+ },
543
+ "request_info": {}
544
+ }
545
+
546
+ @api_app.get("/health")
547
+ async def health_check():
548
+ """Health check endpoint."""
549
+ return {"status": "healthy", "timestamp": datetime.now().isoformat()}
550
+
551
+ @api_app.get("/session-stats")
552
+ async def session_stats():
553
+ """Get session statistics for monitoring."""
554
+ try:
555
+ session_mgr = get_session_manager()
556
+ if session_mgr and session_mgr._session and not session_mgr._session.closed:
557
+ return {
558
+ "session_active": True,
559
+ "session_age_seconds": time.time() - session_mgr._creation_time if session_mgr._creation_time else 0,
560
+ "request_count": session_mgr._request_count,
561
+ "loop_id": session_mgr._loop_id,
562
+ "timestamp": datetime.now().isoformat()
563
+ }
 
 
 
564
  else:
565
+ return {
566
+ "session_active": False,
567
+ "session_age_seconds": 0,
568
+ "request_count": 0,
569
+ "loop_id": None,
570
+ "timestamp": datetime.now().isoformat()
571
+ }
572
+ except Exception as e:
573
+ return {
574
+ "error": str(e),
575
+ "session_active": False,
576
+ "timestamp": datetime.now().isoformat()
577
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
578
 
579
+ # --- Gradio Interface ---
580
+
581
+ def execute_stock_request(
582
+ ticker_input: str,
583
+ start_date: str,
584
+ end_date: str,
585
+ interval: int,
586
+ batch_size: int = 50,
587
+ batch_delay: float = 0.5,
588
+ max_concurrent: int = 50
589
+ ) -> str:
590
+ """Enhanced wrapper function for Gradio interface with loop safety."""
591
+ try:
592
+ # Use the new loop-safe version
593
+ result = process_stock_request_with_new_loop(
594
+ ticker_input, start_date, end_date, interval, "Asia/Kolkata",
595
+ batch_size, batch_delay, max_concurrent
596
+ )
597
+ return safe_json_serialize(result)
598
+ except Exception as e:
599
+ logger.error(f"Error in execute_stock_request: {e}")
600
+ error_result = {
601
+ "success": False,
602
+ "error": str(e),
603
+ "timestamp": datetime.now().isoformat(),
604
+ "processing_summary": {
605
+ "total_duration_seconds": 0,
606
+ "throughput_tickers_per_second": 0,
607
+ "error_type": "gradio_wrapper_error"
608
+ }
609
+ }
610
+ return safe_json_serialize(error_result)
611
+
612
+ def create_gradio_interface():
613
+ """Create optimized Gradio interface with performance controls."""
614
+ with gr.Blocks(title="Optimized Groww Stock Data Fetcher") as demo:
615
+ gr.Markdown("""
616
+ # ⚑ Optimized Groww Stock Data Fetcher v2.0
617
+
618
+ **High-Performance Features:**
619
+ - πŸš€ **Persistent HTTP Sessions**: Reuses connections for 5min/1000 requests
620
+ - πŸ“Š **Detailed Performance Monitoring**: Response times, throughput metrics
621
+ - 🎯 **Optimized Batch Processing**: Smart batching with timing controls
622
+ - ⚑ **Enhanced Async Processing**: Up to 50 concurrent requests per batch
623
+ - πŸ“ˆ **Real-time Statistics**: Success rates, timing analysis
624
+
625
+ **Expected Performance:** ~187 tickers/second (750 tickers in ~4 seconds)
626
+ """)
627
+
628
+ with gr.Row():
629
+ with gr.Column():
630
+ ticker_box = gr.Textbox(
631
+ label="Stock Tickers",
632
+ placeholder='["RELIANCE","TCS","INFY"] or RELIANCE,TCS,INFY',
633
+ value='["RELIANCE","TCS","INFY"]',
634
+ lines=3
635
+ )
636
+
637
+ with gr.Row():
638
+ start_box = gr.Textbox(
639
+ label="Start Date (YYYY-MM-DD)",
640
+ placeholder="2025-08-01",
641
+ value="2025-08-01"
642
+ )
643
+ end_box = gr.Textbox(
644
+ label="End Date (YYYY-MM-DD)",
645
+ placeholder="2025-08-10",
646
+ value="2025-08-10"
647
+ )
648
+
649
+ with gr.Row():
650
+ interval_box = gr.Number(
651
+ label="Interval (minutes)",
652
+ value=15,
653
+ minimum=1,
654
+ maximum=1440
655
+ )
656
+ batch_size_box = gr.Number(
657
+ label="Batch Size",
658
+ value=50,
659
+ minimum=1,
660
+ maximum=100,
661
+ info="Tickers per batch"
662
+ )
663
+
664
+ with gr.Row():
665
+ batch_delay_box = gr.Number(
666
+ label="Batch Delay (seconds)",
667
+ value=0.5,
668
+ minimum=0,
669
+ maximum=10,
670
+ step=0.1,
671
+ info="Delay between batches"
672
+ )
673
+ max_concurrent_box = gr.Number(
674
+ label="Max Concurrent",
675
+ value=50,
676
+ minimum=1,
677
+ maximum=100,
678
+ info="Concurrent requests per batch"
679
+ )
680
+
681
+ fetch_button = gr.Button("πŸš€ Fetch Data (Optimized)", variant="primary", size="lg")
682
+
683
+ gr.Markdown("""
684
+ **Performance Tuning:**
685
+ - **Batch Size**: 50 (optimal for API rate limits)
686
+ - **Batch Delay**: 0.5s (prevents rate limiting)
687
+ - **Max Concurrent**: 50 (parallel requests per batch)
688
+ - **Session Reuse**: Connections kept alive for 5 minutes
689
+ """)
690
+
691
+ with gr.Column():
692
+ output_box = gr.Textbox(
693
+ label="API Response with Performance Metrics",
694
+ lines=25,
695
+ max_lines=40,
696
+ show_copy_button=True,
697
+ container=True
698
+ )
699
+
700
+ gr.Markdown("""
701
+ ### Performance Monitoring
702
+
703
+ The response now includes detailed timing metrics:
704
+ ```json
705
+ {
706
+ "processing_summary": {
707
+ "total_duration_seconds": 4.23,
708
+ "throughput_tickers_per_second": 177.3,
709
+ "avg_response_time_ms": 95.4,
710
+ "max_response_time_ms": 234.1,
711
+ "min_response_time_ms": 67.8,
712
+ "success_rate": "98.75%"
713
+ }
714
+ }
715
+ ```
716
+
717
+ ### API Usage
718
+ ```bash
719
+ curl -X POST "http://localhost:8000/fetch-stock-data" \\
720
+ -H "Content-Type: application/json" \\
721
+ -d '{
722
+ "tickers": ["RELIANCE", "TCS", ...],
723
+ "start_date": "2025-08-01",
724
+ "end_date": "2025-08-10",
725
+ "batch_size": 50,
726
+ "batch_delay": 0.5,
727
+ "max_concurrent": 50
728
+ }'
729
+ ```
730
+ """)
731
+
732
+ fetch_button.click(
733
+ fn=execute_stock_request,
734
+ inputs=[ticker_box, start_box, end_box, interval_box, batch_size_box, batch_delay_box, max_concurrent_box],
735
+ outputs=output_box
736
+ )
737
+
738
+ return demo
739
+
740
+ # --- Main Execution ---
741
+
742
+ def run_api_server(host="0.0.0.0", port=8000):
743
+ """Run the optimized FastAPI server."""
744
+ uvicorn.run(api_app, host=host, port=port, log_level="info")
745
+
746
+ def run_gradio_interface(share=False):
747
+ """Run the optimized Gradio interface."""
748
+ demo = create_gradio_interface()
749
+ demo.launch(share=share, server_name="0.0.0.0")
750
+
751
+ # --- Enhanced Process Management ---
752
+
753
+ def process_stock_request_with_new_loop(
754
+ tickers: Union[str, List[str]],
755
+ start_date: str,
756
+ end_date: str,
757
+ interval: int = 15,
758
+ timezone: str = "Asia/Kolkata",
759
+ batch_size: int = 50,
760
+ batch_delay: float = 0.5,
761
+ max_concurrent: int = 50
762
+ ) -> Dict:
763
+ """Process stock request with a fresh event loop to avoid loop closure issues."""
764
+ request_start = time.time()
765
+ request_id = f"req_{int(request_start)}"
766
+
767
+ logger.info(f"πŸš€ [{request_id}] Starting stock request with fresh event loop")
768
+
769
+ try:
770
+ # Handle tickers input
771
+ if isinstance(tickers, str):
772
+ try:
773
+ tickers_list = json.loads(tickers)
774
+ except json.JSONDecodeError:
775
+ tickers_list = [t.strip().upper() for t in tickers.split(',')]
776
+ else:
777
+ tickers_list = [t.strip().upper() for t in tickers]
778
+
779
+ if not tickers_list:
780
+ raise ValueError("No tickers provided")
781
+
782
+ logger.info(f"πŸ“ [{request_id}] Processing {len(tickers_list)} tickers")
783
+
784
+ # Convert dates to timestamps
785
+ ts_range = get_time_range_in_unix_ms(start_date, end_date, timezone)
786
+ start_ts, end_ts = ts_range["start_timestamp_ms"], ts_range["end_timestamp_ms"]
787
+
788
+ # Create a new event loop for this request to avoid closure issues
789
+ try:
790
+ # Try to get existing loop first
791
+ loop = asyncio.get_event_loop()
792
+ if loop.is_closed():
793
+ raise RuntimeError("Event loop is closed")
794
+ except RuntimeError:
795
+ # Create new loop if none exists or current is closed
796
+ loop = asyncio.new_event_loop()
797
+ asyncio.set_event_loop(loop)
798
+ logger.info(f"πŸ”„ [{request_id}] Created new event loop")
799
+
800
+ try:
801
+ # Run the batch processing
802
+ logger.info(f"⚑ [{request_id}] Using optimized batch processing")
803
+ results = loop.run_until_complete(fetch_stock_data_batch_optimized(
804
+ tickers_list, start_ts, end_ts, interval, batch_size, batch_delay, max_concurrent
805
+ ))
806
+ finally:
807
+ # Clean up session for this loop if we created it
808
+ if request_id in [f"req_{int(request_start)}"]: # Only clean if we created the loop
809
+ try:
810
+ session_mgr = get_session_manager()
811
+ if session_mgr:
812
+ loop.run_until_complete(session_mgr.close())
813
+ except Exception as e:
814
+ logger.warning(f"Error cleaning up session: {e}")
815
+
816
+ # Generate enhanced statistics
817
+ total_tickers = len(tickers_list)
818
+ successful = len([r for r in results if r.get('status') == 'success'])
819
+ failed = len([r for r in results if r.get('status') in ['failed', 'error', 'timeout', 'exception']])
820
+
821
+ # Calculate timing statistics
822
+ response_times = [r.get('response_time_ms', 0) for r in results if r.get('response_time_ms', 0) > 0]
823
+ avg_response_time = sum(response_times) / len(response_times) if response_times else 0
824
+ max_response_time = max(response_times) if response_times else 0
825
+ min_response_time = min(response_times) if response_times else 0
826
+
827
+ total_duration = time.time() - request_start
828
+ throughput = total_tickers / total_duration if total_duration > 0 else 0
829
+
830
+ processing_summary = {
831
+ "total_tickers": total_tickers,
832
+ "successful": successful,
833
+ "failed": failed,
834
+ "success_rate": f"{(successful/total_tickers*100):.2f}%" if total_tickers > 0 else "0%",
835
+ "total_duration_seconds": round(total_duration, 2),
836
+ "throughput_tickers_per_second": round(throughput, 1),
837
+ "avg_response_time_ms": round(avg_response_time, 1),
838
+ "max_response_time_ms": round(max_response_time, 1),
839
+ "min_response_time_ms": round(min_response_time, 1),
840
+ "batch_processing_used": True,
841
+ "batch_size": batch_size,
842
+ "batch_delay": batch_delay,
843
+ "max_concurrent": max_concurrent,
844
+ "request_id": request_id
845
+ }
846
+
847
+ logger.info(f"βœ… [{request_id}] Request completed successfully in {total_duration:.2f}s")
848
+
849
+ return {
850
+ "success": True,
851
+ "data": results,
852
+ "error": None,
853
+ "timestamp": datetime.now().isoformat(),
854
+ "processing_summary": processing_summary,
855
+ "request_info": {
856
+ "tickers": tickers_list[:10] if len(tickers_list) > 10 else tickers_list,
857
+ "total_tickers": len(tickers_list),
858
+ "start_date": start_date,
859
+ "end_date": end_date,
860
+ "interval": interval,
861
+ "timezone": timezone,
862
+ "start_timestamp_ms": start_ts,
863
+ "end_timestamp_ms": end_ts,
864
+ "request_id": request_id
865
+ }
866
+ }
867
+
868
+ except Exception as e:
869
+ total_duration = time.time() - request_start
870
+ logger.error(f"❌ [{request_id}] Error after {total_duration:.2f}s: {e}")
871
+ return {
872
+ "success": False,
873
+ "data": None,
874
+ "error": str(e),
875
+ "timestamp": datetime.now().isoformat(),
876
+ "processing_summary": {
877
+ "total_tickers": len(tickers_list) if 'tickers_list' in locals() else 0,
878
+ "successful": 0,
879
+ "failed": 0,
880
+ "success_rate": "0%",
881
+ "total_duration_seconds": round(total_duration, 2),
882
+ "throughput_tickers_per_second": 0,
883
+ "batch_processing_used": True,
884
+ "request_id": request_id
885
+ },
886
+ "request_info": {
887
+ "tickers": tickers if isinstance(tickers, list) else [tickers],
888
+ "start_date": start_date,
889
+ "end_date": end_date,
890
+ "interval": interval,
891
+ "timezone": timezone,
892
+ "request_id": request_id
893
+ }
894
+ }
895
+
896
+ # --- Cleanup Handler ---
897
+
898
+ import atexit
899
+
900
+ async def cleanup_all_sessions():
901
+ """Cleanup all sessions across all event loops."""
902
+ logger.info("🧹 Starting session cleanup...")
903
+ try:
904
+ for loop, session_mgr in list(_session_managers.items()):
905
+ if not loop.is_closed():
906
+ try:
907
+ await session_mgr.close()
908
+ logger.info(f"βœ… Cleaned up session for loop {id(loop)}")
909
+ except Exception as e:
910
+ logger.warning(f"⚠️ Error cleaning up session for loop {id(loop)}: {e}")
911
+ except Exception as e:
912
+ logger.warning(f"⚠️ Error during session cleanup: {e}")
913
+
914
+ def cleanup_session():
915
+ """Cleanup session on exit."""
916
+ try:
917
+ # Check if there's a running event loop
918
+ try:
919
+ loop = asyncio.get_running_loop()
920
+ # If we're in a running loop, create a task
921
+ if not loop.is_closed():
922
+ loop.create_task(cleanup_all_sessions())
923
+ except RuntimeError:
924
+ # No running loop, create one for cleanup
925
+ try:
926
+ loop = asyncio.new_event_loop()
927
+ asyncio.set_event_loop(loop)
928
+ loop.run_until_complete(cleanup_all_sessions())
929
+ loop.close()
930
+ except Exception as e:
931
+ logger.warning(f"⚠️ Error during final cleanup: {e}")
932
+ except Exception as e:
933
+ logger.warning(f"⚠️ Error during cleanup: {e}")
934
+
935
+ atexit.register(cleanup_session)
936
+
937
+ def cleanup_session():
938
+ """Cleanup session on exit."""
939
+ if session_manager._session and not session_manager._session.closed:
940
+ loop = asyncio.new_event_loop()
941
+ asyncio.set_event_loop(loop)
942
+ loop.run_until_complete(session_manager.close())
943
+ loop.close()
944
+
945
+ atexit.register(cleanup_session)
946
 
947
  if __name__ == "__main__":
948
+ import sys
949
+
950
+ # Set logging level based on environment
951
+ if "--debug" in sys.argv:
952
+ logging.getLogger().setLevel(logging.DEBUG)
953
+ logger.info("πŸ› Debug logging enabled")
954
+
955
+ if len(sys.argv) > 1 and sys.argv[1] == "api":
956
+ # Run only API server
957
+ print("πŸš€ Starting optimized API server...")
958
+ run_api_server()
959
+ elif len(sys.argv) > 1 and sys.argv[1] == "both":
960
+ # Run both API and Gradio in separate threads
961
+ print("πŸš€ Starting both API server and Gradio interface...")
962
+ api_thread = threading.Thread(target=run_api_server, daemon=True)
963
+ api_thread.start()
964
+ print("⏳ Waiting 2 seconds for API server to start...")
965
+ time.sleep(2)
966
+ run_gradio_interface()
967
+ else:
968
+ # Default: Run only Gradio
969
+ print("πŸš€ Starting optimized Gradio interface...")
970
+ run_gradio_interface()