S&P 500 Stock Screening with Ollama: Value Investing Analysis System

Build an AI-powered S&P 500 stock screening system using Ollama for value investing analysis. Automate financial research with local LLMs.

Ever tried to analyze 500 stocks manually and ended up cross-eyed by stock number 47? You're not alone. Most investors either rely on expensive Bloomberg terminals or settle for basic screeners that miss nuanced value opportunities.

This guide shows you how to build a powerful S&P 500 stock screening system using Ollama's local AI models. You'll create an automated value investing analysis tool that processes financial data, identifies undervalued stocks, and generates detailed investment reports—all running locally on your machine.

Why Traditional Stock Screening Falls Short

Most stock screeners use rigid filters that miss context. A stock with high P/E might still be undervalued if it's in a cyclical downturn. Traditional tools can't understand these nuances.

The Ollama advantage:

  • Local processing - Your data stays private
  • Contextual analysis - AI understands market conditions
  • Custom criteria - Build screens matching your strategy
  • Cost-effective - No subscription fees

Setting Up Your Ollama Stock Analysis Environment

Prerequisites and Installation

First, install Ollama and download the required models:

# Install Ollama (macOS/Linux)
curl -fsSL https://ollama.ai/install.sh | sh

# Pull recommended models for financial analysis
ollama pull llama2:13b
ollama pull codellama:13b
ollama pull mistral:7b

# Verify installation
ollama list

Required Python Dependencies

# requirements.txt
import requests
import yfinance as yf
import pandas as pd
import numpy as np
import json
from datetime import datetime, timedelta
import logging
from typing import Dict, List, Optional

Install dependencies:

pip install yfinance pandas numpy requests python-dotenv

Building the Core Stock Screening Framework

S&P 500 Data Collection Module

Create the foundation for fetching S&P 500 company data:

import yfinance as yf
import pandas as pd
import requests
from typing import List, Dict

class SP500DataCollector:
    """Collects and processes S&P 500 stock data for analysis"""
    
    def __init__(self):
        self.sp500_tickers = self._get_sp500_tickers()
        
    def _get_sp500_tickers(self) -> List[str]:
        """Fetch current S&P 500 ticker symbols"""
        url = "https://en.wikipedia.org/wiki/List_of_S%26P_500_companies"
        tables = pd.read_html(url)
        sp500_table = tables[0]
        return sp500_table['Symbol'].tolist()
    
    def get_financial_metrics(self, ticker: str) -> Dict:
        """Extract key financial metrics for value investing analysis"""
        try:
            stock = yf.Ticker(ticker)
            info = stock.info
            
            # Key value investing metrics
            metrics = {
                'ticker': ticker,
                'company_name': info.get('longName', 'N/A'),
                'sector': info.get('sector', 'N/A'),
                'pe_ratio': info.get('trailingPE'),
                'pb_ratio': info.get('priceToBook'),
                'debt_to_equity': info.get('debtToEquity'),
                'roe': info.get('returnOnEquity'),
                'current_ratio': info.get('currentRatio'),
                'dividend_yield': info.get('dividendYield'),
                'market_cap': info.get('marketCap'),
                'revenue_growth': info.get('revenueGrowth'),
                'profit_margin': info.get('profitMargins'),
                'free_cash_flow': info.get('freeCashflow'),
                'book_value': info.get('bookValue'),
                'current_price': info.get('currentPrice')
            }
            
            return metrics
            
        except Exception as e:
            logging.error(f"Error fetching data for {ticker}: {e}")
            return None
    
    def batch_collect_data(self, limit: int = 50) -> pd.DataFrame:
        """Collect financial data for multiple S&P 500 stocks"""
        stock_data = []
        
        for i, ticker in enumerate(self.sp500_tickers[:limit]):
            print(f"Processing {ticker} ({i+1}/{limit})")
            metrics = self.get_financial_metrics(ticker)
            
            if metrics:
                stock_data.append(metrics)
                
            # Rate limiting to respect API limits
            import time
            time.sleep(0.1)
        
        return pd.DataFrame(stock_data)

