From b6c6518ad452e350104ac3028a23224f028a09de Mon Sep 17 00:00:00 2001 From: Bobby Abellana Date: Mon, 3 Feb 2025 22:10:09 -0800 Subject: [PATCH] Annual EPS --- src/main.py | 16 +++++++-------- src/screener/a_canslim.py | 29 ++++++++++++++++++++++++++ src/screener/data_fetcher.py | 40 ++++++++++++++++++------------------ 3 files changed, 56 insertions(+), 29 deletions(-) create mode 100644 src/screener/a_canslim.py diff --git a/src/main.py b/src/main.py index c50f40f..a0db315 100644 --- a/src/main.py +++ b/src/main.py @@ -1,6 +1,7 @@ import datetime from screener.data_fetcher import validate_date_range, fetch_financial_data, get_stocks_in_time_range -from screener.c_canslim import check_quarterly_earnings, check_sales_growth, check_return_on_equity +from screener.c_canslim import check_quarterly_earnings +from screener.a_canslim import check_annual_eps_growth # New module from screener.csv_appender import append_scores_to_csv def main(): @@ -27,21 +28,18 @@ def main(): if not data: print(f"Warning: No data returned for {symbol}. Assigning default score.") scores = { - "EPS_Score": 0.25, # EPS Growth - "Sales_Score": 0.25, # Sales Growth - "ROE_Score": 0.25 # Return on Equity + "EPS_Score": 0.25, # Quarterly EPS Growth + "Annual_EPS_Score": 0.25 # Annual EPS Growth } else: # Extract relevant fields - quarterly_eps = data.get("eps", []) - sales_growth = data.get("sales_growth", []) - roe = 18 # Placeholder, modify if needed + quarterly_eps = data.get("quarterly_eps", []) + annual_eps = data.get("annual_eps", []) # 5️⃣ Compute CANSLIM Scores scores = { "EPS_Score": check_quarterly_earnings(quarterly_eps), - "Sales_Score": check_sales_growth(sales_growth), - "ROE_Score": check_return_on_equity(roe) + "Annual_EPS_Score": check_annual_eps_growth(annual_eps) } # 6️⃣ Append results to a **generic** CSV file in `data/metrics/` diff --git a/src/screener/a_canslim.py b/src/screener/a_canslim.py new file mode 100644 index 0000000..36da09e --- /dev/null +++ b/src/screener/a_canslim.py @@ -0,0 +1,29 @@ +def check_annual_eps_growth(annual_eps): + """ + Checks for 25%-50%+ annual EPS growth over the last three years. + + Args: + annual_eps (list): List of annual EPS values (must contain at least 3 years of data). + + Returns: + float: Score (1 pass, 0 fail, 0.25 insufficient data). + """ + if not annual_eps or len(annual_eps) < 3: + return 0.25 # Not enough data for 3-year growth calculation + + # Calculate year-over-year EPS growth rates + growth_rates = [] + for i in range(1, len(annual_eps)): # Compare consecutive years + prev_eps = annual_eps[i - 1] + current_eps = annual_eps[i] + + if prev_eps <= 0: # Avoid division by zero or negative EPS + return 0.25 + + growth = ((current_eps - prev_eps) / abs(prev_eps)) * 100 + growth_rates.append(growth) + + # Check if all growth rates are 25%+ + if all(rate >= 25 for rate in growth_rates): + return 1 # Pass + return 0 # Fail diff --git a/src/screener/data_fetcher.py b/src/screener/data_fetcher.py index e22f389..6bb021a 100644 --- a/src/screener/data_fetcher.py +++ b/src/screener/data_fetcher.py @@ -31,8 +31,9 @@ def validate_date_range(start_date, end_date, required_quarters=4): def fetch_financial_data(symbol, start_date, end_date): """ - Fetch financial data (EPS, Sales Growth) from stock_financials table - for a given stock symbol within a specific date range. + Fetch financial data for a given stock symbol, including: + - Quarterly EPS for EPS Score + - Annual EPS for Annual EPS Score Args: symbol (str): Stock ticker symbol. @@ -40,21 +41,20 @@ def fetch_financial_data(symbol, start_date, end_date): end_date (str or datetime): End date for data retrieval. Returns: - dict: A dictionary containing calculated EPS and sales growth. + dict: Contains EPS, sales growth, and annual EPS. """ client = create_client() - # Query stock_financials for revenue, net income, and EPS query = f""" SELECT filing_date, + diluted_eps, revenue, - net_income, - diluted_eps -- Using diluted EPS for accuracy + timeframe FROM stock_db.stock_financials WHERE ticker = '{symbol}' AND filing_date BETWEEN toDate('{start_date}') AND toDate('{end_date}') - AND timeframe = 'quarterly' -- Ensure only quarterly reports are used + AND (timeframe = 'quarterly' OR timeframe = 'annual') -- Fetch both annual and quarterly data ORDER BY filing_date ASC """ @@ -63,36 +63,36 @@ def fetch_financial_data(symbol, start_date, end_date): if not result.result_rows: return {} - # Extracting data - dates = [] + quarterly_eps = [] + annual_eps = [] revenues = [] - net_incomes = [] - eps_values = [] sales_growth = [] for row in result.result_rows: - dates.append(row[0]) - revenues.append(row[1]) - net_incomes.append(row[2]) - eps_values.append(row[3]) # Directly using diluted EPS + filing_date, eps, revenue, timeframe = row + + if timeframe == "quarterly": + quarterly_eps.append(eps) + revenues.append(revenue) + elif timeframe == "annual": + annual_eps.append(eps) # Calculate Sales Growth (Quarter-over-Quarter) for i in range(1, len(revenues)): # Start from index 1 since we compare with previous prev_revenue = revenues[i - 1] current_revenue = revenues[i] - if prev_revenue > 0: # Avoid division by zero + if prev_revenue > 0: growth = ((current_revenue - prev_revenue) / prev_revenue) * 100 else: growth = None # Not enough data sales_growth.append(growth) - # Pad sales_growth list to match length of EPS (since first quarter lacks a previous value) - sales_growth.insert(0, None) # First quarter doesn't have a previous comparison + sales_growth.insert(0, None) # First quarter lacks comparison return { "symbol": symbol, - "dates": dates, - "eps": eps_values, + "quarterly_eps": quarterly_eps, # Used for EPS_Score + "annual_eps": annual_eps, # Used for Annual_EPS_Score "sales_growth": sales_growth }