13. Future Enhancements
This section documents the V2.0 specification (eight enhancements, numbered Enhancements 1–8; Enhancement 2b is specified alongside Enhancement 2). Additional long-horizon ideas appear under §14 Version Roadmap (V3.0) and the bullets at the end of this section.
V2.0 — Planned (Q3 2026, after 12-week V1 baseline)¶
Enhancement 1 — Market Regime Filter¶
Add a Market_Pulse input to weekly_run.py based on IBD’s official daily Market Pulse reading. Four regimes with automatic system response:
| Market Pulse | Exposure | RS Floor | Beta Penalty | System Mode |
|---|---|---|---|---|
| Confirmed Uptrend | 80-100% | 83 | Standard | Full scoring |
| Uptrend Under Pressure | 40-60% | 88 | 1.5× | Raise bar |
| Rally Attempt | 20-40% | 88 | 1.5× | Watch only |
| Market in Correction | 0-20% | 90 | 2.0× | Monitor only |
Formula adjustment:
Risk_Adj_Score_V2 = Composite_Rating × Beta_Penalty × Market_Regime_Factor
Where Market_Regime_Factor:
Confirmed Uptrend = 1.00
Uptrend Under Pressure = 0.85
Rally Attempt = 0.70
Market in Correction = 0.50
Implementation: Add --market-pulse argument to weekly_run.py accepting one of four string values. Flows through to ingest_ibd_lists.py and adjusts RS floor, beta penalty weight, and Risk_Adj_Score multiplier.
Enhancement 2 — Sector-Relative Ranking¶
Use Group_RS_Rating already present in BC20/IBD50 exports (currently imported but unused in scoring).
Sector multiplier derived from Group_RS_Rating letter grade:
A = 1.10 (+10% bonus — leading sector)
B = 1.05 (+5% bonus)
C = 1.00 (neutral)
D = 0.95 (-5% penalty)
E = 0.90 (-10% penalty — lagging sector)
Final V2 formula:
Risk_Adj_Score_V2 = Composite_Rating × Beta_Penalty × Sector_Multiplier × Market_Regime_Factor
Add sector concentration flag: warn if >40% of conviction stocks are in one sector (concentration risk).
Data source: Group_RS_Rating already in IBD export column map, already imported, currently unused. No new data sources required.
Enhancement 2b — Smart RISK/WATCH Status for Holdings Off IBD Lists¶
Problem identified (W15, April 14, 2026)¶
Current V1 status logic is purely binary — a holding is either on an IBD list (ON_LIST/LEADER) or it isn’t (RISK/WATCH). When a stock temporarily rotates off an IBD list due to weekly ranking changes, the system flags it RISK regardless of its actual IBD ratings.
Real example: TSEM showed RISK in W15 after rotating off IBD50, despite having Composite 97, RS 99, Group RS A+, Acc/Dis A- — ratings that place it in the top 1-3% of all stocks. The RISK flag was misleading and could have prompted an unnecessary sell decision.
Root cause¶
IBD XLS download files only contain stocks that made the list that week. Holdings that rotate off a list are completely absent from input files — their ratings are unknown to the system that week.
Current status logic (simplified):
if ticker in any_ibd_list: status = ON_LIST or LEADER
elif ticker in holdings: status = RISK ← false negative
else: status = WATCHV2 fix — rating-aware status for unlisted holdings¶
When a holding is not found in any IBD list file, instead of immediately assigning RISK, calculate a proxy score using available data and assign status accordingly:
def get_smart_status(ticker, holdings_data, ibd_lists):
if ticker in ibd_lists:
return "ON_LIST" # or LEADER if conviction
# Not on any list — check proxy metrics
proxy_score = calculate_proxy_score(ticker)
if proxy_score >= 90:
return "WATCH" # Strong — likely temporary rotation
elif proxy_score >= 75:
return "WATCH" # Decent — monitor closely
else:
return "RISK" # Genuinely weakeningProxy score calculation (using available data):
6-month price return vs SPY (calculated RS proxy)
yfinance EPS growth last 4 quarters
Price vs 50-day moving average (above = bullish)
Acc/Dis proxy from volume analysis
Preferred implementation — own calculated RS (Enhancement 4)¶
Once Enhancement 4 (regression beta + own RS calculation) is implemented, use calculated RS directly:
Calculated RS >= 85 AND price > 50-day MA → WATCH
Calculated RS 70-84 → WATCH
Calculated RS < 70 OR price < 50-day MA → RISK
This approach is fully independent of IBD list membership and eliminates false RISK flags from weekly list rotation entirely.
Impact¶
Affects: portfolio_analyzer.py status assignment logic
Affects: dashboard.html status color coding
New column: Add Calculated_RS to report CSV as fallback metric
Priority: Medium — implement alongside Enhancement 4 (Week 12-17)
Lesson documented¶
A stock’s IBD list membership and its IBD ratings are two different things. List membership changes weekly based on relative ranking vs all other stocks. Ratings reflect absolute fundamental and price strength. A stock can have exceptional ratings while temporarily off a list. The system should use ratings as the primary signal and list membership as a secondary confirmation — not the reverse.
Enhancement 3 — Portfolio Performance Metrics (from Modern Portfolio Theory)¶
Add three institutional-grade performance measures to performance_tracker.py once 12 weeks of return data is available:
Sharpe Ratio (total risk-adjusted return):
Sharpe = (Portfolio_Return - Rf) / StdDev(Portfolio_Returns)
Measures reward per unit of total risk. Higher = better.
Allows direct comparison between IRA and Brokerage strategies.
Target: Sharpe > 1.0 indicates strong risk-adjusted performance.
Treynor Ratio (systematic risk-adjusted return):
Treynor = (Portfolio_Return - Rf) / Portfolio_Beta
More relevant than Sharpe for this system because the entire scoring philosophy is beta-centric. A well-constructed conviction portfolio should show Treynor > broad market Treynor.
Jensen’s Alpha (realized abnormal return):
Jensen_Alpha = Actual_Return - [Rf + Beta × (Rm - Rf)]
The realized version of Alpha_Gap. Answers: did actual returns exceed what CAPM predicted given the risk taken?
Positive Jensen’s Alpha = stock selection added value beyond what systematic risk alone would predict. This is the core claim of the IBD + CAPM methodology — Jensen’s Alpha is how it gets proven over time.
Standard Deviation of weekly returns:
Added to KPI summary after Week 4 minimum, meaningful at Week 12.
Completes the risk picture — a strategy returning 8% with StdDev of 2% is superior to one returning 10% with StdDev of 15%.
Institutional Sponsorship Scoring (Enhancement 3 Addition)¶
Background — The I in CAN SLIM¶
William O’Neil’s CAN SLIM methodology identifies Institutional Sponsorship as one of eight essential criteria for winning stocks. The premise: the biggest price moves in history were driven by mutual funds, pension funds, and hedge funds — not retail investors. A stock without institutional backing lacks the buying power to sustain a major advance.
O’Neil’s sweet spot: 40%-80% institutional ownership with a RISING trend in the number of institutional holders quarter over quarter. Too low = institutions haven’t validated it yet. Too high (>90%) = most institutional buying already done, fewer buyers left to drive the price higher.
What the system already captures¶
The ACC/DIS Rating in the current Leader Score formula partially addresses institutional behavior: Leader Score = (RS × 0.6) + (EPS × 0.2) + (ACC_Numeric × 4) ACC map: A=5, B=4, C=3, D=2, E=1
However ACC/DIS is a 13-week price/volume proxy — it does not directly measure institutional ownership percentage, the trend in number of institutional holders, or short interest. These are meaningfully different and complementary data points.
Real-world example (W15, April 14, 2026)¶
TSEM was flagged RISK by the system after rotating off IBD50. Manual IBD research showed Composite 97, RS 99, Acc/Dis A-. Additional institutional research would have shown: Institutional ownership: ~85% Holder trend: Flat (most institutional buying already done) Short interest: Low
This context would have refined the assessment — strong stock but institutional buying largely complete. Compare to AUGO: Institutional ownership: ~45% Holder trend: Rising (institutions still discovering it) Short interest: Moderate
AUGO’s rising institutional trend signals early-stage accumulation — often where the largest gains originate.
New metric — Institutional Score (IS)¶
def calculate_institutional_score(ticker: str) -> dict:
"""
Calculate institutional sponsorship score using yfinance.
Returns score 0-10 and component breakdown.
Data source: yfinance institutional_holders, major_holders
No new dependencies required — yfinance already in stack.
"""
import yfinance as yf
t = yf.Ticker(ticker)
# Component 1: Ownership percentage (0-4 points)
# Sweet spot per O'Neil: 40%-80%
# <40% = not yet validated = 1 point
# 40-60% = early institutional interest = 3 points
# 60-80% = strong validation = 4 points
# >80% = heavily owned, fewer buyers left = 2 points
major = t.major_holders
inst_pct = float(major.iloc[2, 0].strip('%')) / 100
if inst_pct < 0.40:
ownership_score = 1
elif inst_pct < 0.60:
ownership_score = 3
elif inst_pct < 0.80:
ownership_score = 4
else:
ownership_score = 2
# Component 2: Holder trend (0-4 points)
# Compare current holders to prior quarter
# Rising = institutions accumulating = 4 points
# Flat = stable ownership = 2 points
# Falling = institutions exiting = 0 points
holders = t.institutional_holders
if holders is not None and len(holders) >= 2:
recent = holders.head(5)['Shares'].sum()
prior = holders.tail(5)['Shares'].sum()
trend_ratio = recent / prior if prior > 0 else 1.0
if trend_ratio > 1.05:
holder_trend_score = 4 # Rising >5%
elif trend_ratio > 0.95:
holder_trend_score = 2 # Flat
else:
holder_trend_score = 0 # Falling
else:
holder_trend_score = 2 # Unknown — neutral
# Component 3: Short interest inverse (0-2 points)
# Low short interest = less bearish conviction = better
# <5% short float = 2 points
# 5-15% short float = 1 point
# >15% short float = 0 points
try:
info = t.info
short_float = info.get('shortPercentOfFloat', 0.10)
if short_float < 0.05:
short_score = 2
elif short_float < 0.15:
short_score = 1
else:
short_score = 0
except:
short_score = 1 # Unknown — neutral
total_score = ownership_score + holder_trend_score + short_score
return {
'institutional_score': total_score, # 0-10
'ownership_pct': inst_pct,
'ownership_score': ownership_score, # 0-4
'holder_trend_score': holder_trend_score, # 0-4
'short_score': short_score, # 0-2
'inst_grade': 'A' if total_score >= 8 else
'B' if total_score >= 6 else
'C' if total_score >= 4 else 'D'
}Integration into scoring formulas¶
IRA Strategy (quality/compounder focus): Institutional score acts as a multiplier — validates that smart money agrees with the quality assessment:
# Current V1
Risk_Adj_Score = Composite_Rating × Beta_Penalty
# V2 with institutional layer
Inst_Multiplier = 1.0 + (Institutional_Score / 100)
# Range: 1.00 (score=0) to 1.10 (score=10)
Risk_Adj_Score_V2 = Composite_Rating
× Beta_Penalty
× Sector_Multiplier
× Market_Regime_Factor
× Inst_MultiplierBrokerage Strategy (momentum focus): Institutional score replaces some ACC/DIS weight since it provides more granular ownership data:
# Current V1
Leader_Score = (RS × 0.6) + (EPS × 0.2) + (ACC_Numeric × 4)
# V2 with institutional layer
Leader_Score_V2 = (RS × 0.50)
+ (EPS × 0.15)
+ (ACC_Numeric × 3.0)
+ (Institutional_Score × 1.5)
# Institutional_Score 0-10 contributes up to 15 points
# Replaces some ACC weight with more direct ownership dataO’Neil Sweet Spot Filter¶
Add a pre-screening flag for conviction list candidates:
INST_OWNERSHIP_FLOOR = 0.30 # Minimum 30% institutional
INST_OWNERSHIP_CEIL = 0.90 # Maximum 90% institutional
HOLDER_TREND_REQUIRED = True # Prefer rising trend
def meets_oneil_sponsorship(inst_data: dict) -> bool:
"""Returns True if stock meets O'Neil institutional criteria."""
return (
INST_OWNERSHIP_FLOOR
<= inst_data['ownership_pct']
<= INST_OWNERSHIP_CEIL
and inst_data['holder_trend_score'] >= 2
)Stocks failing this filter get a SPONSORSHIP_FLAG warning in the conviction list output — not an automatic exclusion but a note for the investor to investigate further.
New output columns¶
Add to watchlist and report CSVs: Inst_Ownership_Pct — % held by institutions Inst_Holder_Trend — Rising / Flat / Falling Short_Float_Pct — % of float sold short Institutional_Score — composite 0-10 Inst_Grade — A/B/C/D Sponsorship_Flag — True if outside O’Neil sweet spot
Add to dashboard Section 1 (Strategy Scorecard): Avg Institutional Score per strategy Count of holdings with Rising vs Falling holder trend Any Sponsorship_Flag warnings highlighted in orange
Data source¶
yfinance: ticker.institutional_holders, ticker.major_holders,
ticker.info['shortPercentOfFloat']
No new dependencies. yfinance already in requirements.txt. Quarterly 13F data — updates 45 days after quarter end. Limitation: yfinance institutional data has a 1-quarter lag. For real-time 13F tracking consider Polygon.io or Finviz API as a future data source upgrade (Path 2 data independence).
Implementation timeline¶
Week 12: Add calculate_institutional_score() to capm_wacc.py
Week 13: Integrate into scoring formulas — run parallel with V1
Week 14: Add new columns to watchlist and report CSVs
Week 15: Add Inst_Grade and Sponsorship_Flag to dashboard
Week 17: Full cutover — institutional score in primary formula
Why this matters for monetization¶
When publishing the methodology (newsletter → book), being able to say the system incorporates O’Neil’s institutional sponsorship criteria — quantified as a scored metric rather than a subjective check — is a genuine differentiator from retail IBD users who check ownership manually without systematic scoring.
The combination of: IBD momentum signals (RS, Composite)
CAPM risk adjustment (Beta Penalty)
Institutional validation (Sponsorship Score)
Correlation management (portfolio construction)
...creates a four-layer methodology that no single published framework currently combines in this way. This is the core IP.
These metrics are calculated in performance_tracker.py using the existing performance_history.csv weekly return data. No new data sources required — only sufficient weeks of observations.
Add all four to kpi_summary.csv and dashboard.html Section 1 strategy scorecard cards.
Enhancement 4 — Calculated Beta (Own Regression)¶
Replace vendor-sourced beta (yfinance scalar) with regression-calculated beta using 36 months of monthly returns vs SPY. This eliminates vendor discrepancies and makes the methodology fully reproducible.
Why this matters¶
AUGO real-world example (April 6, 2026):
yfinance beta: 0.31
Etrade beta: 2.00
Discrepancy: 6.4×
This 6× difference directly impacts Beta_Penalty and Risk_Adj_Score:
Beta_Penalty (0.31) = 1 / (1 + max(0, 0.31-1.0)) = 1.000 (no discount)
Beta_Penalty (2.00) = 1 / (1 + max(0, 2.00-1.0)) = 0.500 (50% discount)
A stock can appear as a top conviction name or a high-risk name depending entirely on which vendor’s beta you trust. Own beta eliminates this.
Root cause of vendor discrepancies¶
Different providers use different:
Time windows (1yr, 3yr, 5yr — yfinance typically uses 5yr)
Return frequency (daily, weekly, monthly)
Market benchmark (S&P 500, Russell 2000, total market)
Gap-filling methodology for stocks with short histories
None are wrong — they answer different questions. A consistent in-house calculation gives apples-to-apples comparison across all stocks.
Methodology (from OpenStax Principles of Finance §15.5)¶
Pull 36 months of monthly closing prices for stock and SPY via yfinance
Calculate monthly returns: (Price_End - Price_Begin) / Price_Begin
Align stock and SPY returns by date, drop any NaN rows
Run OLS linear regression: Stock_Returns ~ SPY_Returns
Slope of regression line = Beta (systematic risk coefficient)
Intercept of regression line = Jensen’s Alpha proxy (abnormal return)
R-squared = how much of stock’s variance is explained by market moves
For stocks with less than 36 months of history, use all available monthly data (minimum 12 months). Flag stocks with fewer than 24 data points as “beta estimate — limited history.”
New function in capm_wacc.py¶
def calculate_regression_beta(
ticker: str,
months: int = 36,
market: str = 'SPY'
) -> dict:
"""
Calculate beta via OLS regression of monthly returns vs SPY.
Parameters
----------
ticker : stock ticker symbol
months : lookback period in months (default 36)
market : market benchmark ticker (default SPY)
Returns
-------
dict with keys:
beta : float regression slope (systematic risk)
alpha_intercept : float Jensen's Alpha proxy (abnormal return)
r_squared : float variance explained by market (0-1)
data_points : int number of monthly observations used
limited_history : bool True if fewer than 24 data points
error : str error message if calculation failed
"""
from scipy import stats
try:
stock = yf.Ticker(ticker).history(period=f"{months}mo",
interval="1mo")
spy = yf.Ticker(market).history(period=f"{months}mo",
interval="1mo")
stock_returns = stock['Close'].pct_change().dropna()
spy_returns = spy['Close'].pct_change().dropna()
aligned = pd.concat(
[stock_returns, spy_returns], axis=1
).dropna()
aligned.columns = ['stock', 'market']
if len(aligned) < 6:
return {'beta': DEFAULT_BETA, 'error': 'insufficient data',
'data_points': len(aligned)}
slope, intercept, r_value, p_value, std_err = stats.linregress(
aligned['market'], aligned['stock']
)
return {
'beta': round(float(slope), 4),
'alpha_intercept': round(float(intercept), 6),
'r_squared': round(float(r_value**2), 4),
'data_points': len(aligned),
'limited_history': len(aligned) < 24,
'error': None
}
except Exception as exc:
return {
'beta': DEFAULT_BETA,
'error': str(exc),
'data_points': 0,
'limited_history': True
}Additional output from regression (free insights)¶
The regression simultaneously produces two V2 KPIs at no extra cost:
Jensen’s Alpha (intercept):
The intercept represents abnormal return above what CAPM predicts. Positive intercept = stock historically beat its CAPM required return. This is the realized version of Alpha_Gap — historical rather than forward. Adds to performance_history.csv as Regression_Alpha column.
R-squared:
Proportion of stock’s variance explained by market moves. High R-squared (>0.70) = mostly systematic risk — beta is reliable. Low R-squared (<0.30) = mostly firm-specific risk — beta less meaningful. Useful quality flag: low R-squared stocks benefit less from beta penalty. Adds to watchlist output as Beta_R_Squared column.
Implementation sequence¶
Do NOT implement during V1 baseline period (Weeks 1-12). Changing beta source mid-baseline makes performance history incomparable.
Week 12: Calculate regression beta alongside yfinance beta for all watchlist stocks. Store both. Compare distributions.
Week 13: Run 4-week parallel period: score with yfinance beta → existing columns; score with regression beta → new _V2 columns. Compare rankings — document which stocks move significantly.
Week 17: Full cutover to regression beta if parallel results are stable. Archive yfinance beta columns, make regression beta primary. Document the transition in WEEKLY_JOURNAL.md.
Transition impact on scoring¶
Regression beta replaces the yfinance scalar in:
Beta_Penalty calculation
Required_Return_CAPM calculation
Risk_Adj_Score and Risk_Adj_Leader_Score
WACC cost of equity (Re = Rf + beta × MRP)
portfolio_correlation_summary (beta-weighted correlation)
Why this is core IP¶
Vendor beta is a commodity — anyone can look it up. Regression beta calculated from a consistent 36-month monthly return methodology applied uniformly across all stocks in both strategies is a documented, reproducible, proprietary process.
Combined with the Beta_Penalty formula (1 / (1 + max(0, β-1))), which is not found in standard CAPM literature, the regression beta implementation makes the full risk-adjustment layer genuinely original and defensible as intellectual property.
The alpha_intercept (Jensen’s Alpha from regression) also provides the long-term performance validation metric — after 26 weeks, positive average Jensen’s Alpha across conviction stocks is the evidence base for the newsletter and eventual methodology publication.
Enhancement 5 — Correlation Filter for Conviction List¶
Add portfolio-level correlation analysis to find_conviction_stocks() in ingest_ibd_lists.py.
Insight from Modern Portfolio Theory: stocks with lower correlation to each other provide better diversification. The conviction list may contain 9 stocks that score well individually but are highly correlated (e.g. multiple gold miners moving together), defeating the diversification purpose of holding multiple names.
Implementation:
Calculate pairwise return correlation for all conviction stocks using 12 weeks of weekly returns from
performance_history.csvFlag any pair with correlation > 0.75 as “high correlation”
Add
Avg_Pairwise_Correlationto conviction list outputAdd
Sector_Concentration_Pctto conviction summaryDashboard warning if >40% of conviction stocks share a sector
This adds a portfolio construction quality layer on top of individual stock scoring — the same approach used by institutional portfolio managers.
Enhancement 6 — Weekly Journal Integration¶
After each weekly run, weekly_run.py prompts for a one-line Market Pulse reading and auto-populates the journal template:
Week: W##
Market Pulse: [user input]
Jensen Alpha (IRA): [calculated]
Jensen Alpha (Brokerage): [calculated]
Conviction changes: [stocks added/removed vs prior week]
Notes: [user input]
This creates a structured record connecting market conditions, system signals, and realized performance — the foundation of the methodology documentation for future publication.
Enhancement 7 — Volume Analysis Layer: CMF, OBV, and Volume Ratio¶
Background — The institutional timing gap¶
The current V1 system has a timing gap between weekly IBD signals and real-time buy decisions:
ACC/DIS Rating: 13-week lookback — excellent for selection, slow to reflect recent institutional changes IBD List status: Weekly snapshot — binary on/off signal Missing layer: Real-time institutional flow direction
Enhancement 7 adds a Volume Analysis Layer that sits between weekly IBD scores and daily buy decisions, providing current institutional behavior signals calculated from daily price and volume data.
Three volume indicators¶
Indicator 1 — Chaikin Money Flow (CMF, 20-day) Measures whether institutions are accumulating or distributing a stock over the past 20 trading days by analyzing where price closes within the daily range, weighted by volume.
Formula: Money Flow Multiplier (MFM) = ((Close - Low) - (High - Close)) / (High - Low)
Money Flow Volume (MFV) = MFM × Volume
CMF = Sum(MFV, 20 days) / Sum(Volume, 20 days)
Interpretation: CMF >= +0.25 Grade A Strong accumulation CMF >= +0.10 Grade B Moderate accumulation CMF >= -0.10 Grade C Neutral CMF >= -0.25 Grade D Moderate distribution CMF < -0.25 Grade F Strong distribution
Real-world application: TSEM pre-earnings: CMF would have confirmed A- Acc/Dis signal, providing a second institutional signal to override RISK flag.
WWD breakout attempt: CMF neutral or negative would have flagged caution alongside light volume — preventing the premature entry documented in W15 journal.
Indicator 2 — On-Balance Volume (OBV) Running total that adds volume on up days and subtracts on down days. Rising OBV = net buying pressure regardless of price direction. Divergence from price is a powerful signal.
Formula: OBV = Previous OBV + Volume (if Close > Previous Close) OBV = Previous OBV - Volume (if Close < Previous Close) OBV = Previous OBV (if Close = Previous Close)
Key signals: OBV rising while price flat = quiet institutional accumulation OBV falling while price flat = quiet institutional distribution OBV confirming breakout = institutional participation confirmed OBV diverging from price = warning — trend may be weakening
Indicator 3 — Volume Ratio (daily vs 50-day average) Compares current day or week volume to 50-day average volume. The most direct measure of whether a price move has institutional backing.
Formula: Volume_Ratio = Current_Volume / Avg_Volume_50_Day
Interpretation:
= 1.40 Institutional interest confirmed = 2.00 Strong institutional conviction 0.80-1.40 Normal trading — retail dominated < 0.80 Light volume — do not trust the move
IBD requires 40-50% above average volume on breakouts. This is the signal that was absent on WWD’s April 14-15 breakout attempts — price moved but volume never confirmed.
CMF + OBV combined signal matrix¶
| CMF | OBV | Combined Signal | Action |
|---|---|---|---|
| A or B | Rising | Strong accumulation | High conviction buy |
| A or B | Flat | Moderate accumulation | Buy with normal size |
| C | Rising | Mixed — price momentum | Proceed with caution |
| C | Flat | Neutral | Wait for clarity |
| D or F | Falling | Strong distribution | Avoid or exit |
| D or F | Rising | Conflicting | Wait — do not buy |
Integration into workflow — three use cases¶
Use 1 — Breakout confirmation (daily check) Before executing any breakout buy: CMF >= +0.10 AND Volume_Ratio >= 1.40 → Proceed CMF 0 to +0.10 OR Volume_Ratio 1.10-1.40 → Caution CMF < 0 OR Volume_Ratio < 1.10 → Wait
This filter alone would have prevented the WWD premature entry in W15 — CMF was neutral and volume ratio was below 1.40 on both breakout attempts.
Use 2 — Portfolio health monitoring (weekly) Run CMF and OBV direction for all holdings each Sunday. Flag any holding where: CMF drops below -0.10 = distribution detected — watch closely OBV turns negative for 3+ consecutive days = exit warning
Add CMF_Grade and OBV_Direction columns to portfolio report CSV. Add distribution warnings to dashboard Section 3 (My Portfolios).
Use 3 — Conviction list enhancement Add CMF as a secondary filter on conviction list output: CMF Grade A or B = priority candidate CMF Grade C = standard candidate CMF Grade D or F = flagged — investigate before buying
Enhanced conviction scoring (combined with Enhancement 3): Conviction_Score_V2 = Risk_Adj_Score × 0.60
CMF_Score × 0.20 (normalized 0-10)
Institutional_Score × 0.20 (Enhancement 3)
Implementation in capm_wacc.py¶
def calculate_cmf(ticker: str, period: int = 20) -> dict:
"""
Calculate Chaikin Money Flow using yfinance daily OHLCV.
No new data dependencies — yfinance already in stack.
"""
import numpy as np
data = yf.Ticker(ticker).history(period=f"{period + 10}d")
if len(data) < period:
return {'cmf': None, 'cmf_grade': 'C',
'error': 'insufficient data'}
data = data.tail(period)
high = data['High']
low = data['Low']
close = data['Close']
vol = data['Volume']
hl_range = (high - low).replace(0, np.nan)
mfm = ((close - low) - (high - close)) / hl_range
mfv = mfm * vol
cmf_value = mfv.sum() / vol.sum()
grade = ('A' if cmf_value >= 0.25 else
'B' if cmf_value >= 0.10 else
'C' if cmf_value >= -0.10 else
'D' if cmf_value >= -0.25 else 'F')
return {
'cmf': round(float(cmf_value), 4),
'cmf_grade': grade,
'period_days': period,
'interpretation': (
'Strong accumulation' if cmf_value >= 0.25 else
'Moderate accumulation' if cmf_value >= 0.10 else
'Neutral' if cmf_value >= -0.10 else
'Moderate distribution' if cmf_value >= -0.25 else
'Strong distribution'
)
}
def calculate_obv(ticker: str, days: int = 30) -> dict:
"""
Calculate On-Balance Volume trend direction.
Returns direction: Rising / Flat / Falling
"""
data = yf.Ticker(ticker).history(period=f"{days + 5}d")
data = data.tail(days)
obv = [0]
for i in range(1, len(data)):
if data['Close'].iloc[i] > data['Close'].iloc[i-1]:
obv.append(obv[-1] + data['Volume'].iloc[i])
elif data['Close'].iloc[i] < data['Close'].iloc[i-1]:
obv.append(obv[-1] - data['Volume'].iloc[i])
else:
obv.append(obv[-1])
# Compare first half vs second half to determine trend
mid = len(obv) // 2
first_half_avg = sum(obv[:mid]) / mid
second_half_avg = sum(obv[mid:]) / (len(obv) - mid)
pct_change = (second_half_avg - first_half_avg) / abs(first_half_avg) if first_half_avg != 0 else 0
direction = ('Rising' if pct_change > 0.02 else
'Falling' if pct_change < -0.02 else 'Flat')
return {
'obv_direction': direction,
'obv_pct_change': round(pct_change * 100, 2),
'period_days': days
}
def calculate_volume_ratio(ticker: str) -> dict:
"""
Compare today's volume to 50-day average.
"""
data = yf.Ticker(ticker).history(period="60d")
avg_50 = data['Volume'].tail(50).mean()
today_vol = data['Volume'].iloc[-1]
ratio = today_vol / avg_50 if avg_50 > 0 else 1.0
return {
'volume_ratio': round(ratio, 2),
'today_volume': int(today_vol),
'avg_50d_volume': int(avg_50),
'institutional': ratio >= 1.40,
'interpretation': (
'Strong institutional' if ratio >= 2.00 else
'Institutional' if ratio >= 1.40 else
'Normal' if ratio >= 0.80 else
'Light — do not trust'
)
}Known limitation — Gap day distortion¶
CMF can be distorted on large gap days where the intraday range does not reflect the true accumulation/distribution dynamic.
Real example (TSEM April 16, 2026): TSM earnings gap — TSEM gapped up 6.3% on massive volume Raw CMF showed F grade (strong distribution) Actual outcome: +9.50% gain, gap extended Friday
Root cause: When after-hours data is included in High/Low range, a close that appears below the intraday low creates a negative MFM even when the day was bullish.
V2 fix options: Option A: Exclude gap days (>3% gap) from CMF calculation Option B: Use previous close as the Low floor on gap-up days Option C: Cross-reference with OBV — if OBV is rising and CMF is negative on a gap day, trust OBV
Implementation: Add gap_filter parameter to calculate_cmf()
gap_threshold = 0.03 # exclude days with >3% gap
New output columns¶
Add to watchlist and report CSVs: CMF_Value — raw CMF score (-1 to +1) CMF_Grade — A/B/C/D/F OBV_Direction — Rising / Flat / Falling Volume_Ratio — today vs 50-day average Volume_Signal — Institutional / Normal / Light
Add to dashboard: Section 3 (My Portfolios): CMF grade badge per holding Section 5 (Correlation): Volume health summary Red badge on any holding with CMF Grade D or F
Data source¶
yfinance daily OHLCV data. No new dependencies — yfinance already in requirements.txt. CMF and OBV calculated fresh each weekly run. Volume ratio calculated at time of buy decision (manual check).
Implementation timeline¶
Week 12: Add calculate_cmf(), calculate_obv(), calculate_volume_ratio() to capm_wacc.py Week 13: Add CMF and OBV columns to watchlist CSVs Week 14: Add volume signals to dashboard Week 15: Integrate CMF into conviction list scoring Week 17: Full V2 cutover with volume layer active
Why this matters for methodology publication¶
The combination of: IBD momentum signals (RS, Composite — weekly)
CAPM risk adjustment (Beta Penalty — proprietary)
Institutional scoring (O’Neil sponsorship — quarterly)
CMF + OBV volume analysis (Chaikin — 20-day real-time)
Correlation management (Markowitz — 6-month)
...creates a five-layer methodology where each layer operates at a different time horizon:
Quarterly: Institutional sponsorship (13F filings) Monthly: CMF + OBV (20-30 day money flow) Weekly: IBD scores + list membership Daily: Volume ratio + RVOL at breakout
No published retail investment framework combines all five layers at these different time horizons systematically. This multi-horizon architecture is genuinely novel IP.
Enhancement 8 — Entry Signal Generator and Paper Trading Validation¶
Overview and Purpose¶
Enhancement 8 adds three connected systems that transform the weekly watchlist from a static ranked list into a dynamic entry timing engine with statistical validation:
System 1 — Entry Signal Scorer Calculates a composite entry score (0-100) for every watchlist name each week. Tells you when a stock is actionable vs still developing.
System 2 — Entry Tracking Database Records recommended entry prices alongside actual IBD buy points. Automatically tracks what happened 1, 2, 4, and 8 weeks after each signal. Builds the dataset for academic publication.
System 3 — Paper Trading Validation Engine Simulates $2,000 position in every signal generated. Tracks paper gains/losses weekly. Compares your system’s entry points to IBD buy points statistically.
Background — Why This Matters¶
The ticker history tracker (Enhancement 6b) tells you WHICH stocks keep appearing. Enhancement 8 answers WHEN to buy them. Combined they form a complete signal system: Ticker appears 3+ consecutive weeks -> High conviction name Entry score crosses 75+ -> Timing is right Volume confirmed 1.40x+ -> Institutions participating CMF Grade A or B -> Money flowing in -> BUY SIGNAL with full statistical backing
Real example (W17 — FN Fabrinet): 7 appearances across 3 weeks (highest in system) IBD sources: BC20 + IBD50 (both major lists) Entry score would have been calculated as: Price proximity (near buy point): 20/25 CMF Grade B + volume above avg: 18/25 Composite 97, RS 97: 22/25 Acc/Dis B+, Group RS A+: 22/25 Total: 82/100 — HIGH CONVICTION ENTRY Beta 2.70 -> IRA disqualified, Brokerage appropriate Action: Paper trade AND Brokerage watchlist priority
Entry Signal Score Formula¶
def calculate_entry_score(ticker_data: dict) -> dict:
"""
Calculate composite entry score (0-100) for a watchlist name.
Higher score = better entry conditions right now.
Components:
Price Proximity (25): how close to IBD buy point
Volume Quality (25): CMF grade + volume ratio
Momentum (25): RS + Composite ratings
Institutional (25): Acc/Dis + Group RS
"""
score = 0
components = {}
# -- Component 1: Price Proximity (0-25) ------------------
current = ticker_data.get('current_price', 0)
buy_point = ticker_data.get('ibd_buy_point', 0)
if buy_point > 0:
pct_above = (current / buy_point - 1) * 100
if pct_above < 0:
proximity = 0 # Below buy point — not ready
elif pct_above <= 2:
proximity = 25 # Within 2% — ideal entry
elif pct_above <= 5:
proximity = 15 # 2-5% above — still in zone
elif pct_above <= 8:
proximity = 5 # 5-8% — extended, caution
else:
proximity = 0 # Beyond 8% — do not chase
else:
proximity = 10 # No buy point available — neutral
score += proximity
components['price_proximity'] = proximity
# -- Component 2: Volume Quality (0-25) -------------------
cmf_grade = ticker_data.get('cmf_grade', 'C')
volume_ratio = ticker_data.get('volume_ratio', 1.0)
cmf_points = {'A': 15, 'B': 10, 'C': 5, 'D': 0, 'F': 0}
vol_points = (
10 if volume_ratio >= 1.40 else
7 if volume_ratio >= 1.00 else
3 if volume_ratio >= 0.80 else 0
)
volume_quality = cmf_points.get(cmf_grade, 5) + vol_points
score += volume_quality
components['volume_quality'] = volume_quality
# -- Component 3: Momentum (0-25) -------------------------
rs = ticker_data.get('rs_rating', 0)
composite = ticker_data.get('composite_rating', 0)
if rs >= 95 and composite >= 95:
momentum = 25
elif rs >= 85 and composite >= 85:
momentum = 18
elif rs >= 75 and composite >= 75:
momentum = 10
elif rs >= 65 and composite >= 65:
momentum = 5
else:
momentum = 0
score += momentum
components['momentum'] = momentum
# -- Component 4: Institutional (0-25) --------------------
acc_map = {
'A+': 5, 'A': 5, 'A-': 5,
'B+': 4, 'B': 4, 'B-': 4,
'C+': 3, 'C': 3, 'C-': 3,
'D+': 2, 'D': 2, 'D-': 2, 'E': 1
}
group_rs_map = {'A+': 5, 'A': 4, 'B': 3, 'C': 2, 'D': 1, 'E': 0}
acc_score = acc_map.get(
ticker_data.get('acc_dis_rating', 'C'), 3) * 3
group_score = group_rs_map.get(
ticker_data.get('group_rs_rating', 'C'), 2) * 2
institutional = min(25, acc_score + group_score)
score += institutional
components['institutional'] = institutional
# -- Signal Classification ---------------------------------
if score >= 75:
signal = 'BUY_ZONE'
action = 'High conviction — buy on volume confirmation'
elif score >= 60:
signal = 'WATCH'
action = 'Developing — set price alert at buy point'
elif score >= 45:
signal = 'MONITOR'
action = 'Early stage — add to watchlist'
else:
signal = 'NOT_READY'
action = 'Wait — conditions not aligned'
return {
'entry_score': score,
'signal': signal,
'action': action,
'components': components,
'recommended_entry': buy_point * 1.01 if buy_point > 0 else None,
}Recommended Entry Price Calculation¶
The recommended entry is not simply the IBD buy point. It incorporates CMF and volume signals to find a slightly better entry than the mechanical pivot:
def calculate_recommended_entry(
buy_point: float,
cmf_grade: str,
volume_ratio: float,
recent_low: float) -> dict:
"""
Calculate recommended entry price relative to IBD buy point.
Logic:
- Strong CMF (A) + volume confirmed: enter at buy point
- Moderate CMF (B) + near average volume: wait for slight
pullback to 0.5-1% below buy point for better fill
- Weak CMF (C) or light volume: wait for buy point to be
tested and held before entering
"""
if cmf_grade in ('A',) and volume_ratio >= 1.40:
# Strong signal — enter at buy point, slight buffer
entry = buy_point * 1.005
entry_type = 'AT_BREAKOUT'
elif cmf_grade in ('A', 'B') and volume_ratio >= 1.00:
# Moderate — enter at buy point
entry = buy_point * 1.010
entry_type = 'STANDARD'
elif cmf_grade == 'B' and volume_ratio >= 0.80:
# Developing — wait for slight pullback
entry = buy_point * 0.995
entry_type = 'PULLBACK_ENTRY'
else:
# Not ready — wait for volume confirmation
entry = buy_point
entry_type = 'WAIT_FOR_SIGNAL'
return {
'recommended_entry': round(entry, 2),
'entry_type': entry_type,
'vs_ibd_pct': round((entry / buy_point - 1) * 100, 3),
}Paper Trading System¶
def create_paper_trade(
ticker: str,
entry_score: int,
recommended_entry: float,
ibd_buy_point: float,
week_label: str,
paper_position_size: float = 2000.0) -> dict:
"""
Create a paper trade record for tracking purposes.
$2,000 standard position size for all paper trades.
Allows apples-to-apples comparison across all signals.
"""
shares = int(paper_position_size / recommended_entry)
actual_cost = shares * recommended_entry
return {
'ticker': ticker,
'signal_week': week_label,
'signal_date': date.today().isoformat(),
'entry_score': entry_score,
'recommended_entry': recommended_entry,
'ibd_buy_point': ibd_buy_point,
'entry_vs_ibd_pct': round(
(recommended_entry / ibd_buy_point - 1) * 100, 3),
'paper_shares': shares,
'paper_cost': actual_cost,
'paper_size': paper_position_size,
# Populated automatically each Sunday
'price_1wk': None,
'price_2wk': None,
'price_4wk': None,
'price_8wk': None,
'return_1wk': None,
'return_4wk': None,
'return_8wk': None,
'ibd_entry_return_4wk': None,
'your_vs_ibd_4wk': None,
# Outcome — populated when position closes
'signal_result': None, # WIN/LOSS/NEUTRAL
'exit_week': None,
'exit_reason': None,
'final_return_pct': None,
}Entry Signals CSV Schema¶
New file: data/output/entry_signals.csv
One row per signal per week. Master database for research.
Week, Ticker, Company, Signal_Date,
Entry_Score, Signal_Classification,
Recommended_Entry, IBD_Buy_Point, Entry_Vs_IBD_Pct,
CMF_Grade, Volume_Ratio, RS_Rating, Composite_Rating,
Acc_Dis, Group_RS, IBD_Sources, Appearances_To_Date,
Consecutive_Weeks, Price_Proximity_Score,
Volume_Quality_Score, Momentum_Score, Institutional_Score,
Paper_Entry_Taken (Y/N), Actual_Entry_Price,
Price_1wk, Price_4wk, Price_8wk,
Return_1wk, Return_4wk, Return_8wk,
IBD_Entry_Return_4wk, Your_Return_vs_IBD_4wk,
Signal_Result, Notes
Statistical Validation — Research Paper Data¶
After 26 weeks this database enables:
Hit Rate by Score Tier: Entry Score 75-100 (BUY_ZONE): Hit rate = X% Entry Score 60-74 (WATCH): Hit rate = X% Entry Score 45-59 (MONITOR): Hit rate = X% Statistical test: Chi-squared test for independence H0: Entry score has no relationship to outcome H1: Higher entry score predicts positive outcome
Return Analysis by CMF Grade: CMF Grade A entries: Mean 4-week return = X% CMF Grade B entries: Mean 4-week return = X% CMF Grade C entries: Mean 4-week return = X% Statistical test: One-way ANOVA Tests whether CMF grade significantly predicts returns
Your Entries vs IBD Buy Points: H0: Your recommended entry = IBD buy point (no difference) H1: Your entry produces different returns than IBD pivot Metric: Average (Your_Return_4wk - IBD_Entry_Return_4wk) Test: Paired t-test on matched entry pairs Result: Quantifies whether CMF timing adds value over simple pivot entry — directly publishable
R-Squared Validation: Regress: 4-week return ~ Entry_Score R^2 measures: how much return variance is explained by the entry signal score Target: R^2 > 0.30 at 26 weeks (statistically meaningful) R^2 > 0.50 at 52 weeks (publication ready)
New Files Required¶
entry_signal_generator.py
Runs after portfolio_analyzer.py as Step 11a
Inputs: W##IBD*.csv watchlists + CMF data + prices
Output: W##_Entry_Signals.csv + updates entry_signals.csv
Runtime: ~60 seconds (yfinance price lookups)
paper_trader.py
Runs after entry_signal_generator.py as Step 11b
Updates all open paper trades with current prices
Calculates returns at 1/2/4/8 week intervals
Generates paper portfolio performance summary
Dashboard Section 6 — Entry Signal Scorecard¶
Add to dashboard.html after Section 5 (Correlation): ENTRY SIGNAL SCORECARD — W##¶
Score Ticker Company Signal Entry CMF¶
87 MPWR Monolithic Power BUY_ZONE 695 B 79 CLS Celestica WATCH 207 C+ 68 ADI Analog Devices MONITOR $375 B¶
Paper portfolio: +14.2% | Hit rate: 71% | 28 signals Your entries vs IBD buy points: -1.2% avg (enter cheaper)
Color coding: BUY_ZONE (75+): Green background WATCH (60-74): Yellow background MONITOR (45-59): Orange background NOT_READY (<45): Grey background
IBD MarketSurge Comparison Framework¶
When MarketSurge trial begins (Week 26 target):
Step 1: Note MarketSurge Near Pivot list each week Step 2: Compare to your BUY_ZONE signals (score 75+) Step 3: Track overlap percentage week over week Step 4: For divergences — note which was right at 4 weeks Step 5: Calculate: do your BUY_ZONE signals produce better or worse returns than MarketSurge picks?
This comparison is the core of your research paper: “Does a quantitative entry scoring system outperform standard pivot-based entry timing?”
Implementation Timeline¶
Week 18: Build entry_signal_generator.py Manual buy point input (from IBD Digital) Automated CMF + volume + rating scoring
Week 19: Build paper_trader.py Auto-update paper trades each Sunday 4-week return calculations
Week 20: Dashboard Section 6 Entry signal scorecard display Paper portfolio performance widget
Week 22: entry_signals.csv has 4 weeks of data First hit rate calculations available First R^2 estimates
Week 26: 8 weeks of 4-week return data Statistical significance emerging MarketSurge trial begins — comparison starts
Week 52: Full academic dataset 52 weeks of signals 1,300+ data points for publication
Relationship to Other Enhancements¶
Enhancement 7 (CMF/OBV/Volume) provides the Volume_Quality score component. Enhancement 3 (Institutional Sponsorship) provides the Institutional score component. Enhancement 4 (Regression Beta) provides buy point validation by confirming stocks are not extended above their fundamental value. Enhancement 8 synthesizes all prior enhancements into a single actionable entry timing score.
The five-layer methodology becomes six layers with Enhancement 8:
Layer 1: IBD Momentum (weekly selection) Layer 2: CAPM Risk Adjustment (beta penalty) Layer 3: Institutional Sponsorship (O’Neil validation) Layer 4: CMF + Volume Analysis (timing confirmation) Layer 5: Correlation Management (portfolio construction) Layer 6: Entry Signal Score (optimal entry timing) <- NEW
Each layer operates at a different time horizon. Layer 6 operates at the daily/weekly entry level — telling you not just what to own but precisely when the institutional conditions are aligned for entry.
Additional backlog (beyond V2 scope)¶
Historical RS from price data — reduce reliance on IBD RS column; align with §11.6.
Additional vendors — Polygon.io, Tiingo, Zacks for fundamentals and survivorship-bias-aware backtests.
Alerts — email/SMS when
Cross_Strategy_Convictionmembership changes week over week.Options overlay — covered calls on high
Risk_Adj_*names (tie-in to existing covered-call scanner concepts in the broader repo).Multi-user SaaS — tenant isolation, job queue, billing.
Mobile dashboard — PWA or React Native consuming the same JSON API.
Backtesting engine — replay
performance_history.csv+ SPY to compute rolling alpha, drawdowns, and turnover.