# Usage example
collector = SP500DataCollector()
stock_data = collector.batch_collect_data(limit=20)  # Start with 20 stocks
print(f"Collected data for {len(stock_data)} stocks")

Ollama Integration for AI-Powered Analysis

Build the AI analysis engine that leverages Ollama models:

import requests
import json

class OllamaAnalyzer:
    """AI-powered stock analysis using Ollama models"""
    
    def __init__(self, model="llama2:13b", base_url="http://localhost:11434"):
        self.model = model
        self.base_url = base_url
    
    def analyze_stock_value(self, stock_metrics: Dict) -> Dict:
        """Perform comprehensive value investing analysis"""
        
        # Create detailed prompt for analysis
        prompt = self._create_analysis_prompt(stock_metrics)
        
        # Query Ollama model
        response = self._query_ollama(prompt)
        
        # Parse and structure response
        analysis = self._parse_analysis_response(response)
        
        return analysis
    
    def _create_analysis_prompt(self, metrics: Dict) -> str:
        """Generate detailed prompt for value investing analysis"""
        
        prompt = f"""
        As a value investing analyst, analyze this S&P 500 stock:
        
        Company: {metrics.get('company_name')} ({metrics.get('ticker')})
        Sector: {metrics.get('sector')}
        
        Financial Metrics:
        - P/E Ratio: {metrics.get('pe_ratio')}
        - P/B Ratio: {metrics.get('pb_ratio')}
        - Debt-to-Equity: {metrics.get('debt_to_equity')}
        - ROE: {metrics.get('roe')}
        - Current Ratio: {metrics.get('current_ratio')}
        - Dividend Yield: {metrics.get('dividend_yield')}
        - Market Cap: {metrics.get('market_cap')}
        - Revenue Growth: {metrics.get('revenue_growth')}
        - Profit Margin: {metrics.get('profit_margin')}
        
        Provide analysis in this JSON format:
        {{
            "value_score": <1-10 scale>,
            "investment_thesis": "<2-3 sentence summary>",
            "strengths": ["<strength1>", "<strength2>"],
            "concerns": ["<concern1>", "<concern2>"],
            "recommendation": "<BUY/HOLD/SELL>",
            "target_price_reasoning": "<explanation>",
            "risk_level": "<LOW/MEDIUM/HIGH>"
        }}
        """
        
        return prompt
    
    def _query_ollama(self, prompt: str) -> str:
        """Send request to Ollama API"""
        
        payload = {
            "model": self.model,
            "prompt": prompt,
            "stream": False
        }
        
        try:
            response = requests.post(
                f"{self.base_url}/api/generate",
                json=payload,
                timeout=120
            )
            
            if response.status_code == 200:
                return response.json()['response']
            else:
                raise Exception(f"Ollama API error: {response.status_code}")
                
        except Exception as e:
            logging.error(f"Error querying Ollama: {e}")
            return None
    
    def _parse_analysis_response(self, response: str) -> Dict:
        """Extract structured analysis from AI response"""
        
        try:
            # Find JSON block in response
            start_idx = response.find('{')
            end_idx = response.rfind('}') + 1
            
            if start_idx != -1 and end_idx != -1:
                json_str = response[start_idx:end_idx]
                return json.loads(json_str)
            else:
                # Fallback parsing if JSON not found
                return self._fallback_parse(response)
                
        except json.JSONDecodeError:
            return self._fallback_parse(response)
    
    def _fallback_parse(self, response: str) -> Dict:
        """Basic parsing when JSON extraction fails"""
        
        return {
            "value_score": 5,
            "investment_thesis": "Analysis requires manual review",
            "strengths": ["Data available for analysis"],
            "concerns": ["Automated parsing failed"],
            "recommendation": "HOLD",
            "target_price_reasoning": "Manual analysis needed",
            "risk_level": "MEDIUM",
            "raw_response": response
        }

