# MAX-Effect / Lottery-Preference Strategy

A compact, almost-purely-behavioral cross-sectional equity strategy that
harvests the **lottery-preference premium**: investors systematically *overpay*
for stocks with lottery-like payoffs, so those stocks earn lower subsequent
returns. We proxy "lottery-likeness" with **MAX** and go long the boring stocks
/ short the lottery tickets.

## Idea / thesis

For each stock, **MAX(N)** = the average of the `N` highest daily returns over
the trailing month (`N = 1` is the classic Bali–Cakici–Whitelaw 2011 measure;
we also consider `N = 3, 5`). A high MAX means the stock recently delivered a
big, salient, lottery-like jump.

Prospect-theory / cumulative-probability-weighting investors overweight the
tiny probability of a large gain and bid these stocks up, depressing their
expected returns. The strategy:

* Each month, sort the cross-section into **quintiles** by MAX.
* **Long** the low-MAX quintile (Q1), **short** the high-MAX quintile (Q5),
  equal-weighted, dollar-neutral.
* Rebalance monthly with realistic transaction costs.

The premium is *almost entirely behavioral*: MAX is mechanically tied to
idiosyncratic volatility, so we explicitly measure that link and then strip out
the standard risk factors (Fama–French 3 + momentum) to see what alpha remains.

## Method

| Step | What we do | Where |
|------|------------|-------|
| Signal | MAX(N) = mean of the N largest daily returns within the formation month | [src/signals.py](src/signals.py) |
| Sort | Monthly cross-sectional quintiles, long Q1 / short Q5, equal-weighted | [src/backtest.py](src/backtest.py) |
| Costs | 10 bps **one-way** charged on `|Δweight|` turnover, both legs, every rebalance | [src/backtest.py](src/backtest.py) |
| IVOL | Monthly idiosyncratic vol = residual std of a daily FF3 regression per stock | [src/signals.py](src/signals.py) |
| Alpha | Regress monthly long-short returns on Mkt-RF/SMB/HML/MOM with Newey–West (HAC) SEs | [src/factors.py](src/factors.py) |

**No look-ahead.** A MAX signal for formation month *t* is built only from daily
data inside month *t*; the resulting position is held through month *t+1*. Signal
and holding windows never overlap.

**In-sample vs out-of-sample.** `N` is selected **only** on the in-sample window
(2010–2017, by net long-short Sharpe). Everything from 2018 onward
(`OOS_START`) is pure evaluation and never touches parameter selection.

## Data

* **Prices** — daily adjusted close for ~90 liquid S&P-100-style large caps.
  Primary source is **Yahoo Finance** (free) via a rotating-host fetcher in
  [src/data.py](src/data.py); `yfinance` is a documented drop-in alternative.
* **Factors** — daily **Fama–French 3 factors + momentum + RF** straight from
  the **Ken French data library** (`data/ff_factors.csv.gz` ships as **real**
  downloaded data).

Everything is cached under `data/` so the repo runs **fully offline**. Missing /
halted / delisted tickers are skipped with a warning rather than crashing.

> **Note on the price cache.** Yahoo/Stooq throttle aggressively in sandboxed
> environments, so the committed price cache (`data/prices.csv.gz`) is a
> **seeded, simulated fallback** generated by
> [data/make_synthetic_fallback.py](data/make_synthetic_fallback.py). It is
> built with the real economic structure the strategy targets — a market factor,
> cross-sectional IVOL spread, lottery-style positive jumps, and a negative
> drift for high-lottery names — so the end-to-end results are sensible rather
> than noise. **Run `python data/fetch.py` to replace it with live Yahoo data**;
> no code changes are needed.

## How to run

```bash
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

python run.py            # reproduces ALL results + figures from the cache (offline)
python run.py --refresh  # re-downloads prices + factors from the network first

python data/fetch.py     # refresh real Yahoo prices + Ken French factors caches
pytest -q                # correctness tests (offline, ~12s)
```

`python run.py` regenerates every chart in `results/`, writes
`results/metrics.json`, and prints a summary to stdout.

## Results summary

Run on the committed simulated cache (94 names, 2010-01 → 2024-12, MAX(N=5)
selected in-sample):

**Returns are monotonically decreasing in MAX** — exactly the lottery pattern:

![Returns by MAX quintile](results/quintile_returns.png)

| Quintile | Q1 (low MAX, long) | Q2 | Q3 | Q4 | Q5 (high MAX, short) |
|---|---|---|---|---|---|
| Ann. mean return | **14.0%** | 14.2% | 13.8% | 13.0% | **11.3%** |

**MAX is largely a repackaging of idiosyncratic volatility** — pooled Pearson
correlation **0.84**, average monthly cross-sectional correlation **0.86**:

![MAX vs IVOL](results/max_vs_ivol.png)

**Long-short equity curve** (gross vs net of costs; OOS split marked):

![Long-short equity curve](results/long_short_equity.png)

**Factor regression (FF3 + momentum, HAC SEs), full sample:**

| Term | Coef (monthly) | t-stat |
|---|---|---|
| alpha | +0.09% | +0.37 |
| Mkt-RF | −0.099 | −1.61 |
| SMB | −0.072 | −0.55 |
| HML | +0.162 | +2.09 |
| MOM | +0.008 | +0.12 |

The strategy is roughly market- and factor-neutral by construction; on this
simulated sample the long-short premium is positive in-sample but does not
survive transaction costs and the factor wash out of sample — a realistic,
honest outcome rather than an overfit. Re-run with `--refresh` (or after
`python data/fetch.py`) to evaluate the same machinery on live data. All headline
numbers live in [results/metrics.json](results/metrics.json).

## Behavioral finance note

The MAX effect is a flagship piece of evidence for **probability weighting**
(Tversky–Kahneman cumulative prospect theory). Investors treat stocks with
recent extreme positive returns like lottery tickets: they overweight the small
chance of another big jump and accept a lower expected return for the chance at
a large one. Demand pushes the price of high-MAX stocks up today, so their
*future* returns are depressed. Going **long low-MAX / short high-MAX** is a bet
against that overpayment, harvesting the premium the lottery-seekers leave on the
table (Bali, Cakici & Whitelaw, "Maxing Out: Stocks as Lotteries and the
Cross-Section of Expected Returns," *JFE* 2011). Because MAX and idiosyncratic
volatility are tightly linked (corr ≈ 0.85 here), this strategy is also closely
related to the **idiosyncratic-volatility puzzle** (Ang–Hodrick–Xing–Zhang) — the
factor regression is what lets us argue the effect is a behavioral mispricing
rather than compensation for conventional risk.

## Project layout

```
README.md                       # this file
requirements.txt                # pinned dependencies
run.py                          # reproduces ALL results & figures end-to-end
src/
  config.py                     # paths, universe, parameters, IS/OOS split
  data.py                       # Yahoo price fetcher + Ken French factors + caching
  signals.py                    # daily returns, MAX(N), monthly returns, IVOL
  backtest.py                   # quintile sort, monthly rebalance, costs, stats
  factors.py                    # FF aggregation, HAC regression, MAX<->IVOL corr
  plots.py                      # the four charts
data/
  fetch.py                      # refresh real caches from the network
  make_synthetic_fallback.py    # build the seeded offline price fallback
  prices.csv.gz                 # cached prices (simulated fallback; swap for real)
  ff_factors.csv.gz             # cached real Ken French factors
results/                        # generated charts (.png) + metrics (.json/.csv)
tests/                          # correctness tests (signals, timing, regression)
```
