
Institutional Flow Tracker
- 1.1k installs
- 2.5k repo stars
- Updated July 26, 2026
- tradermonty/claude-trading-skills
institutional-flow-tracker is a Claude Code skill that scans SEC 13F filings to surface stocks where institutional investors are accumulating or exiting positions.
About
institutional-flow-tracker is a finance research skill from tradermonty/claude-trading-skills that analyzes SEC 13F filings to track institutional ownership changes across hedge funds, mutual funds, and pension funds. The skill identifies accumulation and distribution patterns from sophisticated investors, helping developers and quant-minded builders validate ideas, spot early opportunities, and detect exits by tracked managers. It references following well-known filers such as Warren Buffett, Seth Klarman, and Bill Ackman as examples of superinvestor signal sources. Reach for institutional-flow-tracker when building investment research workflows, screening for smart-money inflows, or adding 13F-based alerts to trading or analytics tooling.
- Analyzes quarterly 13F filings to detect >10-15% institutional ownership changes
- Signal Quality Framework with tier-based weighting (superinvestors > active funds > index funds)
- Tracks multi-quarter sustained accumulation or distribution trends over 3+ quarters
- Follow specific institutions including Warren Buffett, Seth Klarman, and Bill Ackman
- Delivers concentration analysis and early warning signals on position exits
Institutional Flow Tracker by the numbers
- 1,102 all-time installs (skills.sh)
- +48 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #128 of 1,136 Finance & Trading skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tradermonty/claude-trading-skills --skill institutional-flow-trackerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.1k |
|---|---|
| repo stars | ★ 2.5k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 26, 2026 |
| Repository | tradermonty/claude-trading-skills ↗ |
How do you track institutional accumulation from 13F filings?
Scan SEC 13F filings and surface stocks where institutional investors are accumulating or exiting positions.
Who is it for?
Developers building investment research tools who need SEC 13F-based institutional flow signals for stock screening.
Skip if: Day traders needing intraday order flow or developers without SEC filing data access should skip institutional-flow-tracker because it relies on quarterly 13F disclosures.
When should I use this skill?
User asks about institutional ownership changes, 13F filings, smart money flows, hedge fund accumulation, or superinvestor portfolio tracking.
What you get
13F accumulation/distribution signals, institutional ownership change list, and smart-money screening results
Files
Institutional Flow Tracker
Overview
This skill tracks institutional investor activity through 13F SEC filings to identify "smart money" flows into and out of stocks. By analyzing quarterly changes in institutional ownership, you can discover stocks that sophisticated investors are accumulating before major price moves, or identify potential risks when institutions are reducing positions.
Key Insight: Institutional investors (hedge funds, pension funds, mutual funds) manage trillions of dollars and conduct extensive research. Their collective buying/selling patterns often precede significant price movements by 1-3 quarters.
Prerequisites
- FMP API Key: Set
FMP_API_KEYenvironment variable or pass--api-keyto scripts - Python 3.9+: Required for running analysis scripts
- Dependencies:
pip install requests(scripts handle missing dependencies gracefully)
When to Use This Skill
Use this skill when:
- Validating investment ideas (checking if smart money agrees with your thesis)
- Discovering new opportunities (finding stocks institutions are accumulating)
- Risk assessment (identifying stocks institutions are exiting)
- Portfolio monitoring (tracking institutional support for your holdings)
- Following specific investors (tracking Warren Buffett, Cathie Wood, etc.)
- Sector rotation analysis (identifying where institutions are rotating capital)
Do NOT use when:
- Seeking real-time intraday signals (13F data has 45-day reporting lag)
- Analyzing micro-cap stocks (<$100M market cap with limited institutional interest)
- Looking for short-term trading signals (<3 months horizon)
Data Sources & Requirements
Required: FMP API Key
This skill uses Financial Modeling Prep (FMP) API to access 13F filing data:
Setup:
# Set environment variable (preferred)
export FMP_API_KEY=your_key_here
# Or provide when running scripts
python3 scripts/track_institutional_flow.py --api-key YOUR_KEYAPI Tier Requirements:
- Free Tier: 250 requests/day (sufficient for analyzing 20-30 stocks quarterly)
- Paid Tiers: Higher limits for extensive screening
13F Filing Schedule:
- Filed quarterly within 45 days after quarter end
- Q1 (Jan-Mar): Filed by mid-May
- Q2 (Apr-Jun): Filed by mid-August
- Q3 (Jul-Sep): Filed by mid-November
- Q4 (Oct-Dec): Filed by mid-February
Analysis Workflow
Step 1: Identify Stocks with Significant Institutional Changes
Execute the main screening script to find stocks with notable institutional activity:
Quick scan (top 50 stocks by institutional change):
python3 scripts/track_institutional_flow.py \
--top 50 \
--min-change-percent 10Sector-focused scan:
python3 scripts/track_institutional_flow.py \
--sector Technology \
--min-institutions 20Custom screening:
python3 scripts/track_institutional_flow.py \
--min-market-cap 2000000000 \
--min-change-percent 15 \
--top 100 \
--output institutional_flow_results.jsonOutput includes:
- Stock ticker and company name
- Current institutional ownership % (of shares outstanding)
- Quarter-over-quarter change in shares held
- Number of institutions holding
- Change in number of institutions (new buyers vs sellers)
- Top institutional holders
Step 2: Deep Dive on Specific Stocks
For detailed analysis of a specific stock's institutional ownership:
python3 scripts/analyze_single_stock.py AAPLThis generates:
- Historical institutional ownership trend (8 quarters)
- Top 20 institutional holders with position changes
- Concentration analysis (top 10 holders' % of total institutional ownership)
- New / increased / decreased positions among the largest holders
- Data quality assessment with coverage-based reliability grade
Key metrics to evaluate:
- Ownership %: Higher institutional ownership (>70%) = more stability but limited upside
- Ownership Trend: Rising ownership = bullish, falling = bearish
- Concentration: High concentration (top 10 > 50%) = risk if they sell
- Quality of Holders: Presence of quality long-term investors (Berkshire, Fidelity) vs momentum funds
Step 3: Track Specific Institutional Investors
Note: track_institution_portfolio.py is not yet implemented. FMP API organizesinstitutional holder data by stock (not by institution), making full portfolio reconstruction
impractical via this API alone.
Alternative approach — use `analyze_single_stock.py` to check if a specific institution holds a stock:
# Analyze a stock and look for a specific institution in the output
python3 institutional-flow-tracker/scripts/analyze_single_stock.py AAPL
# Then search the report for "Berkshire" or "ARK" in the Top 20 holders tableFor full institution-level portfolio tracking, use these external resources: 1. WhaleWisdom: https://whalewisdom.com (free tier available, 13F portfolio viewer) 2. SEC EDGAR: https://www.sec.gov/cgi-bin/browse-edgar (official 13F filings) 3. DataRoma: https://www.dataroma.com (superinvestor portfolio tracker)
Step 4: Interpretation and Action
Read the references for interpretation guidance:
references/13f_filings_guide.md- Understanding 13F data and limitationsreferences/institutional_investor_types.md- Different investor types and their strategiesreferences/interpretation_framework.md- How to interpret institutional flow signals
Signal Strength Framework:
Strong Bullish (Consider buying):
- Institutional ownership increasing >15% QoQ
- Number of institutions increasing >10%
- Quality long-term investors adding positions
- Low current ownership (<40%) with room to grow
- Accumulation happening across multiple quarters
Moderate Bullish:
- Institutional ownership increasing 5-15% QoQ
- Mix of new buyers and sellers, net positive
- Current ownership 40-70%
Neutral:
- Minimal change in ownership (<5%)
- Similar number of buyers and sellers
- Stable institutional base
Moderate Bearish:
- Institutional ownership decreasing 5-15% QoQ
- More sellers than buyers
- High ownership (>80%) limiting new buyers
Strong Bearish (Consider selling/avoiding):
- Institutional ownership decreasing >15% QoQ
- Number of institutions decreasing >10%
- Quality investors exiting positions
- Distribution happening across multiple quarters
- Concentration risk (top holder selling large position)
Step 5: Portfolio Application
For new positions: 1. Run institutional analysis on your stock idea 2. Look for confirmation (institutions also accumulating) 3. If strong bearish signals, reconsider or reduce position size 4. If strong bullish signals, gain confidence in thesis
For existing holdings: 1. Quarterly review after 13F filing deadlines 2. Monitor for distribution (early warning system) 3. If institutions are exiting, re-evaluate your thesis 4. Consider trimming if widespread institutional selling
Screening workflow integration: 1. Use Value Dividend Screener or other screeners to find candidates 2. Run Institutional Flow Tracker on top candidates 3. Prioritize stocks with institutional accumulation 4. Avoid stocks with institutional distribution
Output Format
All analysis generates structured markdown reports saved to repository root:
Filename convention: institutional_flow_analysis_<TICKER/THEME>_<DATE>.md
Report sections: 1. Executive Summary (key findings) 2. Institutional Ownership Trend (current vs historical) 3. Top Holders and Changes 4. New Buyers vs Sellers 5. Concentration Analysis 6. Interpretation and Recommendations 7. Data Sources and Timestamp
Data Reliability Grades
All analysis includes a coverage-based reliability grade:
- Grade A: A comparable prior quarter exists and the stock has >= 50 institutional (13F) holders. Dense coverage, safe for ranking.
- Grade B: A comparable prior quarter exists and the stock has >= 10 holders. Usable but thin — reference only.
- Grade C: No comparable prior quarter (change not measurable) or < 10 holders. EXCLUDED from screening results.
The screening script (track_institutional_flow.py) automatically excludes Grade C stocks. The single stock analysis (analyze_single_stock.py) displays the grade with appropriate warnings.
Why coverage, not per-holder reconciliation: Metrics are sourced from FMP's aggregate 13F summary (institutional-ownership/symbol-positions-summary), which reconciles quarter-over-quarter deltas across all filing managers at source. This replaces the retired /api/v3/institutional-holder feed, which returned asymmetric per-holder lists across quarters (e.g., 5,415 holders one quarter, 201 the next) and required client-side filtering to avoid inflated percent changes. With the reconciled summary, the remaining quality signal that matters in practice is breadth (how many managers hold the name) and whether a prior quarter exists to measure change against — which is what the grade now reflects.
Limitations and Caveats
Data Lag:
- 13F filings have 45-day reporting delay
- Positions may have changed since filing date
- Use as confirming indicator, not leading signal
Coverage:
- Only institutions managing >$100M are required to file
- Excludes individual investors and smaller funds
- International institutions may not file 13F
Reporting Rules:
- Only long equity positions reported (no shorts, options, bonds)
- Holdings as of quarter-end snapshot
- Some positions may be confidential (delayed reporting)
Interpretation:
- Correlation ≠ causation (stocks can fall despite institutional buying)
- Consider overall market environment and fundamentals
- Combine with technical analysis and other skills
Advanced Use Cases
Insider + Institutional Combo:
- Look for stocks where both insiders AND institutions are buying
- Particularly powerful signal when aligned
Sector Rotation Detection:
- Track aggregate institutional flows by sector
- Identify early rotation trends before they appear in price
Contrarian Plays:
- Find quality stocks institutions are selling (potential value)
- Requires strong fundamental conviction
Smart Money Validation:
- Before major position, check if smart money agrees
- Gain confidence or find overlooked risks
References
The references/ folder contains detailed guides:
- 13f_filings_guide.md - Comprehensive guide to 13F SEC filings, what they include, reporting requirements, and data quality considerations
- institutional_investor_types.md - Different types of institutional investors (hedge funds, mutual funds, pension funds, etc.), their typical strategies, and how to interpret their moves
- interpretation_framework.md - Detailed framework for interpreting institutional ownership changes, signal quality assessment, and integration with other analysis
Script Parameters
track_institutional_flow.py
Main screening script for finding stocks with significant institutional changes.
Required:
--api-key: FMP API key (or set FMP_API_KEY environment variable)
Optional:
--top N: Return top N stocks by institutional change (default: 50)--min-change-percent X: Minimum % change in institutional ownership (default: 10)--min-market-cap X: Minimum market cap in dollars (default: 1B)--sector NAME: Filter by specific sector--min-institutions N: Minimum number of institutional holders (default: 10)--limit N: Number of stocks to fetch from screener (default: 100). Lower values save API calls.--output FILE: Output JSON file path--output-dir DIR: Output directory for reports (default: reports/)--sort-by FIELD: Sort by 'ownership_change' or 'institution_count_change'
analyze_single_stock.py
Deep dive analysis on a specific stock's institutional ownership.
Required:
- Ticker symbol (positional argument)
--api-key: FMP API key (or set FMP_API_KEY environment variable)
Optional:
--quarters N: Number of quarters to analyze (default: 8, i.e., 2 years)--output FILE: Output markdown report path--output-dir DIR: Output directory for reports (default: reports/)--compare-to TICKER: Compare institutional ownership to another stock (future feature)
track_institution_portfolio.py
Status: NOT YET IMPLEMENTED
This script is a placeholder. It prints alternative resources (WhaleWisdom, SEC EDGAR, DataRoma) and exits with error code 1. FMP API organizes institutional holder data by stock (not by institution), making full portfolio reconstruction impractical.
For institution-specific portfolio tracking, use: 1. WhaleWisdom: https://whalewisdom.com (free tier available) 2. SEC EDGAR: https://www.sec.gov/cgi-bin/browse-edgar 3. DataRoma: https://www.dataroma.com
Data Quality Module (data_quality.py)
Shared utility module used by both track_institutional_flow.py and analyze_single_stock.py:
- coverage_grade(): Assigns A/B/C grade from holder breadth + prior-quarter availability
- latest filed quarter helpers (
current_quarter(),iter_quarters(),quarter_end_date()): walk back to the most recent quarter with filed 13F data - normalize_holder(): Maps a
extract-analytics/holderrow to{name, shares, change, is_new, is_sold_out} - is_tradable_stock(): Filters out ETFs, funds, and inactive stocks
- deduplicate_share_classes(): Removes BRK-A/B, GOOG/GOOGL duplicates
Integration with Other Skills
Value Dividend Screener + Institutional Flow:
1. Run Value Dividend Screener to find candidates
2. For each candidate, check institutional flow
3. Prioritize stocks with rising institutional ownershipUS Stock Analysis + Institutional Flow:
1. Run comprehensive fundamental analysis
2. Validate with institutional ownership trends
3. If institutions are selling, investigate whyPortfolio Manager + Institutional Flow:
1. Fetch current portfolio via Alpaca
2. Run institutional analysis on each holding
3. Flag positions with deteriorating institutional support
4. Consider rebalancing away from distributionTechnical Analyst + Institutional Flow:
1. Identify technical setup (e.g., breakout)
2. Check if institutional buying confirms
3. Higher conviction if both alignBest Practices
1. Quarterly Reviews: Set calendar reminders for 13F filing deadlines 2. Multi-Quarter Trends: Look for sustained trends (3+ quarters), not one-time changes 3. Quality Over Quantity: Berkshire adding > 100 small funds adding 4. Context Matters: Rising ownership in a falling stock may be value investors catching a falling knife 5. Combine Signals: Never use institutional flow in isolation 6. Update Your Data: Re-run analysis each quarter as new 13Fs are filed
Support & Resources
- FMP API Documentation: https://financialmodelingprep.com/developer/docs
- SEC 13F Filings Database: https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&type=13F
- Institutional Investor Database: https://whalewisdom.com (free tier available)
---
Note: This skill is designed for long-term investors (3-12 month horizon). For short-term trading, combine with technical analysis and other momentum indicators.
Institutional Flow Tracker
Track institutional investor ownership changes and portfolio flows using 13F filings data to identify "smart money" accumulation and distribution patterns.
Overview
Institutional Flow Tracker analyzes SEC 13F filings to discover stocks where sophisticated investors (hedge funds, mutual funds, pension funds) are accumulating or distributing positions. By following where institutional money flows, you can:
- Discover opportunities before they become mainstream
- Validate investment ideas by checking if smart money agrees
- Get early warnings when quality investors exit positions
- Follow superinvestors like Warren Buffett, Seth Klarman, Bill Ackman
Key Insight: Institutional investors manage trillions of dollars and conduct extensive research. Their collective actions often precede major price movements by 1-3 quarters.
Features
✅ Stock Screening - Find stocks with significant institutional ownership changes (>10-15% QoQ) ✅ Deep Dive Analysis - Detailed quarterly trends, top holders, position changes for individual stocks ✅ Institution Tracking - Follow specific hedge funds/mutual funds portfolio moves ✅ Signal Quality Framework - Tier-based weighting system (superinvestors > active funds > index funds) ✅ Multi-Quarter Trends - Identify sustained accumulation/distribution (3+ quarters) ✅ Concentration Analysis - Assess ownership concentration risk ✅ FMP API Integration - Free tier (250 calls/day) sufficient for quarterly reviews
Prerequisites
Required: FMP API Key
This skill uses Financial Modeling Prep (FMP) API to access 13F filing data.
Setup:
# Set environment variable (recommended)
export FMP_API_KEY="your_key_here"
# Or provide via command-line when running scripts
python3 scripts/track_institutional_flow.py --api-key YOUR_KEYGet API Key: 1. Visit: https://financialmodelingprep.com/developer/docs 2. Sign up for free account (250 requests/day) 3. Copy your API key
API Usage:
- Free tier: 250 requests/day (sufficient for analyzing 30-50 stocks quarterly)
- Each stock analysis uses 1-2 API calls
- Quarterly review workflow: ~50-100 API calls total
Installation
No installation required beyond Python 3 and the requests library:
pip install requestsUsage
1. Screen for Stocks with Institutional Changes
Find stocks with significant institutional activity:
Quick scan (top 50 stocks):
python3 institutional-flow-tracker/scripts/track_institutional_flow.py \
--top 50 \
--min-change-percent 10Sector-focused screening:
python3 institutional-flow-tracker/scripts/track_institutional_flow.py \
--sector Technology \
--min-institutions 20Custom screening:
python3 institutional-flow-tracker/scripts/track_institutional_flow.py \
--min-market-cap 2000000000 \
--min-change-percent 15 \
--top 100 \
--output institutional_results.jsonOutput: Markdown report with top accumulators/distributors, detailed metrics, interpretation guide
2. Deep Dive on Specific Stock
Comprehensive analysis of institutional ownership for a single stock:
python3 institutional-flow-tracker/scripts/analyze_single_stock.py AAPLExtended history (12 quarters):
python3 institutional-flow-tracker/scripts/analyze_single_stock.py MSFT --quarters 12Output: Detailed report with quarterly trends, new/increased/decreased/closed positions, concentration analysis
3. Track Specific Institutional Investors
Follow portfolio changes of specific hedge funds or investment firms:
# Track Warren Buffett's Berkshire Hathaway
python3 institutional-flow-tracker/scripts/track_institution_portfolio.py \
--cik 0001067983 \
--name "Berkshire Hathaway"
# Track Cathie Wood's ARK Investment Management
python3 institutional-flow-tracker/scripts/track_institution_portfolio.py \
--cik 0001579982 \
--name "ARK Investment Management"Finding CIK (Central Index Key):
- Search SEC EDGAR: https://www.sec.gov/cgi-bin/browse-edgar
- Or use FMP API institutional search
Output: Current portfolio holdings, new positions, exits, largest changes
Signal Interpretation Framework
Strong Buy Signal (95th percentile)
Criteria:
- Institutional ownership increasing >15% QoQ
- 3+ consecutive quarters of accumulation
- Multiple Tier 1/2 investors buying (clustering score >60)
- Quality long-term investors adding positions
Action: BUY with conviction (2-5% portfolio position)
Moderate Buy Signal (75th percentile)
Criteria:
- Institutional ownership increasing 7-15% QoQ
- 2 consecutive quarters of accumulation
- Mix of quality buyers
Action: BUY with moderate conviction (1-3% position)
Neutral Signal
Criteria:
- Institutional ownership change <5% QoQ
- No clear trend
- Mixed buyer/seller activity
Action: HOLD or decide based on other factors
Moderate/Strong Sell Signal
Criteria:
- Institutional ownership decreasing 7-15% QoQ (moderate) or >15% (strong)
- 2-3+ consecutive quarters of distribution
- Quality investors exiting
Action: TRIM/SELL positions, avoid new entries
Institutional Investor Quality Tiers
Tier 1 - Superinvestors (Weight: 3.0-3.5x):
- Warren Buffett (Berkshire Hathaway)
- Seth Klarman (Baupost Group)
- Bill Ackman (Pershing Square)
- David Tepper (Appaloosa Management)
- Patient capital, long-term oriented, concentrated portfolios
Tier 2 - Quality Active Managers (Weight: 2.0-2.5x):
- Fidelity Management & Research
- T. Rowe Price Associates
- Dodge & Cox
- Wellington Management
- Research-driven, solid track records
Tier 3 - Average Institutional (Weight: 1.0-1.5x):
- Regional mutual funds
- Most pension funds
- Benchmark-aware, committee-driven
Tier 4 - Passive/Mechanical (Weight: 0.0-0.5x):
- Index funds (Vanguard, BlackRock, State Street)
- Momentum/quant funds
- No fundamental views, follow index/price action
Workflow Examples
Quarterly Portfolio Review
Objective: Monitor institutional support for your holdings
1. Run institutional analysis on each holding after 13F filing deadlines 2. Flag positions with deteriorating institutional support 3. Re-evaluate thesis for positions showing Strong Sell signals 4. Consider trimming/exiting if quality investors are distributing
13F Filing Deadlines:
- Q1 (Jan-Mar): Mid-May
- Q2 (Apr-Jun): Mid-August
- Q3 (Jul-Sep): Mid-November
- Q4 (Oct-Dec): Mid-February
New Position Validation
Objective: Validate stock ideas with institutional data
1. Run fundamental analysis on stock candidate 2. Check institutional flow signal (accumulation or distribution?) 3. If Strong Buy signal: Gain confidence, initiate position 4. If Strong Sell signal: Reconsider or avoid 5. If Neutral: Decide based on fundamentals and technicals
Smart Money Replication
Objective: Follow superinvestor portfolio moves
1. Track Berkshire Hathaway, Baupost, other Tier 1 investors quarterly 2. Identify new positions or significant increases 3. Research the stock to understand their thesis 4. Initiate positions in highest-conviction ideas
Sector Rotation Detection
Objective: Identify early sector rotation trends
1. Calculate aggregate institutional flow by sector 2. Rank sectors by net institutional inflow/outflow 3. Compare to expected patterns based on economic cycle 4. Overweight sectors with institutional accumulation 5. Underweight/avoid sectors with distribution
Integration with Other Skills
Value Dividend Screener + Institutional Flow:
1. Run Value Dividend Screener to find candidates
2. For each candidate, check institutional flow
3. Prioritize stocks with Strong Buy institutional signalUS Stock Analysis + Institutional Flow:
1. Run comprehensive fundamental analysis
2. Validate with institutional ownership trends
3. If institutions selling despite strong fundamentals: investigate discrepancyPortfolio Manager + Institutional Flow:
1. Fetch current portfolio via Alpaca
2. Run institutional analysis on each holding quarterly
3. Flag positions with deteriorating institutional support
4. Rebalance away from Strong Sell signalsTechnical Analyst + Institutional Flow:
1. Identify technical setup (e.g., breakout, basing pattern)
2. Check if institutional buying confirms
3. Higher conviction if both technical + institutional signals alignData Limitations
Reporting Lag:
- 13F filings due 45 days after quarter end
- Positions as of quarter-end snapshot
- Current positions may have changed since filing
Coverage:
- Only institutions managing >$100M file 13F
- Excludes individual investors and smaller funds
- Only long equity positions (no shorts, options, bonds)
Best Practices:
- Use as confirming indicator, not leading signal
- Look for multi-quarter trends (3+ quarters)
- Weight quality institutions heavily (Tier 1 > Tier 4)
- Combine with fundamental and technical analysis
- Update quarterly, not daily
Reference Materials
The references/ folder contains comprehensive guides:
- 13f_filings_guide.md - Understanding 13F SEC filings, reporting requirements, data quality considerations, common pitfalls
- institutional_investor_types.md - Different investor types (hedge funds, mutual funds, etc.), their strategies, quality tiers, weighting framework
- interpretation_framework.md - Systematic approach to interpreting signals, decision trees, multi-factor integration
Script Parameters Reference
track_institutional_flow.py
Required:
--api-keyor FMP_API_KEY environment variable
Optional:
--top N- Return top N stocks (default: 50)--min-change-percent X- Minimum % ownership change (default: 10)--min-market-cap X- Minimum market cap in dollars (default: 1B)--sector NAME- Filter by sector--min-institutions N- Minimum institutional holders (default: 10)--sort-by FIELD- Sort by ownership_change/institution_count_change/dollar_value_change--output FILE- Output JSON file
analyze_single_stock.py
Required:
- Stock ticker symbol (positional argument)
--api-keyor FMP_API_KEY environment variable
Optional:
--quarters N- Number of quarters to analyze (default: 8)--output FILE- Output markdown report path
track_institution_portfolio.py
Required:
--cik CIK- Central Index Key of institution--name NAME- Institution name for report--api-keyor FMP_API_KEY environment variable
Optional:
--top N- Show top N holdings (default: 50)--output FILE- Output markdown report path
Notable Institutional Investors to Track
Tier 1 Superinvestors
| Investor | CIK | Strategy | Track Because |
|---|---|---|---|
| Berkshire Hathaway (Warren Buffett) | 0001067983 | Value/Quality | Long-term compounders, patient capital |
| Baupost Group (Seth Klarman) | 0001061768 | Deep Value | Distressed opportunities, contrarian |
| Pershing Square (Bill Ackman) | 0001336528 | Activist/Value | Catalytic events, concentrated bets |
| Appaloosa Management (David Tepper) | 0001079114 | Value | Contrarian, high conviction |
| Third Point (Dan Loeb) | 0001040273 | Event-Driven | Catalyst-driven, activism |
Tier 2 Quality Active Managers
| Fund Family | CIK | Strategy | Track Because |
|---|---|---|---|
| Fidelity Management | 0000315066 | Growth & Value | Large analyst team, quality research |
| T. Rowe Price | 0001113169 | Growth | Bottom-up research, growth focus |
| Dodge & Cox | 0000922614 | Deep Value | Contrarian, value-oriented |
| ARK Investment (Cathie Wood) | 0001579982 | Disruptive Innovation | High-conviction tech/innovation bets |
Support & Resources
- FMP API Documentation: https://financialmodelingprep.com/developer/docs
- SEC 13F Filings Database: https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&type=13F
- WhaleWisdom (free tier): https://whalewisdom.com
License
Educational and research purposes. See repository license for details.
13F Filings Comprehensive Guide
What is a 13F Filing?
Form 13F is a quarterly report filed with the SEC by institutional investment managers with at least $100 million in assets under management (AUM). The form discloses their equity holdings as of the end of each calendar quarter.
Legal Requirements
Who Must File:
- Investment advisors
- Banks
- Insurance companies
- Pension funds
- Hedge funds
- Other institutions managing >$100M in "13F securities"
What Must Be Reported:
- Long positions in US-listed equities
- Convertible bonds
- Exchange-traded options
- Shares held as of quarter-end date
What is NOT Reported:
- Short positions
- Non-US securities
- Private equity investments
- Fixed income (except convertibles)
- Cash positions
- Derivatives (except listed options)
Filing Deadline:
- Within 45 days after quarter end
- Q1 (ended March 31): Due by May 15
- Q2 (ended June 30): Due by August 14
- Q3 (ended September 30): Due by November 14
- Q4 (ended December 31): Due by February 14
Key Data Points in 13F Filings
For Each Holding: 1. Issuer Name - Company name 2. Ticker Symbol - Stock ticker 3. CUSIP - Unique security identifier 4. Shares Held - Number of shares owned 5. Market Value - Dollar value of position (shares × price at quarter end) 6. Investment Discretion - Sole/Shared/None 7. Voting Authority - Sole/Shared/None
Aggregate Data:
- Total number of holdings
- Total portfolio value
- Portfolio concentration
- Sector allocation
Understanding the Data
Position Changes
Types of Changes: 1. New Position - Stock not held last quarter, now held 2. Increased Position - More shares than last quarter 3. Decreased Position - Fewer shares than last quarter 4. Closed Position - Stock held last quarter, now zero
Calculating Changes:
Shares Change = Current Quarter Shares - Previous Quarter Shares
% Change = (Shares Change / Previous Quarter Shares) × 100
Dollar Value Change = (Shares Change) × (Current Stock Price)Example:
Previous Quarter: 1,000,000 shares of AAPL @ $150 = $150M
Current Quarter: 1,200,000 shares of AAPL @ $180 = $216M
Shares Change: +200,000 shares (+20%)
Dollar Value Change: +$66M (includes price appreciation + new purchases)Aggregate Institutional Ownership
Portfolio-Level Metrics:
- Total Institutional Ownership % = (Total Shares Held by All Institutions / Shares Outstanding) × 100
- Number of Institutional Holders = Count of unique institutions filing 13F with this stock
- Ownership Concentration = % held by top 10 institutions
- Ownership Trend = QoQ change in total institutional ownership %
Typical Ownership Ranges:
- <20% - Low institutional interest (micro/small caps, speculative stocks)
- 20-40% - Below average (growth stocks, recent IPOs)
- 40-60% - Average (most mid/large caps)
- 60-80% - Above average (blue chips, dividend aristocrats)
- >80% - Very high (mature, stable companies)
Data Quality Considerations
Timing Lags
Reporting Lag:
- Position as of: Quarter-end date (e.g., March 31)
- Filing deadline: 45 days later (e.g., May 15)
- Data available: Mid-May onwards
- Total lag: 6-7 weeks from position date
Real-World Impact:
- Stock may have moved significantly since quarter-end
- Institutions may have already changed positions
- Use 13F data as confirming indicator, not real-time signal
Confidential Treatment
Form 13F-NT (Notice of Confidential Treatment):
- Institutions can request delayed disclosure for certain positions
- Typically granted for 1-2 quarters to prevent front-running
- Common for large accumulations or activist positions
Red Flags:
- Sudden appearance of large positions (may have been confidential)
- Large institutions with unusually low reported AUM (hidden positions)
Aggregation Issues
Double Counting:
- Same shares may be reported by multiple entities within same organization
- Example: Vanguard Group + Vanguard Index Funds + Vanguard ETF Trust
- Need to aggregate related entities for accurate institutional ownership
Custodial vs Beneficial Ownership:
- Some institutions hold shares as custodians (not beneficial owners)
- Example: State Street as custodian for pension funds
- Look for "voting authority" and "investment discretion" fields
Common Pitfalls and How to Avoid Them
Pitfall 1: Ignoring Price Changes
Problem:
- Institutional ownership % can decrease even if institutions hold same share count
- Happens when: Stock price falls, institution's AUM falls below reporting threshold
Solution:
- Track both share count changes AND ownership % changes
- Focus on share count for more accurate signal
Example:
Q1: Institution holds 1M shares, stock at $100, ownership = 5%
Q2: Institution holds 1M shares, stock at $50, ownership = 2.5%
Interpretation: No actual selling, just price decline affecting percentagePitfall 2: Interpreting Passive Index Funds
Problem:
- Index funds must buy/sell based on index composition, not fundamental views
- Large inflows to index funds mechanically increase institutional ownership
Solution:
- Separate active managers from passive funds
- Weight active manager changes more heavily
- Track: ARK, Berkshire, Baupost > Vanguard Index Funds
Active vs Passive Indicators:
- Active: Concentrated portfolios (20-50 stocks), high conviction bets
- Passive: Diversified portfolios (500+ stocks), matching index weights
Pitfall 3: Ignoring Institution Quality
Problem:
- Not all institutional investors are equal
- 100 small funds buying ≠ Warren Buffett buying
Solution:
- Tier institutions by track record and strategy alignment
- Weight Tier 1 (quality long-term investors) more heavily
Institutional Tiers:
- Tier 1 (High conviction): Berkshire, Appaloosa, Baupost, Pershing Square
- Tier 2 (Quality active): Fidelity, T. Rowe Price, Wellington
- Tier 3 (Passive/momentum): Vanguard Index, State Street, momentum funds
Pitfall 4: Overreacting to Single Quarter Changes
Problem:
- One quarter of buying/selling may not indicate trend
- Could be portfolio rebalancing, redemptions, or temporary factors
Solution:
- Look for multi-quarter trends (3+ quarters)
- Higher conviction when sustained accumulation/distribution
- One quarter = noise, three quarters = signal
Trend Quality:
Strong Signal (3+ quarters same direction):
Q1: +10% institutional ownership
Q2: +8% institutional ownership
Q3: +12% institutional ownership
Interpretation: Sustained accumulation, high conviction
Noise (inconsistent):
Q1: +10% institutional ownership
Q2: -5% institutional ownership
Q3: +3% institutional ownership
Interpretation: No clear trend, likely portfolio adjustmentsAdvanced Analysis Techniques
Flow Analysis
Track net shares added/removed across all institutions:
Net Institutional Flow = Sum(All Institutional Share Changes)
Flow Ratio = (Shares Added by Buyers) / (Shares Sold by Sellers)
Flow Ratio > 2.0 = Strong accumulation
Flow Ratio 1.5-2.0 = Moderate accumulation
Flow Ratio 0.8-1.2 = Neutral/Balanced
Flow Ratio 0.5-0.8 = Moderate distribution
Flow Ratio < 0.5 = Strong distributionConcentration Risk Analysis
Herfindahl-Hirschman Index (HHI):
HHI = Sum of (Each Institution's Ownership %)²
HHI < 1000 = Diversified ownership (low concentration risk)
HHI 1000-1800 = Moderate concentration
HHI > 1800 = High concentration (risk if top holder sells)Example:
Top 3 holders: 15%, 12%, 10% of institutional ownership
HHI = 15² + 12² + 10² = 225 + 144 + 100 = 469 (low concentration)
Top 3 holders: 40%, 30%, 20% of institutional ownership
HHI = 40² + 30² + 20² = 1600 + 900 + 400 = 2900 (high concentration)New Buyers vs Sellers Analysis
Quarterly Cohort Tracking:
New Buyers = Institutions with zero shares last Q, non-zero this Q
Increasers = Institutions with more shares this Q
Decreasers = Institutions with fewer shares this Q
Exited = Institutions with shares last Q, zero this Q
Bull Signal: New Buyers > Exited AND Increasers > Decreasers
Bear Signal: Exited > New Buyers AND Decreasers > IncreasersSmart Money Clustering
Identify when multiple quality investors accumulate simultaneously:
Clustering Score:
Score = Sum of (Institution Tier × % Position Change)
Tier 1 institutions: Weight = 3.0
Tier 2 institutions: Weight = 2.0
Tier 3 institutions: Weight = 1.0
Example:
Berkshire (Tier 1) +10% position = 3.0 × 10 = 30 points
Fidelity (Tier 2) +5% position = 2.0 × 5 = 10 points
Index Fund (Tier 3) +2% position = 1.0 × 2 = 2 points
Total Clustering Score = 42
Score > 50 = Strong smart money accumulation
Score 25-50 = Moderate accumulation
Score < 25 = Weak/no clusteringHistorical Success Patterns
Pre-Breakout Accumulation
Pattern:
- Stock trading sideways for 2-4 quarters
- Institutional ownership quietly rising (10-20% increase)
- Stock breaks out 1-2 quarters after accumulation visible in 13F
Example Stocks:
- NVDA (2019): Institutions accumulated before AI boom
- TSLA (2019-2020): Steady institutional buying before 500% run
Early Distribution Warning
Pattern:
- Stock in uptrend
- Institutional ownership declining for 2+ quarters
- Top-tier institutions exiting
- Stock peaks 1-2 quarters after distribution begins
Example Stocks:
- Dot-com stocks (1999-2000): Smart money exited before crash
- Housing stocks (2006-2007): Institutional distribution before crisis
Integration with Fundamental Analysis
Bullish Confirmation:
- Strong fundamentals (revenue growth, margin expansion)
- Rising institutional ownership
- Quality investors adding positions
- Action: High conviction buy
Bearish Warning:
- Strong fundamentals but declining institutional ownership
- Quality investors exiting despite good numbers
- Action: Investigate further, may be hidden risks
Value Opportunity:
- Weak short-term fundamentals
- Stable/rising institutional ownership (value investors accumulating)
- Action: Contrarian opportunity if fundamentals inflect
Avoid:
- Weak fundamentals
- Declining institutional ownership
- Quality investors exiting
- Action: Stay away
Regulatory Changes and Updates
Current as of 2025:
- Reporting threshold: $100M AUM
- Filing deadline: 45 days after quarter end
- Amendment rules: Allowed within filing period
Proposed Changes (Monitor):
- Potential reduction in filing deadline to 30 days
- Potential requirement to disclose short positions
- Potential reduction in AUM threshold to $50M
Stay Updated:
- SEC website: https://www.sec.gov/rules
- Industry news: Follow regulatory announcements
Tools and Resources
Official Sources:
- SEC EDGAR Database: https://www.sec.gov/edgar/searchedgar/companysearch.html
- Form 13F instructions: https://www.sec.gov/pdf/form13f.pdf
Third-Party Aggregators:
- WhaleWisdom: https://whalewisdom.com (free tier available)
- DataRoma: https://www.dataroma.com (tracks superinvestors)
- Fintel: https://fintel.io (institutional ownership data)
API Access:
- FMP API: Institutional ownership endpoints
- SEC API: Direct access to filings (free, rate-limited)
Summary: Using 13F Data Effectively
Best Practices: 1. ✅ Track multi-quarter trends, not single quarters 2. ✅ Focus on share count changes, not just ownership % 3. ✅ Weight quality institutions more heavily 4. ✅ Combine with fundamental analysis 5. ✅ Use as confirming indicator, not standalone signal 6. ✅ Update quarterly after filing deadlines
What to Avoid: 1. ❌ Don't assume 13F = real-time positions 2. ❌ Don't ignore passive fund flows 3. ❌ Don't overweight single institution moves 4. ❌ Don't use for short-term trading 5. ❌ Don't ignore price changes when calculating ownership 6. ❌ Don't forget about confidential treatment requests
Expected Returns:
- Academic studies show 13F-based strategies can generate 2-4% annual alpha
- Best results when combined with momentum and value factors
- Effectiveness highest in small/mid caps where information asymmetry is greater
Institutional Investor Types and Strategies
Overview
Not all institutional investors are created equal. Understanding the different types of institutions, their investment strategies, time horizons, and motivations is critical for interpreting 13F filing changes. This guide categorizes institutional investors and explains how to weight their buy/sell signals.
Major Categories of Institutional Investors
1. Hedge Funds
Characteristics:
- Active management with concentrated portfolios
- High turnover (often 100%+ annually)
- Performance-driven compensation (2% management + 20% performance fees)
- Flexible investment strategies (long/short, derivatives, leverage)
Typical Time Horizon: 3 months to 2 years
Portfolio Structure:
- 20-100 core positions
- High conviction bets (top 10 positions = 50-70% of portfolio)
- Willing to take large positions relative to AUM
Investment Styles:
- Value/Deep Value: Distressed assets, turnarounds, special situations
- Growth: High-growth companies, disruptive tech
- Activist: Catalyze change through board seats, proxy fights
- Event-Driven: M&A arbitrage, restructurings
- Quantitative: Algorithm-driven, factor-based
- Macro: Top-down, sector rotation, global trends
Signal Interpretation:
Strong Signal (Weight: 3.0x):
- Top-tier hedge funds: Berkshire Hathaway, Appaloosa, Baupost, Pershing Square, Third Point
- Why: Deep research, long-term oriented, willing to be contrarian
- When they buy: Often early, before institutional herd
- When they sell: May be ahead of trouble
Moderate Signal (Weight: 2.0x):
- Mid-tier active managers: Smaller hedge funds with solid track records
- Why: Still research-driven but less patient capital
Weak Signal (Weight: 1.0x):
- Momentum/quant funds: Tiger Cubs, momentum-driven strategies
- Why: Often follow trends rather than lead them
- Caution: May be last to buy (top signal) or first to panic sell
Examples of Notable Hedge Funds:
| Fund Name | CIK | Strategy | Best Known For | Weighting |
|---|---|---|---|---|
| Berkshire Hathaway | 0001067983 | Value/Quality | Warren Buffett, long-term compounders | 3.5x |
| Pershing Square | 0001336528 | Activist/Value | Bill Ackman, catalytic events | 3.0x |
| Baupost Group | 0001061768 | Deep Value | Seth Klarman, distressed opportunities | 3.0x |
| Appaloosa Management | 0001079114 | Value | David Tepper, contrarian bets | 3.0x |
| Third Point | 0001040273 | Event-Driven/Activist | Dan Loeb, catalyst-driven | 2.5x |
| Tiger Global | 0001167483 | Growth/Tech | Chase Coleman, high-growth tech | 2.0x |
| Renaissance Technologies | 0001037389 | Quantitative | Jim Simons, algorithmic trading | 1.5x |
2. Mutual Funds
Characteristics:
- Active management with diversified portfolios
- Lower turnover than hedge funds (20-50% annually)
- Must stay fully invested (typically 95%+ equity allocation)
- Fee pressure driving shift toward passive strategies
Typical Time Horizon: 1-5 years
Portfolio Structure:
- 50-200 holdings
- More diversified than hedge funds
- Sector constraints (often cannot exceed index weight by >5%)
Investment Styles:
- Large-Cap Growth: Fidelity Contrafund, T. Rowe Price Growth Stock
- Large-Cap Value: Dodge & Cox Stock Fund, American Funds Washington Mutual
- Small-Cap Growth: Baron Small Cap Fund
- Dividend/Income: Vanguard Dividend Growth
Signal Interpretation:
Strong Signal (Weight: 2.5x):
- Quality active mutual funds: Fidelity, T. Rowe Price, Dodge & Cox
- Why: Rigorous research process, patient capital
- When they buy: Methodical accumulation over multiple quarters
- When they sell: Often early warning of deteriorating fundamentals
Moderate Signal (Weight: 1.5x):
- Typical mutual funds: Regional funds, smaller fund families
- Why: Still research-driven but less differentiated
Weak Signal (Weight: 0.5x):
- Closet indexers: Funds that mimic index but charge active fees
- Why: Minimal value-add, primarily following index weights
Examples of Notable Mutual Fund Families:
| Fund Family | CIK | Strategy | Research Quality | Weighting |
|---|---|---|---|---|
| Fidelity Management & Research | 0000315066 | Growth & Value | Excellent, large analyst team | 2.5x |
| T. Rowe Price Associates | 0001113169 | Growth | Excellent, bottom-up research | 2.5x |
| Dodge & Cox | 0000922614 | Deep Value | Excellent, contrarian | 2.5x |
| Wellington Management | 0000105132 | Multi-strategy | Very Good | 2.0x |
| Capital Group (American Funds) | 0001007039 | Multi-manager | Very Good | 2.0x |
| Baron Capital | 0001047469 | Small/Mid Growth | Good | 2.0x |
3. Index Funds and ETFs
Characteristics:
- Passive management tracking indices
- Extremely low turnover (only when index composition changes)
- Must hold all index constituents
- Forced buyers/sellers based on index rules
Typical Time Horizon: Indefinite (hold until removed from index)
Portfolio Structure:
- Matches index exactly (S&P 500, Russell 2000, etc.)
- Hundreds to thousands of holdings
- No discretionary overweights/underweights
Investment Styles:
- Broad Market: SPY (S&P 500), VTI (Total Stock Market)
- Sector: XLK (Technology), XLE (Energy)
- Factor: QQQ (Nasdaq 100), IWM (Russell 2000)
- Thematic: ARK Innovation (actively managed but ETF structure)
Signal Interpretation:
Weak Signal (Weight: 0.3x):
- Large index funds: Vanguard, State Street, BlackRock index products
- Why: Mechanical buying/selling based on index rules and fund flows
- When they buy: Index inclusion, fund inflows (not fundamental view)
- When they sell: Index deletion, fund redemptions
Zero Signal (Weight: 0.0x):
- Pure index trackers: Changes driven entirely by index methodology
- Ignore for analysis: Provides no fundamental insight
Special Case - ARK ETFs (Weight: 2.0x):
- Actively managed despite ETF structure
- Cathie Wood's high-conviction disruptive innovation bets
- Treat like active mutual fund, not passive ETF
Major Index Fund Providers:
| Provider | CIK | AUM | Passive vs Active | Weighting |
|---|---|---|---|---|
| Vanguard Group (Index) | 0000102909 | $7T+ | 90% Passive | 0.3x |
| BlackRock (iShares) | 0001006249 | $3T+ | 80% Passive | 0.3x |
| State Street (SPDR) | 0001067983 | $1T+ | 90% Passive | 0.3x |
| ARK Investment Management | 0001579982 | $20B | 100% Active | 2.0x |
4. Pension Funds
Characteristics:
- Extremely long-term oriented (liabilities 20-30 years out)
- Conservative mandates with diversification requirements
- Large allocations to fixed income, alternatives
- Equity portion often outsourced to external managers
Typical Time Horizon: 10-30 years
Portfolio Structure:
- Thousands of holdings (extremely diversified)
- Target allocations by asset class (e.g., 60% stocks, 40% bonds)
- Rebalance mechanically to maintain targets
Investment Styles:
- Public Pensions: CalPERS, CalSTRS, New York State Common Retirement Fund
- Corporate Pensions: IBM Retirement Fund, GE Pension Trust
- Endowments: Harvard Management Company, Yale Investments Office
Signal Interpretation:
Weak Signal (Weight: 0.8x):
- Most pension funds: Slow-moving, diversified, often following consultant recommendations
- Why: Not differentiated investors, lag behind trends
Moderate Signal (Weight: 2.0x):
- Top university endowments: Harvard, Yale, Princeton
- Why: Sophisticated investment teams, access to best managers
- David Swensen model: Yale's approach influenced entire industry
Examples:
| Fund Name | CIK | Type | Quality | Weighting |
|---|---|---|---|---|
| CalPERS | 0001133228 | Public Pension | Average | 0.8x |
| Harvard Management Company | 0001082621 | University Endowment | Excellent | 2.0x |
| Yale Investments Office | 0001080232 | University Endowment | Excellent | 2.0x |
| Teacher Retirement System of Texas | 0001023859 | Public Pension | Average | 0.8x |
5. Insurance Companies
Characteristics:
- Conservative, liability-matching strategies
- Heavy fixed income allocation (regulatory capital requirements)
- Equity investments typically in blue chips, dividends
- Low turnover
Typical Time Horizon: 5-15 years
Portfolio Structure:
- Hundreds of holdings
- Focus on large-cap, dividend-paying stocks
- Avoid high-volatility growth stocks
Investment Styles:
- Life Insurance: MetLife, Prudential
- Property & Casualty: Berkshire Hathaway (reinsurance), Markel
Signal Interpretation:
Weak Signal (Weight: 1.0x):
- Most insurance companies: Conservative, follow rating agency guidance
- Why: Limited risk appetite, prefer safety over returns
Strong Signal (Weight: 3.0x):
- Insurance-affiliated value investors: Markel (Tom Gayner), Berkshire (special case)
- Why: Patient capital, value-oriented, long-term compounders
6. Banks and Trust Companies
Characteristics:
- Manage money on behalf of clients (wealth management, trusts)
- Report aggregate positions across all client accounts
- May include both discretionary and non-discretionary accounts
Typical Time Horizon: Varies by client (1-10 years)
Portfolio Structure:
- Thousands of holdings (reflecting diverse client base)
- Often includes model portfolios applied across clients
Signal Interpretation:
Weak Signal (Weight: 0.5x):
- Most banks: Reflect client preferences more than bank's views
- Why: Custodial nature, not proprietary investment decisions
Moderate Signal (Weight: 1.5x):
- Private banks with discretionary mandates: Goldman Sachs Asset Management, JPMorgan Asset Management
- Why: Some proprietary research, but still client-driven
Institutional Investor Quality Tiers
Tier 1: Superinvestors (Weight: 3.0-3.5x)
Characteristics:
- 20+ year track records beating market
- Patient capital, long-term orientation
- Concentrated portfolios indicating high conviction
- Willing to be contrarian
List: 1. Warren Buffett (Berkshire Hathaway) 2. Seth Klarman (Baupost Group) 3. David Tepper (Appaloosa Management) 4. Bill Ackman (Pershing Square) 5. Dan Loeb (Third Point) 6. Mohnish Pabrai (Pabrai Investment Funds) 7. Li Lu (Himalaya Capital) 8. Tom Gayner (Markel)
When to Follow:
- New positions: Investigate immediately
- Large increases: High conviction, validate thesis
- Exits: Warning sign, re-evaluate your holdings
Tier 2: Quality Active Managers (Weight: 2.0-2.5x)
Characteristics:
- Solid long-term track records
- Research-driven process
- Reasonable turnover (20-60%)
- Institutional-grade due diligence
List: 1. Fidelity Management & Research 2. T. Rowe Price Associates 3. Dodge & Cox 4. Wellington Management 5. Capital Group (American Funds) 6. Baron Capital 7. ARK Investment Management (Cathie Wood) 8. Greenhaven Associates
When to Follow:
- Multi-quarter trends: Look for 3+ quarters same direction
- Cluster analysis: Multiple Tier 2 managers buying = strong signal
- Exits: Early warning system
Tier 3: Average Institutional Investors (Weight: 1.0-1.5x)
Characteristics:
- Benchmark-aware (avoid large tracking error)
- Committee-driven decisions (slower, less nimble)
- Follow industry/sector trends
- Moderate research quality
Examples:
- Regional mutual fund families
- Mid-tier hedge funds
- Most pension funds
- Insurance company investment departments
When to Follow:
- Useful for confirming broad trends
- Less useful for individual stock picking
- Aggregate flow analysis (overall institutional sentiment)
Tier 4: Passive and Mechanical (Weight: 0.0-0.5x)
Characteristics:
- No fundamental views
- Index-driven or rules-based
- High correlation to fund flows
- Forced buying/selling
Examples:
- Index funds (Vanguard, BlackRock, State Street index products)
- Quantitative funds with high turnover
- Momentum-driven strategies
When to Follow:
- Generally ignore for fundamental analysis
- Useful for understanding technical supply/demand
- Can create price dislocations (opportunity)
Strategy-Specific Interpretation
Value Investors (Berkshire, Baupost, Dodge & Cox)
Buying Signal:
- Often early (stock still falling)
- Multi-year time horizon
- Looking for margin of safety
How to Interpret:
- Don't expect immediate price action
- Validate their thesis with fundamental research
- Wait for catalyst or technical confirmation
Selling Signal:
- Often late (stock already appreciated)
- Selling into strength
- Taking profits after multi-year holds
Growth Investors (Fidelity Contrafund, Baron, Tiger Global)
Buying Signal:
- Chasing proven growth stories
- Momentum component (buy on strength)
- Paying up for quality
How to Interpret:
- Stock may already have moved
- Validate growth trajectory is sustainable
- Watch for slowdown in growth rate
Selling Signal:
- Often early at first sign of deceleration
- Growth disappointment
- Momentum reversal
Activist Investors (Pershing Square, Third Point, Elliot Management)
Buying Signal:
- Catalyst-driven (board changes, asset sales, buybacks)
- Concentrated positions (5-10% stakes)
- Public campaigns (13D filings, letters)
How to Interpret:
- High potential returns if catalyst realized
- Higher risk (activists don't always win)
- Timeframe: 1-3 years for catalyst to play out
Selling Signal:
- Catalyst achieved
- Taking profits after successful campaign
- Activist fight lost
Momentum/Quantitative Funds (Renaissance, AQR)
Buying Signal:
- Trend-following
- Factor-driven (value, momentum, quality)
- Short-term oriented
How to Interpret:
- Confirms technical strength
- May reverse quickly
- Don't overweight this signal
Selling Signal:
- Momentum breakdown
- Factor signal reversal
- Can accelerate declines
Institutional Clustering Signals
Strong Bullish: Quality Investor Clustering
Pattern:
- 3+ Tier 1 or Tier 2 investors buying simultaneously
- Sustained accumulation (2+ quarters)
- Increasing position sizes (not just maintenance)
Example:
Stock XYZ - Q3 2024 Institutional Activity:
- Berkshire: New position, 5% of portfolio
- Dodge & Cox: Increased existing position by 30%
- Baupost: New position, 3% of portfolio
- Fidelity Contrafund: Increased position by 15%
Clustering Score: (3.5×5) + (2.5×30) + (3.0×3) + (2.5×15) = 17.5 + 75 + 9 + 37.5 = 139
Interpretation: Very strong accumulation by quality investorsAction: High conviction buy if fundamentals support thesis
Moderate Bullish: Broad Institutional Accumulation
Pattern:
- Rising institutional ownership across many Tier 2-3 investors
- Index fund flows also positive
- Gradual, sustained trend
Interpretation: Consensus building, mainstream acceptance
Action: Consider buying if not overvalued
Neutral: Mixed Signals
Pattern:
- Some quality investors buying, others selling
- Similar number of increasers vs decreasers
- Net change near zero
Interpretation: No clear institutional consensus
Action: Look to other factors (fundamentals, technicals)
Moderate Bearish: Quality Investors Exiting
Pattern:
- 2-3 Tier 1 or Tier 2 investors reducing/closing positions
- Tier 3 investors still buying (lagging indicators)
- Early stage of trend
Interpretation: Smart money getting out, warning sign
Action: Re-evaluate thesis, consider trimming
Strong Bearish: Widespread Distribution
Pattern:
- 3+ Tier 1/2 investors exiting
- Sustained distribution (2+ quarters)
- Only Tier 4 (passive) buyers remaining
Interpretation: Serious fundamental concerns
Action: Sell or avoid, investigate for hidden risks
Time Horizon Alignment
Long-Term Investors (Your time horizon: 3+ years)
Focus on:
- Tier 1 superinvestors (Berkshire, Baupost)
- Value-oriented mutual funds (Dodge & Cox)
- University endowments
Ignore:
- Momentum funds
- Short-term traders
- Index fund flows
Medium-Term Investors (Your time horizon: 1-3 years)
Focus on:
- Quality growth mutual funds (Fidelity, T. Rowe Price)
- Tier 2 hedge funds
- Activist investors
Monitor:
- Tier 1 superinvestors (for major themes)
- Aggregate institutional flow
Short-Term Traders (Your time horizon: <1 year)
Focus on:
- Recent quarter changes (not multi-year trends)
- Momentum funds
- Aggregate institutional flow (technical signal)
Note: 13F data less useful for short-term trading due to 45-day lag
Practical Application: Institutional Investor Scorecard
For Each Stock, Calculate:
Institutional Quality Score =
(Tier 1 Holders × 3.5) +
(Tier 2 Holders × 2.5) +
(Tier 3 Holders × 1.0) +
(Tier 4 Holders × 0.3)
Institutional Flow Score =
(Tier 1 Buyers × 3.5) - (Tier 1 Sellers × 3.5) +
(Tier 2 Buyers × 2.5) - (Tier 2 Sellers × 2.5) +
(Tier 3 Buyers × 1.0) - (Tier 3 Sellers × 1.0)
Combined Score = Institutional Quality Score + Institutional Flow Score
Score > 50: Strong institutional support
Score 25-50: Moderate institutional support
Score 0-25: Weak institutional support
Score < 0: Institutional distribution underwayExample Calculation:
Stock ABC Analysis:
Tier 1 Holders: 2 (Berkshire, Baupost)
Tier 2 Holders: 5 (Fidelity, T. Rowe Price, Dodge & Cox, Wellington, Baron)
Tier 3 Holders: 20
Tier 4 Holders: 50 (mostly index funds)
Quality Score = (2 × 3.5) + (5 × 2.5) + (20 × 1.0) + (50 × 0.3)
= 7 + 12.5 + 20 + 15 = 54.5
Recent Changes (Q3 2024):
Tier 1 Buyers: 1 (Baupost increased 20%)
Tier 2 Buyers: 3 (Fidelity +10%, Baron +15%, new buyer +5%)
Tier 1 Sellers: 0
Tier 2 Sellers: 1 (T. Rowe Price -5%)
Flow Score = (1 × 3.5) - 0 + (3 × 2.5) - (1 × 2.5)
= 3.5 + 7.5 - 2.5 = 8.5
Combined Score = 54.5 + 8.5 = 63
Interpretation: Strong institutional support with ongoing accumulation
Action: High conviction buy if fundamentals alignSummary: Institutional Investor Weighting Guide
High Weight (3.0-3.5x):
- Berkshire Hathaway, Baupost, Pershing Square, Third Point
- Value-oriented with long track records
- Contrarian, patient capital
Moderate Weight (2.0-2.5x):
- Fidelity, T. Rowe Price, Dodge & Cox, Wellington, ARK
- Quality active managers
- Research-driven
Low Weight (1.0-1.5x):
- Average mutual funds, most pension funds
- Committee-driven, benchmark-aware
Minimal/Zero Weight (0.0-0.5x):
- Index funds, momentum funds
- Mechanical, no fundamental views
When in Doubt:
- Weight quality over quantity
- Multi-quarter trends over single quarter
- Active over passive
- Concentrated portfolios over diversified
Institutional Flow Interpretation Framework
Overview
This framework provides a systematic approach to interpreting institutional ownership changes and converting 13F filing data into actionable investment signals. Use this as a decision tree for analyzing stocks discovered through institutional flow tracking.
Signal Quality Assessment Matrix
Dimension 1: Change Magnitude
Strong Change (High Signal Quality):
- Institutional ownership change: >15% QoQ
- Number of institutions change: >10 net new or exited
- Dollar value change: >$100M net flow
- Quality: Unmistakable signal, warrants immediate attention
Moderate Change (Medium Signal Quality):
- Institutional ownership change: 5-15% QoQ
- Number of institutions change: 3-10 net change
- Dollar value change: $25M-$100M net flow
- Quality: Meaningful signal, validate with other factors
Weak Change (Low Signal Quality):
- Institutional ownership change: <5% QoQ
- Number of institutions change: <3 net change
- Dollar value change: <$25M net flow
- Quality: Noise, likely portfolio adjustments or rebalancing
Dimension 2: Consistency (Multi-Quarter Trend)
Sustained Trend (High Quality):
- 3+ quarters in same direction
- Accelerating magnitude (Q1: +5%, Q2: +8%, Q3: +12%)
- Consistent across market environments
- Quality: High conviction signal
Emerging Trend (Medium Quality):
- 2 consecutive quarters in same direction
- Stable magnitude
- Quality: Developing signal, monitor next quarter
Single Quarter (Low Quality):
- Only 1 quarter of change
- May reverse next quarter
- Quality: Inconclusive, wait for confirmation
Inconsistent (No Signal):
- Flip-flopping between quarters
- No clear direction
- Quality: Noise, ignore
Dimension 3: Institution Quality Mix
High Quality Mix (Weight 3.0x):
- Dominated by Tier 1 superinvestors (Berkshire, Baupost, etc.)
- Multiple quality value investors agreeing
- Long-term oriented capital
- Interpretation: Strong validation
Medium Quality Mix (Weight 2.0x):
- Mix of Tier 2 active mutual funds
- Some quality participants
- Mostly research-driven
- Interpretation: Moderate validation
Low Quality Mix (Weight 0.5x):
- Dominated by passive index funds
- Momentum/quant funds
- Following price action
- Interpretation: Weak validation, likely late to trend
Dimension 4: Concentration of Changes
Clustered Buying/Selling (High Quality):
- Multiple quality institutions moving same direction simultaneously
- Coordinated timing (not literally, but similar conclusions)
- High clustering score (>50 using framework from institutional_investor_types.md)
- Interpretation: Independent research reaching same conclusion = high conviction
Scattered Activity (Medium Quality):
- Mix of buyers and sellers
- No clear consensus
- Clustering score 20-50
- Interpretation: Divergent views, less clear signal
Single Institution (Low Quality):
- Only one significant mover
- Others flat or opposite
- Clustering score <20
- Interpretation: Idiosyncratic, institution-specific reason
Comprehensive Signal Interpretation Framework
Level 1: Strong Buy Signal (95th percentile)
Criteria (ALL must be met): 1. Magnitude: Institutional ownership increasing >15% QoQ 2. Consistency: 3+ consecutive quarters of accumulation 3. Quality: Clustering score >60 (multiple Tier 1/2 investors buying) 4. Concentration: Rising institutional concentration (top 10 holders increasing %) 5. Type: Quality investors adding, not just index funds 6. Fundamental Support: Revenue/earnings growth positive 7. Price Action: Stock underperformed or neutral (not already up 50%+)
Interpretation:
- Smart money accumulating before broader market recognition
- High probability of significant price appreciation in next 1-4 quarters
- Likely catalysts being anticipated by institutions
Action:
- New position: BUY with conviction (2-5% portfolio position)
- Existing position: ADD to position
- Risk management: Normal position sizing, standard stop-loss
Historical Success Rate: ~75-80% positive returns over 12 months
Example Pattern:
Stock XYZ - Sustained Quality Accumulation
Q1 2024:
- Institutional ownership: 45% → 50% (+5%)
- New holders: Baupost Group (Tier 1)
- Clustering score: 35
Q2 2024:
- Institutional ownership: 50% → 58% (+8%)
- Increasers: Berkshire (+20%), Fidelity (+15%), Dodge & Cox (+10%)
- Clustering score: 55
Q3 2024:
- Institutional ownership: 58% → 68% (+10%)
- Increasers: Berkshire (+10% more), Baupost (+25%), T. Rowe Price (new position)
- Clustering score: 72
Stock price during period: $45 → $48 (+6.7%)
Revenue growth: +12% YoY
P/E: 18x (sector average: 22x)
Signal: STRONG BUY
Interpretation: Quality value investors seeing opportunity before market, sustained accumulation
Expected outcome: Stock re-rating to sector average P/E = $48 × (22/18) = $58.67 (+22% upside)Level 2: Moderate Buy Signal (75th percentile)
Criteria (Most must be met): 1. Magnitude: Institutional ownership increasing 7-15% QoQ 2. Consistency: 2 consecutive quarters of accumulation 3. Quality: Clustering score 40-60 (mix of Tier 1/2 investors) 4. Type: More quality buyers than sellers 5. Fundamental Support: At least stable fundamentals 6. Price Action: Not extended (not up >30% in past quarter)
Interpretation:
- Institutions building positions
- Positive outlook but not unanimously bullish
- Moderate probability of price appreciation
Action:
- New position: BUY with moderate conviction (1-3% portfolio position)
- Existing position: HOLD or small ADD
- Risk management: Tighter stop-loss, monitor quarterly
Historical Success Rate: ~60-65% positive returns over 12 months
Level 3: Neutral/Hold Signal (50th percentile)
Criteria: 1. Magnitude: Institutional ownership change <5% QoQ 2. Consistency: No clear trend 3. Quality: Mixed clustering score (20-40) 4. Type: Similar number of buyers and sellers
Interpretation:
- No clear institutional consensus
- Status quo
- Let other factors drive decision
Action:
- New position: PASS (wait for clearer signal)
- Existing position: HOLD (no change)
- Risk management: Standard position sizing
Historical Success Rate: ~50% (random walk)
Level 4: Moderate Sell Signal (25th percentile)
Criteria (Most must be met): 1. Magnitude: Institutional ownership decreasing 7-15% QoQ 2. Consistency: 2 consecutive quarters of distribution 3. Quality: Tier 1/2 investors reducing positions 4. Type: More quality sellers than buyers 5. Price Action: Stock may still be rising (distribution into strength)
Interpretation:
- Smart money reducing exposure
- Early warning sign
- Potential deterioration in fundamentals or valuation concerns
Action:
- New position: AVOID
- Existing position: TRIM or SELL (reduce to 50% of normal position size)
- Risk management: Tighten stop-loss, prepare to exit
Historical Success Rate: ~60-65% underperform market over 12 months
Level 5: Strong Sell Signal (5th percentile)
Criteria (ALL must be met): 1. Magnitude: Institutional ownership decreasing >15% QoQ 2. Consistency: 3+ consecutive quarters of distribution 3. Quality: Clustering score >60 on SELL side (multiple Tier 1/2 investors exiting) 4. Concentration: Declining institutional concentration (top holders exiting) 5. Type: Quality investors exiting, only passive/momentum funds remaining 6. Price Action: May still be positive (smart money exits before crash)
Interpretation:
- Widespread institutional exodus
- Serious fundamental concerns (even if not yet visible)
- High probability of significant decline
- Institutional investors know something market doesn't
Action:
- New position: DO NOT BUY (even if stock looks cheap)
- Existing position: SELL immediately (full exit)
- Short consideration: Consider short if technical setup supports
Historical Success Rate: ~75-80% negative returns over 12 months
Example Pattern:
Stock ABC - Sustained Quality Distribution
Q1 2024:
- Institutional ownership: 72% → 68% (-4%)
- Exits: Small hedge fund
- Clustering score: -15
Q2 2024:
- Institutional ownership: 68% → 60% (-8%)
- Decreasers: Fidelity (-20%), Wellington (-15%)
- Exits: 2 more funds
- Clustering score: -42
Q3 2024:
- Institutional ownership: 60% → 48% (-12%)
- Decreasers: Berkshire (-30%), Dodge & Cox (-25%), T. Rowe Price (-20%)
- Exits: 5 quality funds
- Clustering score: -85
Stock price during period: $80 → $75 (-6.25%)
Revenue growth: Slowing from +20% to +8% YoY
Management: Guiding lower for next quarter (not yet public in Q2)
Signal: STRONG SELL
Interpretation: Quality investors exiting ahead of visible deterioration
Expected outcome: Stock decline to $50-60 range as problems become apparent (-25% to -40%)
Actual outcome (next 2 quarters): Stock fell to $52 (-35% from Q3 peak)Contextual Adjustments
Adjustment 1: Market Cap Considerations
Large Cap (>$10B):
- Harder for institutions to accumulate without moving price
- Sustained accumulation = very strong signal
- Distribution easier to hide
- Adjustment: Increase signal strength for accumulation (+0.5 levels)
Mid Cap ($2B-$10B):
- Sweet spot for institutional accumulation
- Standard signal strength
- Adjustment: No adjustment (baseline)
Small Cap ($300M-$2B):
- Can be moved easily by single institution
- Need broader participation for strong signal
- Adjustment: Require higher clustering score (+10 points)
Micro Cap (<$300M):
- Limited institutional interest
- 13F data less relevant
- Adjustment: De-weight institutional signals (-0.5 levels)
Adjustment 2: Current Valuation
Undervalued (P/E <15, P/B <2, FCF Yield >8%):
- Institutional accumulation = value investors seeing opportunity
- Higher probability of success
- Adjustment: +0.5 signal strength
Fairly Valued (P/E 15-25, typical metrics):
- Standard interpretation
- Adjustment: No adjustment
Overvalued (P/E >30, P/B >5, FCF Yield <3%):
- Institutional accumulation may be late-stage momentum
- Distribution more significant (quality investors taking profits)
- Adjustment: -0.5 signal strength for accumulation, +0.5 for distribution
Adjustment 3: Sector/Industry Dynamics
Secular Growth Sector (Tech, Healthcare Innovation):
- Institutional accumulation normal
- Need higher magnitude for strong signal
- Adjustment: Require +5% higher ownership change
Cyclical Sector (Industrials, Materials, Energy):
- Pay attention to economic cycle
- Early cycle accumulation = strong signal
- Late cycle distribution = strong signal
- Adjustment: Context-dependent, weight by macro environment
Defensive Sector (Utilities, Consumer Staples, REITs):
- Lower growth, stable ownership
- Large changes more meaningful
- Adjustment: Lower thresholds (-3% ownership change)
Adjustment 4: Recent Price Action
Stock Down >20% in past quarter:
- Institutional accumulation = contrarian buying (value opportunity)
- Interpretation: Very strong signal if quality investors
- Adjustment: +1.0 signal strength
Stock Flat to +10%:
- Standard interpretation
- Adjustment: No adjustment
Stock Up >30% in past quarter:
- Institutional accumulation may be chasing momentum (late)
- Distribution less meaningful (profit-taking)
- Adjustment: -0.5 signal strength for accumulation
Stock Up >100% in past year:
- Institutional distribution = smart money taking profits (very significant)
- New accumulation suspect (likely late-stage momentum)
- Adjustment: +1.0 signal strength for distribution, -1.0 for accumulation
Multi-Factor Integration
Combining Institutional Flow with Other Signals
Institutional Flow + Fundamental Analysis:
| Institutional Signal | Fundamental Signal | Combined Interpretation | Action |
|---|---|---|---|
| Strong Buy | Strong Buy | Very High Conviction | BUY LARGE (5%+ position) |
| Strong Buy | Neutral | High Conviction | BUY (3-5% position) |
| Strong Buy | Weak | Contrarian Value | BUY SMALL (1-2%, monitor) |
| Moderate Buy | Strong Buy | High Conviction | BUY (3-5% position) |
| Moderate Buy | Neutral | Moderate Conviction | BUY SMALL (1-3% position) |
| Neutral | Strong Buy | Fundamental-Driven | BUY (2-4% position) |
| Moderate Sell | Strong Buy | Investigate Divergence | HOLD (research further) |
| Strong Sell | Strong Buy | Major Red Flag | AVOID (institutions know something) |
| Strong Sell | Weak | Confirmed Decline | SELL or SHORT |
Institutional Flow + Technical Analysis:
| Institutional Signal | Technical Signal | Combined Interpretation | Action |
|---|---|---|---|
| Strong Buy | Breakout | Confirmed Uptrend | BUY on breakout |
| Strong Buy | Basing | Accumulation Before Move | BUY in base, add on breakout |
| Strong Buy | Downtrend | Early/Contrarian | WAIT for technical confirmation |
| Moderate Buy | Breakout | Confirming Move | BUY on pullback to breakout level |
| Neutral | Breakout | Technically Driven | TRADE (not invest) |
| Moderate Sell | Breakdown | Confirmed Downtrend | SELL |
| Strong Sell | Breakdown | Accelerating Decline | SELL IMMEDIATELY |
Institutional Flow + Insider Trading:
| Institutional Signal | Insider Signal | Combined Interpretation | Action |
|---|---|---|---|
| Strong Buy | Insider Buying | Maximum Conviction | BUY LARGE |
| Strong Buy | Neutral | Strong Signal | BUY |
| Strong Buy | Insider Selling | Investigate Discrepancy | BUY SMALL (monitor) |
| Neutral | Insider Buying | Insider Conviction | BUY MODERATE |
| Moderate Sell | Insider Buying | Conflicting Signals | HOLD (investigate) |
| Moderate Sell | Insider Selling | Confirming Distribution | SELL |
| Strong Sell | Insider Selling | Maximum Conviction SELL | EXIT IMMEDIATELY |
Sector Rotation Framework
Using Institutional Flow to Identify Sector Rotation
Step 1: Calculate Aggregate Sector-Level Institutional Flow
For each sector (Technology, Healthcare, Financials, etc.):
Sector Institutional Flow Score =
Sum of (Stock Institutional Ownership Change × Market Cap) for all stocks in sector
/ Total Sector Market Cap
Positive score = Net institutional inflow to sector
Negative score = Net institutional outflow from sectorStep 2: Rank Sectors by Flow Score
Top 3 sectors (highest positive flow) = Accumulation sectors
Middle sectors = Neutral
Bottom 3 sectors (most negative flow) = Distribution sectorsStep 3: Interpret by Market Cycle
Early Cycle (Post-Recession Recovery):
- Expect Accumulation: Technology, Consumer Discretionary, Financials
- Expect Distribution: Utilities, Consumer Staples, Healthcare
- Signal: If institutional flow matches expectations = confirmation
- Signal: If institutional flow opposes expectations = investigate (possible false recovery or delayed cycle)
Mid Cycle (Economic Expansion):
- Expect Accumulation: Industrials, Materials, Energy
- Expect Distribution: Defensive sectors
- Signal: Rotation into cyclicals confirms expansion
Late Cycle (Peak Growth):
- Expect Accumulation: Energy, Materials (inflation protection)
- Expect Distribution: Technology, Consumer Discretionary (profit-taking)
- Signal: Rotation to inflation hedges signals late cycle
Recession:
- Expect Accumulation: Utilities, Consumer Staples, Healthcare
- Expect Distribution: Cyclicals, Growth stocks
- Signal: Flight to safety
Step 4: Portfolio Allocation Based on Sector Flow
High Institutional Inflow Sectors:
- Overweight (30-40% of equity allocation)
- Select best stocks within sector using institutional flow
Neutral Sectors:
- Market weight (10-20% of equity allocation)
High Institutional Outflow Sectors:
- Underweight or zero weight (0-10% of equity allocation)
- Sell existing positions showing institutional distributionPractical Decision Tree
For New Position Consideration:
1. Run institutional flow analysis on stock
↓
2. What is the signal level?
↓
├─ Strong Buy Signal (Level 1)
│ ├─ Check fundamentals: Strong → BUY LARGE (5%+)
│ ├─ Check fundamentals: Neutral → BUY (3-5%)
│ └─ Check fundamentals: Weak → BUY SMALL (1-2%), monitor
│
├─ Moderate Buy Signal (Level 2)
│ ├─ Check fundamentals: Strong → BUY (3-5%)
│ ├─ Check fundamentals: Neutral → BUY SMALL (1-3%)
│ └─ Check fundamentals: Weak → PASS
│
├─ Neutral (Level 3)
│ └─ Decide based on other factors (fundamental, technical)
│
├─ Moderate Sell Signal (Level 4)
│ └─ AVOID (do not initiate)
│
└─ Strong Sell Signal (Level 5)
└─ AVOID or SHORT (if appropriate)For Existing Position Review:
1. Run quarterly institutional flow analysis
↓
2. What is the signal level?
↓
├─ Strong Buy Signal (Level 1)
│ └─ ADD to position (up to maximum 10% portfolio weight)
│
├─ Moderate Buy Signal (Level 2)
│ └─ HOLD or small ADD
│
├─ Neutral (Level 3)
│ └─ HOLD (no change)
│
├─ Moderate Sell Signal (Level 4)
│ ├─ Check fundamentals: Deteriorating → TRIM to 50% or SELL
│ ├─ Check fundamentals: Stable → TRIM to 75%
│ └─ Check fundamentals: Strong → HOLD (monitor closely)
│
└─ Strong Sell Signal (Level 5)
└─ SELL immediately (full exit)Common Mistakes to Avoid
Mistake 1: Overreacting to Single Quarter
Problem: One quarter of institutional buying/selling may not be a trend
Solution:
- Require 2+ quarters for moderate signals
- Require 3+ quarters for strong conviction
- View single quarter as hypothesis, not confirmation
Mistake 2: Ignoring Index Fund Flows
Problem: Treating passive inflows like active accumulation
Solution:
- Separate active from passive using institutional tiers
- Weight Tier 1/2 heavily, Tier 4 (index funds) minimally
- Focus on WHO is buying, not just THAT institutions are buying
Mistake 3: Following Too Late
Problem: Institutional buying visible in 13F after 45-day lag; stock may have already moved
Solution:
- Use 13F as confirming signal, not entry trigger
- Combine with technical analysis for timing
- Be willing to buy after initial move if institutional thesis strong
Mistake 4: Ignoring Price Action Context
Problem: Institutional buying in falling stock (catching falling knife) vs rising stock (momentum)
Solution:
- Institutional accumulation + falling price = potential value opportunity (higher conviction)
- Institutional accumulation + rising price = momentum (lower conviction, may be late)
- Adjust signal strength based on recent price action
Mistake 5: Equal Weighting All Institutions
Problem: Treating index fund flows same as Berkshire Hathaway
Solution:
- Use institutional tier weighting framework
- Tier 1 (Superinvestors): 3.0-3.5x weight
- Tier 2 (Quality Active): 2.0-2.5x weight
- Tier 3 (Average): 1.0-1.5x weight
- Tier 4 (Passive): 0.0-0.5x weight
Summary Checklist
Before making investment decision based on institutional flow:
✅ Verification Checklist:
- [ ] Signal level determined (Strong/Moderate/Neutral Buy or Sell)
- [ ] Multi-quarter trend confirmed (2+ quarters same direction)
- [ ] Institution quality assessed (Tier 1/2 involvement?)
- [ ] Clustering score calculated (>50 for strong signals)
- [ ] Market cap context considered (adjustment needed?)
- [ ] Valuation context considered (adjustment needed?)
- [ ] Recent price action reviewed (adjustment needed?)
- [ ] Fundamental analysis completed (aligns with institutional thesis?)
- [ ] Technical analysis reviewed (entry timing optimized?)
- [ ] Position sizing determined based on signal strength
- [ ] Risk management plan established (stop-loss, monitoring frequency)
Final Decision Framework:
- 5+ yes = High Conviction: BUY/SELL with conviction
- 3-4 yes = Moderate Conviction: HOLD or Small BUY/TRIM
- <3 yes = Low Conviction: PASS or minimal position
---
This interpretation framework should be used as a systematic guide, not rigid rules. Market conditions, individual stock characteristics, and portfolio constraints should also inform final decisions. Institutional flow is a powerful signal but works best when combined with fundamental and technical analysis.
#!/usr/bin/env python3
"""
Institutional Flow Tracker - Single Stock Deep Dive
Provides detailed analysis of institutional ownership for a specific stock,
including the multi-quarter ownership trend, top holders, and the largest
holders' position changes.
Reads FMP's aggregate 13F summary (institutional-ownership/symbol-positions-summary)
for the quarter-over-quarter trend, and extract-analytics/holder for the named
top holders. Reliability is graded from FMP's coverage signals (holder breadth +
whether a comparable prior quarter exists) via data_quality.coverage_grade.
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 --output-dir reports/
Requirements:
- FMP API key (set FMP_API_KEY environment variable or pass --api-key)
"""
import argparse
import datetime
import os
import sys
from typing import Optional
try:
import requests
except ImportError:
print("Error: 'requests' library not installed. Install with: pip install requests")
sys.exit(1)
# Add scripts directory to path for data_quality import
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from data_quality import (
coverage_grade,
current_quarter,
iter_quarters,
normalize_holder,
quarter_end_date,
)
STABLE_URL = "https://financialmodelingprep.com/stable"
class SingleStockAnalyzer:
"""Analyze institutional ownership for a single stock"""
def __init__(self, api_key: str, as_of: Optional[datetime.date] = None):
self.api_key = api_key
self.base_url = STABLE_URL
self._as_of = as_of or datetime.date.today()
self._latest_yq: Optional[tuple[int, int]] = None
def _get(self, path: str, **params) -> Optional[object]:
"""GET a /stable endpoint, returning parsed JSON or None.
The API key is sent via header (not query string) so it never appears
in a URL — including any URL embedded in a raised exception message.
"""
symbol = params.get("symbol", "")
try:
response = requests.get(
f"{self.base_url}/{path}",
params=params,
headers={"apikey": self.api_key},
timeout=30,
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error fetching {path} for {symbol}: {e}")
return None
def get_company_profile(self, symbol: str) -> dict:
"""Get company profile information (/stable profile)."""
data = self._get("profile", symbol=symbol)
profile = data[0] if isinstance(data, list) and data else {}
# /stable returns marketCap; expose mktCap alias for legacy readers.
if profile and "mktCap" not in profile and "marketCap" in profile:
profile = {**profile, "mktCap": profile["marketCap"]}
return profile
def get_ownership_summary(self, symbol: str, year: int, quarter: int) -> Optional[dict]:
"""Get the aggregate 13F positions summary for one symbol/quarter."""
data = self._get(
"institutional-ownership/symbol-positions-summary",
symbol=symbol,
year=year,
quarter=quarter,
)
if isinstance(data, list) and data:
return data[0]
return None
def latest_summary(
self, symbol: str, max_lookback: int = 5
) -> tuple[Optional[int], Optional[int], Optional[dict]]:
"""Return (year, quarter, summary) for the most recent filed quarter.
13F data lags quarter end by ~45 days, so probe backward from the
current calendar quarter until a quarter with data is found.
"""
y0, q0 = current_quarter(self._as_of)
for year, quarter in iter_quarters(y0, q0, max_lookback):
summary = self.get_ownership_summary(symbol, year, quarter)
if summary:
self._latest_yq = (year, quarter)
return (year, quarter, summary)
return (None, None, None)
def get_top_holders(self, symbol: str, year: int, quarter: int, limit: int = 20) -> list[dict]:
"""Get the top institutional holders (by market value) for a quarter.
extract-analytics/holder returns 10 rows per page, sorted by position
size. Pages forward (bounded) until ``limit`` holders are collected.
"""
holders: list[dict] = []
page = 0
while len(holders) < limit and page < 5:
rows = self._get(
"institutional-ownership/extract-analytics/holder",
symbol=symbol,
year=year,
quarter=quarter,
page=page,
)
if not isinstance(rows, list) or not rows:
break
holders.extend(normalize_holder(r) for r in rows)
if len(rows) < 10:
break
page += 1
return holders[:limit]
def analyze_stock(self, symbol: str, quarters: int = 8) -> dict:
"""Perform institutional ownership analysis on a stock.
Builds the multi-quarter ownership trend from FMP's aggregate summary
and the named top holders from extract-analytics/holder. Position-change
lists reflect the largest holders (top of the holder list).
"""
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("marketCap", profile.get("mktCap", 0)) or 0
print(f"Company: {company_name}")
print(f"Sector: {sector}")
print(f"Market Cap: ${market_cap:,}")
# Find the most recent filed quarter, then walk back N quarters.
year, quarter, latest = self.latest_summary(symbol)
if not latest:
print(f"No institutional ownership data available for {symbol}")
return {}
summaries = []
for yy, qq in iter_quarters(year, quarter, quarters):
summary = self.get_ownership_summary(symbol, yy, qq)
if summary:
summaries.append((yy, qq, summary))
if len(summaries) < 2:
print(f"Insufficient data (need at least 2 quarters, found {len(summaries)})")
return {}
# Named top holders for the most recent quarter only.
top_year, top_quarter, current_summary = summaries[0]
top_holders = self.get_top_holders(symbol, top_year, top_quarter, limit=20)
# Build quarterly metrics (most recent first).
quarterly_metrics = []
for idx, (yy, qq, summary) in enumerate(summaries):
quarterly_metrics.append(
{
"quarter": summary.get("date") or quarter_end_date(yy, qq),
"total_shares": summary.get("numberOf13Fshares", 0) or 0,
"num_holders": summary.get("investorsHolding", 0) or 0,
"top_holders": top_holders if idx == 0 else [],
}
)
# Reliability assessment for the most recent quarter.
cur_investors = current_summary.get("investorsHolding", 0) or 0
last_investors = current_summary.get("lastInvestorsHolding", 0) or 0
grade = coverage_grade(cur_investors, last_investors)
data_quality = {
"grade": grade,
"institution_count": cur_investors,
"ownership_percent": round(current_summary.get("ownershipPercent", 0) or 0, 2),
"ownership_percent_change": round(
current_summary.get("ownershipPercentChange", 0) or 0, 2
),
"prior_quarter_available": last_investors > 0,
"increased": current_summary.get("increasedPositions", 0) or 0,
"reduced": current_summary.get("reducedPositions", 0) or 0,
"new": current_summary.get("newPositions", 0) or 0,
"closed": current_summary.get("closedPositions", 0) or 0,
}
# Trends across the available quarters (oldest is the last entry).
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 None
)
holders_trend = most_recent["num_holders"] - oldest["num_holders"]
# Position changes from the largest holders (named, bounded to top holders).
new_positions = []
increased_positions = []
decreased_positions = []
for holder in top_holders:
shares = holder["shares"]
change = holder["change"]
if holder["is_new"]:
new_positions.append({"name": holder["name"], "shares": shares})
elif change > 0:
previous_shares = shares - change
pct_change = (change / previous_shares * 100) if previous_shares > 0 else 0
increased_positions.append(
{
"name": holder["name"],
"current_shares": shares,
"change": change,
"pct_change": pct_change,
}
)
elif change < 0:
previous_shares = shares - change
pct_change = (change / previous_shares * 100) if previous_shares > 0 else 0
decreased_positions.append(
{
"name": holder["name"],
"current_shares": shares,
"change": change,
"pct_change": pct_change,
}
)
increased_positions.sort(key=lambda x: x["change"], reverse=True)
decreased_positions.sort(key=lambda x: x["change"])
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,
"data_quality": data_quality,
}
def generate_report(
self, analysis: dict, output_file: Optional[str] = None, output_dir: Optional[str] = None
):
"""Generate detailed markdown report"""
if not analysis:
print("No analysis data available")
return
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
symbol = analysis["symbol"]
data_quality = analysis.get("data_quality", {})
grade = data_quality.get("grade", "N/A")
report = f"""# Institutional Ownership Analysis: {symbol}
**Company:** {analysis["company_name"]}
**Sector:** {analysis["sector"]}
**Market Cap:** ${analysis["market_cap"]:,}
**Analysis Date:** {timestamp}
**Data Reliability:** Grade {grade}
"""
# Data quality warning
if grade == "C":
report += """**WARNING: INSUFFICIENT COVERAGE**
This stock has too few 13F holders (or no comparable prior quarter) to rank
reliably. Metrics below may be misleading. Do NOT use for investment decisions.
"""
elif grade == "B":
report += """**CAUTION: Reference Only**
This stock has thin institutional coverage (fewer than 50 13F holders).
Use metrics as reference only, with additional verification.
"""
report += "## Executive Summary\n\n"
# Determine overall signal
shares_trend = analysis["shares_trend"]
holders_trend = analysis["holders_trend"]
if shares_trend is None:
signal = "**DATA QUALITY INSUFFICIENT**"
interpretation = (
"Shares trend cannot be reliably calculated due to insufficient "
"institutional coverage in the endpoint quarters."
)
trend_str = "N/A (insufficient data quality)"
else:
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."
trend_str = f"{shares_trend:+.2f}%"
report += f"""**Signal:** {signal}
**Interpretation:** {interpretation}
**Trend ({len(analysis["quarterly_metrics"])} Quarters):**
- Institutional Shares: {trend_str}
- Number of Institutions: {holders_trend:+d}
## Data Quality Assessment
| Metric | Value |
|--------|-------|
| Reliability Grade | **{grade}** |
| Institutional Holders (breadth) | {data_quality.get("institution_count", 0):,} |
| Institutional Ownership | {data_quality.get("ownership_percent", 0):.2f}% |
| Ownership Change (QoQ) | {data_quality.get("ownership_percent_change", 0):+.2f} pp |
| Prior Quarter Available | {"Yes" if data_quality.get("prior_quarter_available") else "No"} |
| Increased / Reduced / New / Closed | {data_quality.get("increased", 0)} / {data_quality.get("reduced", 0)} / {data_quality.get("new", 0)} / {data_quality.get("closed", 0)} |
## Historical Institutional Ownership Trend
| Quarter | Total Shares Held | 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['num_holders']} | {qoq_str} |\n"
)
# Recent changes
report += f"""
## Largest Holders' Changes ({metrics[0]["quarter"]})
> Position-change lists below reflect the **largest institutional holders**
> (top of the holder list), not every filer.
### New Positions (largest holders 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"
if len(analysis["new_positions"]) > 10:
report += f"\n*...and {len(analysis['new_positions']) - 10} more new positions*\n"
else:
report += "No new institutional positions among the largest holders.\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 among the largest holders.\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 among the largest holders.\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("shares", 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('name', 'Unknown')} | {shares:,} | {pct_of_inst:.2f}% | {change:+,} |\n"
# Concentration analysis
if len(metrics[0]["top_holders"]) >= 10:
top_10_shares = sum(h.get("shares", 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 += """
## Methodology Note
This analysis reads FMP's aggregate 13F summary
(`institutional-ownership/symbol-positions-summary`), which reconciles
quarter-over-quarter deltas across all filing managers at source:
- **Ownership trend:** total 13F shares (`numberOf13Fshares`) and holder
breadth (`investorsHolding`) per quarter
- **Flow counts:** managers that increased, reduced, newly opened, or closed
positions
Named top holders and their position changes come from
`extract-analytics/holder` (sorted by position size), so the New / Increased /
Decreased lists reflect the **largest** holders rather than the full tail.
## 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, /stable)
**Data Lag:** ~45 days after quarter end
**Note:** Use as confirming indicator alongside fundamental and technical analysis
"""
# Determine output path
if output_file:
output_path = output_file if output_file.endswith(".md") else f"{output_file}.md"
else:
filename = (
f"institutional_analysis_{symbol}_{datetime.datetime.now().strftime('%Y%m%d')}.md"
)
if output_dir:
os.makedirs(output_dir, exist_ok=True)
output_path = os.path.join(output_dir, filename)
else:
output_path = filename
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="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 directory
python3 analyze_single_stock.py TSLA --output-dir reports/
""",
)
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(
"--output-dir",
type=str,
default="reports/",
help="Output directory for reports (default: reports/)",
)
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, output_dir=args.output_dir)
# Print summary
dq = analysis.get("data_quality", {})
trend = analysis["shares_trend"]
trend_str = f"{trend:+.2f}%" if trend is not None else "N/A"
print("\n" + "=" * 80)
print(f"INSTITUTIONAL OWNERSHIP SUMMARY: {args.symbol}")
print("=" * 80)
print(
f"Data Reliability: Grade {dq.get('grade', 'N/A')} "
f"({dq.get('institution_count', 0):,} holders, "
f"{dq.get('ownership_percent', 0):.1f}% ownership)"
)
print(
f"Trend ({args.quarters} quarters): {trend_str} shares, "
f"{analysis['holders_trend']:+d} institutions"
)
print("Largest-Holder Activity:")
print(f" - New Positions: {len(analysis['new_positions'])}")
print(f" - Increased: {len(analysis['increased_positions'])}")
print(f" - Decreased: {len(analysis['decreased_positions'])}")
if __name__ == "__main__":
main()
"""Shared data quality utilities for Institutional Flow Tracker.
Provides tradability filtering, share-class deduplication, and reliability
grading shared across track_institutional_flow.py and analyze_single_stock.py.
Data source (FMP /stable):
- institutional-ownership/symbol-positions-summary -> per-quarter aggregate
flows (investorsHolding, numberOf13Fshares, increasedPositions, ...),
already reconciled server-side by FMP.
- institutional-ownership/extract-analytics/holder -> per-investor rows
(investorName, sharesNumber, changeInSharesNumber, isNew, isSoldOut, ...).
Reliability is graded from FMP's own coverage signals (holder breadth +
whether a comparable prior quarter exists), which is how institutional-flow
analysis is graded in practice (ownership breadth / dynamics). The legacy
per-holder genuine/coverage/match reconciliation was a client-side workaround
for the retired /api/v3 institutional-holder feed, which returned asymmetric
holder lists across quarters; the /stable summary endpoint reconciles those
deltas at source, so that machinery no longer applies.
"""
import re
# --- Known share-class groups for deduplication ---
# Each tuple: (base_symbol_pattern, group_key)
SHARE_CLASS_GROUPS = [
(re.compile(r"^BRK[.-]?[AB]$"), "BRK"),
(re.compile(r"^GOOG[L]?$"), "GOOG"),
(re.compile(r"^PBR[.-]?A?$"), "PBR"),
(re.compile(r"^RDS[.-]?[AB]$"), "RDS"),
(re.compile(r"^FCAU?$"), "FCA"),
(re.compile(r"^(LBTYA|LBTYB|LBTYK)$"), "LBTY"),
(re.compile(r"^FOX[A]?$"), "FOX"),
(re.compile(r"^(DISCA|DISCB|DISCK)$"), "DISC"),
(re.compile(r"^NWS[A]?$"), "NWS"),
(re.compile(r"^(VIACA|VIAC)$"), "VIAC"),
]
# Default breadth thresholds for coverage_grade(). A stock needs a comparable
# prior quarter and enough 13F holders for the aggregate change to be meaningful.
MIN_RELIABLE_HOLDERS = 50 # Grade A: dense, well-covered name
MIN_USABLE_HOLDERS = 10 # Grade B floor; below this -> Grade C (excluded)
def coverage_grade(
investors_holding: int,
last_investors_holding: int,
*,
min_reliable: int = MIN_RELIABLE_HOLDERS,
min_usable: int = MIN_USABLE_HOLDERS,
) -> str:
"""Grade reliability of an aggregate 13F summary from FMP coverage signals.
Institutional-flow signals are graded on ownership *breadth* (how many
managers hold the name) and whether a comparable prior quarter exists to
measure change against.
Grades:
A: prior quarter present AND breadth >= min_reliable
-> dense coverage, safe for ranking.
B: prior quarter present AND breadth >= min_usable
-> usable but thin; reference only.
C: no prior quarter (change not measurable) OR breadth < min_usable
-> insufficient coverage; exclude from rankings.
"""
if last_investors_holding <= 0 or investors_holding < min_usable:
return "C"
if investors_holding >= min_reliable:
return "A"
return "B"
def iter_quarters(year: int, quarter: int, count: int):
"""Yield ``count`` (year, quarter) tuples descending from (year, quarter).
Example: iter_quarters(2026, 1, 3) -> (2026, 1), (2025, 4), (2025, 3).
"""
y, q = year, quarter
for _ in range(count):
yield (y, q)
q -= 1
if q == 0:
q = 4
y -= 1
def current_quarter(as_of) -> tuple[int, int]:
"""Return the (year, quarter) of the calendar quarter containing ``as_of``.
``as_of`` is a datetime.date (or datetime). Used as the starting point for
walking back to the most recent quarter that has 13F data filed.
"""
return (as_of.year, (as_of.month - 1) // 3 + 1)
def quarter_end_date(year: int, quarter: int) -> str:
"""Return the calendar quarter-end date as 'YYYY-MM-DD'."""
month, day = {1: (3, 31), 2: (6, 30), 3: (9, 30), 4: (12, 31)}[quarter]
return f"{year:04d}-{month:02d}-{day:02d}"
def normalize_holder(raw: dict) -> dict:
"""Map a /stable extract-analytics/holder row to a compact holder record.
Returns keys: name, shares, change, is_new, is_sold_out. Investor names are
occasionally blank in the feed (a CIK with no resolved name); those fall
back to a 'CIK <id>' label, or 'Unknown' if the CIK is also missing.
"""
name = (raw.get("investorName") or "").strip()
if not name:
cik = str(raw.get("cik") or "").strip()
name = f"CIK {cik}" if cik else "Unknown"
return {
"name": name,
"shares": raw.get("sharesNumber", 0) or 0,
"change": raw.get("changeInSharesNumber", 0) or 0,
"is_new": bool(raw.get("isNew", False)),
"is_sold_out": bool(raw.get("isSoldOut", False)),
}
def is_tradable_stock(profile: dict) -> bool:
"""Check if a stock profile represents a tradable common stock.
Excludes:
- ETFs (isEtf == True)
- Funds (isFund == True)
- Inactive / delisted stocks (isActivelyTrading == False)
- Empty profiles (no symbol)
"""
if not profile or not profile.get("symbol"):
return False
if profile.get("isEtf", False):
return False
if profile.get("isFund", False):
return False
if profile.get("isActivelyTrading", True) is False:
return False
return True
def _get_share_class_group(symbol: str) -> str:
"""Return the group key for a symbol if it's a known share class variant."""
for pattern, group_key in SHARE_CLASS_GROUPS:
if pattern.match(symbol):
return group_key
return ""
def deduplicate_share_classes(results: list[dict]) -> list[dict]:
"""Remove duplicate share class entries, keeping the one with higher market_cap.
Known share class groups: BRK-A/B, GOOG/GOOGL, PBR/PBR-A, etc.
Args:
results: list of dicts with at least 'symbol' and 'market_cap' keys.
Returns:
Deduplicated list preserving original order for non-duplicate entries.
"""
if not results:
return []
# Group by share class
groups: dict[str, list[int]] = {}
for i, r in enumerate(results):
group = _get_share_class_group(r.get("symbol", ""))
if group:
groups.setdefault(group, []).append(i)
# Find indices to remove (keep the one with highest market_cap)
remove_indices = set()
for _group_key, indices in groups.items():
if len(indices) <= 1:
continue
# Keep the one with highest market_cap
best_idx = max(indices, key=lambda i: results[i].get("market_cap", 0))
for idx in indices:
if idx != best_idx:
remove_indices.add(idx)
return [r for i, r in enumerate(results) if i not in remove_indices]
"""Shared fixtures for Institutional Flow Tracker tests"""
import os
import sys
# Add scripts directory to path so modules can be imported
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
# Add tests directory to path so helpers can be imported
sys.path.insert(0, os.path.dirname(__file__))
"""Tests for the /stable aggregate helpers in data_quality.
Covers coverage_grade (breadth-based reliability), the quarter-walk-back
helpers (current_quarter, iter_quarters, quarter_end_date), and normalize_holder.
"""
import datetime
from data_quality import (
coverage_grade,
current_quarter,
iter_quarters,
normalize_holder,
quarter_end_date,
)
class TestCoverageGrade:
"""coverage_grade grades reliability from holder breadth + prior quarter."""
def test_grade_a_dense_with_prior(self):
assert coverage_grade(6000, 6300) == "A"
def test_grade_a_boundary(self):
# Exactly 50 holders with a prior quarter -> A
assert coverage_grade(50, 40) == "A"
def test_grade_b_thin_but_usable(self):
assert coverage_grade(20, 18) == "B"
def test_grade_b_boundary(self):
# Exactly 10 holders -> still usable (B)
assert coverage_grade(10, 8) == "B"
def test_grade_c_no_prior_quarter(self):
# No comparable prior quarter -> change not measurable -> C
assert coverage_grade(500, 0) == "C"
def test_grade_c_too_few_holders(self):
assert coverage_grade(9, 8) == "C"
def test_grade_c_zero_holders(self):
assert coverage_grade(0, 0) == "C"
def test_custom_thresholds(self):
assert coverage_grade(30, 25, min_reliable=20) == "A"
assert coverage_grade(15, 12, min_usable=20) == "C"
class TestIterQuarters:
def test_descends_across_year_boundary(self):
assert list(iter_quarters(2026, 1, 3)) == [(2026, 1), (2025, 4), (2025, 3)]
def test_single_quarter(self):
assert list(iter_quarters(2025, 3, 1)) == [(2025, 3)]
def test_wraps_multiple_years(self):
result = list(iter_quarters(2026, 2, 6))
assert result == [(2026, 2), (2026, 1), (2025, 4), (2025, 3), (2025, 2), (2025, 1)]
class TestCurrentQuarter:
def test_q1(self):
assert current_quarter(datetime.date(2026, 2, 15)) == (2026, 1)
def test_q2(self):
assert current_quarter(datetime.date(2026, 5, 20)) == (2026, 2)
def test_q4(self):
assert current_quarter(datetime.date(2025, 12, 31)) == (2025, 4)
class TestQuarterEndDate:
def test_q1(self):
assert quarter_end_date(2026, 1) == "2026-03-31"
def test_q4(self):
assert quarter_end_date(2025, 4) == "2025-12-31"
class TestNormalizeHolder:
def test_maps_stable_fields(self):
raw = {
"investorName": "BLACKROCK, INC.",
"sharesNumber": 1_144_695_425,
"changeInSharesNumber": -9_970_306,
"isNew": False,
"isSoldOut": False,
}
h = normalize_holder(raw)
assert h == {
"name": "BLACKROCK, INC.",
"shares": 1_144_695_425,
"change": -9_970_306,
"is_new": False,
"is_sold_out": False,
}
def test_blank_name_becomes_unknown(self):
h = normalize_holder({"investorName": "", "sharesNumber": 100, "changeInSharesNumber": 10})
assert h["name"] == "Unknown"
def test_blank_name_falls_back_to_cik(self):
h = normalize_holder({"investorName": "", "cik": "0002012383", "sharesNumber": 100})
assert h["name"] == "CIK 0002012383"
def test_missing_fields_default_to_zero(self):
h = normalize_holder({"investorName": "Acme"})
assert h["shares"] == 0
assert h["change"] == 0
assert h["is_new"] is False
"""Filter tests for data_quality module.
Tests ETF exclusion, inactive stock exclusion, acquired stock exclusion,
and share class deduplication.
"""
from data_quality import _get_share_class_group, deduplicate_share_classes, is_tradable_stock
class TestIsTradableStock:
"""is_tradable_stock filters out ETFs, inactive stocks, and acquired companies."""
def test_normal_stock_is_tradable(self):
profile = {
"symbol": "AAPL",
"companyName": "Apple Inc.",
"isEtf": False,
"isActivelyTrading": True,
"isFund": False,
}
assert is_tradable_stock(profile) is True
def test_etf_excluded(self):
profile = {
"symbol": "SPY",
"companyName": "SPDR S&P 500 ETF Trust",
"isEtf": True,
"isActivelyTrading": True,
"isFund": False,
}
assert is_tradable_stock(profile) is False
def test_fund_excluded(self):
profile = {
"symbol": "VFINX",
"companyName": "Vanguard 500 Index Fund",
"isEtf": False,
"isActivelyTrading": True,
"isFund": True,
}
assert is_tradable_stock(profile) is False
def test_inactive_stock_excluded(self):
profile = {
"symbol": "ATVI",
"companyName": "Activision Blizzard",
"isEtf": False,
"isActivelyTrading": False,
"isFund": False,
}
assert is_tradable_stock(profile) is False
def test_missing_fields_defaults_to_tradable(self):
"""If fields are missing, assume tradable (don't over-filter)."""
profile = {"symbol": "NEWCO", "companyName": "New Company"}
assert is_tradable_stock(profile) is True
def test_empty_profile_is_not_tradable(self):
assert is_tradable_stock({}) is False
def test_empty_symbol_is_not_tradable(self):
profile = {"symbol": "", "companyName": "No Symbol"}
assert is_tradable_stock(profile) is False
class TestDeduplicateShareClasses:
"""deduplicate_share_classes removes duplicate share classes like BRK-A/B."""
def test_brk_dedup_keeps_higher_market_cap(self):
results = [
{"symbol": "BRK-A", "market_cap": 800_000_000_000, "percent_change": 5.0},
{"symbol": "BRK-B", "market_cap": 800_000_000_000, "percent_change": 5.1},
{"symbol": "AAPL", "market_cap": 3_000_000_000_000, "percent_change": 2.0},
]
deduped = deduplicate_share_classes(results)
symbols = [r["symbol"] for r in deduped]
assert "AAPL" in symbols
# Only one BRK variant should remain
brk_count = sum(1 for s in symbols if s.startswith("BRK"))
assert brk_count == 1
def test_pbr_dedup(self):
results = [
{"symbol": "PBR", "market_cap": 100_000_000_000, "percent_change": 3.0},
{"symbol": "PBR-A", "market_cap": 100_000_000_000, "percent_change": 3.2},
]
deduped = deduplicate_share_classes(results)
assert len(deduped) == 1
def test_no_duplicates_unchanged(self):
results = [
{"symbol": "AAPL", "market_cap": 3_000_000_000_000, "percent_change": 2.0},
{"symbol": "MSFT", "market_cap": 2_500_000_000_000, "percent_change": 1.5},
]
deduped = deduplicate_share_classes(results)
assert len(deduped) == 2
def test_empty_list(self):
assert deduplicate_share_classes([]) == []
def test_goog_googl_dedup(self):
results = [
{"symbol": "GOOG", "market_cap": 2_000_000_000_000, "percent_change": 1.0},
{"symbol": "GOOGL", "market_cap": 2_000_000_000_000, "percent_change": 1.1},
]
deduped = deduplicate_share_classes(results)
assert len(deduped) == 1
class TestShareClassGroupRegex:
"""Regression tests for SHARE_CLASS_GROUPS regex patterns."""
def test_lbtya_matches(self):
assert _get_share_class_group("LBTYA") == "LBTY"
def test_lbtyb_matches(self):
assert _get_share_class_group("LBTYB") == "LBTY"
def test_lbtyk_matches(self):
assert _get_share_class_group("LBTYK") == "LBTY"
def test_lbtya_extra_no_match(self):
"""Symbols with extra chars must NOT match (anchor regression)."""
assert _get_share_class_group("LBTYA_EXTRA") == ""
def test_disca_matches(self):
assert _get_share_class_group("DISCA") == "DISC"
def test_discb_matches(self):
assert _get_share_class_group("DISCB") == "DISC"
def test_disck_matches(self):
assert _get_share_class_group("DISCK") == "DISC"
def test_disca_extra_no_match(self):
"""Symbols with extra chars must NOT match (anchor regression)."""
assert _get_share_class_group("DISCA_EXTRA") == ""
def test_brk_a_matches(self):
assert _get_share_class_group("BRK-A") == "BRK"
def test_brk_b_matches(self):
assert _get_share_class_group("BRK-B") == "BRK"
def test_goog_matches(self):
assert _get_share_class_group("GOOG") == "GOOG"
def test_googl_matches(self):
assert _get_share_class_group("GOOGL") == "GOOG"
def test_viaca_matches(self):
assert _get_share_class_group("VIACA") == "VIAC"
def test_viac_matches(self):
assert _get_share_class_group("VIAC") == "VIAC"
def test_viaca_extra_no_match(self):
"""Symbols with extra chars must NOT match (anchor regression)."""
assert _get_share_class_group("VIACA_EXTRA") == ""
def test_unrelated_symbol_no_match(self):
assert _get_share_class_group("AAPL") == ""
"""Tests for analyze_single_stock.py (/stable aggregate).
Verifies:
1. Source code no longer references the retired /api/v3 institutional-holder feed
2. Multi-quarter trend is built from the aggregate summary
3. Reliability (coverage) grade is included in analysis output
4. New / increased / decreased lists are derived from the named top holders
"""
import datetime
import inspect
from analyze_single_stock import SingleStockAnalyzer
from data_quality import quarter_end_date
AS_OF = datetime.date(2026, 3, 31)
# Four quarters of accumulation, most recent first.
SHARES = {
(2026, 1): 6_000_000,
(2025, 4): 5_500_000,
(2025, 3): 5_000_000,
(2025, 2): 4_500_000,
}
HOLDERS = {
(2026, 1): 220,
(2025, 4): 215,
(2025, 3): 205,
(2025, 2): 200,
}
def _make_summary(year, quarter):
shares = SHARES[(year, quarter)]
holders = HOLDERS[(year, quarter)]
return {
"date": quarter_end_date(year, quarter),
"investorsHolding": holders,
"lastInvestorsHolding": holders - 10,
"investorsHoldingChange": 10,
"numberOf13Fshares": shares,
"lastNumberOf13Fshares": shares - 200_000,
"numberOf13FsharesChange": 200_000,
"increasedPositions": 120,
"reducedPositions": 50,
"newPositions": 20,
"closedPositions": 10,
"ownershipPercent": 65.0,
"ownershipPercentChange": 1.5,
}
def _make_top_holders():
return [
{
"name": "Vanguard",
"shares": 1_000_000,
"change": 50_000,
"is_new": False,
"is_sold_out": False,
},
{
"name": "BlackRock",
"shares": 900_000,
"change": -30_000,
"is_new": False,
"is_sold_out": False,
},
{
"name": "NewFund",
"shares": 200_000,
"change": 200_000,
"is_new": True,
"is_sold_out": False,
},
{"name": "FlatFund", "shares": 100_000, "change": 0, "is_new": False, "is_sold_out": False},
]
class TestSourceCodeContract:
"""Source must not reference the retired /api/v3 institutional-holder feed."""
def test_no_v3_api_path(self):
source = inspect.getsource(SingleStockAnalyzer)
assert "/api/v3" not in source
assert "institutional-holder/" not in source
class TestAnalyzeStockOutput:
"""analyze_stock output structure with mocked /stable responses."""
def _analyzer(self, monkeypatch, top_holders=None):
analyzer = SingleStockAnalyzer("fake_key", as_of=AS_OF)
monkeypatch.setattr(
analyzer,
"get_company_profile",
lambda symbol: {
"companyName": "Test Corp",
"sector": "Technology",
"marketCap": 1_000_000_000,
},
)
def mock_summary(symbol, year, quarter):
key = (year, quarter)
return _make_summary(year, quarter) if key in SHARES else None
monkeypatch.setattr(analyzer, "get_ownership_summary", mock_summary)
monkeypatch.setattr(
analyzer,
"get_top_holders",
lambda *a, **k: top_holders if top_holders is not None else _make_top_holders(),
)
return analyzer
def test_analysis_returns_coverage_grade(self, monkeypatch):
result = self._analyzer(monkeypatch).analyze_stock("TEST", quarters=4)
dq = result["data_quality"]
assert dq["grade"] in ("A", "B", "C")
assert dq["institution_count"] == 220
assert dq["ownership_percent"] == 65.0
assert dq["prior_quarter_available"] is True
def test_quarterly_metrics_count(self, monkeypatch):
result = self._analyzer(monkeypatch).analyze_stock("TEST", quarters=4)
assert len(result["quarterly_metrics"]) == 4
# Most recent quarter carries the named top holders; older ones do not.
assert result["quarterly_metrics"][0]["top_holders"]
assert result["quarterly_metrics"][-1]["top_holders"] == []
def test_shares_trend_accumulation(self, monkeypatch):
result = self._analyzer(monkeypatch).analyze_stock("TEST", quarters=4)
# (6.0M - 4.5M) / 4.5M * 100 == +33.33%
assert result["shares_trend"] > 30
assert result["holders_trend"] == 20 # 220 - 200
def test_new_positions_from_top_holders(self, monkeypatch):
result = self._analyzer(monkeypatch).analyze_stock("TEST", quarters=4)
names = [p["name"] for p in result["new_positions"]]
assert names == ["NewFund"]
def test_increased_positions_positive_change(self, monkeypatch):
result = self._analyzer(monkeypatch).analyze_stock("TEST", quarters=4)
assert [p["name"] for p in result["increased_positions"]] == ["Vanguard"]
for pos in result["increased_positions"]:
assert pos["change"] > 0
def test_decreased_positions_negative_change(self, monkeypatch):
result = self._analyzer(monkeypatch).analyze_stock("TEST", quarters=4)
assert [p["name"] for p in result["decreased_positions"]] == ["BlackRock"]
for pos in result["decreased_positions"]:
assert pos["change"] < 0
def test_insufficient_data_returns_empty(self, monkeypatch):
analyzer = SingleStockAnalyzer("fake_key", as_of=AS_OF)
monkeypatch.setattr(
analyzer, "get_company_profile", lambda s: {"companyName": "X", "sector": "Y"}
)
# Only the latest quarter has data -> fewer than 2 quarters -> {}
def one_quarter(symbol, year, quarter):
return _make_summary(2026, 1) if (year, quarter) == (2026, 1) else None
monkeypatch.setattr(analyzer, "get_ownership_summary", one_quarter)
monkeypatch.setattr(analyzer, "get_top_holders", lambda *a, **k: _make_top_holders())
assert analyzer.analyze_stock("TEST", quarters=4) == {}
class TestSingleStockReport:
"""generate_report() must produce valid markdown with methodology and warnings."""
def _make_mock_analysis(self, grade="A"):
return {
"symbol": "TEST",
"company_name": "Test Corp",
"sector": "Technology",
"market_cap": 1_000_000_000,
"quarterly_metrics": [
{
"quarter": "2026-03-31",
"total_shares": 5_000_000,
"num_holders": 220,
"top_holders": [
{"name": f"Fund{i}", "shares": 100_000 - i * 1000, "change": 5_000}
for i in range(20)
],
},
{
"quarter": "2025-12-31",
"total_shares": 4_800_000,
"num_holders": 210,
"top_holders": [],
},
],
"shares_trend": 4.17,
"holders_trend": 10,
"new_positions": [{"name": "NewFund1", "shares": 10_000}],
"increased_positions": [
{"name": "Fund0", "current_shares": 100_000, "change": 5_000, "pct_change": 5.26}
],
"decreased_positions": [
{"name": "Fund50", "current_shares": 50_000, "change": -2_000, "pct_change": -3.85}
],
"data_quality": {
"grade": grade,
"institution_count": 220 if grade == "A" else 30,
"ownership_percent": 65.0,
"ownership_percent_change": 1.5,
"prior_quarter_available": True,
"increased": 120,
"reduced": 50,
"new": 20,
"closed": 10,
},
}
def test_report_contains_methodology(self, tmp_path):
analyzer = SingleStockAnalyzer("fake_key", as_of=AS_OF)
report = analyzer.generate_report(self._make_mock_analysis("A"), output_dir=str(tmp_path))
assert "symbol-positions-summary" in report
def test_grade_b_report_contains_caution(self, tmp_path):
analyzer = SingleStockAnalyzer("fake_key", as_of=AS_OF)
report = analyzer.generate_report(self._make_mock_analysis("B"), output_dir=str(tmp_path))
assert "CAUTION: Reference Only" in report
def test_grade_c_report_contains_warning(self, tmp_path):
analyzer = SingleStockAnalyzer("fake_key", as_of=AS_OF)
report = analyzer.generate_report(self._make_mock_analysis("C"), output_dir=str(tmp_path))
assert "INSUFFICIENT COVERAGE" in report
Related skills
FAQ
What data source does institutional-flow-tracker use?
institutional-flow-tracker analyzes SEC 13F filings to discover stocks where hedge funds, mutual funds, and pension funds are accumulating or distributing positions. Holdings reflect quarterly disclosures, not real-time trades.
Who benefits most from institutional-flow-tracker?
institutional-flow-tracker suits developers building investment research or screening workflows that need smart-money validation, early accumulation signals, or exit warnings based on institutional portfolio changes in 13F data.
Is Institutional Flow Tracker safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.