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
- Cache financial data to avoid repeated API calls
- Use batch processing for large datasets
- Implement incremental updates rather than full rescreening
- 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.