You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

198 lines
7.6 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""P22-pre: Universe-shift gainers_leader proxy diagnostic.
V49 trades on a $10+ price / $25M+ ADV slice. This probe checks whether the
gainers_leader pattern (top %-gap-up names → buy at open, exit at close)
produces positive expectancy on OTHER price × ADV slices of the snapshot.
The pinned daily snapshot orb_daily_v46_20260423 has 6540 tickers — a
superset of V49's 3408-ticker screener universe. We bin all tickers by
price × ADV, then for each bin × each day, pick top-5 by gap (gap >= +2%)
and average open-to-close return. That's the gainers_leader 'long the
biggest gappers at open' expectancy proxy.
KILL gate: if no slice has materially stronger expectancy than the baseline
($10+/$25M+) and no slice has positive expectancy in a previously-untouched
zone, autonomous probe space is exhausted — append V49 FINAL to lineage.
"""
from __future__ import annotations
from pathlib import Path
import pandas as pd
DAILY_DIR = Path("data/cache/daily_snapshots/orb_daily_v46_20260423")
WINDOW_START = pd.Timestamp("2025-07-09")
WINDOW_END = pd.Timestamp("2026-04-23")
GAP_THR = 0.02
TOP_N = 5
PRICE_BINS = [
("$2-$5", 2.0, 5.0),
("$5-$10", 5.0, 10.0),
("$10-$25", 10.0, 25.0),
("$25-$100", 25.0, 100.0),
("$100+", 100.0, 1e9),
]
ADV_BINS = [
("$0.5M-$5M", 0.5e6, 5e6),
("$5M-$25M", 5e6, 25e6),
("$25M-$100M", 25e6, 100e6),
("$100M+", 100e6, 1e12),
]
def main() -> None:
files = sorted(DAILY_DIR.glob("*.parquet"))
print(f"Loading {len(files)} ticker daily snapshots...")
rows = []
skipped = 0
for fp in files:
try:
df = pd.read_parquet(fp)
except Exception:
skipped += 1
continue
if len(df) < 25:
skipped += 1
continue
sym = fp.stem
df = df.sort_values("date").reset_index(drop=True)
df["date"] = pd.to_datetime(df["date"])
df["prev_close"] = df["close"].shift(1)
df["gap"] = (df["open"] / df["prev_close"]) - 1.0
df["oc_ret"] = (df["close"] / df["open"]) - 1.0
df["dollar_vol"] = df["close"] * df["volume"]
df["adv20"] = df["dollar_vol"].rolling(20).mean()
# filter to window and to rows with all features
sub = df[
(df["date"] >= WINDOW_START)
& (df["date"] <= WINDOW_END)
& df["gap"].notna()
& df["adv20"].notna()
& (df["open"] > 0)
& (df["close"] > 0)
].copy()
if len(sub):
sub["ticker"] = sym
sub["price"] = sub["close"] # use close as price band proxy
rows.append(sub[["ticker", "date", "gap", "oc_ret", "adv20", "price"]])
if not rows:
print("No data.")
return
big = pd.concat(rows, ignore_index=True)
print(f"Total ticker-days: {len(big):,} (skipped {skipped} files)")
print(f"Unique tickers: {big['ticker'].nunique():,}")
print(f"Days in window: {big['date'].nunique()}")
print()
# Per-bin gainers_leader proxy
print(f"=== Gainers_leader proxy: top-{TOP_N} by gap (gap >= +{GAP_THR*100:.0f}%) per day ===")
print(f"Trade: long at open, exit at close. Universe = each (price × ADV) bin.")
print()
print(f"{'price_bin':>10} | {'adv_bin':>13} | {'tickers':>7} | {'days':>5} | {'n_trades':>8} | {'gap_avg%':>9} | {'WR%':>6} | {'avg_win%':>9} | {'avg_loss%':>10} | {'expectancy%':>12} | {'cum%':>10}")
print("-" * 145)
summary = []
for pname, plo, phi in PRICE_BINS:
for aname, alo, ahi in ADV_BINS:
mask = (
(big["price"] >= plo)
& (big["price"] < phi)
& (big["adv20"] >= alo)
& (big["adv20"] < ahi)
& (big["gap"] >= GAP_THR)
)
slice_df = big[mask]
n_tickers = slice_df["ticker"].nunique()
n_days = slice_df["date"].nunique()
if n_days == 0:
continue
# For each day, pick top-N by gap
picks = (
slice_df.sort_values(["date", "gap"], ascending=[True, False])
.groupby("date")
.head(TOP_N)
)
n = len(picks)
if n < 30:
continue
wins = picks["oc_ret"][picks["oc_ret"] > 0]
losses = picks["oc_ret"][picks["oc_ret"] < 0]
wr = len(wins) / n
avg_win = wins.mean() if len(wins) else 0
avg_loss = -losses.mean() if len(losses) else 0
expectancy = picks["oc_ret"].mean()
gap_avg = picks["gap"].mean()
cum = picks["oc_ret"].sum()
summary.append({
"price_bin": pname,
"adv_bin": aname,
"tickers": n_tickers,
"days": n_days,
"n_trades": n,
"gap_avg": gap_avg,
"wr": wr,
"avg_win": avg_win,
"avg_loss": avg_loss,
"expectancy": expectancy,
"cum": cum,
})
print(f"{pname:>10} | {aname:>13} | {n_tickers:>7} | {n_days:>5} | {n:>8} | {gap_avg*100:>8.2f} | {wr*100:>5.1f} | {avg_win*100:>8.3f} | {avg_loss*100:>9.3f} | {expectancy*100:>+11.4f} | {cum*100:>+9.2f}")
if not summary:
print("No bins met n>=30 threshold.")
return
print()
print(f"=== V49 baseline reference: $10+ price, $25M+ ADV ===")
print(f"V49 actually trades 166 trades over 200d with +155.58% return and 50%+ WR via")
print(f"the full gainers_leader engine (event_catalyst, obv, ORB breakout, etc.).")
print(f"This proxy uses ONLY 'long top-N gappers at open, exit at close' — far weaker.")
print(f"Look for slices where the *proxy* itself shows materially better expectancy")
print(f"than the 'comparable' bins (price=$10-$25 / adv=$25M+) — those are the")
print(f"directional candidates worth engine work.")
print()
# Highlight best/worst slices
sdf = pd.DataFrame(summary).sort_values("expectancy", ascending=False)
print("=== Top 5 slices by expectancy ===")
print(sdf.head(5).to_string(index=False, formatters={
"gap_avg": "{:+.2%}".format,
"wr": "{:.1%}".format,
"avg_win": "{:.2%}".format,
"avg_loss": "{:.2%}".format,
"expectancy": "{:+.4%}".format,
"cum": "{:+.2%}".format,
}))
print()
print("=== Bottom 5 slices by expectancy ===")
print(sdf.tail(5).to_string(index=False, formatters={
"gap_avg": "{:+.2%}".format,
"wr": "{:.1%}".format,
"avg_win": "{:.2%}".format,
"avg_loss": "{:.2%}".format,
"expectancy": "{:+.4%}".format,
"cum": "{:+.2%}".format,
}))
# Comparison: V49-equivalent slices vs alternative slices
v49_eq = sdf[(sdf["price_bin"].isin(["$10-$25", "$25-$100", "$100+"])) & (sdf["adv_bin"].isin(["$25M-$100M", "$100M+"]))]
if len(v49_eq):
print()
print(f"=== V49-equivalent slices (price >= $10, ADV >= $25M), n_bins={len(v49_eq)} ===")
print(f"Avg expectancy: {v49_eq['expectancy'].mean()*100:+.4f}%")
print(f"Avg WR: {v49_eq['wr'].mean()*100:.2f}%")
alt = sdf[~sdf.index.isin(v49_eq.index)]
if len(alt):
print()
print(f"=== Alternative slices (NEW universe candidates), n_bins={len(alt)} ===")
print(f"Avg expectancy: {alt['expectancy'].mean()*100:+.4f}%")
print(f"Avg WR: {alt['wr'].mean()*100:.2f}%")
better_alt = alt[alt["expectancy"] > v49_eq["expectancy"].mean() if len(v49_eq) else 0]
print(f"Alt slices that beat V49-equivalent avg expectancy: {len(better_alt)}/{len(alt)}")
if __name__ == "__main__":
main()