Subham9126 commited on
Commit
eca7785
·
verified ·
1 Parent(s): 48d1fa3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +113 -288
app.py CHANGED
@@ -1,313 +1,138 @@
1
  import gradio as gr
2
- import re
3
  import asyncio
4
- import aiohttp
5
- from datetime import datetime, time, date
6
- from typing import Dict, Optional, List
7
- import pytz
8
- import tzlocal
9
- import logging
10
  import json
11
- import ast
12
-
13
- # --- Basic Setup ---
14
-
15
- # 1. Logger Configuration
16
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
17
- logger = logging.getLogger(__name__)
18
-
19
- # 2. Custom Exception
20
- class DateTimeValidationError(ValueError):
21
- """Custom exception for datetime validation errors."""
22
- pass
23
-
24
- # 3. API URL
25
- hist_url = "https://groww.in/v1/api/charting_service/v2/chart/exchange/NSE/segment/CASH"
26
-
27
-
28
- # --- Date/Time Functions ---
29
-
30
- def validate_datetime_format(dt_str: str) -> datetime:
31
- """Validate datetime string in strict 'YYYY-MM-DD' format."""
32
- if not isinstance(dt_str, str):
33
- raise TypeError(f"Input must be a string, got {type(dt_str).__name__}")
34
- date_pattern = re.compile(r'^\d{4}-\d{2}-\d{2}$')
35
- if not date_pattern.match(dt_str):
36
- raise DateTimeValidationError(f"Invalid date format: '{dt_str}'. Expected 'YYYY-MM-DD'")
37
- try:
38
- return datetime.strptime(dt_str, '%Y-%m-%d')
39
- except ValueError as e:
40
- raise DateTimeValidationError(f"Invalid date value: '{dt_str}'.") from e
41
-
42
- def _resolve_timezone(timezone: Optional[str]) -> pytz.BaseTzInfo:
43
- """Resolve timezone string to pytz timezone object."""
44
- if timezone:
45
- try:
46
- return pytz.timezone(timezone)
47
- except pytz.UnknownTimeZoneError as e:
48
- raise ValueError(f"Unknown timezone: '{timezone}'") from e
49
- return tzlocal.get_localzone()
50
-
51
- def convert_to_unixtimestamp(date_time_str: str, timezone: Optional[str] = None) -> int:
52
- """Convert 'YYYY-MM-DD HH:MM' string to Unix timestamp in milliseconds."""
53
- if not isinstance(date_time_str, str):
54
- raise TypeError(f"DateTime input must be a string, got {type(date_time_str).__name__}")
55
- try:
56
- dt = datetime.strptime(date_time_str, '%Y-%m-%d %H:%M')
57
- except ValueError as e:
58
- raise DateTimeValidationError(f"Invalid format: '{date_time_str}'. Expected 'YYYY-MM-DD HH:MM'") from e
59
-
60
- target_tz = _resolve_timezone(timezone)
61
- localized_dt = target_tz.localize(dt) if dt.tzinfo is None else dt.astimezone(target_tz)
62
- return int(localized_dt.timestamp() * 1000)
63
-
64
- def get_time_range_in_unix_ms(start_date_str: str, end_date_str: str, timezone: str = 'Asia/Kolkata') -> Dict[str, int]:
65
- """Calculates the start (00:00) and end (23:59) Unix timestamps for a date range."""
66
- start_date = validate_datetime_format(start_date_str)
67
- end_date = validate_datetime_format(end_date_str)
68
- start_datetime = datetime.combine(start_date, time.min)
69
- end_datetime = datetime.combine(end_date, time(23, 59))
70
- start_timestamp = convert_to_unixtimestamp(start_datetime.strftime('%Y-%m-%d %H:%M'), timezone)
71
- end_timestamp = convert_to_unixtimestamp(end_datetime.strftime('%Y-%m-%d %H:%M'), timezone)
72
- return {"start_timestamp_ms": start_timestamp, "end_timestamp_ms": end_timestamp}
73
-
74
 
