AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning

This commit is contained in:
Krilly
2026-03-04 13:29:22 +00:00
parent 29a98137a7
commit 57dd294675
13706 changed files with 2114953 additions and 237629 deletions

View File

@@ -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()

View File

@@ -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())

View File

@@ -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()