Subham9126 commited on
Commit
af60e73
·
verified ·
1 Parent(s): fc38b7e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +85 -83
app.py CHANGED
@@ -8,7 +8,8 @@ import pytz
8
  import tzlocal
9
  import logging
10
  import json
11
- import ast # Import the ast module for safe string evaluation
 
12
 
13
  # --- Basic Setup ---
14
 
@@ -25,7 +26,7 @@ class DateTimeValidationError(ValueError):
25
  hist_url = "https://groww.in/v1/api/charting_service/v2/chart/exchange/NSE/segment/CASH"
26
 
27
 
28
- # --- Date/Time Functions (No changes here) ---
29
 
30
  def validate_datetime_format(dt_str: str) -> datetime:
31
  """Validate datetime string in strict 'YYYY-MM-DD' format."""
@@ -72,10 +73,10 @@ def get_time_range_in_unix_ms(start_date_str: str, end_date_str: str, timezone:
72
  return {"start_timestamp_ms": start_timestamp, "end_timestamp_ms": end_timestamp}
73
 
74
 
75
- # --- FIXED Asynchronous API & Processing Functions ---
76
 
77
- async def call_price_api_async(session: aiohttp.ClientSession, ticker: str, start: int, end: int, interval: int) -> Dict:
78
- """Asynchronously calls the candle API."""
79
  url = f"{hist_url}/{ticker}"
80
  params = {"startTimeInMillis": start, "endTimeInMillis": end, "intervalInMinutes": interval}
81
  try:
@@ -83,67 +84,69 @@ async def call_price_api_async(session: aiohttp.ClientSession, ticker: str, star
83
  response.raise_for_status()
84
  json_data = await response.json()
85
  return {"ticker": ticker, "interval": interval, "data": json_data, "error": None}
86
- except aiohttp.ClientError as e:
 
87
  return {"ticker": ticker, "interval": interval, "data": None, "error": str(e)}
88
 
89
- async def main_task(tickers: List[str], start_time: int, end_time: int, intervals: List[int], progress: gr.Progress):
90
- """OPTIMIZED: Calculate all requests first, then process in batches of 30."""
91
 
92
- # Step 1: Calculate all requests upfront
93
- all_requests = []
94
  for ticker in tickers:
95
  for interval in intervals:
96
- all_requests.append((ticker, start_time, end_time, interval))
97
 
98
- total_requests = len(all_requests)
99
- logger.info(f"Total requests to make: {total_requests}")
100
 
101
- # Step 2: Process requests in batches of 30
102
- results = []
103
- batch_size = 30
 
 
 
104
 
105
- async with aiohttp.ClientSession(
106
- timeout=aiohttp.ClientTimeout(total=30), # Add timeout for better reliability
107
- connector=aiohttp.TCPConnector(limit=50) # Limit concurrent connections
108
- ) as session:
109
-
110
- for i in progress.tqdm(range(0, total_requests, batch_size), desc="Processing Request Batches"):
111
- batch_requests = all_requests[i:i+batch_size]
112
-
113
- # Create tasks for this batch
114
- tasks = [
115
- call_price_api_async(session, ticker, start, end, interval)
116
- for ticker, start, end, interval in batch_requests
117
- ]
118
-
119
- # Execute batch concurrently
120
- batch_results = await asyncio.gather(*tasks, return_exceptions=True)
121
-
122
- # Handle any exceptions in results
123
- for result in batch_results:
124
- if isinstance(result, Exception):
125
- logger.error(f"Request failed with exception: {result}")
126
- # You can add error handling here if needed
127
- else:
128
- results.append(result)
129
-
130
- # Optional: Add small delay between batches to be respectful to the API
131
- if i + batch_size < total_requests:
132
- await asyncio.sleep(0.1)
133
 
134
- logger.info(f"Completed {len(results)} requests out of {total_requests}")
135
- return results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
- def process_and_merge_data(results: list) -> str:
138
- """Processes and merges the raw API results for different intervals."""
139
  merged_data = {}
 
140
  for result in results:
141
- ticker, interval, data, error = result.get("ticker"), result.get("interval"), result.get("data"), result.get("error")
 
 
 
 
142
  if ticker not in merged_data:
143
  merged_data[ticker] = {"symbol": ticker}
 
144
  if error:
145
  merged_data[ticker][f"error_{interval}m"] = error
146
  continue
 
147
  if data and data.get("candles") and data["candles"]:
148
  first_candle = data["candles"][0]
149
  if len(first_candle) > 4:
@@ -154,86 +157,85 @@ def process_and_merge_data(results: list) -> str:
154
  merged_data[ticker][f"{prefix} close"] = first_candle[4]
155
  else:
156
  merged_data[ticker][f"error_{interval}m"] = "No data or candles found"
157
-
158
  return json.dumps(list(merged_data.values()), indent=4)
159
 
160
 
161
- # --- Gradio Backend Function ---
162
 
163
  async def run_backend_processing(tickers_text: str, progress=gr.Progress(track_tqdm=True)):
164
- """The main function to be called by the Gradio interface."""
165
  if not tickers_text.strip():
166
  yield "Error: Input is empty. Please provide a list of tickers.", "{}"
167
  return
168
 
169
  try:
170
- # Safely parse the string input into a Python list
171
  parsed_input = ast.literal_eval(tickers_text)
172
  if not isinstance(parsed_input, list):
173
  raise TypeError("Input must be a list.")
174
- # Clean and validate tickers
175
  tickers = [str(item).strip().upper() for item in parsed_input]
176
  if not tickers:
177
  yield "Error: The provided list is empty.", "{}"
178
  return
179
-
180
- except (ValueError, SyntaxError, TypeError) as e:
181
- error_message = 'Invalid input format. Please provide a list of strings, e.g., ["RELIANCE", "INFY"]'
182
- yield error_message, "{}"
183
  return
184
 
 
185
  today_str = date.today().strftime("%Y-%m-%d")
 
 
186
  intervals = [1440, 15]
187
- total_requests = len(tickers) * len(intervals)
188
 
189
- yield f"Starting processing for {len(tickers)} tickers: {tickers}\nTotal API requests: {total_requests}", "{}"
 
190
 
191
  try:
192
- time_range = get_time_range_in_unix_ms(today_str, today_str)
193
- start_time, end_time = time_range["start_timestamp_ms"], time_range["end_timestamp_ms"]
194
-
195
- yield f"Fetching data from API in batches of 30 (Total: {total_requests} requests)...", "{}"
196
- start_fetch_time = datetime.now()
197
 
198
- results = await main_task(tickers, start_time, end_time, intervals, progress)
 
199
 
200
- fetch_duration = (datetime.now() - start_fetch_time).total_seconds()
201
- yield f"Data fetching completed in {fetch_duration:.2f} seconds. Processing and merging data...", "{}"
202
 
203
- processed_json = process_and_merge_data(results)
204
 
205
- total_duration = (datetime.now() - start_fetch_time).total_seconds()
206
- yield f"Processing complete in {total_duration:.2f} seconds. Successfully processed {len(results)} API responses.", processed_json
207
 
208
  except Exception as e:
209
- logger.error(f"An unhandled error occurred: {e}")
210
  yield f"An error occurred: {e}", "{}"
211
 
212
 
213
  # --- Gradio UI ---
214
 
215
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
216
- gr.Markdown("## Stock Data Backend Processor (Optimized)")
217
- gr.Markdown("Enter a list of stock tickers in Python list format. The app will make concurrent API requests in batches of 30.")
218
 
219
  with gr.Row():
220
  with gr.Column(scale=1):
221
  tickers_input = gr.Textbox(
222
- lines=2,
223
- label='Enter Tickers as a List',
224
- value='["RELIANCE", "INFY", "TCS"]',
225
  placeholder='["TICKER1", "TICKER2", "TICKER3"]'
226
  )
227
- start_button = gr.Button("Start Processing", variant="primary")
228
 
229
- # Add some helpful info
230
- gr.Markdown("**Note:** Each ticker makes 2 API calls (1440m and 15m intervals)")
 
 
 
 
231
 
232
  with gr.Column(scale=2):
233
- logs_output = gr.Textbox(label="Logs & Progress", lines=15, interactive=False)
234
- json_output = gr.JSON(label="Processed JSON Output")
235
 
236
- # --- Triggers ---
237
  start_button.click(
238
  fn=run_backend_processing,
239
  inputs=[tickers_input],
 
8
  import tzlocal
9
  import logging
10
  import json
11
+ import ast
12
+ from concurrent.futures import ThreadPoolExecutor
13
 
14
  # --- Basic Setup ---
15
 
 
26
  hist_url = "https://groww.in/v1/api/charting_service/v2/chart/exchange/NSE/segment/CASH"
27
 
28
 
29
+ # --- Date/Time Functions ---
30
 
31
  def validate_datetime_format(dt_str: str) -> datetime:
32
  """Validate datetime string in strict 'YYYY-MM-DD' format."""
 
73
  return {"start_timestamp_ms": start_timestamp, "end_timestamp_ms": end_timestamp}
74
 
75
 
76
+ # --- OPTIMIZED Async Functions ---
77
 
78
+ async def fetch_single_request(session: aiohttp.ClientSession, ticker: str, start: int, end: int, interval: int) -> Dict:
79
+ """Fetch data for a single ticker-interval combination."""
80
  url = f"{hist_url}/{ticker}"
81
  params = {"startTimeInMillis": start, "endTimeInMillis": end, "intervalInMinutes": interval}
82
  try:
 
84
  response.raise_for_status()
85
  json_data = await response.json()
86
  return {"ticker": ticker, "interval": interval, "data": json_data, "error": None}
87
+ except Exception as e:
88
+ logger.error(f"API call failed for {ticker} ({interval}m): {e}")
89
  return {"ticker": ticker, "interval": interval, "data": None, "error": str(e)}
90
 
91
+ async def fetch_all_data_optimized(tickers: List[str], start_time: int, end_time: int, intervals: List[int]) -> List[Dict]:
92
+ """Fetch all data with maximum concurrency - NO artificial batching."""
93
 
94
+ # Create ALL tasks at once
95
+ all_tasks = []
96
  for ticker in tickers:
97
  for interval in intervals:
98
+ all_tasks.append((ticker, start_time, end_time, interval))
99
 
100
+ logger.info(f"Creating {len(all_tasks)} concurrent requests...")
 
101
 
102
+ # Configure session for high concurrency
103
+ connector = aiohttp.TCPConnector(
104
+ limit=100, # Max concurrent connections
105
+ limit_per_host=50, # Max per host
106
+ keepalive_timeout=30
107
+ )
108
 
109
+ timeout = aiohttp.ClientTimeout(total=30, connect=10)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
+ async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
112
+ # Create all coroutines
113
+ tasks = [
114
+ fetch_single_request(session, ticker, start, end, interval)
115
+ for ticker, start, end, interval in all_tasks
116
+ ]
117
+
118
+ # Execute ALL requests concurrently
119
+ logger.info("Executing all requests concurrently...")
120
+ start_time_fetch = datetime.now()
121
+
122
+ results = await asyncio.gather(*tasks, return_exceptions=True)
123
+
124
+ fetch_duration = (datetime.now() - start_time_fetch).total_seconds()
125
+ logger.info(f"All API calls completed in {fetch_duration:.2f} seconds")
126
+
127
+ # Filter out exceptions
128
+ valid_results = [r for r in results if not isinstance(r, Exception)]
129
+ logger.info(f"Got {len(valid_results)} valid responses out of {len(all_tasks)} requests")
130
+
131
+ return valid_results
132
 
133
+ def process_data_sync(results: List[Dict]) -> str:
134
+ """Process data synchronously - this is fast."""
135
  merged_data = {}
136
+
137
  for result in results:
138
+ ticker = result.get("ticker")
139
+ interval = result.get("interval")
140
+ data = result.get("data")
141
+ error = result.get("error")
142
+
143
  if ticker not in merged_data:
144
  merged_data[ticker] = {"symbol": ticker}
145
+
146
  if error:
147
  merged_data[ticker][f"error_{interval}m"] = error
148
  continue
149
+
150
  if data and data.get("candles") and data["candles"]:
151
  first_candle = data["candles"][0]
152
  if len(first_candle) > 4:
 
157
  merged_data[ticker][f"{prefix} close"] = first_candle[4]
158
  else:
159
  merged_data[ticker][f"error_{interval}m"] = "No data or candles found"
160
+
161
  return json.dumps(list(merged_data.values()), indent=4)
162
 
163
 
164
+ # --- STREAMLINED Gradio Backend Function ---
165
 
166
  async def run_backend_processing(tickers_text: str, progress=gr.Progress(track_tqdm=True)):
167
+ """Streamlined backend processing with minimal yields."""
168
  if not tickers_text.strip():
169
  yield "Error: Input is empty. Please provide a list of tickers.", "{}"
170
  return
171
 
172
  try:
 
173
  parsed_input = ast.literal_eval(tickers_text)
174
  if not isinstance(parsed_input, list):
175
  raise TypeError("Input must be a list.")
 
176
  tickers = [str(item).strip().upper() for item in parsed_input]
177
  if not tickers:
178
  yield "Error: The provided list is empty.", "{}"
179
  return
180
+ except (ValueError, SyntaxError, TypeError):
181
+ yield 'Invalid input format. Please provide a list of strings, e.g., ["RELIANCE", "INFY"]', "{}"
 
 
182
  return
183
 
184
+ # Get timestamps
185
  today_str = date.today().strftime("%Y-%m-%d")
186
+ time_range = get_time_range_in_unix_ms(today_str, today_str)
187
+ start_time, end_time = time_range["start_timestamp_ms"], time_range["end_timestamp_ms"]
188
  intervals = [1440, 15]
 
189
 
190
+ total_requests = len(tickers) * len(intervals)
191
+ yield f"Fetching data for {len(tickers)} tickers ({total_requests} total requests)...", "{}"
192
 
193
  try:
194
+ # Time the entire operation
195
+ overall_start = datetime.now()
 
 
 
196
 
197
+ # Fetch all data with maximum concurrency
198
+ results = await fetch_all_data_optimized(tickers, start_time, end_time, intervals)
199
 
200
+ # Process data (this is fast)
201
+ processed_json = process_data_sync(results)
202
 
203
+ total_time = (datetime.now() - overall_start).total_seconds()
204
 
205
+ yield f"✅ Complete! Processed {len(results)} responses in {total_time:.2f} seconds", processed_json
 
206
 
207
  except Exception as e:
208
+ logger.error(f"Error: {e}")
209
  yield f"An error occurred: {e}", "{}"
210
 
211
 
212
  # --- Gradio UI ---
213
 
214
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
215
+ gr.Markdown("## ⚡ High-Performance Stock Data Processor")
216
+ gr.Markdown("Optimized for maximum concurrency - fetches all data simultaneously!")
217
 
218
  with gr.Row():
219
  with gr.Column(scale=1):
220
  tickers_input = gr.Textbox(
221
+ lines=3,
222
+ label='Enter Tickers as List',
223
+ value='["RELIANCE", "INFY", "TCS", "HDFCBANK", "ICICIBANK"]',
224
  placeholder='["TICKER1", "TICKER2", "TICKER3"]'
225
  )
226
+ start_button = gr.Button("🚀 Start Processing", variant="primary")
227
 
228
+ gr.Markdown("""
229
+ **Performance Notes:**
230
+ - Each ticker = 2 API calls (1440m + 15m intervals)
231
+ - All requests execute concurrently
232
+ - No artificial batching delays
233
+ """)
234
 
235
  with gr.Column(scale=2):
236
+ logs_output = gr.Textbox(label="⏱️ Progress & Timing", lines=10, interactive=False)
237
+ json_output = gr.JSON(label="📊 Processed Results")
238
 
 
239
  start_button.click(
240
  fn=run_backend_processing,
241
  inputs=[tickers_input],