75
- # --- OPTIMIZED API Functions - Interval-Based Batching ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
- async def fetch_single_ticker_interval(session: aiohttp.ClientSession, ticker: str, start: int, end: int, interval: int, max_retries: int = 2) -> Dict:
78
- """Fetch data for a single ticker-interval combination with retry logic."""
79
  url = f"{hist_url}/{ticker}"
80
- params = {"startTimeInMillis": start, "endTimeInMillis": end, "intervalInMinutes": interval}
81
-
82
- for attempt in range(max_retries + 1):
83
- try:
84
- async with session.get(url, params=params) as response:
85
- response.raise_for_status()
86
- json_data = await response.json()
87
- return {"ticker": ticker, "interval": interval, "data": json_data, "error": None}
88
- except asyncio.TimeoutError:
89
- if attempt < max_retries:
90
- logger.warning(f"Timeout for {ticker} ({interval}m) - Retry {attempt + 1}/{max_retries}")
91
- await asyncio.sleep(0.5 * (attempt + 1)) # Exponential backoff
92
- continue
93
- return {"ticker": ticker, "interval": interval, "data": None, "error": f"Timeout after {max_retries + 1} attempts"}
94
- except Exception as e:
95
- if attempt < max_retries and "timeout" in str(e).lower():
96
- logger.warning(f"Error for {ticker} ({interval}m) - Retry {attempt + 1}/{max_retries}: {e}")
97
- await asyncio.sleep(0.5 * (attempt + 1))
98
- continue
99
- return {"ticker": ticker, "interval": interval, "data": None, "error": str(e)}
100
-
101
- return {"ticker": ticker, "interval": interval, "data": None, "error": "Max retries exceeded"}
102
 
103
- async def fetch_interval_batch(session: aiohttp.ClientSession, tickers: List[str], start_time: int, end_time: int, interval: int, batch_size: int = 30) -> List[Dict]:
104
- """Fetch data for all tickers for a specific interval in batches."""
105
- results = []
106
-
107
- for i in range(0, len(tickers), batch_size):
108
- batch_tickers = tickers[i:i+batch_size]
109
- logger.info(f"Processing {interval}m batch {i//batch_size + 1}: {len(batch_tickers)} tickers")
110
-
111
- batch_start = datetime.now()
112
-
113
- # Create tasks for this batch with retry logic
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  tasks = [
115
- fetch_single_ticker_interval(session, ticker, start_time, end_time, interval, max_retries=2)
116
- for ticker in batch_tickers
117
  ]
118
-
119
- # Execute batch concurrently
120
- batch_results = await asyncio.gather(*tasks, return_exceptions=True)
121
-
122
- # Handle any remaining exceptions
123
- for result in batch_results:
124
- if isinstance(result, Exception):
125
- logger.error(f"Unhandled exception in batch: {result}")
126
- else:
127
- results.append(result)
128
-
129
- batch_duration = (datetime.now() - batch_start).total_seconds()
130
- logger.info(f"Batch {i//batch_size + 1} completed in {batch_duration:.2f}s")
131
-
132
- # Longer delay between batches to reduce API load
133
- if i + batch_size < len(tickers):
134
- await asyncio.sleep(0.2)
135
-
136
- return results
137
 