# Usage example
analyzer = OllamaAnalyzer()

Advanced Value Investing Screening Logic

Custom Screening Criteria Implementation

Build sophisticated screening logic that goes beyond basic filters:

class ValueInvestingScreener:
    """Advanced screening system for value investing opportunities"""
    
    def __init__(self, analyzer: OllamaAnalyzer):
        self.analyzer = analyzer
        
    def apply_value_screens(self, stock_data: pd.DataFrame) -> pd.DataFrame:
        """Apply multiple value investing screens"""
        
        # Screen 1: Basic Value Metrics
        value_filtered = self._basic_value_screen(stock_data)
        
        # Screen 2: Financial Health Check
        health_filtered = self._financial_health_screen(value_filtered)
        
        # Screen 3: AI-Enhanced Analysis
        ai_analyzed = self._ai_enhanced_screen(health_filtered)
        
        return ai_analyzed
    
    def _basic_value_screen(self, df: pd.DataFrame) -> pd.DataFrame:
        """Apply fundamental value investing filters"""
        
        # Benjamin Graham criteria (modernized)
        criteria = (
            (df['pe_ratio'] < 15) |  # Reasonable P/E
            (df['pb_ratio'] < 1.5) |  # Low price-to-book
            (df['debt_to_equity'] < 0.5) |  # Low debt
            (df['current_ratio'] > 1.5) |  # Good liquidity
            (df['roe'] > 0.15)  # Strong returns
        )
        
        # Must meet at least 2 criteria
        df['value_criteria_met'] = criteria.sum(axis=1)
        filtered = df[df['value_criteria_met'] >= 2].copy()
        
        return filtered
    
    def _financial_health_screen(self, df: pd.DataFrame) -> pd.DataFrame:
        """Screen for financial stability"""
        
        # Remove stocks with concerning metrics
        healthy_stocks = df[
            (df['current_ratio'] > 1.0) &  # Can pay short-term debt
            (df['debt_to_equity'] < 2.0) &  # Not overleveraged
            (df['profit_margin'] > 0)  # Profitable
        ].copy()
        
        return healthy_stocks
    
    def _ai_enhanced_screen(self, df: pd.DataFrame) -> pd.DataFrame:
        """Apply AI analysis to remaining candidates"""
        
        ai_results = []
        
        for _, row in df.iterrows():
            print(f"Analyzing {row['ticker']} with AI...")
            
            # Convert row to dict for analysis
            stock_metrics = row.to_dict()
            
            # Get AI analysis
            analysis = self.analyzer.analyze_stock_value(stock_metrics)
            
            # Add AI results to stock data
            if analysis:
                stock_metrics.update(analysis)
                ai_results.append(stock_metrics)
        
        # Convert back to DataFrame
        enhanced_df = pd.DataFrame(ai_results)
        
        # Filter by AI recommendations
        buy_candidates = enhanced_df[
            enhanced_df['recommendation'].isin(['BUY']) &
            (enhanced_df['value_score'] >= 7)
        ].copy()
        
        return buy_candidates

# Usage example
screener = ValueInvestingScreener(analyzer)
filtered_stocks = screener.apply_value_screens(stock_data)
print(f"Found {len(filtered_stocks)} value investing candidates")

Portfolio Optimization and Ranking

Create a ranking system for your screened stocks:

class PortfolioOptimizer:
    """Rank and optimize stock selections for portfolio construction"""
    
    def rank_investment_candidates(self, screened_stocks: pd.DataFrame) -> pd.DataFrame:
        """Rank stocks by investment attractiveness"""
        
        # Create composite score
        ranked = screened_stocks.copy()
        
        # Normalize scores (0-1 scale)
        ranked['value_score_norm'] = ranked['value_score'] / 10
        ranked['pe_score'] = 1 / (1 + ranked['pe_ratio'])  # Lower is better
        ranked['pb_score'] = 1 / (1 + ranked['pb_ratio'])  # Lower is better
        ranked['roe_score'] = ranked['roe']  # Higher is better
        
        # Calculate composite investment score
        ranked['investment_score'] = (
            ranked['value_score_norm'] * 0.4 +  # AI analysis weight
            ranked['pe_score'] * 0.2 +          # Valuation weight
            ranked['pb_score'] * 0.2 +          # Book value weight
            ranked['roe_score'] * 0.2           # Profitability weight
        )
        
        # Sort by investment score
        ranked = ranked.sort_values('investment_score', ascending=False)
        
        return ranked
    
    def generate_portfolio_report(self, ranked_stocks: pd.DataFrame, top_n: int = 10) -> str:
        """Generate detailed portfolio recommendation report"""
        
        top_picks = ranked_stocks.head(top_n)
        
        report = f"""
# S&P 500 Value Investing Portfolio Report
Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

## Top {top_n} Investment Candidates

"""
        
        for i, (_, stock) in enumerate(top_picks.iterrows(), 1):
            report += f"""
### {i}. {stock['company_name']} ({stock['ticker']})
- **Investment Score:** {stock['investment_score']:.2f}/1.00
- **AI Value Score:** {stock['value_score']}/10
- **Sector:** {stock['sector']}
- **Recommendation:** {stock['recommendation']}
- **Investment Thesis:** {stock['investment_thesis']}

**Key Metrics:**
- P/E Ratio: {stock['pe_ratio']:.1f}
- P/B Ratio: {stock['pb_ratio']:.1f}
- ROE: {stock['roe']:.1%}
- Debt/Equity: {stock['debt_to_equity']:.1f}

**AI Analysis:**
- Strengths: {', '.join(stock['strengths']) if isinstance(stock['strengths'], list) else stock['strengths']}
- Risk Level: {stock['risk_level']}

---
"""
        
        return report

# Usage example
optimizer = PortfolioOptimizer()
ranked_stocks = optimizer.rank_investment_candidates(filtered_stocks)
portfolio_report = optimizer.generate_portfolio_report(ranked_stocks)
print(portfolio_report)

Complete Implementation Example

Putting It All Together

Here's the complete workflow that ties everything together:

def run_sp500_value_screening():
    """Complete S&P 500 value investing screening workflow"""
    
    print("🚀 Starting S&P 500 Value Investing Analysis...")
    
    # Step 1: Collect Stock Data
    print("\n📊 Collecting S&P 500 stock data...")
    collector = SP500DataCollector()
    stock_data = collector.batch_collect_data(limit=50)  # Adjust limit as needed
    
    # Step 2: Initialize AI Analyzer
    print("\n🤖 Initializing Ollama AI analyzer...")
    analyzer = OllamaAnalyzer(model="llama2:13b")
    
    # Step 3: Apply Screening Logic
    print("\n🔍 Applying value investing screens...")
    screener = ValueInvestingScreener(analyzer)
    filtered_stocks = screener.apply_value_screens(stock_data)
    
    # Step 4: Rank and Optimize
    print("\n📈 Ranking investment candidates...")
    optimizer = PortfolioOptimizer()
    ranked_stocks = optimizer.rank_investment_candidates(filtered_stocks)
    
    # Step 5: Generate Report
    print("\n📋 Generating portfolio report...")
    report = optimizer.generate_portfolio_report(ranked_stocks, top_n=10)
    
    # Save report to file
    with open(f"sp500_value_report_{datetime.now().strftime('%Y%m%d')}.md", 'w') as f:
        f.write(report)
    
    print(f"\n✅ Analysis complete! Found {len(ranked_stocks)} candidates")
    print(f"📄 Report saved to sp500_value_report_{datetime.now().strftime('%Y%m%d')}.md")
    
    return ranked_stocks

# Run the complete analysis
if __name__ == "__main__":
    results = run_sp500_value_screening()

