|
|
|
|
|
|
| import pandas as pd
|
| import re
|
| import requests
|
| import os
|
| from datetime import datetime, timedelta
|
|
|
|
|
|
|
| processed_dir = './processed'
|
|
|
| temp_dir = './tempfiles'
|
|
|
|
|
| os.makedirs(processed_dir, exist_ok=True)
|
| os.makedirs(temp_dir, exist_ok=True)
|
|
|
|
|
| output_file = os.path.join(processed_dir, 'nse_dashboard_corpact.json')
|
|
|
|
|
| DOWNLOAD_URL_TEMPLATE = "https://www.nseindia.com/api/corporates-corporateActions?index=equities&from_date=01-01-2020&to_date={}&csv=true"
|
|
|
|
|
| NSE_CORPORATE_ACTIONS_PAGE = "https://www.nseindia.com/companies-listing/corporate-filings-actions"
|
|
|
|
|
|
|
| def get_last_nse_working_day():
|
| """
|
| Determines the last NSE working day (i.e., the most recent weekday).
|
| NSE is typically closed on Saturdays and Sundays.
|
| """
|
| today = datetime.now()
|
|
|
|
|
|
|
|
|
| while today.weekday() > 4:
|
| today -= timedelta(days=1)
|
| return today.strftime('%d-%m-%Y')
|
|
|
| def download_nse_corporate_actions_file():
|
| """
|
| Downloads the corporate actions CSV file from NSE to the tempfiles directory.
|
| It returns the path to the downloaded file in the temporary directory.
|
| Returns None if download fails.
|
| """
|
| last_working_day = get_last_nse_working_day()
|
| download_url = DOWNLOAD_URL_TEMPLATE.format(last_working_day)
|
|
|
|
|
| temp_file_name = f"CF-CA-equities-01-01-2020-to-{last_working_day}.csv"
|
| temp_file_path = os.path.join(temp_dir, temp_file_name)
|
|
|
|
|
|
|
|
|
| print(f"Attempting to download corporate actions data to temporary directory.")
|
|
|
|
|
| session = requests.Session()
|
|
|
| try:
|
|
|
| print(f"Fetching cookies.")
|
| session_headers = {
|
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
| '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',
|
| 'Accept-Language': 'en-US,en;q=0.9',
|
| 'Connection': 'keep-alive',
|
| 'Upgrade-Insecure-Requests': '1',
|
| }
|
| session.get(NSE_CORPORATE_ACTIONS_PAGE, headers=session_headers)
|
|
|
|
|
| print(f"Downloading CSV using established session.")
|
| download_headers = {
|
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
| 'Accept': 'text/csv',
|
| 'Accept-Language': 'en-US,en;q=0.9',
|
| 'Connection': 'keep-alive',
|
| 'Referer': NSE_CORPORATE_ACTIONS_PAGE,
|
| 'Sec-Fetch-Dest': 'document',
|
| 'Sec-Fetch-Mode': 'navigate',
|
| 'Sec-Fetch-Site': 'same-origin',
|
| 'Sec-Fetch-User': '?1',
|
| 'Upgrade-Insecure-Requests': '1',
|
| }
|
| response = session.get(download_url, headers=download_headers)
|
| response.raise_for_status()
|
|
|
|
|
| raw_bytes_content = response.content
|
|
|
|
|
|
|
| if raw_bytes_content.startswith(b'\xef\xbb\xbf'):
|
|
|
| raw_bytes_content = raw_bytes_content[3:]
|
|
|
|
|
|
|
| raw_content_string = raw_bytes_content.decode('latin1', errors='ignore')
|
|
|
|
|
| cleaned_lines = [line.strip() for line in raw_content_string.splitlines()]
|
| cleaned_content = "\n".join(cleaned_lines)
|
|
|
|
|
| with open(temp_file_path, 'w', encoding='latin1') as f:
|
| f.write(cleaned_content)
|
|
|
| print(f"Successfully downloaded and preprocessed to temporary file.")
|
| return temp_file_path
|
| except requests.exceptions.RequestException as e:
|
| print(f"Error downloading file: {e}")
|
| print("This often indicates an issue with the NSE API (e.g., authentication required, rate limiting, or changed endpoint).")
|
| print("You may still need to manually download the file if this persists.")
|
| return None
|
|
|
|
|
|
|
|
|
| def calculate_bonus_adj_factor(purpose):
|
| """Extracts and calculates the adjustment factor for bonus shares."""
|
| if "Bonus" in purpose:
|
| try:
|
|
|
|
|
| match = re.search(r"Bonus (\d+:\d+)", purpose)
|
| if match:
|
| ratio = match.group(1)
|
| bonus, base = map(int, ratio.split(":"))
|
| return f"{bonus}:{base}", "Bonus"
|
| except ValueError:
|
| print(f"Could not parse bonus ratio from: {purpose}")
|
| return None, None
|
|
|
|
|
| def calculate_split_adj_factor(purpose):
|
| """Extracts and calculates the adjustment factor for stock splits."""
|
| if "Face Value Split" in purpose:
|
| try:
|
|
|
|
|
| match = re.search(r"From Rs (\d+(?:\.\d+)?)/?-? Per Share To (?:Re|Rs) (\d+(?:\.\d+)?)/?-? Per Share", purpose)
|
| if match:
|
| from_value_str = match.group(1)
|
| to_value_str = match.group(2)
|
|
|
|
|
| from_value = float(from_value_str)
|
| to_value = float(to_value_str)
|
|
|
|
|
|
|
| return f"{int(to_value)}:{int(from_value)}", "Split"
|
| else:
|
| pass
|
| except ValueError:
|
| print(f"Could not parse split values from: {purpose}")
|
| return None, None
|
|
|
|
|
|
|
| if __name__ == "__main__":
|
| temp_downloaded_file_path = None
|
| input_file = None
|
| df = None
|
|
|
|
|
| temp_downloaded_file_path = download_nse_corporate_actions_file()
|
|
|
|
|
| final_processed_file_name = 'Corporate_action.parquet'
|
| final_processed_file_path = os.path.join(processed_dir, final_processed_file_name)
|
|
|
| if temp_downloaded_file_path and os.path.exists(temp_downloaded_file_path):
|
| try:
|
|
|
| df_temp = pd.read_csv(temp_downloaded_file_path, encoding='latin1')
|
|
|
| df_temp.to_parquet(final_processed_file_path, index=False)
|
|
|
| os.remove(temp_downloaded_file_path)
|
|
|
| legacy_csv = os.path.join(processed_dir, 'Corporate_action.csv')
|
| if os.path.exists(legacy_csv):
|
| os.remove(legacy_csv)
|
| input_file = final_processed_file_path
|
| print("CSV downloaded file converted to Parquet and saved successfully.")
|
| except Exception as e:
|
| print(f"Error processing downloaded CSV file: {e}")
|
| input_file = final_processed_file_path if os.path.exists(final_processed_file_path) else None
|
| else:
|
| print("\nAutomatic download to temporary directory failed or file not found in tempfiles.")
|
|
|
| print(f"Checking for '{final_processed_file_name}' in '{processed_dir}' as a fallback...")
|
| if os.path.exists(final_processed_file_path):
|
| input_file = final_processed_file_path
|
| print(f"Using existing file as input: '{input_file}'")
|
| else:
|
|
|
| fallback_csv = os.path.join(processed_dir, 'Corporate_action.csv')
|
| if os.path.exists(fallback_csv):
|
| try:
|
| df_temp = pd.read_csv(fallback_csv, encoding='latin1')
|
| df_temp.to_parquet(final_processed_file_path, index=False)
|
| os.remove(fallback_csv)
|
| input_file = final_processed_file_path
|
| print(f"Auto-converted existing '{fallback_csv}' to Parquet.")
|
| except Exception as e:
|
| print(f"Failed to auto-convert existing CSV fallback: {e}")
|
|
|
| if not input_file:
|
| print(f"No '{final_processed_file_name}' found in '{processed_dir}'. Cannot proceed without an input file.")
|
| print(f"Please manually download the latest CSV from the following URL and place it in the '{temp_dir}' directory, then rerun the script:")
|
| print(f" {DOWNLOAD_URL_TEMPLATE.format(get_last_nse_working_day())}")
|
| exit()
|
|
|
|
|
| if input_file and os.path.exists(input_file):
|
| try:
|
| df = pd.read_parquet(input_file)
|
| print(f"Successfully loaded data from '{input_file}'.")
|
| except Exception as e:
|
| print(f"Error reading input Parquet file '{input_file}': {e}")
|
| exit()
|
| else:
|
| print("No valid input file determined or file does not exist. Exiting without processing.")
|
| exit()
|
|
|
|
|
| output_data = []
|
|
|
|
|
| if df is not None:
|
| for _, row in df.iterrows():
|
| symbol = row['SYMBOL']
|
| purpose = str(row['PURPOSE'])
|
| ex_date = row['EX-DATE']
|
|
|
| ratio, corp_action = None, None
|
|
|
|
|
| if "Bonus" in purpose:
|
| ratio, corp_action = calculate_bonus_adj_factor(purpose)
|
| elif "Face Value Split" in purpose:
|
| ratio, corp_action = calculate_split_adj_factor(purpose)
|
|
|
|
|
| if ratio and corp_action:
|
| try:
|
|
|
| formatted_date = pd.to_datetime(ex_date, format='%d-%b-%Y').strftime('%Y-%m-%d')
|
| output_data.append({
|
| "symbol": symbol,
|
| "type": corp_action,
|
| "date": formatted_date,
|
| "ratio": ratio
|
| })
|
| except ValueError as ve:
|
| print(f"Error parsing date '{ex_date}' for symbol '{symbol}': {ve}")
|
| except Exception as e:
|
| print(f"An unexpected error occurred processing row for symbol '{symbol}': {e}")
|
|
|
|
|
|
|
| output_df = pd.DataFrame(output_data)
|
|
|
|
|
| output_df.to_json(output_file, orient='records', indent=4)
|
|
|
| print(f"\nCorporate action data processed and saved to {output_file}")
|
| print(f"Total records processed: {len(df)}")
|
| print(f"Total valid corporate actions extracted: {len(output_df)}")
|
| else:
|
| print("Dataframe 'df' was not created due to previous errors. No processing performed.")
|
|
|