"""Generate the committed *offline fallback* price cache (fully seeded).

Why this exists
---------------
The primary data path is real Yahoo Finance data via ``data/fetch.py``.  In
sandboxed / offline environments Yahoo throttles aggressively, so the repo ships
a small, deterministic **simulated** price panel as a fallback so that
``python run.py`` and the tests always run with no network.

The simulation is intentionally built with the lottery-preference structure the
strategy is designed to harvest, so the end-to-end results are economically
sensible rather than noise:

  * a common market factor (so a real FF-market beta exists),
  * cross-sectional spread in idiosyncratic volatility,
  * a per-stock "lottery intensity" that drives occasional large positive daily
    jumps  -> high MAX and high IVOL are linked, as in the data,
  * a small NEGATIVE drift for high-lottery stocks -> investors overpay for the
    lottery, so low-MAX outperforms high-MAX (Bali-Cakici-Whitelaw).

Run ``python data/make_synthetic_fallback.py`` to regenerate ``prices.csv.gz``.
Replace it any time with real data via ``python data/fetch.py``.
"""
from __future__ import annotations

import sys
from pathlib import Path

import numpy as np
import pandas as pd

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from src import config  # noqa: E402


def generate_prices(seed: int = config.SEED) -> pd.DataFrame:
    rng = np.random.default_rng(seed)
    tickers = list(config.UNIVERSE)
    n = len(tickers)
    dates = pd.bdate_range(config.START, config.END)
    T = len(dates)

    # Common market factor.
    mkt = rng.normal(0.0004, 0.010, T)

    # Per-stock characteristics.
    beta = rng.uniform(0.7, 1.4, n)
    idio_vol = rng.uniform(0.008, 0.028, n)          # spread in IVOL
    lottery = rng.uniform(0.0, 1.0, n)               # lottery intensity in [0,1]

    # Lottery stocks: more frequent & larger positive jumps -> higher MAX.
    jump_prob = 0.01 + 0.06 * lottery                # per-day jump probability
    jump_scale = 0.02 + 0.06 * lottery               # jump magnitude

    # The jumps carry a large positive *expected* value.  Investors overpay for
    # that lottery payoff, so realised expected return must DECREASE in lottery
    # intensity.  We therefore (a) neutralise the mechanical expected jump value
    # and (b) subtract an extra premium proportional to lottery intensity -- the
    # premium the long-low / short-high strategy is designed to harvest.
    expected_jump = jump_prob * jump_scale * np.sqrt(2.0 / np.pi)
    premium = 0.0005                                 # daily lottery premium slope
    alpha = 0.0005 - expected_jump - premium * lottery

    rets = np.empty((T, n))
    for j in range(n):
        eps = rng.normal(0.0, idio_vol[j], T)
        jumps = (rng.random(T) < jump_prob[j]) * np.abs(
            rng.normal(0.0, jump_scale[j], T)
        )
        rets[:, j] = alpha[j] + beta[j] * mkt + eps + jumps

    prices = 100.0 * np.cumprod(1.0 + rets, axis=0)
    df = pd.DataFrame(prices, index=dates, columns=tickers)
    df.index.name = "date"
    return df


def main() -> None:
    config.ensure_dirs()
    prices = generate_prices()
    prices.to_csv(config.PRICE_CACHE)
    print(f"Wrote simulated fallback cache: {prices.shape[1]} tickers x "
          f"{prices.shape[0]} days -> {config.PRICE_CACHE}")


if __name__ == "__main__":
    main()
