Minor webapp update
This commit is contained in:
@@ -105,11 +105,11 @@ This project constructs and backtests a **sector-neutralized momentum factor** a
|
||||
> 5. **Decompose** portfolio risk into systematic vs. idiosyncratic components via PCA (eigendecomposition + random matrix theory)
|
||||
> 6. **Stress test** (notebook 06) by generating synthetic markets and re-running the backtest across alternative histories
|
||||
|
||||
The project is structured in three parts, all complete:
|
||||
The project is structured in three parts:
|
||||
|
||||
* **Part I — Data and Factor Analysis** (notebooks 01–03) — *complete*
|
||||
* **Part II — Backtest and Risk Decomposition** (notebooks 04–05) — *complete*
|
||||
* **Part III — Synthetic Markets and Stress Testing** (notebook 06) — *complete*
|
||||
* **Part I — Data and Factor Analysis** (notebooks 01–03)
|
||||
* **Part II — Backtest and Risk Decomposition** (notebooks 04–05)
|
||||
* **Part III — Synthetic Markets and Stress Testing** (notebook 06)
|
||||
|
||||
---
|
||||
|
||||
@@ -181,7 +181,7 @@ This part builds the data matrix $\mathbf{R}$, diagnoses individual factor vecto
|
||||
|
||||
### 1. Data Overview and Market Statistics
|
||||
|
||||
We use a cached snapshot of S&P 500 constituents and download adjusted close prices. Because that snapshot is still based on a modern S&P 500 membership list, it introduces **survivorship bias** — names that went bankrupt or were delisted between 2005 and today won't appear. In linear-algebra terms: the columns of $\mathbf{R}$ are a non-random subset of all stocks that existed; the columns we *don't* see are exactly the ones that went to zero, biasing returns upward. Notebook 04 includes a sensitivity analysis for this.
|
||||
We use a cached snapshot of S&P 500 constituents and download adjusted close prices. Because that snapshot is still based on a modern S&P 500 membership list, it introduces **survivorship bias** — names that went bankrupt or were delisted between 2005 and today won't appear. In linear-algebra terms: the columns of $\mathbf{R}$ are a non-random subset of all stocks that existed; the columns we *don't* see are exactly the ones that went to zero or got delisted, biasing returns upward. Notebook 04 includes a sensitivity analysis for this.
|
||||
|
||||
Key findings:
|
||||
* **Universe breadth** rises from ~385 to ~501 stocks over the sample — but the column set is fixed to *today's* constituents, so this counts how many of today's survivors had price data in month $t$. The matrix isn't truly "getting wider"; its survivor-only columns fill in over time.
|
||||
@@ -355,7 +355,7 @@ frd.example.com {
|
||||
}
|
||||
```
|
||||
|
||||
The data stays **mounted read-only** (`./data:/app/data`), mirroring the `.gitignore`. The app validates the required CSV artifacts at startup and tells you to run notebooks `01 -> 06` if anything is missing or malformed.
|
||||
The data stays **mounted read-only** (`./data:/app/data`). The app validates the required CSV artifacts at startup and tells you to run notebooks `01 -> 06` if anything is missing or malformed.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+19
-3
@@ -133,9 +133,25 @@ def form_decile_portfolios(
|
||||
|
||||
|
||||
def decile_long_returns(signal_df: pd.DataFrame, ret_df: pd.DataFrame, decile: float = 0.1) -> pd.Series:
|
||||
"""Top-decile equal-weight long-only monthly returns."""
|
||||
port = form_decile_portfolios(signal_df, ret_df, decile=decile)
|
||||
return port["long"] if "long" in port else pd.Series(dtype=float)
|
||||
"""Top-decile equal-weight long-only monthly returns (signal at t, return at t+1).
|
||||
|
||||
Vectorized for speed — the webapp's live stress-test button calls this once
|
||||
per click. Ranks cross-sectionally per month, takes the top decile, and
|
||||
averages next-month returns. (Semantically equivalent to the per-month loop
|
||||
in ``form_decile_portfolios`` but without computing holdings/turnover.)
|
||||
"""
|
||||
ranks = signal_df.rank(axis=1, pct=True) # NaN stays NaN
|
||||
rvals = ranks.values[:-1] # signal at t (T-1, N)
|
||||
next_ret = ret_df.values[1:] # return at t+1 (T-1, N)
|
||||
with np.errstate(invalid="ignore", all="ignore"):
|
||||
thr = np.nanquantile(rvals, 1 - decile, axis=1) # per-row (1-decile) quantile of valid scores
|
||||
top = (rvals >= thr[:, None]) & ~np.isnan(rvals)
|
||||
valid = (~np.isnan(rvals)).sum(axis=1)
|
||||
contrib = np.where(top, next_ret, np.nan)
|
||||
with np.errstate(invalid="ignore"):
|
||||
long_ret = np.nanmean(contrib, axis=1)
|
||||
long_ret = np.where(valid >= 50, long_ret, np.nan)
|
||||
return pd.Series(long_ret, index=np.arange(1, signal_df.shape[0]), dtype=float)
|
||||
|
||||
|
||||
def _is_datetime_like(index: pd.Index) -> bool:
|
||||
|
||||
+8
-2
@@ -23,7 +23,7 @@ from typing import Any
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.responses import HTMLResponse, PlainTextResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from sklearn.decomposition import PCA
|
||||
|
||||
@@ -223,7 +223,7 @@ def _json_safe(o: Any) -> Any:
|
||||
return o
|
||||
|
||||
|
||||
@app.get("/")
|
||||
@app.api_route("/", methods=["GET", "HEAD"])
|
||||
def index():
|
||||
html = (STATIC_DIR / "index.html").read_text(encoding="utf-8")
|
||||
css_v = _file_version(STATIC_DIR / "app.css")
|
||||
@@ -233,6 +233,12 @@ def index():
|
||||
return HTMLResponse(content=html)
|
||||
|
||||
|
||||
@app.get("/robots.txt", response_class=PlainTextResponse)
|
||||
def robots():
|
||||
# Disallow crawling — this is an interactive demo, not a site to index.
|
||||
return "User-agent: *\nDisallow: /\n"
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
def health():
|
||||
s = app.state
|
||||
|
||||
Reference in New Issue
Block a user