Advanced Features and Customization

Custom Screening Criteria

Modify the screening logic for different investment styles:

def create_custom_screen(style="growth_at_reasonable_price"):
    """Create custom screening criteria based on investment style"""
    
    screens = {
        "deep_value": {
            "pe_ratio": {"max": 10},
            "pb_ratio": {"max": 1.0},
            "debt_to_equity": {"max": 0.3},
            "description": "Benjamin Graham deep value approach"
        },
        
        "growth_at_reasonable_price": {
            "pe_ratio": {"max": 20},
            "revenue_growth": {"min": 0.1},
            "roe": {"min": 0.15},
            "description": "GARP strategy - growth at reasonable prices"
        },
        
        "dividend_value": {
            "dividend_yield": {"min": 0.03},
            "current_ratio": {"min": 1.5},
            "debt_to_equity": {"max": 0.5},
            "description": "Dividend-focused value investing"
        }
    }
    
    return screens.get(style, screens["deep_value"])

Integration with External Data Sources

Enhance your analysis with additional data:

def enhance_with_market_data(stock_data: pd.DataFrame) -> pd.DataFrame:
    """Add market sentiment and technical indicators"""
    
    # Add technical indicators
    for ticker in stock_data['ticker']:
        stock = yf.Ticker(ticker)
        hist = stock.history(period="1y")
        
        # Calculate moving averages
        hist['SMA_50'] = hist['Close'].rolling(50).mean()
        hist['SMA_200'] = hist['Close'].rolling(200).mean()
        
        # Current price vs moving averages
        current_price = hist['Close'].iloc[-1]
        sma_50 = hist['SMA_50'].iloc[-1]
        sma_200 = hist['SMA_200'].iloc[-1]
        
        # Add technical signals
        stock_data.loc[stock_data['ticker'] == ticker, 'above_sma_50'] = current_price > sma_50
        stock_data.loc[stock_data['ticker'] == ticker, 'above_sma_200'] = current_price > sma_200
    
    return stock_data

Performance Optimization and Scaling

Parallel Processing for Large-Scale Analysis

Handle the full S&P 500 efficiently:

import concurrent.futures
import threading

class ParallelScreener:
    """Parallel processing for large-scale stock analysis"""
    
    def __init__(self, max_workers=4):
        self.max_workers = max_workers
        self.lock = threading.Lock()
    
    def parallel_analysis(self, tickers: List[str], analyzer: OllamaAnalyzer) -> List[Dict]:
        """Analyze multiple stocks in parallel"""
        
        results = []
        
        with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
            # Submit all analysis tasks
            future_to_ticker = {
                executor.submit(self._analyze_single_stock, ticker, analyzer): ticker 
                for ticker in tickers
            }
            
            # Collect results as they complete
            for future in concurrent.futures.as_completed(future_to_ticker):
                ticker = future_to_ticker[future]
                try:
                    result = future.result()
                    if result:
                        with self.lock:
                            results.append(result)
                            print(f"✅ Completed analysis for {ticker}")
                except Exception as e:
                    print(f"❌ Error analyzing {ticker}: {e}")
        
        return results
    
    def _analyze_single_stock(self, ticker: str, analyzer: OllamaAnalyzer) -> Dict:
        """Analyze a single stock (thread-safe)"""
        
        # Fetch financial data
        collector = SP500DataCollector()
        metrics = collector.get_financial_metrics(ticker)
        
        if not metrics:
            return None
        
        # Get AI analysis
        analysis = analyzer.analyze_stock_value(metrics)
        
        # Combine results
        if analysis:
            metrics.update(analysis)
        
        return metrics

# Usage for full S&P 500 analysis
parallel_screener = ParallelScreener(max_workers=8)
all_sp500_results = parallel_screener.parallel_analysis(
    collector.sp500_tickers, 
    analyzer
)

Deployment and Automation

Automated Daily Screening

