Skip to content

Commit 1d21cbd

Browse files
Merge pull request #50 from tidy-finance/docs/update-great-docs-config
Update great-docs.yml config
2 parents 4fe0c77 + f15cd34 commit 1d21cbd

39 files changed

Lines changed: 3752 additions & 1859 deletions

CLAUDE.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# CLAUDE.md
2+
3+
Guidance for working in this repository.
4+
5+
## Project layout
6+
7+
- `tidyfinance/` — the package.
8+
- `__init__.py` — builds the public API automatically by scanning the
9+
public submodules and re-exporting their functions/classes (see
10+
`__all__`). Data-bearing functions are wrapped at this boundary by
11+
`backend._use_backend` so they honor the active polars/pandas backend.
12+
Keep this file's namespace clean: discovery loop variables, imports
13+
(`importlib`, `pkgutil`, `types`), and internal toggles are deleted or
14+
underscore-prefixed at the end so they don't leak into `dir(tidyfinance)`
15+
or the docs.
16+
- `core.py` — analytics functions (portfolio sorts, breakpoints, beta /
17+
Fama-MacBeth estimation, lagging, summary statistics).
18+
- `data_download.py``download_data` and the WRDS / Fama-French / FRED /
19+
OSAP / Hugging Face download helpers.
20+
- `backend.py``set_backend` / `get_backend` and the internal
21+
`_use_backend` decorator.
22+
- `utilities.py`, `supported_datasets.py` — helpers and dataset metadata.
23+
- `_internal.py`, `_pseudo.py` — private modules.
24+
25+
## Conventions
26+
27+
### Docstrings
28+
29+
- NumPy-style docstrings, parsed by Great Docs (griffe, `parser: numpy`).
30+
- **Examples must use fenced ` ```python ` code blocks, NOT doctest `>>>` /
31+
`...` prompts.** The Great Docs copy button copies code verbatim, and
32+
prompts make examples impossible to paste and run. Write:
33+
34+
````
35+
Examples
36+
--------
37+
```python
38+
import numpy as np
39+
from tidyfinance import winsorize
40+
data = np.random.default_rng(123).standard_normal(100)
41+
winsorized = winsorize(data, 0.05)
42+
```
43+
````
44+
45+
Do not reintroduce `>>>` examples. If you ever need verifiable doctests
46+
with expected output, raise it as a deliberate change — the current
47+
examples are input-only and carry no doctest assertions.
48+
49+
### Public API
50+
51+
- A function is part of the public API by living in a public (non-`_`)
52+
submodule as a function/class defined within the package — it is then
53+
auto-discovered and re-exported from `tidyfinance`.
54+
- To keep something out of the public API and the Reference page, prefix it
55+
with `_` (module or name).
56+
- Do not name a module `core`, `utils`, `helpers`, `constants`, `config`, or
57+
`settings` and expect it to appear in the docs: Great Docs auto-excludes
58+
those names. `core` is kept only because `great-docs.yml` lists it under
59+
`auto_include`.
60+
61+
### Style
62+
63+
- Ruff, `line-length = 80` (`[tool.ruff]` in `pyproject.toml`). Keep code
64+
and docstrings within 80 columns.
65+
66+
## Common commands
67+
68+
This project uses `uv`.
69+
70+
```bash
71+
uv run pytest # run the test suite (tests/test_*.py)
72+
uv run pytest tests/test_core.py
73+
uv run ruff check . # lint
74+
uv run ruff format . # format
75+
```
76+
77+
Every public function should have a matching `tests/test_<name>.py`.
78+
79+
## Documentation (Great Docs)
80+
81+
- Config: `great-docs.yml`. Generated output lives in `great-docs/`
82+
(git-ignored build artifacts).
83+
84+
```bash
85+
uv run great-docs build # build the site
86+
uv run great-docs preview --port 3000 # local preview (auto-rebuilds)
87+
uv run great-docs scan # preview what will be discovered as public API
88+
```
89+

great-docs.yml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# ----------------------
66
# Set this if your importable module name differs from the project name.
77
# Example: project 'py-yaml12' with module name 'yaml12'
8-
# module: yaml12
8+
module: tidyfinance
99

1010
# Docstring Parser
1111
# ----------------
@@ -25,6 +25,12 @@ dynamic: true
2525
# - InternalClass
2626
# - helper_function
2727

28+
# Force-include modules that Great Docs auto-excludes as generic
29+
# "re-export" names. Our analytics functions live in 'core', which is
30+
# on that blocklist, so it must be re-included for them to be documented.
31+
auto_include:
32+
- core
33+
2834
# Logo & Favicon
2935
# ---------------
3036
# Point to a single logo file (replaces the text title in the navbar):
@@ -48,14 +54,21 @@ authors:
4854
email: christoph.frey@gmail.com
4955
# github:
5056
# orcid:
51-
# homepage:
57+
homepage: https://sites.google.com/site/christophfrey/
5258
- name: Christoph Scheuch
5359
role: Author
5460
# affiliation:
5561
email: christoph@tidy-intelligence.com
5662
# github:
5763
# orcid:
58-
# homepage:
64+
homepage: https://christophscheuch.github.io/
65+
- name: Stefan Voigt
66+
role: Author
67+
# affiliation:
68+
email: stefan.voigt@econ.ku.dk
69+
# github:
70+
# orcid:
71+
homepage: https://www.voigtstefan.me/
5972

6073
# Funding / Copyright Holder
6174
# --------------------------

tests/test_add_lagged_columns.py

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@ def test_exact_lag_with_by_returns_correct_lagged_values():
1818
data = pd.DataFrame(
1919
{
2020
"permno": [1] * 4 + [2] * 4,
21-
"date": list(
22-
pd.date_range("2023-01-01", periods=4, freq="MS")
23-
) * 2,
21+
"date": list(pd.date_range("2023-01-01", periods=4, freq="MS")) * 2,
2422
"size": [float(i) for i in range(1, 9)],
2523
}
2624
)
@@ -41,9 +39,7 @@ def test_exact_lag_without_by_returns_correct_values():
4139
"size": [1.0, 2.0, 3.0],
4240
}
4341
)
44-
result = add_lagged_columns(
45-
data, cols="size", lag=pd.DateOffset(months=1)
46-
)
42+
result = add_lagged_columns(data, cols="size", lag=pd.DateOffset(months=1))
4743
assert pd.isna(result["size_lag"].iloc[0])
4844
assert result["size_lag"].iloc[1] == 1
4945
assert result["size_lag"].iloc[2] == 2
@@ -53,9 +49,7 @@ def test_window_lag_handles_all_src_date_conditions():
5349
"""Test window lag: NA, in-window, and below-lower-bound cases."""
5450
data = pd.DataFrame(
5551
{
56-
"date": pd.to_datetime(
57-
["2023-01-01", "2023-02-01", "2023-06-01"]
58-
),
52+
"date": pd.to_datetime(["2023-01-01", "2023-02-01", "2023-06-01"]),
5953
"size": [1.0, 2.0, 3.0],
6054
}
6155
)
@@ -104,9 +98,7 @@ def test_ff_adjustment_without_by_uses_year_grouping_only():
10498
"""Test ff_adjustment without by uses year grouping only."""
10599
data = pd.DataFrame(
106100
{
107-
"date": pd.to_datetime(
108-
["2022-06-01", "2022-12-01", "2023-06-01"]
109-
),
101+
"date": pd.to_datetime(["2022-06-01", "2022-12-01", "2023-06-01"]),
110102
"size": [10.0, 20.0, 30.0],
111103
}
112104
)
@@ -152,18 +144,14 @@ def test_error_when_date_column_is_absent_from_data():
152144

153145
def test_error_when_lag_is_negative():
154146
"""Test error when lag is negative."""
155-
data = pd.DataFrame(
156-
{"date": [pd.Timestamp("2023-01-01")], "size": [1.0]}
157-
)
147+
data = pd.DataFrame({"date": [pd.Timestamp("2023-01-01")], "size": [1.0]})
158148
with pytest.raises(ValueError, match="non-negative"):
159149
add_lagged_columns(data, cols="size", lag=-1)
160150

161151

162152
def test_error_when_max_lag_is_less_than_lag():
163153
"""Test error when max_lag is less than lag."""
164-
data = pd.DataFrame(
165-
{"date": [pd.Timestamp("2023-01-01")], "size": [1.0]}
166-
)
154+
data = pd.DataFrame({"date": [pd.Timestamp("2023-01-01")], "size": [1.0]})
167155
with pytest.raises(ValueError, match="max_lag"):
168156
add_lagged_columns(
169157
data,
@@ -175,9 +163,7 @@ def test_error_when_max_lag_is_less_than_lag():
175163

176164
def test_error_when_requested_column_is_absent_from_data():
177165
"""Test error when a requested column is absent from data."""
178-
data = pd.DataFrame(
179-
{"date": [pd.Timestamp("2023-01-01")], "size": [1.0]}
180-
)
166+
data = pd.DataFrame({"date": [pd.Timestamp("2023-01-01")], "size": [1.0]})
181167
with pytest.raises(ValueError, match="missing"):
182168
add_lagged_columns(
183169
data, cols="no_such_col", lag=pd.DateOffset(months=1)
@@ -186,9 +172,7 @@ def test_error_when_requested_column_is_absent_from_data():
186172

187173
def test_error_when_by_column_is_absent_from_data():
188174
"""Test error when a by column is absent from data."""
189-
data = pd.DataFrame(
190-
{"date": [pd.Timestamp("2023-01-01")], "size": [1.0]}
191-
)
175+
data = pd.DataFrame({"date": [pd.Timestamp("2023-01-01")], "size": [1.0]})
192176
with pytest.raises(ValueError, match="missing"):
193177
add_lagged_columns(
194178
data,
@@ -207,9 +191,7 @@ def test_error_when_join_key_is_not_unique():
207191
}
208192
)
209193
with pytest.raises(ValueError, match="unique"):
210-
add_lagged_columns(
211-
data, cols="size", lag=pd.DateOffset(months=1)
212-
)
194+
add_lagged_columns(data, cols="size", lag=pd.DateOffset(months=1))
213195

214196

215197
def test_error_when_upper_helper_column_already_exists():
@@ -233,6 +215,7 @@ def test_error_when_upper_helper_column_already_exists():
233215
def test_data_options_specifies_date_column_name():
234216
"""Test data_options dict specifies the date column name."""
235217
from tidyfinance.core import data_options
218+
236219
data = pd.DataFrame(
237220
{
238221
"my_date": pd.date_range("2023-01-01", periods=3, freq="MS"),
@@ -249,6 +232,7 @@ def test_data_options_specifies_date_column_name():
249232
assert pd.isna(result["size_lag"].iloc[0])
250233
assert result["size_lag"].iloc[1] == 1
251234

235+
252236
if __name__ == "__main__":
253237
# Run all tests
254238
pytest.main([__file__])

tests/test_assign_portfolio.py

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
from tidyfinance.core import assign_portfolio # noqa: E402
1616

17-
1817
# %% mock breakpoint functions
1918

2019

@@ -31,9 +30,7 @@ def mock_breakpoint_percentiles(
3130
data, sorting_variable, bp_options=None, data_options=None
3231
):
3332
"""Percentile-based mock breakpoint function."""
34-
percentiles = (bp_options or {}).get(
35-
"percentiles", [0.2, 0.4, 0.6, 0.8]
36-
)
33+
percentiles = (bp_options or {}).get("percentiles", [0.2, 0.4, 0.6, 0.8])
3734
probs = [0] + list(percentiles) + [1]
3835
return np.quantile(data[sorting_variable].dropna().values, probs)
3936

@@ -156,9 +153,7 @@ def test_constant_sorting_variable_returns_vector_of_correct_length():
156153

157154
def test_two_distinct_values_with_2_portfolios_produces_two_groups():
158155
"""Test two distinct values with 2 portfolios produces two groups."""
159-
data = pd.DataFrame(
160-
{"id": range(100), "value": [1] * 50 + [2] * 50}
161-
)
156+
data = pd.DataFrame({"id": range(100), "value": [1] * 50 + [2] * 50})
162157
result = assign_portfolio(
163158
data,
164159
"value",
@@ -189,9 +184,7 @@ def test_single_row_data_frame_with_constant_variable_triggers_warning():
189184

190185
def test_warning_when_clusters_reduce_number_of_portfolios():
191186
"""Test warning is issued when clusters reduce the number of portfolios."""
192-
data = pd.DataFrame(
193-
{"id": range(100), "value": [1] * 50 + [100] * 50}
194-
)
187+
data = pd.DataFrame({"id": range(100), "value": [1] * 50 + [100] * 50})
195188

196189
def mock_bp_5(data, sv, bp_options=None, data_options=None):
197190
return np.array([1, 20, 40, 60, 80, 100])
@@ -335,9 +328,7 @@ def test_function_works_with_large_datasets():
335328
"""Test function works with large datasets."""
336329
rng = np.random.default_rng(1)
337330
n = 100_000
338-
data = pd.DataFrame(
339-
{"id": np.arange(n), "value": rng.standard_normal(n)}
340-
)
331+
data = pd.DataFrame({"id": np.arange(n), "value": rng.standard_normal(n)})
341332
result = assign_portfolio(
342333
data,
343334
"value",
@@ -416,9 +407,7 @@ def test_percentile_based_breakpoints_produce_correct_number_of_groups():
416407
def test_two_portfolios_split_data_roughly_in_half():
417408
"""Test two portfolios split data roughly in half."""
418409
rng = np.random.default_rng(10)
419-
data = pd.DataFrame(
420-
{"id": range(1000), "value": rng.standard_normal(1000)}
421-
)
410+
data = pd.DataFrame({"id": range(1000), "value": rng.standard_normal(1000)})
422411
result = assign_portfolio(
423412
data,
424413
"value",
@@ -444,9 +433,7 @@ def spy_bp(data, sorting_variable, bp_options=None, data_options=None):
444433
received["data_options"] = data_options
445434
return np.array([0.0, 0.5, 1.0])
446435

447-
data = pd.DataFrame(
448-
{"id": range(10), "value": np.linspace(0, 1, 10)}
449-
)
436+
data = pd.DataFrame({"id": range(10), "value": np.linspace(0, 1, 10)})
450437
my_bp_opts = {"n_portfolios": 2}
451438
my_data_opts = {"date": "date_col"}
452439
assign_portfolio(

0 commit comments

Comments
 (0)