Refactor research pipeline and add webapp

This commit is contained in:
Pawel Sarkowicz
2026-07-31 08:33:52 -04:00
parent 47672c76bf
commit 69a0d51f3c
23 changed files with 4841 additions and 707 deletions
+107
View File
@@ -0,0 +1,107 @@
from pathlib import Path
import numpy as np
import pandas as pd
import pytest
from frd.research import (
ArtifactError,
ArtifactSpec,
block_indices,
fama_french_alpha,
form_decile_portfolios,
momentum_signal,
validate_artifacts,
)
def test_momentum_signal_uses_prior_eleven_months():
dates = pd.date_range("2020-01-31", periods=13, freq="ME")
returns = pd.DataFrame({"AAA": np.ones(13) * 0.01}, index=dates)
signal = momentum_signal(returns)
assert pd.isna(signal.iloc[10, 0])
assert signal.iloc[11, 0] == pytest.approx(0.11)
assert signal.iloc[12, 0] == pytest.approx(0.11)
def test_decile_portfolios_include_initial_cost_and_weight_turnover():
dates = pd.date_range("2020-01-31", periods=3, freq="ME")
signal = pd.DataFrame(
{
"A": [4.0, 4.0, 4.0],
"B": [3.0, 1.0, 1.0],
"C": [2.0, 3.0, 3.0],
"D": [1.0, 2.0, 2.0],
},
index=dates,
)
returns = pd.DataFrame(
{
"A": [0.00, 0.10, 0.20],
"B": [0.00, 0.00, 0.00],
"C": [0.00, 0.04, 0.06],
"D": [0.00, 0.00, 0.00],
},
index=dates,
)
port = form_decile_portfolios(signal, returns, decile=0.5, min_names=1)
assert port.index.tolist() == list(dates[1:])
assert port["long"].iloc[0] == pytest.approx(0.05)
assert port["long"].iloc[1] == pytest.approx(0.13)
assert port["long_turnover"].iloc[0] == pytest.approx(1.0)
assert port["long_turnover"].iloc[1] == pytest.approx(0.5)
assert port["ls_turnover"].iloc[0] == pytest.approx(2.0)
def test_fama_french_alpha_aligns_month_start_and_month_end():
dates = pd.date_range("2020-01-31", periods=24, freq="ME")
ff_dates = pd.date_range("2020-01-01", periods=24, freq="MS")
returns = pd.Series(0.01 + np.tile([-0.001, 0.001], 12), index=dates)
ff = pd.DataFrame(
{
"Mkt-RF": np.zeros(24),
"SMB": np.zeros(24),
"HML": np.zeros(24),
"Mom": np.zeros(24),
"RF": np.zeros(24),
},
index=ff_dates,
)
alpha, tstat, r2 = fama_french_alpha(returns, ff)
assert alpha == pytest.approx(0.12)
assert np.isfinite(tstat)
assert r2 >= 0
def test_block_indices_are_in_bounds_and_requested_length():
rng = np.random.RandomState(3)
idx = block_indices(25, 5, rng)
assert len(idx) == 25
assert idx.min() >= 0
assert idx.max() < 25
def test_validate_artifacts_reports_missing_and_malformed(tmp_path: Path):
good = tmp_path / "good.csv"
bad = tmp_path / "bad.csv"
good.write_text("a,b\n1,2\n", encoding="utf-8")
bad.write_text("a\n1\n", encoding="utf-8")
with pytest.raises(ArtifactError, match="Missing notebook-generated"):
validate_artifacts(
tmp_path,
{
"good": ArtifactSpec(good, ("a", "b")),
"missing": ArtifactSpec(tmp_path / "missing.csv", ("x",)),
},
)
with pytest.raises(ArtifactError, match="Malformed notebook-generated"):
validate_artifacts(tmp_path, {"bad": ArtifactSpec(bad, ("a", "b"))})