Created
October 27, 2024 18:36
-
-
Save qubitrenegade/769b133ce4a8b23824b9668ee3501ba3 to your computer and use it in GitHub Desktop.
stock-weekend-gaps.py - Analyze stock price gaps between last trading day of week and first trading day of next week
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| stock-weekend-gaps.py - Analyze stock price gaps between last trading day of week and first trading day of next week | |
| This script fetches historical stock data and analyzes the price gaps that occur over weekends | |
| and holidays. It captures the opening and closing prices of the last trading day of each week | |
| and the first trading day of the following week, accounting for holidays and irregular trading days. | |
| Installation: | |
| pip install yfinance pandas | |
| Usage: | |
| python3 stock-weekend-gaps.py AAPL # Analyze Apple stock for last 5 years | |
| python3 stock-weekend-gaps.py AAPL --years=10 # Analyze Apple stock for last 10 years | |
| python3 stock-weekend-gaps.py AAPL --since=2019-10-10 # Analyze from specific date | |
| python3 stock-weekend-gaps.py AAPL -o custom_filename.csv # Specify output file | |
| The script will create a CSV with columns: | |
| Week_End_Date: Date of last trading day of week (typically Friday) | |
| Week_End_Day: Day of week for last trading day | |
| Week_End_Open: Opening price of last trading day | |
| Week_End_Close: Closing price of last trading day | |
| Next_Open_Date: Date of first trading day of next week (typically Monday) | |
| Next_Open_Day: Day of week for first trading day | |
| Next_Open: Opening price of first trading day | |
| Next_Close: Closing price of first trading day | |
| Days_Between: Number of calendar days between trading days | |
| Price_Gap: Difference between Week_End_Close and Next_Open | |
| Price_Gap_Percent: Percentage change in price over the gap | |
| Data is fetched using Yahoo Finance API via yfinance library. | |
| """ | |
| import yfinance as yf | |
| import pandas as pd | |
| from datetime import datetime, timedelta | |
| import argparse | |
| import sys | |
| import os | |
| def parse_date(date_str): | |
| """Parse date string in YYYY-MM-DD format""" | |
| try: | |
| return datetime.strptime(date_str, '%Y-%m-%d') | |
| except ValueError: | |
| raise argparse.ArgumentTypeError(f'Invalid date format: {date_str}. Use YYYY-MM-DD') | |
| def get_weekend_prices(ticker_symbol, start_date=None, years=None, output_file=None): | |
| """ | |
| Fetch weekend (Friday close, Monday open/close) stock prices. | |
| Args: | |
| ticker_symbol (str): Stock ticker symbol | |
| start_date (datetime, optional): Start date for data collection | |
| years (int, optional): Number of years of historical data to fetch | |
| output_file (str, optional): Custom output file path | |
| """ | |
| if not ticker_symbol: | |
| raise ValueError("Ticker symbol is required") | |
| ticker = yf.Ticker(ticker_symbol) | |
| try: | |
| ticker.info | |
| except: | |
| raise ValueError(f"Could not find ticker: {ticker_symbol}") | |
| end_date = datetime.now() | |
| if start_date: | |
| if start_date > end_date: | |
| raise ValueError("Start date cannot be in the future") | |
| elif years: | |
| start_date = end_date - timedelta(days=years*365) | |
| else: | |
| start_date = end_date - timedelta(days=5*365) | |
| print(f"Fetching data for {ticker_symbol} from {start_date.date()} to {end_date.date()}...") | |
| # Get the historical data with some buffer days to handle holidays | |
| df = ticker.history(start=start_date, end=end_date) | |
| if df.empty: | |
| raise ValueError(f"No data found for {ticker_symbol} in the specified date range") | |
| df = df.reset_index() | |
| df['DayOfWeek'] = df['Date'].dt.dayofweek | |
| # Create a new dataframe with weekend transitions | |
| results = [] | |
| # Iterate through the dataframe | |
| i = 0 | |
| while i < len(df) - 1: | |
| current_row = df.iloc[i] | |
| # Look for trading days near weekend (Thursday/Friday for end of week) | |
| if current_row['DayOfWeek'] <= 4: # Monday to Friday | |
| # Find the last trading day of the week | |
| week_end_idx = i | |
| while (week_end_idx < len(df) - 1 and | |
| df.iloc[week_end_idx]['Date'].week == current_row['Date'].week): | |
| week_end_idx += 1 | |
| week_end_idx -= 1 | |
| # Find the first trading day of next week | |
| next_week_idx = week_end_idx + 1 | |
| while (next_week_idx < len(df) and | |
| df.iloc[next_week_idx]['Date'].week == df.iloc[week_end_idx]['Date'].week): | |
| next_week_idx += 1 | |
| if next_week_idx < len(df): | |
| week_end_row = df.iloc[week_end_idx] | |
| next_week_row = df.iloc[next_week_idx] | |
| # Calculate the number of calendar days between close and next open | |
| days_between = (next_week_row['Date'] - week_end_row['Date']).days | |
| results.append({ | |
| 'Week_End_Date': week_end_row['Date'].strftime('%Y-%m-%d'), | |
| 'Week_End_Day': week_end_row['Date'].strftime('%A'), | |
| 'Week_End_Open': week_end_row['Open'], | |
| 'Week_End_Close': week_end_row['Close'], | |
| 'Next_Open_Date': next_week_row['Date'].strftime('%Y-%m-%d'), | |
| 'Next_Open_Day': next_week_row['Date'].strftime('%A'), | |
| 'Next_Open': next_week_row['Open'], | |
| 'Next_Close': next_week_row['Close'], | |
| 'Days_Between': days_between, | |
| 'Price_Gap': next_week_row['Open'] - week_end_row['Close'], | |
| 'Price_Gap_Percent': ((next_week_row['Open'] - week_end_row['Close']) / week_end_row['Close'] * 100) | |
| }) | |
| i = next_week_idx | |
| else: | |
| i += 1 | |
| results_df = pd.DataFrame(results) | |
| if results_df.empty: | |
| print("No weekend transitions found in the specified date range") | |
| return None | |
| if not output_file: | |
| output_file = f"{ticker_symbol}_weekend_prices_{start_date.strftime('%Y%m%d')}-{end_date.strftime('%Y%m%d')}.csv" | |
| output_dir = os.path.dirname(output_file) | |
| if output_dir: | |
| os.makedirs(output_dir, exist_ok=True) | |
| # Save to CSV with original precision | |
| results_df.to_csv(output_file, index=False, float_format='%g') | |
| print(f"\nData saved to {output_file}") | |
| # Print summary statistics | |
| print("\nSummary Statistics:") | |
| print(f"Total weekend transitions: {len(results_df)}") | |
| print(f"Regular Friday-Monday transitions: {len(results_df[results_df['Days_Between'] == 3])}") | |
| print(f"Extended weekend transitions: {len(results_df[results_df['Days_Between'] > 3])}") | |
| print(f"Average price gap: {results_df['Price_Gap'].mean()}") | |
| print(f"Average price gap %: {results_df['Price_Gap_Percent'].mean()}%") | |
| print(f"Largest positive gap: {results_df['Price_Gap'].max()}") | |
| print(f"Largest negative gap: {results_df['Price_Gap'].min()}") | |
| # Print holiday weekend summary | |
| holiday_weekends = results_df[results_df['Days_Between'] > 3] | |
| if not holiday_weekends.empty: | |
| print("\nHoliday/Extended Weekends Found:") | |
| for _, row in holiday_weekends.iterrows(): | |
| print(f"{row['Week_End_Day']} {row['Week_End_Date']} to {row['Next_Open_Day']} {row['Next_Open_Date']} " | |
| f"({row['Days_Between']} days)") | |
| return results_df | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description='Fetch weekend (Friday close, Monday open/close) stock prices.', | |
| formatter_class=argparse.RawTextHelpFormatter | |
| ) | |
| parser.add_argument('ticker', nargs='?', help='Stock ticker symbol (e.g., AAPL)') | |
| parser.add_argument('--since', type=parse_date, help='Start date (YYYY-MM-DD)') | |
| parser.add_argument('--years', type=int, help='Number of years of historical data') | |
| parser.add_argument('--output', '-o', help='Output file path (default: TICKER_weekend_prices_STARTDATE-ENDDATE.csv)') | |
| args = parser.parse_args() | |
| if not args.ticker: | |
| args.ticker = input("Enter ticker symbol (e.g., AAPL): ").strip().upper() | |
| try: | |
| get_weekend_prices(args.ticker, args.since, args.years, args.output) | |
| except Exception as e: | |
| print(f"Error: {str(e)}", file=sys.stderr) | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment