#!/usr/bin/env python3 """ Titan Daily Report - One-File Plug-and-Play Skeleton This script: - Fetches recent OHLCV data using free Yahoo Finance via yfinance - Computes simplified versions of: - Liquidity Score - Trend / Regime - Volume Pressure - Basic SR context - Produces a Daily Report with: - 🟢 GET IN - 🟩 IN - 🟠 GET OUT - 🔴 OUT plus short explanations PER SYMBOL. This is a RESEARCH / ANALYSIS tool only. It does NOT send orders, and is NOT trading advice. """ import os from datetime import datetime, timedelta import numpy as np import pandas as pd import yfinance as yf # ========================= # CONFIGURATION # ========================= # Universe of symbols to analyze SYMBOLS = [ "SPY", "QQQ", "AAPL", "TSLA", "NVDA", ] # How many days of data to pull LOOKBACK_DAYS = 60 # Output directory for reports OUTPUT_DIR = "titan_reports" # ========================= # DATA FETCH # ========================= def fetch_ohlcv(symbol: str, days: int = LOOKBACK_DAYS) -> pd.DataFrame: """ Fetch daily OHLCV data for a symbol using yfinance. """ end = datetime.utcnow() start = end - timedelta(days=days * 2) # buffer for weekends/holidays df = yf.download(symbol, start=start, end=end, interval="1d", progress=False) if df.empty: raise ValueError(f"No data for {symbol}") df = df.tail(days) # last N trading days return df # ========================= # SIMPLE ENGINE HELPERS # (Liquidity, Regime, Pressure, SR, etc.) # ========================= def compute_liquidity_score(df: pd.DataFrame) -> float: """ Approximate liquidity score (0–10) from volume & range behavior. """ vol = df["Volume"] tr = df["High"] - df["Low"] # Use last 20 bars for context window = min(20, len(df)) vol_recent = vol.tail(window) tr_recent = tr.tail(window) avg_vol = vol_recent.mean() std_vol = vol_recent.std(ddof=0) + 1e-9 latest_vol = vol_recent.iloc[-1] vol_z = (latest_vol - avg_vol) / std_vol avg_tr = tr_recent.mean() std_tr = tr_recent.std(ddof=0) + 1e-9 latest_tr = tr_recent.iloc[-1] range_ratio = latest_tr / (avg_tr + 1e-9) # Volume depth score vol_norm = (vol_z + 2) / 5 # map [-2,3] → [0,1] vol_norm = max(0.0, min(1.0, vol_norm)) vds = 1 + 9 * vol_norm # Range tightness (tight = better liquidity) if avg_tr > 0: rr = range_ratio else: rr = 1.0 if rr <= 0.5: rts = 9.0 elif rr >= 2.0: rts = 1.5 else: rts = 9.0 - (rr - 0.5) * (9.0 - 1.5) / (2.0 - 0.5) # Combine liquidity_score = 0.6 * vds + 0.4 * rts liquidity_score = float(max(0.0, min(10.0, liquidity_score))) return liquidity_score def compute_trend_regime(df: pd.DataFrame) -> str: """ Very simple regime classification from moving averages and volatility. """ close = df["Close"] if len(close) < 30: return "Unknown" ma_fast = close.rolling(10).mean() ma_slow = close.rolling(30).mean() latest = close.iloc[-1] ma_fast_latest = ma_fast.iloc[-1] ma_slow_latest = ma_slow.iloc[-1] # Volatility returns = close.pct_change() vol = returns.tail(20).std(ddof=0) # Very simple regime rules if ma_fast_latest > ma_slow_latest and vol < 0.02: return "Uptrend-Stable" elif ma_fast_latest > ma_slow_latest and vol >= 0.02: return "Uptrend-Volatile" elif ma_fast_latest < ma_slow_latest and vol < 0.02: return "Downtrend-Stable" elif ma_fast_latest < ma_slow_latest and vol >= 0.02: return "Downtrend-Volatile" elif abs(ma_fast_latest - ma_slow_latest) / latest < 0.005 and vol < 0.015: return "Range-LowVol" else: return "Range-Choppy" def compute_volume_pressure(df: pd.DataFrame) -> str: """ Simple volume pressure classification: Rising / Stable / Falling. """ vol = df["Volume"] if len(vol) < 10: return "Stable" recent = vol.tail(10) early = recent.head(5).mean() late = recent.tail(5).mean() if late > early * 1.2: return "Rising" elif late < early * 0.8: return "Falling" else: return "Stable" def compute_gravity_bias(df: pd.DataFrame) -> str: """ Very rough 'gravity' bias from recent price action. Up / Down / Neutral. """ close = df["Close"] if len(close) < 20: return "Neutral" recent = close.tail(10) change = (recent.iloc[-1] - recent.iloc[0]) / recent.iloc[0] if change > 0.03: return "Up" elif change < -0.03: return "Down" else: return "Neutral" def find_sr_context(df: pd.DataFrame) -> dict: """ Simple SR context: recent swing high/low and current position within that band. """ close = df["Close"] if len(close) < 20: return {"support": None, "resistance": None, "position": None} recent = close.tail(20) support = float(recent.min()) resistance = float(recent.max()) last = float(recent.iloc[-1]) if resistance - support <= 0: position = None else: position = (last - support) / (resistance - support) return { "support": support, "resistance": resistance, "position": position, } def detect_simple_trap(df: pd.DataFrame, sr: dict) -> str: """ Simple trap fingerprint: - Long wick through support/resistance, close back inside. Returns: "None", "StopRun-Down", "StopRun-Up" """ if len(df) < 3: return "None" last = df.iloc[-1] support = sr["support"] resistance = sr["resistance"] if support is None or resistance is None: return "None" atr = (df["High"] - df["Low"]).rolling(14).mean().iloc[-1] if np.isnan(atr) or atr == 0: atr = (df["High"] - df["Low"]).tail(14).mean() # Sweep below support if last["Low"] < support - 0.3 * atr and last["Close"] > support: return "StopRun-Down" # Sweep above resistance if last["High"] > resistance + 0.3 * atr and last["Close"] < resistance: return "StopRun-Up" return "None" # ========================= # SIGNAL CLASSIFICATION # ========================= def classify_signal(df: pd.DataFrame) -> dict: """ Combine all simple engines into one GET IN / IN / GET OUT / OUT label. Returns dict: { "label": "GET IN"|"IN"|"GET OUT"|"OUT", "icon": "🟢"|"🟩"|"🟠"|"🔴", "reasons": [str, ...], } """ liquidity = compute_liquidity_score(df) regime = compute_trend_regime(df) pressure = compute_volume_pressure(df) gravity = compute_gravity_bias(df) sr = find_sr_context(df) trap = detect_simple_trap(df, sr) reasons = [] # Basic reason strings reasons.append(f"Regime: {regime}") reasons.append(f"Liquidity Score: {liquidity:.1f}/10") reasons.append(f"Volume Pressure: {pressure}") reasons.append(f"Gravity Bias: {gravity}") if sr["support"] is not None and sr["resistance"] is not None: reasons.append( f"SR Band: {sr['support']:.2f}–{sr['resistance']:.2f}" ) if trap != "None": reasons.append(f"Trap Fingerprint: {trap}") # Heuristic scoring score = 0.0 # Regime contribution if "Uptrend" in regime: score += 2.0 elif "Downtrend" in regime: score -= 2.0 elif "Range-LowVol" in regime: score += 0.5 elif "Range-Choppy" in regime: score -= 0.5 # Liquidity contribution if liquidity >= 7.0: score += 1.5 elif liquidity <= 3.0: score -= 1.0 # Pressure contribution if pressure == "Rising": score += 1.5 elif pressure == "Falling": score -= 1.0 # Gravity contribution if gravity == "Up": score += 1.0 elif gravity == "Down": score -= 1.0 # Trap penalty or bounce opportunity if trap == "StopRun-Down": # Often bullish *after* sweep score += 1.0 reasons.append("Stop-run below support; often followed by bounces.") elif trap == "StopRun-Up": score -= 1.0 reasons.append("Stop-run above resistance; often followed by fades.") # Map score → label # You can tune thresholds; these are just initial heuristics. if score >= 3.5: label = "GET IN" icon = "🟢" elif 1.5 <= score < 3.5: label = "IN" icon = "🟩" elif -1.5 <= score < 1.5: label = "GET OUT" icon = "🟠" else: # score < -1.5 label = "OUT" icon = "🔴" return { "label": label, "icon": icon, "reasons": reasons, "score": score, } # ========================= # REPORT GENERATION # ========================= def build_text_report(results: dict) -> str: """ Build a multi-line text report for console or log use. results: { symbol: { 'label', 'icon', 'reasons', 'score' }, ...} """ lines = [] now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") lines.append(f"TITAN DAILY REPORT - {now}") lines.append("=" * 60) lines.append("Legend: 🟢 GET IN | 🟩 IN | 🟠 GET OUT | 🔴 OUT\n") for symbol, info in results.items(): lines.append(f"{symbol}: {info['icon']} {info['label']} (score={info['score']:.2f})") for r in info["reasons"]: lines.append(f" - {r}") lines.append("") return "\n".join(lines) def build_html_report(results: dict) -> str: """ Build a very simple HTML report. """ now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") def color_for_label(label: str) -> str: return { "GET IN": "#2ecc71", # green "IN": "#27ae60", # darker green "GET OUT": "#e67e22", # orange "OUT": "#e74c3c", # red }.get(label, "#bdc3c7") rows_html = [] for symbol, info in results.items(): color = color_for_label(info["label"]) reasons_html = "
".join(info["reasons"]) row = f""" {symbol} {info['icon']} {info['label']} {info['score']:.2f} {reasons_html} """ rows_html.append(row) table_html = "\n".join(rows_html) html = f""" Titan Daily Report

Titan Daily Report

Generated at: {now}

Legend: 🟢 GET IN | 🟩 IN | 🟠 GET OUT | 🔴 OUT

{table_html}
Symbol Status Score Context
""" return html # ========================= # MAIN ENTRY POINT # ========================= def main(): os.makedirs(OUTPUT_DIR, exist_ok=True) results = {} for symbol in SYMBOLS: try: df = fetch_ohlcv(symbol, LOOKBACK_DAYS) info = classify_signal(df) results[symbol] = info except Exception as e: results[symbol] = { "label": "ERROR", "icon": "❌", "reasons": [f"Error fetching/analyzing: {e}"], "score": 0.0, } # Build text report for console text_report = build_text_report(results) print(text_report) # Save HTML report html_report = build_html_report(results) date_str = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") html_path = os.path.join(OUTPUT_DIR, f"titan_daily_report_{date_str}.html") with open(html_path, "w", encoding="utf-8") as f: f.write(html_report) print(f"\nHTML report saved to: {html_path}") if __name__ == "__main__": main()