138
- async def fetch_all_data_by_intervals(tickers: List[str], start_time: int, end_time: int, intervals: List[int]) -> List[Dict]:
139
- """Fetch data by processing each interval separately in batches of 30."""
140
- all_results = []
141
-
142
- # INCREASED timeout limits and better connection management
143
- connector = aiohttp.TCPConnector(
144
- limit=40, # Reduced concurrent connections
145
- limit_per_host=25, # Reduced per-host connections
146
- keepalive_timeout=60,
147
- enable_cleanup_closed=True
148
- )
149
-
150
- # MUCH LONGER timeouts
151
- timeout = aiohttp.ClientTimeout(
152
- total=60, # Increased from 20 to 60 seconds
153
- connect=15, # Increased from 5 to 15 seconds
154
- sock_read=45 # Added socket read timeout
155
- )
156
-
157
- async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
158
-
159
- # Process each interval separately
160
- for interval in intervals:
161
- logger.info(f"Starting {interval}m interval requests for {len(tickers)} tickers...")
162
- interval_start = datetime.now()
163
-
164
- interval_results = await fetch_interval_batch(session, tickers, start_time, end_time, interval, batch_size=25) # Reduced batch size
165
-
166
- interval_duration = (datetime.now() - interval_start).total_seconds()
167
- successful = len([r for r in interval_results if r.get("error") is None])
168
- logger.info(f"Completed {interval}m interval in {interval_duration:.2f}s - {successful}/{len(interval_results)} successful")
169
-
170
- all_results.extend(interval_results)
171
-
172
- # Longer pause between intervals to let API recover
173
- if interval != intervals[-1]: # Not the last interval
174
- logger.info("Pausing 1 second between intervals...")
175
- await asyncio.sleep(1.0)
176
-
177
- return all_results
178
 
179
- def merge_data_fast(results: List[Dict]) -> str:
180
- """Fast data merging without unnecessary overhead."""
181
- merged_data = {}
182
-
183
- for result in results:
184
- ticker = result["ticker"]
185
- interval = result["interval"]
186
- data = result["data"]
187
- error = result["error"]
188
-
189
- # Initialize ticker entry if not exists
190
- if ticker not in merged_data:
191
- merged_data[ticker] = {"symbol": ticker}
192
-
193
- # Handle errors
194
- if error:
195
- merged_data[ticker][f"error_{interval}m"] = error
196
- continue
197
-
198
- # Process successful data
199
- if data and data.get("candles") and len(data["candles"]) > 0:
200
- candle = data["candles"][0]
201
- if len(candle) > 4:
202
- prefix = "day" if interval == 1440 else "start"
203
- merged_data[ticker].update({
204
- f"{prefix} open": candle[1],
205
- f"{prefix} high": candle[2],
206
- f"{prefix} low": candle[3],
207
- f"{prefix} close": candle[4]
208
- })
209
- else:
210
- merged_data[ticker][f"error_{interval}m"] = "No candles data"
211
-
212
- return json.dumps(list(merged_data.values()), indent=2)
213
 
 
 
 
 
 
 
 
214
 
215
- # --- MAIN Gradio Backend Function ---
 
216
 
217
- async def run_backend_processing(tickers_text: str, progress=gr.Progress(track_tqdm=True)):
218
- """Main processing function - optimized for speed."""
219
-
220
- # Input validation
221
- if not tickers_text.strip():
222
- yield "❌ Error: Input is empty", "{}"
223
- return
224
 
225
- try:
226
- parsed_input = ast.literal_eval(tickers_text)
227
- if not isinstance(parsed_input, list):
228
- raise TypeError("Must be a list")
229
- tickers = [str(item).strip().upper() for item in parsed_input if str(item).strip()]
230
- if not tickers:
231
- yield "❌ Error: No valid tickers found", "{}"
232
- return
233
- except:
234
- yield '❌ Invalid format. Use: ["TICKER1", "TICKER2"]', "{}"
235
- return
236
-
237
- # Setup
238
- today_str = date.today().strftime("%Y-%m-%d")
239
- time_range = get_time_range_in_unix_ms(today_str, today_str)
240
- start_time, end_time = time_range["start_timestamp_ms"], time_range["end_timestamp_ms"]
241
- intervals = [15, 1440] # Process 15m first, then 1440m
242
-
243
- total_requests = len(tickers) * len(intervals)
244
-
245
- try:
246
- overall_start = datetime.now()
247
-
248
- # Update progress and show timing
249
- yield f"🚀 Starting: {len(tickers)} tickers × {len(intervals)} intervals = {total_requests} requests", "{}"
250
-
251
- # Fetch all data using interval-based batching with improved timeouts
252
- results = await fetch_all_data_by_intervals(tickers, start_time, end_time, intervals)
253
-
254
- # Quick merge
255
- final_json = merge_data_fast(results)
256
-
257
- total_time = (datetime.now() - overall_start).total_seconds()
258
- successful = len([r for r in results if r.get("error") is None])
259
- failed = total_requests - successful
260
-
261
- status = "✅ Complete!" if failed == 0 else f"⚠️ Complete with {failed} timeouts"
262
- yield f"{status} {successful}/{total_requests} successful in {total_time:.2f}s", final_json
263
-
264
  except Exception as e:
265
- logger.error(f"Processing error: {e}")
266
- yield f"❌ Error: {str(e)}", "{}"
267
 
268
 
269
- # --- Gradio UI ---
 
270
 
271
- with gr.Blocks(theme=gr.themes.Soft(), title="Stock Data Processor") as demo:
272
- gr.Markdown("# ⚡ Stock Data Processor - Interval-Optimized")
273
- gr.Markdown("**Strategy**: Process 15m requests in batches of 30, then 1440m requests in batches of 30, then merge results.")
274
-
275
  with gr.Row():
276
- with gr.Column(scale=1):
277
- tickers_input = gr.Textbox(
278
- lines=4,
279
- label='📈 Stock Tickers (Python List Format)',
280
- value='["RELIANCE", "INFY", "TCS"]',
281
- placeholder='["TICKER1", "TICKER2", "TICKER3", ...]'
282
- )
283
- start_button = gr.Button("🚀 Process Data", variant="primary", size="lg")
284
-
285
- with gr.Accordion("ℹ️ Performance Info", open=False):
286
- gr.Markdown("""
287
- **Optimization Strategy:**
288
- 1. Process all 15m interval requests first (batches of 30)
289
- 2. Process all 1440m interval requests next (batches of 30)
290
- 3. Merge all results into final JSON
291
-
292
- **Expected Speed:** ~1-3 seconds for small ticker lists
293
- """)
294
-
295
- with gr.Column(scale=2):
296
- logs_output = gr.Textbox(label="📊 Processing Logs", lines=8, interactive=False)
297
- json_output = gr.JSON(label="📋 Final Results", show_label=True)
298
 
299
- # Event handlers
300
- start_button.click(
301
- fn=run_backend_processing,
302
- inputs=[tickers_input],
303
- outputs=[logs_output, json_output]
 
304
  )
305
-
306
- tickers_input.submit(
307
- fn=run_backend_processing,
308
- inputs=[tickers_input],
309
- outputs=[logs_output, json_output]
310
  )
311
 
 
312
  if __name__ == "__main__":
313
- demo.launch(share=False, server_name="0.0.0.0")
 
1
  import gradio as gr
 
2
  import asyncio
 
 
 
 
 
 
3
  import json
4
+ from typing import List, Dict
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
+ import asyncio
7
+ import aiohttp
8
+ from typing import Dict, List
9
+
10
+ # Assume 'hist_url' is globally defined
11
+ # For example:
12
+ hist_url="https://groww.in/v1/api/charting_service/v2/chart/exchange/NSE/segment/CASH"
13
+
14
+
15
+ async def call_price_api_async(
16
+ session: aiohttp.ClientSession,
17
+ ticker: str,
18
+ start: int,
19
+ end: int,
20
+ interval: int,
21
+ debug: bool = False
22
+ ) -> Dict:
23
+ """
24
+ Asynchronously calls the Groww candle API and returns the raw JSON response.
25
+
26
+ Parameters:
27
+ - session (aiohttp.ClientSession): An aiohttp client session.
28
+ - ticker (str): NSE stock symbol or Groww-compliant identifier.
29
+ - start (int): Start time in epoch milliseconds (UTC).
30
+ - end (int): End time in epoch milliseconds (UTC).
31
+ - interval (int): Candle interval in minutes.
32
+ - debug (bool): If True, includes detailed debug logs.
33
+
34
+ Returns:
35
+ - dict: A dictionary with "data", "debug_info", and "error" keys.
36
+ """
37
+ debug_logs = []
38
+ if debug:
39
+ debug_logs.append(f"[DEBUG] Ticker: {ticker} - Starting API call.")
40
 
 
 