Set up automated daily analysis:

import schedule
import time
from datetime import datetime

class AutomatedScreener:
    """Automated daily stock screening system"""
    
    def __init__(self):
        self.last_run = None
        
    def daily_screen(self):
        """Run daily screening process"""
        
        try:
            print(f"\n🕐 Starting daily screen at {datetime.now()}")
            
            # Run full screening workflow
            results = run_sp500_value_screening()
            
            # Send alerts for top picks
            self._send_alerts(results.head(5))
            
            self.last_run = datetime.now()
            print(f"✅ Daily screen completed successfully")
            
        except Exception as e:
            print(f"❌ Daily screen failed: {e}")
    
    def _send_alerts(self, top_picks: pd.DataFrame):
        """Send alerts for top investment candidates"""
        
        alert_message = "🔥 Top Value Investment Alerts:\n\n"
        
        for _, stock in top_picks.iterrows():
            alert_message += f"• {stock['ticker']} - Score: {stock['investment_score']:.2f}\n"
        
        print(alert_message)
        # Add email/Slack notification here

# Schedule daily screening
screener = AutomatedScreener()
schedule.every().day.at("09:00").do(screener.daily_screen)

# Keep the scheduler running
while True:
    schedule.run_pending()
    time.sleep(60)

Web Dashboard Integration

Create a simple web interface for your results:

from flask import Flask, render_template, jsonify
import json

app = Flask(__name__)

@app.route('/')
def dashboard():
    """Main dashboard showing screening results"""
    return render_template('dashboard.html')

@app.route('/api/screening-results')
def get_screening_results():
    """API endpoint for latest screening results"""
    
    # Load latest results
    latest_file = f"sp500_value_report_{datetime.now().strftime('%Y%m%d')}.json"
    
    try:
        with open(latest_file, 'r') as f:
            results = json.load(f)
        return jsonify(results)
    except FileNotFoundError:
        return jsonify({"error": "No recent results available"})

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Troubleshooting and Best Practices

Common Issues and Solutions

Ollama Connection Problems:

# Check if Ollama is running
ollama ps

# Restart Ollama service
ollama serve

# Test model availability
ollama run llama2:13b "Hello"

Rate Limiting with Financial APIs:

def handle_rate_limits():
    """Implement proper rate limiting for API calls"""
    
    import time
    from functools import wraps
    
    def rate_limit(calls_per_minute=60):
        min_interval = 60.0 / calls_per_minute
        last_called = [0.0]
        
        def decorator(func):
            @wraps(func)
            def wrapper(*args, **kwargs):
                elapsed = time.time() - last_called[0]
                left_to_wait = min_interval - elapsed
                if left_to_wait > 0:
                    time.sleep(left_to_wait)
                ret = func(*args, **kwargs)
                last_called[0] = time.time()
                return ret
            return wrapper
        return decorator
    
    return rate_limit

Performance Optimization Tips

  1. Cache financial data to avoid repeated API calls
  2. Use batch processing for large datasets
  3. Implement incremental updates rather than full rescreening
  4. Monitor Ollama model performance and switch models if needed

Conclusion

You now have a complete S&P 500 stock screening system powered by Ollama's AI capabilities. This value investing analysis tool processes financial data, applies sophisticated screening logic, and generates actionable investment insights—all running locally on your machine.

The system combines traditional value investing principles with modern AI analysis, giving you a competitive edge in identifying undervalued opportunities in the S&P 500. Start with small batches to test your setup, then scale to analyze the full index as your system stabilizes.

Key benefits of this approach:

  • Complete data privacy with local AI processing
  • Customizable screening criteria for your investment style
  • Automated analysis that scales to 500+ stocks
  • Cost-effective alternative to expensive financial platforms

Ready to find your next value investment? Fire up Ollama and start screening!


Want to enhance this system further? Consider adding sector-specific analysis, ESG scoring, or integration with your existing portfolio management tools.