Refactor research pipeline and add webapp
This commit is contained in:
@@ -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"))})
|
||||
Reference in New Issue
Block a user