|
|
"""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()
|