jquantstats 0.9.2__tar.gz → 0.9.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jquantstats-0.9.2 → jquantstats-0.9.4}/.gitignore +3 -0
- jquantstats-0.9.4/.rhiza/completions/README.md +263 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/PKG-INFO +5 -3
- {jquantstats-0.9.2 → jquantstats-0.9.4}/pyproject.toml +29 -5
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_plots/_data.py +1 -1
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_plots/_portfolio.py +4 -4
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_portfolio_attribution.py +3 -3
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_portfolio_cost.py +17 -6
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_portfolio_nav.py +10 -7
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_portfolio_turnover.py +3 -3
- jquantstats-0.9.4/src/jquantstats/_protocol.py +75 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_reports/_data.py +8 -8
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_reports/_protocol.py +1 -9
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_stats/_basic.py +5 -2
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_stats/_core.py +86 -13
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_stats/_montecarlo.py +2 -2
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_stats/_performance.py +8 -10
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_stats/_reporting.py +1 -1
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_stats/_rolling.py +1 -1
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_utils/_data.py +2 -2
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/data.py +2 -5
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/exceptions.py +105 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/portfolio.py +18 -9
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/result.py +16 -0
- jquantstats-0.9.2/.github/actions/configure-git-auth/README.md +0 -80
- jquantstats-0.9.2/src/jquantstats/_protocol.py +0 -41
- jquantstats-0.9.2/src/jquantstats/_stats/_protocol.py +0 -45
- {jquantstats-0.9.2 → jquantstats-0.9.4}/.rhiza/requirements/README.md +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/.rhiza/tests/README.md +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/.rhiza/tests/stress/README.md +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/LICENSE +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/README.md +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/__init__.py +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_cost_model.py +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_plots/__init__.py +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_plots/_protocol.py +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_reports/__init__.py +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_reports/_formatting.py +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_reports/_portfolio.py +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_stats/__init__.py +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_stats/_internals.py +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_stats/_stats.py +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_types.py +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_utils/__init__.py +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_utils/_portfolio.py +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/_utils/_protocol.py +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/py.typed +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/templates/_base.html +0 -0
- {jquantstats-0.9.2 → jquantstats-0.9.4}/src/jquantstats/templates/portfolio_report.html +0 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Shell Completion for Rhiza Make Targets
|
|
2
|
+
|
|
3
|
+
This directory contains shell completion scripts for Bash and Zsh that provide tab-completion for make targets in Rhiza-based projects.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ Tab-complete all available make targets
|
|
8
|
+
- ✅ Show target descriptions in Zsh
|
|
9
|
+
- ✅ Complete common make variables (DRY_RUN, BUMP, ENV, etc.)
|
|
10
|
+
- ✅ Works with any Rhiza-based project
|
|
11
|
+
- ✅ Auto-discovers targets from Makefile and included .mk files
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### Bash
|
|
16
|
+
|
|
17
|
+
#### Method 1: Source in your shell config
|
|
18
|
+
|
|
19
|
+
Add to your `~/.bashrc` or `~/.bash_profile`:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Rhiza make completion
|
|
23
|
+
if [ -f /path/to/project/.rhiza/completions/rhiza-completion.bash ]; then
|
|
24
|
+
source /path/to/project/.rhiza/completions/rhiza-completion.bash
|
|
25
|
+
fi
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Replace `/path/to/project` with the actual path to your Rhiza project.
|
|
29
|
+
|
|
30
|
+
#### Method 2: System-wide installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Copy to bash completion directory
|
|
34
|
+
sudo cp .rhiza/completions/rhiza-completion.bash /etc/bash_completion.d/rhiza
|
|
35
|
+
|
|
36
|
+
# Reload completions
|
|
37
|
+
source /etc/bash_completion.d/rhiza
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
#### Method 3: User-local installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Create local completion directory
|
|
44
|
+
mkdir -p ~/.local/share/bash-completion/completions
|
|
45
|
+
|
|
46
|
+
# Copy completion script
|
|
47
|
+
cp .rhiza/completions/rhiza-completion.bash ~/.local/share/bash-completion/completions/make
|
|
48
|
+
|
|
49
|
+
# Reload bash
|
|
50
|
+
source ~/.bashrc
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Zsh
|
|
54
|
+
|
|
55
|
+
#### Method 1: User-local installation (Recommended)
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Create completion directory
|
|
59
|
+
mkdir -p ~/.zsh/completion
|
|
60
|
+
|
|
61
|
+
# Copy completion script
|
|
62
|
+
cp .rhiza/completions/rhiza-completion.zsh ~/.zsh/completion/_make
|
|
63
|
+
|
|
64
|
+
# Add to ~/.zshrc (if not already present)
|
|
65
|
+
echo 'fpath=(~/.zsh/completion $fpath)' >> ~/.zshrc
|
|
66
|
+
echo 'autoload -U compinit && compinit' >> ~/.zshrc
|
|
67
|
+
|
|
68
|
+
# Reload zsh
|
|
69
|
+
source ~/.zshrc
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### Method 2: Source directly
|
|
73
|
+
|
|
74
|
+
Add to your `~/.zshrc`:
|
|
75
|
+
|
|
76
|
+
```zsh
|
|
77
|
+
# Rhiza make completion
|
|
78
|
+
if [ -f /path/to/project/.rhiza/completions/rhiza-completion.zsh ]; then
|
|
79
|
+
source /path/to/project/.rhiza/completions/rhiza-completion.zsh
|
|
80
|
+
fi
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### Method 3: System-wide installation
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Copy to system completion directory
|
|
87
|
+
sudo cp .rhiza/completions/rhiza-completion.zsh /usr/local/share/zsh/site-functions/_make
|
|
88
|
+
|
|
89
|
+
# Reload zsh
|
|
90
|
+
exec zsh
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Usage
|
|
94
|
+
|
|
95
|
+
Once installed, you can tab-complete make targets:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Tab-complete targets
|
|
99
|
+
make <TAB>
|
|
100
|
+
|
|
101
|
+
# Complete with prefix
|
|
102
|
+
make te<TAB> # Expands to: make test
|
|
103
|
+
|
|
104
|
+
# Complete variables
|
|
105
|
+
make BUMP=<TAB> # Shows: patch, minor, major
|
|
106
|
+
|
|
107
|
+
# Works with any target
|
|
108
|
+
make doc<TAB> # Shows: docs, docker-build, docker-run, etc.
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Zsh Benefits
|
|
112
|
+
|
|
113
|
+
In Zsh, you'll also see descriptions for targets:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
make <TAB>
|
|
117
|
+
# Shows:
|
|
118
|
+
# test -- run all tests
|
|
119
|
+
# fmt -- check the pre-commit hooks and the linting
|
|
120
|
+
# install -- install
|
|
121
|
+
# book -- build documentation site via zensical
|
|
122
|
+
# ...
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Common Variables
|
|
126
|
+
|
|
127
|
+
The completion scripts understand these common variables:
|
|
128
|
+
|
|
129
|
+
| Variable | Values | Description |
|
|
130
|
+
|----------|--------|-------------|
|
|
131
|
+
| `DRY_RUN` | `1` | Preview mode without making changes |
|
|
132
|
+
| `BUMP` | `patch`, `minor`, `major` | Version bump type |
|
|
133
|
+
| `ENV` | `dev`, `staging`, `prod` | Target environment |
|
|
134
|
+
| `COVERAGE_FAIL_UNDER` | (number) | Minimum coverage threshold |
|
|
135
|
+
| `PYTHON_VERSION` | (version) | Override Python version |
|
|
136
|
+
|
|
137
|
+
Example usage:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# Tab-complete after typing DRY_
|
|
141
|
+
make DRY_<TAB> # Expands to: make DRY_RUN=1
|
|
142
|
+
|
|
143
|
+
# Tab-complete variable values
|
|
144
|
+
make BUMP=<TAB> # Shows: patch minor major
|
|
145
|
+
|
|
146
|
+
# Combine with targets
|
|
147
|
+
make bump BUMP=<TAB>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Troubleshooting
|
|
151
|
+
|
|
152
|
+
### Bash: Completions not working
|
|
153
|
+
|
|
154
|
+
1. Check if bash-completion is installed:
|
|
155
|
+
```bash
|
|
156
|
+
# Debian/Ubuntu
|
|
157
|
+
sudo apt-get install bash-completion
|
|
158
|
+
|
|
159
|
+
# macOS
|
|
160
|
+
brew install bash-completion@2
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
2. Ensure completion is enabled in your shell:
|
|
164
|
+
```bash
|
|
165
|
+
# Add to ~/.bashrc if not present
|
|
166
|
+
if [ -f /etc/bash_completion ]; then
|
|
167
|
+
. /etc/bash_completion
|
|
168
|
+
fi
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
3. Reload your shell configuration:
|
|
172
|
+
```bash
|
|
173
|
+
source ~/.bashrc
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Zsh: Completions not working
|
|
177
|
+
|
|
178
|
+
1. Check if compinit is called in your `~/.zshrc`:
|
|
179
|
+
```zsh
|
|
180
|
+
autoload -U compinit && compinit
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
2. Clear the completion cache:
|
|
184
|
+
```bash
|
|
185
|
+
rm -f ~/.zcompdump
|
|
186
|
+
compinit
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
3. Ensure the script is in your fpath:
|
|
190
|
+
```zsh
|
|
191
|
+
echo $fpath
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
4. Reload your shell configuration:
|
|
195
|
+
```zsh
|
|
196
|
+
source ~/.zshrc
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### No targets appearing
|
|
200
|
+
|
|
201
|
+
1. Ensure you're in a directory with a Makefile:
|
|
202
|
+
```bash
|
|
203
|
+
ls -la Makefile
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
2. Test that make can parse the Makefile:
|
|
207
|
+
```bash
|
|
208
|
+
make -qp 2>/dev/null | head
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
3. Manually source the completion script to test:
|
|
212
|
+
```bash
|
|
213
|
+
# Bash
|
|
214
|
+
source .rhiza/completions/rhiza-completion.bash
|
|
215
|
+
|
|
216
|
+
# Zsh
|
|
217
|
+
source .rhiza/completions/rhiza-completion.zsh
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Optional Aliases
|
|
221
|
+
|
|
222
|
+
You can add shortcuts in your shell config:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
# Add to ~/.bashrc or ~/.zshrc
|
|
226
|
+
alias m='make'
|
|
227
|
+
|
|
228
|
+
# For bash:
|
|
229
|
+
complete -F _rhiza_make_completion m
|
|
230
|
+
|
|
231
|
+
# For zsh:
|
|
232
|
+
compdef _rhiza_make m
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Then use:
|
|
236
|
+
```bash
|
|
237
|
+
m te<TAB> # Expands to: m test
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Technical Details
|
|
241
|
+
|
|
242
|
+
### How it works
|
|
243
|
+
|
|
244
|
+
1. **Target Discovery**: Parses `make -qp` output to find all targets
|
|
245
|
+
2. **Description Extraction**: Looks for `##` comments after target names
|
|
246
|
+
3. **Variable Detection**: Includes common Makefile variables
|
|
247
|
+
4. **Dynamic Completion**: Regenerates list each time you tab
|
|
248
|
+
|
|
249
|
+
### Performance
|
|
250
|
+
|
|
251
|
+
- Completions are generated on-demand (when you press Tab)
|
|
252
|
+
- For large Makefiles (100+ targets), there may be a small delay
|
|
253
|
+
- Results are not cached to ensure targets are always current
|
|
254
|
+
|
|
255
|
+
## See Also
|
|
256
|
+
|
|
257
|
+
- [Tools Reference](../../docs/reference/TOOLS_REFERENCE.md) - Complete command reference
|
|
258
|
+
- [Quick Reference](../../docs/guides/QUICK_REFERENCE.md) - Quick command reference
|
|
259
|
+
- [Extending Rhiza](../../docs/guides/EXTENDING_RHIZA.md) - How to add custom targets
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
*Last updated: 2026-02-15*
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jquantstats
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.4
|
|
4
4
|
Summary: Analytics for quants
|
|
5
|
-
Project-URL:
|
|
5
|
+
Project-URL: Homepage, https://github.com/jebel-quant/jquantstats
|
|
6
|
+
Project-URL: Repository, https://github.com/jebel-quant/jquantstats
|
|
6
7
|
Author-email: tschm <thomas.schmelzer@gmail.com>
|
|
7
8
|
License-Expression: MIT
|
|
8
9
|
License-File: LICENSE
|
|
9
10
|
Classifier: Development Status :: 5 - Production/Stable
|
|
10
11
|
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
11
12
|
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
14
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -20,7 +22,7 @@ Requires-Dist: jinja2>=3.1.0
|
|
|
20
22
|
Requires-Dist: narwhals>=2.0.0
|
|
21
23
|
Requires-Dist: numpy>=2.0.0
|
|
22
24
|
Requires-Dist: plotly>=6.0.0
|
|
23
|
-
Requires-Dist: polars>=1.
|
|
25
|
+
Requires-Dist: polars>=1.35.2
|
|
24
26
|
Requires-Dist: scipy>=1.14.1
|
|
25
27
|
Provides-Extra: plot
|
|
26
28
|
Requires-Dist: kaleido==1.3.0; extra == 'plot'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Main project metadata
|
|
2
2
|
[project]
|
|
3
3
|
name = 'jquantstats'
|
|
4
|
-
version = "0.9.
|
|
4
|
+
version = "0.9.4"
|
|
5
5
|
description = "Analytics for quants"
|
|
6
6
|
authors = [{name='tschm', email= 'thomas.schmelzer@gmail.com'}]
|
|
7
7
|
readme = "README.md"
|
|
@@ -11,7 +11,7 @@ dependencies = [
|
|
|
11
11
|
"narwhals>=2.0.0",
|
|
12
12
|
"numpy>=2.0.0",
|
|
13
13
|
"plotly>=6.0.0",
|
|
14
|
-
"polars>=1.
|
|
14
|
+
"polars>=1.35.2",
|
|
15
15
|
"scipy>=1.14.1"
|
|
16
16
|
]
|
|
17
17
|
license = "MIT"
|
|
@@ -26,11 +26,13 @@ classifiers = [
|
|
|
26
26
|
"Programming Language :: Python :: 3.11",
|
|
27
27
|
"Programming Language :: Python :: 3.12",
|
|
28
28
|
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"License :: OSI Approved :: MIT License",
|
|
29
30
|
]
|
|
30
31
|
|
|
31
32
|
# Project URLs for documentation and reference
|
|
32
33
|
[project.urls]
|
|
33
|
-
|
|
34
|
+
Homepage = "https://github.com/jebel-quant/jquantstats"
|
|
35
|
+
Repository = "https://github.com/jebel-quant/jquantstats"
|
|
34
36
|
|
|
35
37
|
# Optional dependencies that can be installed with extras (e.g., pip install jquantstats[plot])
|
|
36
38
|
[project.optional-dependencies]
|
|
@@ -45,12 +47,29 @@ web = [
|
|
|
45
47
|
dev = [
|
|
46
48
|
"pandas>=2.2.3", # required by quantstats (comparison tests only — not a runtime dependency)
|
|
47
49
|
"pyarrow>=22.0.0",
|
|
48
|
-
"yfinance==1.
|
|
49
|
-
"ipython==9.
|
|
50
|
+
"yfinance==1.4.1",
|
|
51
|
+
"ipython==9.14.1",
|
|
50
52
|
"quantstats==0.0.81", # reference implementation used in test_quantstats.py for metric validation
|
|
51
53
|
"httpx>=0.28.1",
|
|
52
54
|
"marimo>=0.23.6",
|
|
53
55
|
]
|
|
56
|
+
test = [
|
|
57
|
+
"pytest>=8.0",
|
|
58
|
+
"pytest-cov>=6.0",
|
|
59
|
+
"pytest-html>=4.0",
|
|
60
|
+
"pytest-mock>=3.0",
|
|
61
|
+
"pytest-xdist>=3.0",
|
|
62
|
+
"pytest-timeout>=2.0",
|
|
63
|
+
"pytest-benchmark>=5.2.3",
|
|
64
|
+
"hypothesis>=6.150.0",
|
|
65
|
+
"syrupy>=4.9.1",
|
|
66
|
+
"pygal>=3.1.0",
|
|
67
|
+
"python-dotenv>=1.0",
|
|
68
|
+
]
|
|
69
|
+
lint = [
|
|
70
|
+
"pre-commit>=4.0",
|
|
71
|
+
"ty>=0.0.30",
|
|
72
|
+
]
|
|
54
73
|
|
|
55
74
|
|
|
56
75
|
# Build system configuration
|
|
@@ -101,10 +120,15 @@ pyarrow = "pyarrow"
|
|
|
101
120
|
httpx = "httpx"
|
|
102
121
|
|
|
103
122
|
# Coverage configuration
|
|
123
|
+
[tool.coverage.run]
|
|
124
|
+
branch = true
|
|
125
|
+
|
|
104
126
|
[tool.coverage.report]
|
|
127
|
+
fail_under = 100
|
|
105
128
|
exclude_lines = [
|
|
106
129
|
"pragma: no cover",
|
|
107
130
|
"if TYPE_CHECKING:", # type-only imports are never executed at runtime
|
|
131
|
+
"@overload", # overload stubs have no runtime body
|
|
108
132
|
]
|
|
109
133
|
|
|
110
134
|
|
|
@@ -967,7 +967,7 @@ class DataPlots:
|
|
|
967
967
|
fig = go.Figure()
|
|
968
968
|
for ticker in tickers:
|
|
969
969
|
hist_returns = df[ticker].tail(sample_len).fill_null(0.0).cast(pl.Float64).to_numpy()
|
|
970
|
-
if hist_returns.size == 0:
|
|
970
|
+
if hist_returns.size == 0: # pragma: no cover
|
|
971
971
|
continue
|
|
972
972
|
|
|
973
973
|
simulated_metrics = [
|
|
@@ -188,7 +188,7 @@ class PortfolioPlots:
|
|
|
188
188
|
fig.update_yaxes(type="log", row=1, col=1)
|
|
189
189
|
# Ensure the first y-axis is explicitly set for environments
|
|
190
190
|
# where subplot updates may not propagate to layout alias.
|
|
191
|
-
if hasattr(fig.layout, "yaxis"):
|
|
191
|
+
if hasattr(fig.layout, "yaxis"): # pragma: no branch — plotly figures always have .yaxis
|
|
192
192
|
fig.layout.yaxis.type = "log"
|
|
193
193
|
|
|
194
194
|
return fig
|
|
@@ -212,7 +212,7 @@ class PortfolioPlots:
|
|
|
212
212
|
|
|
213
213
|
if log_scale:
|
|
214
214
|
fig.update_yaxes(type="log")
|
|
215
|
-
if hasattr(fig.layout, "yaxis"):
|
|
215
|
+
if hasattr(fig.layout, "yaxis"): # pragma: no branch — plotly figures always have .yaxis
|
|
216
216
|
fig.layout.yaxis.type = "log"
|
|
217
217
|
|
|
218
218
|
def lagged_performance_plot(self, lags: list[int] | None = None, log_scale: bool = False) -> go.Figure:
|
|
@@ -267,7 +267,7 @@ class PortfolioPlots:
|
|
|
267
267
|
ValueError: If ``window`` is not a positive integer.
|
|
268
268
|
"""
|
|
269
269
|
if not isinstance(window, int) or window <= 0:
|
|
270
|
-
raise ValueError
|
|
270
|
+
raise ValueError(f"window must be a positive integer, got {window!r}") # noqa: TRY003
|
|
271
271
|
|
|
272
272
|
rolling = self._portfolio.stats.rolling_sharpe(rolling_period=window)
|
|
273
273
|
|
|
@@ -308,7 +308,7 @@ class PortfolioPlots:
|
|
|
308
308
|
ValueError: If ``window`` is not a positive integer.
|
|
309
309
|
"""
|
|
310
310
|
if not isinstance(window, int) or window <= 0:
|
|
311
|
-
raise ValueError
|
|
311
|
+
raise ValueError(f"window must be a positive integer, got {window!r}") # noqa: TRY003
|
|
312
312
|
|
|
313
313
|
rolling = self._portfolio.stats.rolling_volatility(rolling_period=window)
|
|
314
314
|
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import contextlib
|
|
6
5
|
from typing import TYPE_CHECKING, Self
|
|
7
6
|
|
|
8
7
|
import polars as pl
|
|
@@ -65,8 +64,9 @@ class PortfolioAttributionMixin:
|
|
|
65
64
|
cost_per_unit=self.cost_per_unit,
|
|
66
65
|
cost_bps=self.cost_bps,
|
|
67
66
|
)
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
# Direct write is safe: Portfolio is a frozen, slotted dataclass that
|
|
68
|
+
# declares every cache field, so object.__setattr__ cannot fail here.
|
|
69
|
+
object.__setattr__(self, "_tilt_cache", result)
|
|
70
70
|
return result
|
|
71
71
|
|
|
72
72
|
@property
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import math
|
|
5
6
|
from typing import TYPE_CHECKING
|
|
6
7
|
|
|
7
8
|
import polars as pl
|
|
8
9
|
|
|
10
|
+
from .exceptions import InvalidMaxBpsError, NegativeCostBpsError
|
|
11
|
+
|
|
9
12
|
if TYPE_CHECKING:
|
|
10
13
|
from .data import Data
|
|
11
14
|
|
|
@@ -120,7 +123,9 @@ class PortfolioCostMixin:
|
|
|
120
123
|
``returns`` column reduced by the per-period trading cost.
|
|
121
124
|
|
|
122
125
|
Raises:
|
|
123
|
-
|
|
126
|
+
TypeError: If ``cost_bps`` is not a number.
|
|
127
|
+
ValueError: If ``cost_bps`` is not finite (NaN or infinity).
|
|
128
|
+
NegativeCostBpsError: If ``cost_bps`` is negative.
|
|
124
129
|
|
|
125
130
|
Examples:
|
|
126
131
|
>>> from jquantstats.portfolio import Portfolio
|
|
@@ -135,8 +140,13 @@ class PortfolioCostMixin:
|
|
|
135
140
|
True
|
|
136
141
|
"""
|
|
137
142
|
effective_bps = cost_bps if cost_bps is not None else self.cost_bps
|
|
143
|
+
if isinstance(effective_bps, bool) or not isinstance(effective_bps, int | float):
|
|
144
|
+
raise TypeError(f"cost_bps must be a number, got {type(effective_bps).__name__}") # noqa: TRY003
|
|
145
|
+
effective_bps = float(effective_bps)
|
|
146
|
+
if not math.isfinite(effective_bps):
|
|
147
|
+
raise ValueError(f"cost_bps must be finite, got {effective_bps}") # noqa: TRY003
|
|
138
148
|
if effective_bps < 0:
|
|
139
|
-
raise
|
|
149
|
+
raise NegativeCostBpsError(effective_bps)
|
|
140
150
|
base = self.returns
|
|
141
151
|
daily_cost = self.turnover["turnover"] * (effective_bps / 10_000.0)
|
|
142
152
|
return base.with_columns((pl.col("returns") - daily_cost).alias("returns"))
|
|
@@ -160,7 +170,7 @@ class PortfolioCostMixin:
|
|
|
160
170
|
``max_bps`` inclusive.
|
|
161
171
|
|
|
162
172
|
Raises:
|
|
163
|
-
|
|
173
|
+
InvalidMaxBpsError: If ``max_bps`` is not a positive integer.
|
|
164
174
|
|
|
165
175
|
Examples:
|
|
166
176
|
>>> from jquantstats.portfolio import Portfolio
|
|
@@ -183,11 +193,12 @@ class PortfolioCostMixin:
|
|
|
183
193
|
[0, 1, 2, 3, 4, 5]
|
|
184
194
|
"""
|
|
185
195
|
if not isinstance(max_bps, int) or max_bps < 1:
|
|
186
|
-
raise
|
|
196
|
+
raise InvalidMaxBpsError(max_bps)
|
|
187
197
|
import numpy as np
|
|
188
198
|
|
|
199
|
+
from ._stats._core import _std_is_negligible
|
|
200
|
+
|
|
189
201
|
periods = self.data._periods_per_year # one Data object, outside the loop
|
|
190
|
-
_eps = np.finfo(np.float64).eps
|
|
191
202
|
sqrt_periods = float(np.sqrt(periods))
|
|
192
203
|
cost_levels = list(range(0, max_bps + 1))
|
|
193
204
|
|
|
@@ -204,7 +215,7 @@ class PortfolioCostMixin:
|
|
|
204
215
|
sharpe_values: list[float] = []
|
|
205
216
|
for mean_raw, std_raw in zip(means_row, stds_row, strict=False):
|
|
206
217
|
mean_val = 0.0 if mean_raw is None else float(mean_raw)
|
|
207
|
-
if
|
|
218
|
+
if _std_is_negligible(std_raw, mean_val):
|
|
208
219
|
sharpe_values.append(float("nan"))
|
|
209
220
|
else:
|
|
210
221
|
sharpe_values.append(mean_val / float(std_raw) * sqrt_periods)
|
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import contextlib
|
|
6
5
|
from typing import TYPE_CHECKING
|
|
7
6
|
|
|
8
7
|
import polars as pl
|
|
9
8
|
|
|
10
|
-
from .exceptions import MissingDateColumnError
|
|
9
|
+
from .exceptions import MissingDateColumnError, NoAssetColumnsError
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
class PortfolioNavMixin:
|
|
@@ -68,8 +67,9 @@ class PortfolioNavMixin:
|
|
|
68
67
|
pl.when(pl.col(c).is_finite()).then(pl.col(c)).otherwise(0.0).fill_null(0.0).alias(c) for c in assets
|
|
69
68
|
)
|
|
70
69
|
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
# Direct write is safe: Portfolio is a frozen, slotted dataclass that
|
|
71
|
+
# declares every cache field, so object.__setattr__ cannot fail here.
|
|
72
|
+
object.__setattr__(self, "_profits_cache", result)
|
|
73
73
|
return result
|
|
74
74
|
|
|
75
75
|
@property
|
|
@@ -78,12 +78,15 @@ class PortfolioNavMixin:
|
|
|
78
78
|
|
|
79
79
|
Aggregates per-asset profits into a single ``'profit'`` column and
|
|
80
80
|
validates that no day's total profit is NaN/null.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
NoAssetColumnsError: If the profits frame has no numeric asset columns.
|
|
81
84
|
"""
|
|
82
85
|
df_profits = self.profits
|
|
83
86
|
assets = [c for c in df_profits.columns if df_profits[c].dtype.is_numeric()]
|
|
84
87
|
|
|
85
88
|
if not assets:
|
|
86
|
-
raise
|
|
89
|
+
raise NoAssetColumnsError("profits")
|
|
87
90
|
|
|
88
91
|
non_assets = [c for c in df_profits.columns if c not in set(assets)]
|
|
89
92
|
|
|
@@ -121,8 +124,8 @@ class PortfolioNavMixin:
|
|
|
121
124
|
result = self.nav_accumulated.with_columns(
|
|
122
125
|
(pl.col("profit") / self.aum).alias("returns"),
|
|
123
126
|
)
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
# Direct write is safe: see the comment on the profits cache above.
|
|
128
|
+
object.__setattr__(self, "_returns_cache", result)
|
|
126
129
|
return result
|
|
127
130
|
|
|
128
131
|
@property
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import contextlib
|
|
6
5
|
from typing import TYPE_CHECKING
|
|
7
6
|
|
|
8
7
|
import polars as pl
|
|
@@ -61,8 +60,9 @@ class PortfolioTurnoverMixin:
|
|
|
61
60
|
cols.append(daily_abs_chg)
|
|
62
61
|
result = self.cashposition.select(cols)
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
# Direct write is safe: Portfolio is a frozen, slotted dataclass that
|
|
64
|
+
# declares every cache field, so object.__setattr__ cannot fail here.
|
|
65
|
+
object.__setattr__(self, "_turnover_cache", result)
|
|
66
66
|
return result
|
|
67
67
|
|
|
68
68
|
@property
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Shared protocol definitions used across jquantstats subpackages.
|
|
2
|
+
|
|
3
|
+
Design rationale
|
|
4
|
+
----------------
|
|
5
|
+
The analytics subpackages (``_stats``, ``_plots``, ``_reports``, ``_utils``)
|
|
6
|
+
must not import the concrete `Data` / `Portfolio` classes at runtime — that
|
|
7
|
+
would create circular imports, since those classes compose the subpackages.
|
|
8
|
+
Instead, each consumer annotates against a structural Protocol:
|
|
9
|
+
|
|
10
|
+
- `DataLike` and `StatsLike` (this module) are shared by every subpackage —
|
|
11
|
+
there is exactly one definition of each.
|
|
12
|
+
- ``PortfolioLike`` is deliberately *not* shared: each subpackage declares its
|
|
13
|
+
own (``_plots/_protocol.py``, ``_reports/_protocol.py``,
|
|
14
|
+
``_utils/_protocol.py``) listing only the members it actually consumes
|
|
15
|
+
(interface segregation). Keep it that way — a merged PortfolioLike would
|
|
16
|
+
re-couple the subpackages to the full Portfolio surface.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from collections.abc import Iterator
|
|
22
|
+
from typing import Protocol, runtime_checkable
|
|
23
|
+
|
|
24
|
+
import polars as pl
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StatsLike(Protocol): # pragma: no cover
|
|
28
|
+
"""Structural interface for the statistics facade used by reports."""
|
|
29
|
+
|
|
30
|
+
def summary(self) -> pl.DataFrame:
|
|
31
|
+
"""Full summary DataFrame (one row per metric, one column per asset)."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@runtime_checkable
|
|
36
|
+
class DataLike(Protocol): # pragma: no cover
|
|
37
|
+
"""Authoritative structural interface for Data consumers.
|
|
38
|
+
|
|
39
|
+
Union of the members required by the stats mixins, plots, reports, and
|
|
40
|
+
utils — annotating against the superset is harmless for consumers that
|
|
41
|
+
use only part of it, and keeps a single definition.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
returns: pl.DataFrame
|
|
45
|
+
index: pl.DataFrame
|
|
46
|
+
benchmark: pl.DataFrame | None
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def all(self) -> pl.DataFrame:
|
|
50
|
+
"""Combined DataFrame of date index, return, and benchmark columns."""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def assets(self) -> list[str]:
|
|
55
|
+
"""Names of the asset return columns."""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def date_col(self) -> list[str]:
|
|
60
|
+
"""Column names used as the date/time index."""
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def stats(self) -> StatsLike:
|
|
65
|
+
"""Statistics facade used by reports."""
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def _periods_per_year(self) -> float:
|
|
70
|
+
"""Estimated number of return periods per calendar year."""
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
def items(self) -> Iterator[tuple[str, pl.Series]]:
|
|
74
|
+
"""Iterate over (asset_name, returns_series) pairs."""
|
|
75
|
+
...
|