41
  url = f"{hist_url}/{ticker}"
42
+ params = {
43
+ "startTimeInMillis": start,
44
+ "endTimeInMillis": end,
45
+ "intervalInMinutes": interval,
46
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
+ try:
49
+ async with session.get(url, params=params) as response:
50
+ if debug:
51
+ debug_logs.append(f"[DEBUG] Ticker: {ticker} - Status Code: {response.status}")
52
+
53
+ response.raise_for_status()
54
+ json_data = await response.json()
55
+
56
+ if debug:
57
+ debug_logs.append(f"[DEBUG] Ticker: {ticker} - API call successful.")
58
+
59
+ return {
60
+ "ticker": ticker,
61
+ "data": json_data,
62
+ "debug_info": debug_logs if debug else None,
63
+ "error": None,
64
+ }
65
+ except aiohttp.ClientError as e:
66
+ if debug:
67
+ debug_logs.append(f"[DEBUG] Ticker: {ticker} - Request failed: {e}")
68
+
69
+ return {
70
+ "ticker": ticker,
71
+ "data": None,
72
+ "debug_info": debug_logs if debug else None,
73
+ "error": [str(e)],
74
+ }
75
+
76
+
77
+ async def main(tickers: List[str], start_time: int, end_time: int, interval: int) -> List[Dict]:
78
+ """
79
+ Main function to run asynchronous API calls for multiple tickers.
80
+ """
81
+ async with aiohttp.ClientSession() as session:
82
  tasks = [
83
+ call_price_api_async(session, ticker, start_time, end_time, interval, debug=True)
84
+ for ticker in tickers
85
  ]
86
+ results = await asyncio.gather(*tasks)
87
+ return results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
+ def run_main(ticker_input: str, start: int, end: int, interval: int) -> str:
92
+ """
93
+ Wrapper to run the async main() function in sync for Gradio.
94
+ """
95
+ try:
96
+ # Convert input string like ["TCS","HDFCBANK"] into list
97
+ tickers: List[str] = json.loads(ticker_input)
98
 
99
+ # Run the async loop
100
+ results: List[Dict] = asyncio.run(main(tickers, start, end, interval))
101
 
102
+ # Pretty-print JSON
103
+ return json.dumps(results, indent=2)
 
 
 
 
 
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  except Exception as e:
106
+ return f"Error: {str(e)}"
 
107
 
108
 
109
+ with gr.Blocks() as demo:
110
+ gr.Markdown("## 📈 Groww Async Candle API Fetcher")
111
 
 
 
 
 
112
  with gr.Row():
113
+ ticker_box = gr.Textbox(
114
+ label="Tickers (JSON list)",
115
+ placeholder='["TCS","HDFCBANK"]'
116
+ )
117
+ with gr.Row():
118
+ start_box = gr.Number(label="Start (epoch millis)", value=1660707000000)
119
+ end_box = gr.Number(label="End (epoch millis)", value=1660793400000)
120
+ interval_box = gr.Number(label="Interval (minutes)", value=15)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
+ run_button = gr.Button("Fetch Data")
123
+
124
+ output_box = gr.Textbox(
125
+ label="Raw API Output",
126
+ lines=20,
127
+ placeholder="Results will appear here..."
128
  )
129
+
130
+ run_button.click(
131
+ fn=run_main,
132
+ inputs=[ticker_box, start_box, end_box, interval_box],
133
+ outputs=output_box
134
  )
135
 
136
+
137
  if __name__ == "__main__":
138
+ demo.launch()