Subham9126 commited on
Commit
93d6a6b
·
verified ·
1 Parent(s): 27d79e6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +274 -975
app.py CHANGED
@@ -1,991 +1,290 @@
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
- import threading
950
- import time
951
-
952
- # Set logging level based on environment
953
- if "--debug" in sys.argv:
954
- logging.getLogger().setLevel(logging.DEBUG)
955
- logger.info("🐛 Debug logging enabled")
956
-
957
- # --- Thread-safe function to run the API server ---
958
- def run_api():
959
- print("🚀 Starting optimized API server on http://0.0.0.0:8000")
960
- run_api_server(host="0.0.0.0", port=8000)
961
-
962
- # --- Main function to start Gradio ---
963
- def run_gradio():
964
- print("🚀 Starting optimized Gradio interface...")
965
- # The demo.launch() will block the main thread
966
- run_gradio_interface()
967
-
968
- # --- Logic to run API, Gradio, or both ---
969
- if len(sys.argv) > 1 and sys.argv[1] == "api":
970
- # Run only the API server
971
- run_api()
972
-
973
- elif len(sys.argv) > 1 and sys.argv[1] == "gradio":
974
- # Run only the Gradio interface
975
- run_gradio()
976
-
977
  else:
978
- # Default behavior: Run both API and Gradio
979
- print("🚀 Starting both API server and Gradio interface...")
980
-
981
- # Run the API server in a daemon thread
982
- # A daemon thread will exit when the main program exits.
983
- api_thread = threading.Thread(target=run_api, daemon=True)
984
- api_thread.start()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
985
 
986
- # Give the API server a moment to initialize
987
- print("⏳ Waiting 2 seconds for API server to start...")
988
- time.sleep(2)
 
989
 
990
- # Run the Gradio interface in the main thread
991
- run_gradio()
 
 
 
 
 
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()