xpectral 1.0__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.
xpectral-1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 BayQuant
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
xpectral-1.0/PKG-INFO ADDED
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: xpectral
3
+ Version: 1.0
4
+ Summary: Quant Research Library
5
+ Author: BayQuant
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 BayQuant
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/bayquant/xpectral
29
+ Keywords: finance,quant,bokeh,polars
30
+ Classifier: Programming Language :: Python :: 3
31
+ Classifier: Programming Language :: Python :: 3.12
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Operating System :: OS Independent
34
+ Requires-Python: >=3.12
35
+ Description-Content-Type: text/markdown
36
+ License-File: LICENSE
37
+ Requires-Dist: bokeh==3.8.1
38
+ Requires-Dist: massive>=2.0.2
39
+ Requires-Dist: matplotlib>=3.10.7
40
+ Requires-Dist: pandas>=2.3.3
41
+ Requires-Dist: polars>=1.34.0
42
+ Requires-Dist: pyarrow>=21.0.0
43
+ Requires-Dist: python-dotenv>=1.2.1
44
+ Dynamic: license-file
45
+
46
+ # Xpectral
47
+
48
+ ![Spectral decomposition](assets/xpectral_banner.gif)
49
+
50
+ A quantitative research library that extends **Polars** DataFrames with charting and financial analytics.
51
+
52
+ ## Modules
53
+
54
+ - **`xpectral.charts`** — Fluent Bokeh visualization via `df.bokeh.line(...)`, `df.bokeh.scatter(...)`, etc.
55
+ - **`xpectral.quant`** — Financial metrics (returns, volatility, beta) via `pl.col(...).quant.returns()`
56
+ - **`xpectral.data`** — Market data from the Polygon/Massive API with caching and rate limiting
57
+
58
+ ## Usage
59
+
60
+ ```python
61
+ import polars as pl
62
+ import xpectral # registers .bokeh and .quant accessors
63
+
64
+ df = pl.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]})
65
+ fig = df.bokeh.line(x="x", y="y")
66
+ ```
67
+
68
+ ## Install
69
+
70
+ ```bash
71
+ uv sync
72
+ ```
xpectral-1.0/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # Xpectral
2
+
3
+ ![Spectral decomposition](assets/xpectral_banner.gif)
4
+
5
+ A quantitative research library that extends **Polars** DataFrames with charting and financial analytics.
6
+
7
+ ## Modules
8
+
9
+ - **`xpectral.charts`** — Fluent Bokeh visualization via `df.bokeh.line(...)`, `df.bokeh.scatter(...)`, etc.
10
+ - **`xpectral.quant`** — Financial metrics (returns, volatility, beta) via `pl.col(...).quant.returns()`
11
+ - **`xpectral.data`** — Market data from the Polygon/Massive API with caching and rate limiting
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ import polars as pl
17
+ import xpectral # registers .bokeh and .quant accessors
18
+
19
+ df = pl.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]})
20
+ fig = df.bokeh.line(x="x", y="y")
21
+ ```
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ uv sync
27
+ ```
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "xpectral"
7
+ version = "1.0"
8
+ description = "Quant Research Library"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = { file = "LICENSE" }
12
+ authors = [
13
+ { name = "BayQuant" }
14
+ ]
15
+ keywords = ["finance", "quant", "bokeh", "polars"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.12",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent"
21
+ ]
22
+
23
+ dependencies = [
24
+ "bokeh==3.8.1",
25
+ "massive>=2.0.2",
26
+ "matplotlib>=3.10.7",
27
+ "pandas>=2.3.3",
28
+ "polars>=1.34.0",
29
+ "pyarrow>=21.0.0",
30
+ "python-dotenv>=1.2.1",
31
+ ]
32
+
33
+ [dependency-groups]
34
+ dev = [
35
+ "black", # code formatter
36
+ "ipykernel>=7.0.1", # Jupyter notebook support
37
+ "furo>=2024.0", # Sphinx theme for docs
38
+ "myst-parser>=3.0", # Markdown support in Sphinx docs
39
+ "sphinx>=8.0", # Sphinx documentation generator
40
+ "sphinx-autodoc-typehints>=2.0", # type hint rendering in Sphinx docs
41
+ ]
42
+
43
+ [project.urls]
44
+ Homepage = "https://github.com/bayquant/xpectral"
45
+
46
+ [tool.setuptools.packages.find]
47
+ where = ["."]
48
+ include = ["xpectral*"]
xpectral-1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,71 @@
1
+ import unittest
2
+
3
+ import polars as pl
4
+ from bokeh.plotting import figure
5
+
6
+ from xpectral.charts import polars_accessors
7
+
8
+
9
+ class TestPolarsBokehAccessor(unittest.TestCase):
10
+ def setUp(self) -> None:
11
+ self.df = pl.DataFrame(
12
+ {
13
+ "x": [1, 2, 3],
14
+ "y": [10, 20, 30],
15
+ "y2": [12, 18, 36],
16
+ "low": [8, 16, 24],
17
+ "high": [14, 24, 40],
18
+ }
19
+ )
20
+
21
+ def test_unknown_glyph_raises(self) -> None:
22
+ with self.assertRaisesRegex(ValueError, "Unknown glyph 'area'"):
23
+ self.df.bokeh.plot("area", glyph_params={"x": "x", "y": "y"})
24
+
25
+ def test_missing_column_fails_through_bokeh_validation(self) -> None:
26
+ with self.assertRaisesRegex(Exception, "missing"):
27
+ self.df.bokeh.plot("line", glyph_params={"x": "x", "y": "missing"})
28
+
29
+ def test_unsupported_figure_param_raises(self) -> None:
30
+ with self.assertRaisesRegex(ValueError, "Unsupported figure_params: not_a_figure_prop"):
31
+ self.df.bokeh.plot(
32
+ "line",
33
+ glyph_params={"x": "x", "y": "y"},
34
+ figure_params={"not_a_figure_prop": 1},
35
+ )
36
+
37
+ def test_default_x_uses_generated_index_column(self) -> None:
38
+ df = self.df.drop("x")
39
+ fig = df.bokeh.plot("line", glyph_params={"y": "y"})
40
+
41
+ self.assertEqual(fig.renderers[0].glyph.x, "__index")
42
+ self.assertIn("__index", fig.renderers[0].data_source.column_names)
43
+ self.assertNotIn("__index", df.columns)
44
+
45
+ def test_existing_figure_is_reused_and_updated(self) -> None:
46
+ existing = figure(width=250)
47
+ fig = self.df.bokeh.plot(
48
+ "scatter",
49
+ glyph_params={"x": "x", "y": "y"},
50
+ figure=existing,
51
+ figure_params={"title": "Updated"},
52
+ )
53
+
54
+ self.assertIs(fig, existing)
55
+ self.assertEqual(existing.title.text, "Updated")
56
+ self.assertEqual(len(existing.renderers), 1)
57
+
58
+ def test_accepted_figure_params_api(self) -> None:
59
+ fig_keys = self.df.bokeh.accepted_figure_params()
60
+ self.assertIsInstance(fig_keys, set)
61
+ self.assertIn("title", fig_keys)
62
+
63
+ def test_accepted_figure_params_pattern_filter_returns_set(self) -> None:
64
+ fig_keys = self.df.bokeh.accepted_figure_params(pattern="title")
65
+ self.assertIsInstance(fig_keys, set)
66
+ self.assertTrue(fig_keys)
67
+ self.assertTrue(all("title" in key for key in fig_keys))
68
+
69
+
70
+ if __name__ == "__main__":
71
+ unittest.main()
@@ -0,0 +1,20 @@
1
+ #-----------------------------------------------------------------------------
2
+ # Imports
3
+ #-----------------------------------------------------------------------------
4
+
5
+ # Standard library imports
6
+ from pathlib import Path
7
+
8
+ # Other imports
9
+ from dotenv import load_dotenv
10
+ from . import charts
11
+ from . import data
12
+ from . import quant
13
+
14
+ #-----------------------------------------------------------------------------
15
+ # Globals and constants
16
+ #-----------------------------------------------------------------------------
17
+
18
+ load_dotenv(
19
+ dotenv_path=Path(__file__).resolve().parent / ".env"
20
+ )
@@ -0,0 +1,17 @@
1
+ #-----------------------------------------------------------------------------
2
+ # Imports
3
+ #-----------------------------------------------------------------------------
4
+
5
+ # Standard library imports
6
+
7
+ # Other imports
8
+ from .polars_accessors import BokehAccessor
9
+ from .polars_accessors import Figure
10
+
11
+ #-----------------------------------------------------------------------------
12
+ # Globals and constants
13
+ #-----------------------------------------------------------------------------
14
+
15
+ __all__ = [
16
+ "BokehAccessor"
17
+ ]
@@ -0,0 +1,73 @@
1
+ #-----------------------------------------------------------------------------
2
+ # Imports
3
+ #-----------------------------------------------------------------------------
4
+
5
+ # Standard library imports
6
+ from __future__ import annotations
7
+ from functools import wraps
8
+ from inspect import Parameter
9
+ from inspect import Signature
10
+ from inspect import signature
11
+
12
+ # Other imports
13
+ from bokeh.plotting._docstring import generate_docstring
14
+ from bokeh.plotting._renderer import create_renderer
15
+
16
+ #-----------------------------------------------------------------------------
17
+ # Globals and constants
18
+ #-----------------------------------------------------------------------------
19
+
20
+ #-----------------------------------------------------------------------------
21
+ # General API
22
+ #-----------------------------------------------------------------------------
23
+
24
+ def glyph_method(glyphclass):
25
+ def decorator(func):
26
+ parameters = glyphclass.parameters()
27
+
28
+ # TODO: send issue so that this signature only takes glyphclass.args instead of [x[0] for x in parameters]
29
+ sigparams = (
30
+ [Parameter("self", Parameter.POSITIONAL_OR_KEYWORD)]
31
+ + [x[0] for x in parameters]
32
+ + [Parameter("source", Parameter.KEYWORD_ONLY, default=None)]
33
+ + [Parameter("kwargs", Parameter.VAR_KEYWORD)]
34
+ )
35
+
36
+ @wraps(func)
37
+ def wrapped(self, *args, **kwargs):
38
+ # Validate positional arguments against the glyph's declared positional field order.
39
+ if len(args) > len(glyphclass._args):
40
+ raise TypeError(f"{func.__name__} takes {len(glyphclass._args)} positional argument but {len(args)} were given")
41
+ # Map positional inputs to their corresponding glyph kwargs by param name.
42
+ for arg, param in zip(args, sigparams[1:]):
43
+ kwargs[param.name] = arg
44
+ # Default source comes from the accessor unless explicitly provided.
45
+ kwargs.setdefault("source", self.source)
46
+ # Preserve any coordinate context passed through a custom Figure subclass.
47
+ if self.coordinates is not None:
48
+ kwargs.setdefault("coordinates", self.coordinates)
49
+ # Inspect/create synthetic fields on the source when required by the glyph.
50
+ source = kwargs["source"]
51
+ n = len(next(iter(source.data.values()), ()))
52
+ # If x is required but missing, inject a 1..n index column into source and reference it by name.
53
+ if "x" in glyphclass._args and "x" not in kwargs and "x" not in source.data:
54
+ source.data["x"] = list(range(1, n + 1))
55
+ kwargs["x"] = "x"
56
+ # If y is required but missing, inject a 1..n index column into source and reference it by name.
57
+ if "y" in glyphclass._args and "y" not in kwargs and "y" not in source.data:
58
+ source.data["y"] = list(range(1, n + 1))
59
+ kwargs["y"] = "y"
60
+ # Delegate to Bokeh with normalized kwargs and possibly augmented source columns.
61
+ return create_renderer(glyphclass, self.plot, **kwargs)
62
+
63
+ wrapped.__signature__ = Signature(parameters=sigparams, return_annotation=signature(func).return_annotation)
64
+
65
+ wrapped.__doc__ = generate_docstring(glyphclass, parameters, func.__doc__)
66
+
67
+ return wrapped
68
+
69
+ return decorator
70
+
71
+ #-----------------------------------------------------------------------------
72
+ # Private API
73
+ #-----------------------------------------------------------------------------
@@ -0,0 +1,86 @@
1
+ #-----------------------------------------------------------------------------
2
+ # Imports
3
+ #-----------------------------------------------------------------------------
4
+
5
+ # Standard library imports
6
+ from __future__ import annotations
7
+
8
+ # Other imports
9
+ from bokeh.models import Plot
10
+ from bokeh.plotting._figure import FigureOptions
11
+ from bokeh.plotting._plot import get_range
12
+ from bokeh.plotting._plot import get_scale
13
+ from bokeh.plotting._plot import process_axis_and_grid
14
+ from bokeh.plotting._tools import process_active_tools
15
+ from bokeh.plotting._tools import process_tools_arg
16
+
17
+ #-----------------------------------------------------------------------------
18
+ # Globals and constants
19
+ #-----------------------------------------------------------------------------
20
+
21
+ __all__ = ["Figure"]
22
+
23
+ #-----------------------------------------------------------------------------
24
+ # General API
25
+ #-----------------------------------------------------------------------------
26
+
27
+ class Figure(Plot):
28
+
29
+ def __init__(self, *arg, **kwargs) -> None:
30
+ opts = FigureOptions(kwargs)
31
+
32
+ names = self.properties()
33
+ for name in kwargs.keys():
34
+ if name not in names:
35
+ self._raise_attribute_error_with_matches(name, names | opts.properties())
36
+
37
+ super().__init__(*arg, **kwargs)
38
+
39
+ self.x_range = get_range(opts.x_range)
40
+ self.y_range = get_range(opts.y_range)
41
+
42
+ self.x_scale = get_scale(self.x_range, opts.x_axis_type)
43
+ self.y_scale = get_scale(self.y_range, opts.y_axis_type)
44
+
45
+ process_axis_and_grid(
46
+ self,
47
+ opts.x_axis_type,
48
+ opts.x_axis_location,
49
+ opts.x_minor_ticks,
50
+ opts.x_axis_label,
51
+ self.x_range,
52
+ 0,
53
+ )
54
+ process_axis_and_grid(
55
+ self,
56
+ opts.y_axis_type,
57
+ opts.y_axis_location,
58
+ opts.y_minor_ticks,
59
+ opts.y_axis_label,
60
+ self.y_range,
61
+ 1,
62
+ )
63
+
64
+ tool_objs, tool_map = process_tools_arg(self, opts.tools, opts.tooltips)
65
+ self.add_tools(*tool_objs)
66
+ process_active_tools(
67
+ self.toolbar,
68
+ tool_map,
69
+ opts.active_drag,
70
+ opts.active_inspect,
71
+ opts.active_scroll,
72
+ opts.active_tap,
73
+ opts.active_multi,
74
+ )
75
+
76
+ @property
77
+ def plot(self):
78
+ return self
79
+
80
+ @property
81
+ def coordinates(self):
82
+ return None
83
+
84
+ #-----------------------------------------------------------------------------
85
+ # Private API
86
+ #-----------------------------------------------------------------------------