#!/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"""
Generated at: {now}
Legend: 🟢 GET IN | 🟩 IN | 🟠 GET OUT | 🔴 OUT
| Symbol | Status | Score | Context |
|---|