AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning
This commit is contained in:
@@ -0,0 +1,457 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Institutional Flow Tracker - Single Stock Deep Dive
|
||||
|
||||
Provides detailed analysis of institutional ownership for a specific stock,
|
||||
including historical trends, top holders, and position changes.
|
||||
|
||||
Usage:
|
||||
python3 analyze_single_stock.py AAPL
|
||||
python3 analyze_single_stock.py MSFT --quarters 12 --api-key YOUR_KEY
|
||||
python3 analyze_single_stock.py TSLA --compare-to GM
|
||||
|
||||
Requirements:
|
||||
- FMP API key (set FMP_API_KEY environment variable or pass --api-key)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional
|
||||
from collections import defaultdict
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("Error: 'requests' library not installed. Install with: pip install requests")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class SingleStockAnalyzer:
|
||||
"""Analyze institutional ownership for a single stock"""
|
||||
|
||||
def __init__(self, api_key: str):
|
||||
self.api_key = api_key
|
||||
self.base_url = "https://financialmodelingprep.com/api/v3"
|
||||
|
||||
def get_institutional_holders(self, symbol: str) -> List[Dict]:
|
||||
"""Get all institutional holders data for a stock"""
|
||||
url = f"{self.base_url}/institutional-holder/{symbol}"
|
||||
params = {"apikey": self.api_key}
|
||||
|
||||
try:
|
||||
response = requests.get(url, params=params, timeout=30)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return data if isinstance(data, list) else []
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error fetching institutional holders for {symbol}: {e}")
|
||||
return []
|
||||
|
||||
def get_company_profile(self, symbol: str) -> Dict:
|
||||
"""Get company profile information"""
|
||||
url = f"{self.base_url}/profile/{symbol}"
|
||||
params = {"apikey": self.api_key}
|
||||
|
||||
try:
|
||||
response = requests.get(url, params=params, timeout=30)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return data[0] if isinstance(data, list) and data else {}
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error fetching company profile for {symbol}: {e}")
|
||||
return {}
|
||||
|
||||
def analyze_stock(self, symbol: str, quarters: int = 8) -> Dict:
|
||||
"""Perform comprehensive institutional analysis on a stock"""
|
||||
|
||||
print(f"Analyzing institutional ownership for {symbol}...")
|
||||
|
||||
# Get company profile
|
||||
profile = self.get_company_profile(symbol)
|
||||
company_name = profile.get('companyName', symbol)
|
||||
sector = profile.get('sector', 'Unknown')
|
||||
market_cap = profile.get('mktCap', 0)
|
||||
|
||||
print(f"Company: {company_name}")
|
||||
print(f"Sector: {sector}")
|
||||
print(f"Market Cap: ${market_cap:,}")
|
||||
|
||||
# Get institutional holders
|
||||
holders = self.get_institutional_holders(symbol)
|
||||
|
||||
if not holders:
|
||||
print(f"No institutional holder data available for {symbol}")
|
||||
return {}
|
||||
|
||||
# Group by quarter
|
||||
quarters_data = defaultdict(list)
|
||||
for holder in holders:
|
||||
date = holder.get('dateReported', '')
|
||||
if date:
|
||||
quarters_data[date].append(holder)
|
||||
|
||||
# Get most recent N quarters
|
||||
sorted_quarters = sorted(quarters_data.keys(), reverse=True)[:quarters]
|
||||
|
||||
if len(sorted_quarters) < 2:
|
||||
print(f"Insufficient data (need at least 2 quarters, found {len(sorted_quarters)})")
|
||||
return {}
|
||||
|
||||
# Calculate quarterly metrics
|
||||
quarterly_metrics = []
|
||||
for q in sorted_quarters:
|
||||
holders_q = quarters_data[q]
|
||||
total_shares = sum(h.get('totalShares', 0) for h in holders_q)
|
||||
total_value = sum(h.get('totalInvested', 0) for h in holders_q)
|
||||
num_holders = len(holders_q)
|
||||
|
||||
quarterly_metrics.append({
|
||||
'quarter': q,
|
||||
'total_shares': total_shares,
|
||||
'total_value': total_value,
|
||||
'num_holders': num_holders,
|
||||
'top_holders': sorted(holders_q, key=lambda x: x.get('totalShares', 0), reverse=True)[:20]
|
||||
})
|
||||
|
||||
# Calculate trends
|
||||
most_recent = quarterly_metrics[0]
|
||||
oldest = quarterly_metrics[-1]
|
||||
|
||||
shares_trend = ((most_recent['total_shares'] - oldest['total_shares']) / oldest['total_shares'] * 100) if oldest['total_shares'] > 0 else 0
|
||||
holders_trend = most_recent['num_holders'] - oldest['num_holders']
|
||||
|
||||
# Analyze position changes (recent quarter vs previous)
|
||||
if len(quarterly_metrics) >= 2:
|
||||
current_q = quarterly_metrics[0]
|
||||
previous_q = quarterly_metrics[1]
|
||||
|
||||
# Create holder dictionaries for comparison
|
||||
current_holders_map = {h.get('holder', ''): h for h in current_q['top_holders']}
|
||||
previous_holders_map = {h.get('holder', ''): h for h in previous_q['top_holders']}
|
||||
|
||||
# Categorize changes
|
||||
new_positions = []
|
||||
increased_positions = []
|
||||
decreased_positions = []
|
||||
closed_positions = []
|
||||
|
||||
# Check current holders
|
||||
for name, holder in current_holders_map.items():
|
||||
current_shares = holder.get('totalShares', 0)
|
||||
if name in previous_holders_map:
|
||||
previous_shares = previous_holders_map[name].get('totalShares', 0)
|
||||
change = current_shares - previous_shares
|
||||
pct_change = (change / previous_shares * 100) if previous_shares > 0 else 0
|
||||
|
||||
if change > 0:
|
||||
increased_positions.append({
|
||||
'name': name,
|
||||
'current_shares': current_shares,
|
||||
'change': change,
|
||||
'pct_change': pct_change
|
||||
})
|
||||
elif change < 0:
|
||||
decreased_positions.append({
|
||||
'name': name,
|
||||
'current_shares': current_shares,
|
||||
'change': change,
|
||||
'pct_change': pct_change
|
||||
})
|
||||
else:
|
||||
new_positions.append({
|
||||
'name': name,
|
||||
'shares': current_shares
|
||||
})
|
||||
|
||||
# Check for closed positions
|
||||
for name, holder in previous_holders_map.items():
|
||||
if name not in current_holders_map:
|
||||
closed_positions.append({
|
||||
'name': name,
|
||||
'previous_shares': holder.get('totalShares', 0)
|
||||
})
|
||||
|
||||
# Sort by magnitude
|
||||
increased_positions.sort(key=lambda x: x['change'], reverse=True)
|
||||
decreased_positions.sort(key=lambda x: x['change'])
|
||||
|
||||
else:
|
||||
new_positions = []
|
||||
increased_positions = []
|
||||
decreased_positions = []
|
||||
closed_positions = []
|
||||
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'company_name': company_name,
|
||||
'sector': sector,
|
||||
'market_cap': market_cap,
|
||||
'quarterly_metrics': quarterly_metrics,
|
||||
'shares_trend': shares_trend,
|
||||
'holders_trend': holders_trend,
|
||||
'new_positions': new_positions,
|
||||
'increased_positions': increased_positions,
|
||||
'decreased_positions': decreased_positions,
|
||||
'closed_positions': closed_positions
|
||||
}
|
||||
|
||||
def generate_report(self, analysis: Dict, output_file: Optional[str] = None):
|
||||
"""Generate detailed markdown report"""
|
||||
|
||||
if not analysis:
|
||||
print("No analysis data available")
|
||||
return
|
||||
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
symbol = analysis['symbol']
|
||||
|
||||
report = f"""# Institutional Ownership Analysis: {symbol}
|
||||
|
||||
**Company:** {analysis['company_name']}
|
||||
**Sector:** {analysis['sector']}
|
||||
**Market Cap:** ${analysis['market_cap']:,}
|
||||
**Analysis Date:** {timestamp}
|
||||
|
||||
## Executive Summary
|
||||
|
||||
"""
|
||||
|
||||
# Determine overall signal
|
||||
shares_trend = analysis['shares_trend']
|
||||
holders_trend = analysis['holders_trend']
|
||||
|
||||
if shares_trend > 15 and holders_trend > 5:
|
||||
signal = "🟢 **STRONG ACCUMULATION**"
|
||||
interpretation = "Strong institutional buying with increasing participation. Positive signal."
|
||||
elif shares_trend > 7 and holders_trend > 0:
|
||||
signal = "🟢 **MODERATE ACCUMULATION**"
|
||||
interpretation = "Steady institutional buying. Moderately positive signal."
|
||||
elif shares_trend < -15 or holders_trend < -5:
|
||||
signal = "🔴 **STRONG DISTRIBUTION**"
|
||||
interpretation = "Significant institutional selling. Warning sign - investigate further."
|
||||
elif shares_trend < -7:
|
||||
signal = "🔴 **MODERATE DISTRIBUTION**"
|
||||
interpretation = "Institutional selling detected. Monitor closely."
|
||||
else:
|
||||
signal = "⚪ **NEUTRAL**"
|
||||
interpretation = "No significant institutional flow changes. Stable ownership."
|
||||
|
||||
report += f"""**Signal:** {signal}
|
||||
|
||||
**Interpretation:** {interpretation}
|
||||
|
||||
**Trend ({len(analysis['quarterly_metrics'])} Quarters):**
|
||||
- Institutional Shares: {shares_trend:+.2f}%
|
||||
- Number of Institutions: {holders_trend:+d}
|
||||
|
||||
## Historical Institutional Ownership Trend
|
||||
|
||||
| Quarter | Total Shares Held | Total Value | Number of Institutions | QoQ Change |
|
||||
|---------|-------------------|-------------|----------------------|------------|
|
||||
"""
|
||||
|
||||
# Add quarterly data
|
||||
metrics = analysis['quarterly_metrics']
|
||||
for i, q in enumerate(metrics):
|
||||
if i < len(metrics) - 1:
|
||||
prev_shares = metrics[i+1]['total_shares']
|
||||
qoq_change = ((q['total_shares'] - prev_shares) / prev_shares * 100) if prev_shares > 0 else 0
|
||||
qoq_str = f"{qoq_change:+.2f}%"
|
||||
else:
|
||||
qoq_str = "N/A"
|
||||
|
||||
report += f"| {q['quarter']} | {q['total_shares']:,} | ${q['total_value']:,} | {q['num_holders']} | {qoq_str} |\n"
|
||||
|
||||
# Recent changes
|
||||
report += f"""
|
||||
## Recent Quarter Changes ({metrics[0]['quarter']} vs {metrics[1]['quarter']})
|
||||
|
||||
### New Positions (Institutions that newly initiated)
|
||||
|
||||
"""
|
||||
if analysis['new_positions']:
|
||||
report += "| Institution | Shares Acquired |\n"
|
||||
report += "|-------------|----------------|\n"
|
||||
for pos in analysis['new_positions'][:10]:
|
||||
report += f"| {pos['name']} | {pos['shares']:,} |\n"
|
||||
else:
|
||||
report += "No new institutional positions detected.\n"
|
||||
|
||||
report += "\n### Increased Positions (Top 10)\n\n"
|
||||
if analysis['increased_positions']:
|
||||
report += "| Institution | Current Shares | Change | % Change |\n"
|
||||
report += "|-------------|----------------|--------|----------|\n"
|
||||
for pos in analysis['increased_positions'][:10]:
|
||||
report += f"| {pos['name']} | {pos['current_shares']:,} | {pos['change']:+,} | {pos['pct_change']:+.2f}% |\n"
|
||||
else:
|
||||
report += "No significant position increases detected.\n"
|
||||
|
||||
report += "\n### Decreased Positions (Top 10)\n\n"
|
||||
if analysis['decreased_positions']:
|
||||
report += "| Institution | Current Shares | Change | % Change |\n"
|
||||
report += "|-------------|----------------|--------|----------|\n"
|
||||
for pos in analysis['decreased_positions'][:10]:
|
||||
report += f"| {pos['name']} | {pos['current_shares']:,} | {pos['change']:,} | {pos['pct_change']:.2f}% |\n"
|
||||
else:
|
||||
report += "No significant position decreases detected.\n"
|
||||
|
||||
report += "\n### Closed Positions (Institutions that exited)\n\n"
|
||||
if analysis['closed_positions']:
|
||||
report += "| Institution | Previous Shares |\n"
|
||||
report += "|-------------|-----------------|\n"
|
||||
for pos in analysis['closed_positions'][:10]:
|
||||
report += f"| {pos['name']} | {pos['previous_shares']:,} |\n"
|
||||
else:
|
||||
report += "No institutional exits detected.\n"
|
||||
|
||||
# Top current holders
|
||||
report += f"\n## Top 20 Current Institutional Holders ({metrics[0]['quarter']})\n\n"
|
||||
report += "| Rank | Institution | Shares Held | % of Institutional | Latest Change |\n"
|
||||
report += "|------|-------------|-------------|-------------------|---------------|\n"
|
||||
|
||||
total_inst_shares = metrics[0]['total_shares']
|
||||
for i, holder in enumerate(metrics[0]['top_holders'], 1):
|
||||
shares = holder.get('totalShares', 0)
|
||||
pct_of_inst = (shares / total_inst_shares * 100) if total_inst_shares > 0 else 0
|
||||
change = holder.get('change', 0)
|
||||
report += f"| {i} | {holder.get('holder', 'Unknown')} | {shares:,} | {pct_of_inst:.2f}% | {change:+,} |\n"
|
||||
|
||||
# Concentration analysis
|
||||
if len(metrics[0]['top_holders']) >= 10:
|
||||
top_10_shares = sum(h.get('totalShares', 0) for h in metrics[0]['top_holders'][:10])
|
||||
concentration = (top_10_shares / total_inst_shares * 100) if total_inst_shares > 0 else 0
|
||||
|
||||
report += f"""
|
||||
## Concentration Analysis
|
||||
|
||||
**Top 10 Holders Concentration:** {concentration:.2f}%
|
||||
|
||||
**Interpretation:**
|
||||
"""
|
||||
if concentration > 60:
|
||||
report += "- **High Concentration** - Top 10 institutions control majority of institutional ownership\n"
|
||||
report += "- **Risk:** Significant price impact if top holders sell\n"
|
||||
report += "- **Opportunity:** May indicate high conviction by quality investors\n"
|
||||
elif concentration > 40:
|
||||
report += "- **Moderate Concentration** - Balanced institutional ownership\n"
|
||||
report += "- **Risk:** Moderate concentration risk\n"
|
||||
else:
|
||||
report += "- **Low Concentration** - Widely distributed institutional ownership\n"
|
||||
report += "- **Risk:** Lower concentration risk, more stable ownership\n"
|
||||
|
||||
report += """
|
||||
## Interpretation Guide
|
||||
|
||||
**For detailed interpretation framework, see:**
|
||||
`institutional-flow-tracker/references/interpretation_framework.md`
|
||||
|
||||
**Next Steps:**
|
||||
1. Validate institutional signal with fundamental analysis
|
||||
2. Check technical setup for entry timing
|
||||
3. Review sector-wide institutional trends
|
||||
4. Monitor quarterly for trend continuation/reversal
|
||||
|
||||
---
|
||||
|
||||
**Data Source:** FMP API (13F SEC Filings)
|
||||
**Data Lag:** ~45 days after quarter end
|
||||
**Note:** Use as confirming indicator alongside fundamental and technical analysis
|
||||
"""
|
||||
|
||||
# Save report
|
||||
if output_file:
|
||||
output_path = output_file if output_file.endswith('.md') else f"{output_file}.md"
|
||||
else:
|
||||
output_path = f"institutional_analysis_{symbol}_{datetime.now().strftime('%Y%m%d')}.md"
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
f.write(report)
|
||||
|
||||
print(f"\n✅ Report saved to: {output_path}")
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Analyze institutional ownership for a specific stock',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Basic analysis
|
||||
python3 analyze_single_stock.py AAPL
|
||||
|
||||
# Extended history (12 quarters)
|
||||
python3 analyze_single_stock.py MSFT --quarters 12
|
||||
|
||||
# With custom output
|
||||
python3 analyze_single_stock.py TSLA --output tesla_analysis.md
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'symbol',
|
||||
type=str,
|
||||
help='Stock ticker symbol to analyze'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--api-key',
|
||||
type=str,
|
||||
default=os.getenv('FMP_API_KEY'),
|
||||
help='FMP API key (or set FMP_API_KEY environment variable)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--quarters',
|
||||
type=int,
|
||||
default=8,
|
||||
help='Number of quarters to analyze (default: 8, i.e., 2 years)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--output',
|
||||
type=str,
|
||||
help='Output file path for markdown report'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--compare-to',
|
||||
type=str,
|
||||
help='Compare to another stock (optional, future feature)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate API key
|
||||
if not args.api_key:
|
||||
print("Error: FMP API key required")
|
||||
print("Set FMP_API_KEY environment variable or pass --api-key argument")
|
||||
print("Get free API key at: https://financialmodelingprep.com/developer/docs")
|
||||
sys.exit(1)
|
||||
|
||||
# Initialize analyzer
|
||||
analyzer = SingleStockAnalyzer(args.api_key)
|
||||
|
||||
# Run analysis
|
||||
analysis = analyzer.analyze_stock(args.symbol.upper(), quarters=args.quarters)
|
||||
|
||||
if not analysis:
|
||||
print(f"Unable to complete analysis for {args.symbol}")
|
||||
sys.exit(1)
|
||||
|
||||
# Generate report
|
||||
analyzer.generate_report(analysis, output_file=args.output)
|
||||
|
||||
# Print summary
|
||||
print("\n" + "="*80)
|
||||
print(f"INSTITUTIONAL OWNERSHIP SUMMARY: {args.symbol}")
|
||||
print("="*80)
|
||||
print(f"Trend ({args.quarters} quarters): {analysis['shares_trend']:+.2f}% shares, {analysis['holders_trend']:+d} institutions")
|
||||
print(f"Recent Activity:")
|
||||
print(f" - New Positions: {len(analysis['new_positions'])}")
|
||||
print(f" - Increased: {len(analysis['increased_positions'])}")
|
||||
print(f" - Decreased: {len(analysis['decreased_positions'])}")
|
||||
print(f" - Exited: {len(analysis['closed_positions'])}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Institutional Flow Tracker - Portfolio Tracking
|
||||
|
||||
Track portfolio changes of specific institutional investors (hedge funds, mutual funds)
|
||||
by analyzing their 13F filings over time. Follow superinvestors like Warren Buffett.
|
||||
|
||||
Usage:
|
||||
python3 track_institution_portfolio.py --cik 0001067983 --name "Berkshire Hathaway"
|
||||
python3 track_institution_portfolio.py --cik 0001579982 --name "ARK Investment"
|
||||
|
||||
Note: This is a simplified version. For full portfolio tracking, use WhaleWisdom or
|
||||
SEC EDGAR directly, as FMP API has limited institution-specific endpoints.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
print("""
|
||||
================================================================================
|
||||
TRACK INSTITUTION PORTFOLIO - SIMPLIFIED VERSION
|
||||
================================================================================
|
||||
|
||||
This is a placeholder script. For comprehensive institution portfolio tracking:
|
||||
|
||||
1. Use WhaleWisdom (free tier available): https://whalewisdom.com
|
||||
2. Use SEC EDGAR directly: https://www.sec.gov/cgi-bin/browse-edgar
|
||||
3. Use DataRoma for superinvestors: https://www.dataroma.com
|
||||
|
||||
FMP API has limited institution-specific portfolio endpoints. The institutional
|
||||
holder data is organized by stock (not by institution), making it difficult to
|
||||
efficiently reconstruct an institution's full portfolio.
|
||||
|
||||
Recommended Workflow:
|
||||
--------------------
|
||||
1. Visit WhaleWisdom or DataRoma
|
||||
2. Search for your target institution by name or CIK
|
||||
3. View their current portfolio and quarterly changes
|
||||
4. Use this skill's other scripts to analyze specific stocks they hold
|
||||
|
||||
Notable Institutions to Track:
|
||||
-----------------------------
|
||||
- Berkshire Hathaway (CIK: 0001067983) - Warren Buffett
|
||||
- Baupost Group (CIK: 0001061768) - Seth Klarman
|
||||
- Pershing Square (CIK: 0001336528) - Bill Ackman
|
||||
- Appaloosa Management (CIK: 0001079114) - David Tepper
|
||||
- Third Point (CIK: 0001040273) - Dan Loeb
|
||||
- ARK Investment (CIK: 0001579982) - Cathie Wood
|
||||
- Fidelity Management (CIK: 0000315066)
|
||||
- T. Rowe Price (CIK: 0001113169)
|
||||
- Dodge & Cox (CIK: 0000922614)
|
||||
|
||||
Alternative Approach:
|
||||
-------------------
|
||||
If you know the top holdings of an institution, you can use the
|
||||
analyze_single_stock.py script to see if they've changed their positions:
|
||||
|
||||
Example for Berkshire's top holdings:
|
||||
python3 analyze_single_stock.py AAPL # Check if Berkshire changed position
|
||||
python3 analyze_single_stock.py KO # Coca-Cola
|
||||
python3 analyze_single_stock.py BAC # Bank of America
|
||||
|
||||
================================================================================
|
||||
""")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Track institutional investor portfolio changes (simplified)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--cik',
|
||||
type=str,
|
||||
required=True,
|
||||
help='Central Index Key of the institution'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--name',
|
||||
type=str,
|
||||
required=True,
|
||||
help='Institution name'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--api-key',
|
||||
type=str,
|
||||
default=os.getenv('FMP_API_KEY'),
|
||||
help='FMP API key (currently not used in simplified version)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"Institution: {args.name}")
|
||||
print(f"CIK: {args.cik}")
|
||||
print()
|
||||
print("For detailed portfolio tracking, please use:")
|
||||
print(f"1. WhaleWisdom: https://whalewisdom.com/filer/{args.name.lower().replace(' ', '-')}")
|
||||
print(f"2. SEC EDGAR: https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK={args.cik}&type=13F")
|
||||
print(f"3. DataRoma: https://www.dataroma.com (if superinvestor)")
|
||||
print()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,450 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Institutional Flow Tracker - Main Screening Script
|
||||
|
||||
Screens for stocks with significant institutional ownership changes by analyzing
|
||||
13F filings data. Identifies stocks where smart money is accumulating or distributing.
|
||||
|
||||
Usage:
|
||||
python3 track_institutional_flow.py --top 50 --min-change-percent 10
|
||||
python3 track_institutional_flow.py --sector Technology --min-institutions 20
|
||||
python3 track_institutional_flow.py --api-key YOUR_KEY --output results.json
|
||||
|
||||
Requirements:
|
||||
- FMP API key (set FMP_API_KEY environment variable or pass --api-key)
|
||||
- Free tier: 250 requests/day (sufficient for ~40-50 stocks)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional
|
||||
import time
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("Error: 'requests' library not installed. Install with: pip install requests")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class InstitutionalFlowTracker:
|
||||
"""Track institutional ownership changes across stocks"""
|
||||
|
||||
def __init__(self, api_key: str):
|
||||
self.api_key = api_key
|
||||
self.base_url = "https://financialmodelingprep.com/api/v3"
|
||||
self.base_url_v4 = "https://financialmodelingprep.com/api/v4"
|
||||
|
||||
def get_stock_screener(
|
||||
self,
|
||||
market_cap_min: int = 1000000000,
|
||||
limit: int = 500
|
||||
) -> List[Dict]:
|
||||
"""Get list of stocks meeting market cap criteria"""
|
||||
url = f"{self.base_url}/stock-screener"
|
||||
params = {
|
||||
"marketCapMoreThan": market_cap_min,
|
||||
"limit": limit,
|
||||
"apikey": self.api_key
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(url, params=params, timeout=30)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error fetching stock screener: {e}")
|
||||
return []
|
||||
|
||||
def get_institutional_holders(self, symbol: str) -> List[Dict]:
|
||||
"""Get institutional holders for a specific stock"""
|
||||
url = f"{self.base_url}/institutional-holder/{symbol}"
|
||||
params = {"apikey": self.api_key}
|
||||
|
||||
try:
|
||||
response = requests.get(url, params=params, timeout=30)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return data if isinstance(data, list) else []
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error fetching institutional holders for {symbol}: {e}")
|
||||
return []
|
||||
|
||||
def calculate_ownership_metrics(
|
||||
self,
|
||||
symbol: str,
|
||||
company_name: str,
|
||||
market_cap: float
|
||||
) -> Optional[Dict]:
|
||||
"""Calculate institutional ownership metrics for a stock"""
|
||||
holders = self.get_institutional_holders(symbol)
|
||||
|
||||
if not holders or len(holders) < 2:
|
||||
return None
|
||||
|
||||
# Group by date to get quarterly snapshots
|
||||
quarters = {}
|
||||
for holder in holders:
|
||||
date = holder.get('dateReported', '')
|
||||
if not date:
|
||||
continue
|
||||
if date not in quarters:
|
||||
quarters[date] = []
|
||||
quarters[date].append(holder)
|
||||
|
||||
# Need at least 2 quarters for comparison
|
||||
if len(quarters) < 2:
|
||||
return None
|
||||
|
||||
# Get most recent 2 quarters
|
||||
sorted_quarters = sorted(quarters.keys(), reverse=True)
|
||||
current_q = sorted_quarters[0]
|
||||
previous_q = sorted_quarters[1]
|
||||
|
||||
current_holders = quarters[current_q]
|
||||
previous_holders = quarters[previous_q]
|
||||
|
||||
# Calculate aggregate metrics
|
||||
current_total_shares = sum(h.get('totalShares', 0) for h in current_holders)
|
||||
previous_total_shares = sum(h.get('totalShares', 0) for h in previous_holders)
|
||||
|
||||
# Calculate changes
|
||||
shares_change = current_total_shares - previous_total_shares
|
||||
if previous_total_shares > 0:
|
||||
percent_change = (shares_change / previous_total_shares) * 100
|
||||
else:
|
||||
percent_change = 0
|
||||
|
||||
# Count institutions
|
||||
current_count = len(current_holders)
|
||||
previous_count = len(previous_holders)
|
||||
institution_change = current_count - previous_count
|
||||
|
||||
# Calculate dollar value (approximate)
|
||||
# Note: This is approximate as we don't have exact prices
|
||||
current_value = sum(h.get('totalInvested', 0) for h in current_holders)
|
||||
previous_value = sum(h.get('totalInvested', 0) for h in previous_holders)
|
||||
value_change = current_value - previous_value
|
||||
|
||||
# Get top holders
|
||||
top_holders = sorted(
|
||||
current_holders,
|
||||
key=lambda x: x.get('totalShares', 0),
|
||||
reverse=True
|
||||
)[:10]
|
||||
|
||||
top_holder_names = [
|
||||
{
|
||||
'name': h.get('holder', 'Unknown'),
|
||||
'shares': h.get('totalShares', 0),
|
||||
'change': h.get('change', 0)
|
||||
}
|
||||
for h in top_holders
|
||||
]
|
||||
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'company_name': company_name,
|
||||
'market_cap': market_cap,
|
||||
'current_quarter': current_q,
|
||||
'previous_quarter': previous_q,
|
||||
'current_total_shares': current_total_shares,
|
||||
'previous_total_shares': previous_total_shares,
|
||||
'shares_change': shares_change,
|
||||
'percent_change': round(percent_change, 2),
|
||||
'current_institution_count': current_count,
|
||||
'previous_institution_count': previous_count,
|
||||
'institution_count_change': institution_change,
|
||||
'current_value': current_value,
|
||||
'previous_value': previous_value,
|
||||
'value_change': value_change,
|
||||
'top_holders': top_holder_names
|
||||
}
|
||||
|
||||
def screen_stocks(
|
||||
self,
|
||||
min_market_cap: int = 1000000000,
|
||||
min_change_percent: float = 10.0,
|
||||
min_institutions: int = 10,
|
||||
sector: Optional[str] = None,
|
||||
top: int = 50,
|
||||
sort_by: str = 'ownership_change'
|
||||
) -> List[Dict]:
|
||||
"""Screen for stocks with significant institutional changes"""
|
||||
|
||||
print(f"Fetching stocks with market cap >= ${min_market_cap:,}...")
|
||||
stocks = self.get_stock_screener(market_cap_min=min_market_cap, limit=500)
|
||||
|
||||
if not stocks:
|
||||
print("No stocks found in screener")
|
||||
return []
|
||||
|
||||
# Filter by sector if specified
|
||||
if sector:
|
||||
stocks = [s for s in stocks if s.get('sector', '').lower() == sector.lower()]
|
||||
print(f"Filtered to {len(stocks)} stocks in {sector} sector")
|
||||
|
||||
print(f"Analyzing institutional ownership for {len(stocks)} stocks...")
|
||||
print("This may take a few minutes. Please wait...\n")
|
||||
|
||||
results = []
|
||||
for i, stock in enumerate(stocks, 1):
|
||||
symbol = stock.get('symbol', '')
|
||||
company_name = stock.get('companyName', '')
|
||||
market_cap = stock.get('marketCap', 0)
|
||||
|
||||
if i % 10 == 0:
|
||||
print(f"Progress: {i}/{len(stocks)} stocks analyzed...")
|
||||
|
||||
# Rate limiting: max 5 requests per second
|
||||
time.sleep(0.2)
|
||||
|
||||
metrics = self.calculate_ownership_metrics(symbol, company_name, market_cap)
|
||||
|
||||
if metrics:
|
||||
# Apply filters
|
||||
if abs(metrics['percent_change']) >= min_change_percent:
|
||||
if metrics['current_institution_count'] >= min_institutions:
|
||||
results.append(metrics)
|
||||
|
||||
print(f"\nFound {len(results)} stocks meeting criteria")
|
||||
|
||||
# Sort results
|
||||
if sort_by == 'ownership_change':
|
||||
results.sort(key=lambda x: abs(x['percent_change']), reverse=True)
|
||||
elif sort_by == 'institution_count_change':
|
||||
results.sort(key=lambda x: abs(x['institution_count_change']), reverse=True)
|
||||
elif sort_by == 'dollar_value_change':
|
||||
results.sort(key=lambda x: abs(x['value_change']), reverse=True)
|
||||
|
||||
return results[:top]
|
||||
|
||||
def generate_report(self, results: List[Dict], output_file: str = None):
|
||||
"""Generate markdown report from screening results"""
|
||||
|
||||
if not results:
|
||||
print("No results to report")
|
||||
return
|
||||
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
report = f"""# Institutional Flow Analysis Report
|
||||
**Generated:** {timestamp}
|
||||
**Stocks Analyzed:** {len(results)}
|
||||
|
||||
## Summary
|
||||
|
||||
This report identifies stocks with significant institutional ownership changes based on 13F filings data.
|
||||
|
||||
### Key Findings
|
||||
|
||||
**Top Accumulators (Institutions Buying):**
|
||||
"""
|
||||
|
||||
# Top accumulators
|
||||
accumulators = [r for r in results if r['percent_change'] > 0][:10]
|
||||
if accumulators:
|
||||
report += "\n| Symbol | Company | Ownership Change | Institution Change | Top Holder |\n"
|
||||
report += "|--------|---------|-----------------|-------------------|------------|\n"
|
||||
for r in accumulators:
|
||||
top_holder = r['top_holders'][0]['name'] if r['top_holders'] else 'N/A'
|
||||
report += f"| {r['symbol']} | {r['company_name'][:30]} | **+{r['percent_change']}%** | +{r['institution_count_change']} | {top_holder[:30]} |\n"
|
||||
else:
|
||||
report += "\nNo significant accumulation detected.\n"
|
||||
|
||||
report += "\n**Top Distributors (Institutions Selling):**\n"
|
||||
|
||||
# Top distributors
|
||||
distributors = [r for r in results if r['percent_change'] < 0][:10]
|
||||
if distributors:
|
||||
report += "\n| Symbol | Company | Ownership Change | Institution Change | Previously Top Holder |\n"
|
||||
report += "|--------|---------|-----------------|-------------------|-----------------------|\n"
|
||||
for r in distributors:
|
||||
top_holder = r['top_holders'][0]['name'] if r['top_holders'] else 'N/A'
|
||||
report += f"| {r['symbol']} | {r['company_name'][:30]} | **{r['percent_change']}%** | {r['institution_count_change']} | {top_holder[:30]} |\n"
|
||||
else:
|
||||
report += "\nNo significant distribution detected.\n"
|
||||
|
||||
report += "\n## Detailed Results\n\n"
|
||||
|
||||
for r in results[:20]: # Top 20 detailed
|
||||
direction = "Accumulation" if r['percent_change'] > 0 else "Distribution"
|
||||
report += f"""### {r['symbol']} - {r['company_name']}
|
||||
|
||||
**Signal:** {direction} ({r['percent_change']:+.2f}% institutional ownership change)
|
||||
|
||||
**Metrics:**
|
||||
- Market Cap: ${r['market_cap']:,.0f}
|
||||
- Current Quarter: {r['current_quarter']}
|
||||
- Previous Quarter: {r['previous_quarter']}
|
||||
- Institution Count Change: {r['institution_count_change']:+d} ({r['previous_institution_count']} → {r['current_institution_count']})
|
||||
- Total Shares Change: {r['shares_change']:+,.0f}
|
||||
- Estimated Value Change: ${r['value_change']:+,.0f}
|
||||
|
||||
**Top 5 Current Holders:**
|
||||
"""
|
||||
for i, holder in enumerate(r['top_holders'][:5], 1):
|
||||
report += f"{i}. {holder['name']}: {holder['shares']:,} shares (Change: {holder['change']:+,})\n"
|
||||
|
||||
report += "\n---\n\n"
|
||||
|
||||
report += f"""
|
||||
## Interpretation Guide
|
||||
|
||||
**Strong Accumulation (>15% increase):**
|
||||
- Monitor for potential breakout
|
||||
- Validate with fundamental analysis
|
||||
- Consider initiating/adding to position
|
||||
|
||||
**Moderate Accumulation (7-15% increase):**
|
||||
- Positive signal
|
||||
- Combine with other analysis
|
||||
- Watch for continuation
|
||||
|
||||
**Strong Distribution (>15% decrease):**
|
||||
- Warning sign
|
||||
- Re-evaluate thesis
|
||||
- Consider trimming/exiting
|
||||
|
||||
**Moderate Distribution (7-15% decrease):**
|
||||
- Early warning
|
||||
- Monitor closely
|
||||
- Tighten stop-loss
|
||||
|
||||
For detailed interpretation framework, see:
|
||||
`institutional-flow-tracker/references/interpretation_framework.md`
|
||||
|
||||
---
|
||||
|
||||
**Data Source:** Financial Modeling Prep API (13F Filings)
|
||||
**Note:** 13F data has ~45-day reporting lag. Use as confirming indicator, not real-time signal.
|
||||
"""
|
||||
|
||||
# Save to file
|
||||
if output_file:
|
||||
output_path = output_file if output_file.endswith('.md') else f"{output_file}.md"
|
||||
with open(output_path, 'w') as f:
|
||||
f.write(report)
|
||||
print(f"\nReport saved to: {output_path}")
|
||||
else:
|
||||
output_path = f"institutional_flow_screening_{datetime.now().strftime('%Y%m%d')}.md"
|
||||
with open(output_path, 'w') as f:
|
||||
f.write(report)
|
||||
print(f"\nReport saved to: {output_path}")
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Track institutional ownership changes across stocks',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Screen top 50 stocks by institutional change (>10%)
|
||||
python3 track_institutional_flow.py --top 50 --min-change-percent 10
|
||||
|
||||
# Focus on Technology sector
|
||||
python3 track_institutional_flow.py --sector Technology --min-institutions 20
|
||||
|
||||
# Custom screening with output
|
||||
python3 track_institutional_flow.py --min-market-cap 2000000000 --top 100 --output results.json
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--api-key',
|
||||
type=str,
|
||||
default=os.getenv('FMP_API_KEY'),
|
||||
help='FMP API key (or set FMP_API_KEY environment variable)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--top',
|
||||
type=int,
|
||||
default=50,
|
||||
help='Number of top stocks to return (default: 50)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--min-change-percent',
|
||||
type=float,
|
||||
default=10.0,
|
||||
help='Minimum %% change in institutional ownership (default: 10.0)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--min-market-cap',
|
||||
type=int,
|
||||
default=1000000000,
|
||||
help='Minimum market cap in dollars (default: 1B)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--sector',
|
||||
type=str,
|
||||
help='Filter by specific sector (e.g., Technology, Healthcare)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--min-institutions',
|
||||
type=int,
|
||||
default=10,
|
||||
help='Minimum number of institutional holders (default: 10)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--sort-by',
|
||||
type=str,
|
||||
choices=['ownership_change', 'institution_count_change', 'dollar_value_change'],
|
||||
default='ownership_change',
|
||||
help='Sort results by metric (default: ownership_change)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--output',
|
||||
type=str,
|
||||
help='Output file path for JSON results'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate API key
|
||||
if not args.api_key:
|
||||
print("Error: FMP API key required")
|
||||
print("Set FMP_API_KEY environment variable or pass --api-key argument")
|
||||
print("Get free API key at: https://financialmodelingprep.com/developer/docs")
|
||||
sys.exit(1)
|
||||
|
||||
# Initialize tracker
|
||||
tracker = InstitutionalFlowTracker(args.api_key)
|
||||
|
||||
# Run screening
|
||||
results = tracker.screen_stocks(
|
||||
min_market_cap=args.min_market_cap,
|
||||
min_change_percent=args.min_change_percent,
|
||||
min_institutions=args.min_institutions,
|
||||
sector=args.sector,
|
||||
top=args.top,
|
||||
sort_by=args.sort_by
|
||||
)
|
||||
|
||||
# Save JSON results if requested
|
||||
if args.output:
|
||||
json_output = args.output if args.output.endswith('.json') else f"{args.output}.json"
|
||||
with open(json_output, 'w') as f:
|
||||
json.dump(results, f, indent=2)
|
||||
print(f"JSON results saved to: {json_output}")
|
||||
|
||||
# Generate markdown report
|
||||
tracker.generate_report(results)
|
||||
|
||||
# Print summary
|
||||
if results:
|
||||
print("\n" + "="*80)
|
||||
print("TOP 10 INSTITUTIONAL FLOW CHANGES")
|
||||
print("="*80)
|
||||
print(f"{'Symbol':<8} {'Company':<30} {'Change':>10} {'Institutions':>12}")
|
||||
print("-"*80)
|
||||
for r in results[:10]:
|
||||
print(f"{r['symbol']:<8} {r['company_name'][:30]:<30} {r['percent_change']:>9.2f}% {r['institution_count_change']:>+11d}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user