tesorotools-python 0.0.24__tar.gz → 0.0.25__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.
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/.gitignore +7 -1
- tesorotools_python-0.0.25/PKG-INFO +18 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/pyproject.toml +15 -12
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/__init__.py +3 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/artists/barh_plot.py +0 -3
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/artists/line_plot.py +24 -13
- tesorotools_python-0.0.25/src/tesorotools/artists/stacked.py +264 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/artists/type_curve.py +8 -47
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/convert.py +1 -1
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/data_sources/lseg.py +26 -6
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/database/push.py +0 -11
- tesorotools_python-0.0.25/src/tesorotools/dependencies/node.py +48 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/dependencies/resolution.py +39 -17
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/offsets/offsets.py +69 -6
- tesorotools_python-0.0.25/src/tesorotools/offsets/outliers.py +25 -0
- tesorotools_python-0.0.25/src/tesorotools/pipeline/engine.py +77 -0
- tesorotools_python-0.0.25/src/tesorotools/pipeline/rules.py +153 -0
- tesorotools_python-0.0.25/src/tesorotools/providers/__init__.py +0 -0
- tesorotools_python-0.0.25/src/tesorotools/providers/base.py +72 -0
- tesorotools_python-0.0.25/src/tesorotools/providers/bde.py +267 -0
- tesorotools_python-0.0.25/src/tesorotools/py.typed +0 -0
- tesorotools_python-0.0.25/src/tesorotools/render/content/__init__.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/utils/series.py +4 -4
- tesorotools_python-0.0.24/PKG-INFO +0 -16
- tesorotools_python-0.0.24/src/tesorotools/dependencies/functions.py +0 -11
- tesorotools_python-0.0.24/src/tesorotools/dependencies/node.py +0 -35
- tesorotools_python-0.0.24/src/tesorotools/offsets/outliers.py +0 -15
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/artists/__init__.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/artists/barh.md +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/artists/table.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/README.md +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/fonts/README.md +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/plots.yaml +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/tesoro.mplstyle +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/data_sources/README.md +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/data_sources/__init__.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/data_sources/debug.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/database/__init__.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/database/local.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/dependencies/__init__.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/main.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/offsets/__init__.py +0 -0
- {tesorotools_python-0.0.24/src/tesorotools/render/content → tesorotools_python-0.0.25/src/tesorotools/pipeline}/__init__.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/render/__init__.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/render/content/content.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/render/content/images.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/render/content/section.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/render/content/subtitle.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/render/content/table.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/render/content/text.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/render/content/title.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/render/report.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/utils/__init__.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/utils/config.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/utils/format.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/utils/globals.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/utils/matplotlib.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/utils/shortcuts.py +0 -0
- {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/utils/template.py +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tesorotools-python
|
|
3
|
+
Version: 0.0.25
|
|
4
|
+
Requires-Python: >=3.13
|
|
5
|
+
Requires-Dist: babel>=2.17
|
|
6
|
+
Requires-Dist: eikon>=1.1
|
|
7
|
+
Requires-Dist: lseg-data>=2.0
|
|
8
|
+
Requires-Dist: matplotlib>=3.10
|
|
9
|
+
Requires-Dist: openpyxl>=3.1
|
|
10
|
+
Requires-Dist: pandas>=2.2
|
|
11
|
+
Requires-Dist: psycopg[binary]>=3.1
|
|
12
|
+
Requires-Dist: pyarrow>=18.0
|
|
13
|
+
Requires-Dist: python-docx>=1.1
|
|
14
|
+
Requires-Dist: pywin32>=311; sys_platform == 'win32'
|
|
15
|
+
Requires-Dist: pyyaml>=6.0
|
|
16
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
17
|
+
Provides-Extra: bde
|
|
18
|
+
Requires-Dist: requests>=2.31; extra == 'bde'
|
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tesorotools-python"
|
|
3
3
|
requires-python = ">=3.13"
|
|
4
|
-
version = "0.0.
|
|
4
|
+
version = "0.0.25"
|
|
5
5
|
dependencies = [
|
|
6
6
|
# database and ORM
|
|
7
|
-
"psycopg[binary]",
|
|
7
|
+
"psycopg[binary]>=3.1",
|
|
8
8
|
"SQLAlchemy>=2.0",
|
|
9
9
|
|
|
10
10
|
# data analysis
|
|
11
|
-
"pandas",
|
|
12
|
-
"pyarrow",
|
|
13
|
-
"openpyxl",
|
|
11
|
+
"pandas>=2.2",
|
|
12
|
+
"pyarrow>=18.0",
|
|
13
|
+
"openpyxl>=3.1",
|
|
14
14
|
|
|
15
15
|
# utils
|
|
16
|
-
"PyYAML",
|
|
17
|
-
"babel",
|
|
18
|
-
"eikon",
|
|
19
|
-
"lseg-data",
|
|
16
|
+
"PyYAML>=6.0",
|
|
17
|
+
"babel>=2.17",
|
|
18
|
+
"eikon>=1.1",
|
|
19
|
+
"lseg-data>=2.0",
|
|
20
20
|
|
|
21
21
|
# data visualization
|
|
22
|
-
"matplotlib",
|
|
23
|
-
"python-docx",
|
|
22
|
+
"matplotlib>=3.10",
|
|
23
|
+
"python-docx>=1.1",
|
|
24
24
|
|
|
25
25
|
# os dependencies
|
|
26
|
-
"pywin32>=311; sys_platform == 'win32'"
|
|
26
|
+
"pywin32>=311; sys_platform == 'win32'",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
bde = ["requests>=2.31"]
|
|
31
|
+
|
|
29
32
|
[dependency-groups]
|
|
30
33
|
dev = [
|
|
31
34
|
"ruff>=0.8",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from tesorotools.artists.line_plot import Format, Legend, LinePlot
|
|
2
|
+
from tesorotools.artists.stacked import StackedAreaPlot, StackedBarPlot
|
|
2
3
|
from tesorotools.utils.config import TemplateLoader
|
|
3
4
|
|
|
4
5
|
TemplateLoader.add_constructor("!line_plot", LinePlot.from_yaml)
|
|
5
6
|
TemplateLoader.add_constructor("!format", Format.from_yaml)
|
|
6
7
|
TemplateLoader.add_constructor("!legend", Legend.from_yaml)
|
|
8
|
+
TemplateLoader.add_constructor("!stacked_area", StackedAreaPlot.from_yaml)
|
|
9
|
+
TemplateLoader.add_constructor("!stacked_bar", StackedBarPlot.from_yaml)
|
{tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/artists/barh_plot.py
RENAMED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
# Añadir anotaciones de tipos, el objetivo sería eliminar todos los diccionarios con anotaciones del tipo dict[str, Any] que dicen más bien poco
|
|
4
|
-
# Tener cuidado para que nada de lo que está hecho con esta librería deje de funcionar
|
|
5
|
-
|
|
6
3
|
from enum import Enum
|
|
7
4
|
from pathlib import Path
|
|
8
5
|
from typing import Any, Self
|
{tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/artists/line_plot.py
RENAMED
|
@@ -26,7 +26,7 @@ AX_CONFIG: dict[str, Any] = PLOT_CONFIG["ax"]
|
|
|
26
26
|
FIG_CONFIG: dict[str, Any] = PLOT_CONFIG["figure"]
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def
|
|
29
|
+
def style_spines(
|
|
30
30
|
ax: Axes,
|
|
31
31
|
decimals: int,
|
|
32
32
|
units: str,
|
|
@@ -62,7 +62,7 @@ def _style_spines(
|
|
|
62
62
|
tick.set_markeredgecolor(color)
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
def
|
|
65
|
+
def style_baseline(
|
|
66
66
|
ax: Axes,
|
|
67
67
|
reference: float = 0,
|
|
68
68
|
**baseline_config: Any,
|
|
@@ -106,8 +106,8 @@ def plot_line_chart(
|
|
|
106
106
|
pass
|
|
107
107
|
|
|
108
108
|
reference = 100 if base_100 else 0
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
style_spines(ax, **format, **AX_CONFIG["spines"])
|
|
110
|
+
style_baseline(ax, reference, **AX_CONFIG["baseline"])
|
|
111
111
|
ax.legend( # type: ignore[reportUnknownMemberType]
|
|
112
112
|
loc="upper center",
|
|
113
113
|
bbox_to_anchor=(0.5, LINE_PLOT_CONFIG["legend_sep"]),
|
|
@@ -179,8 +179,8 @@ class LinePlot:
|
|
|
179
179
|
def __init__(
|
|
180
180
|
self,
|
|
181
181
|
out_path: Path,
|
|
182
|
-
data_path: Path,
|
|
183
|
-
series: dict[str, str],
|
|
182
|
+
data_path: Path | None = None,
|
|
183
|
+
series: dict[str, str] | None = None,
|
|
184
184
|
scale: float = 1,
|
|
185
185
|
start_date: datetime.datetime | None = None,
|
|
186
186
|
end_date: datetime.datetime | None = None,
|
|
@@ -189,17 +189,28 @@ class LinePlot:
|
|
|
189
189
|
baseline: bool = False,
|
|
190
190
|
format: Format | None = None,
|
|
191
191
|
legend: Legend | None = None,
|
|
192
|
+
data: pd.DataFrame | None = None,
|
|
192
193
|
) -> None:
|
|
193
194
|
|
|
194
195
|
if out_path.suffix != ".png":
|
|
195
196
|
raise ValueError(f"The out file {out_path} should be a .png file")
|
|
196
197
|
self.out_path = out_path
|
|
197
198
|
|
|
198
|
-
if data_path
|
|
199
|
-
raise ValueError(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
if data is not None and data_path is not None:
|
|
200
|
+
raise ValueError("Provide data or data_path, not both")
|
|
201
|
+
if data is not None:
|
|
202
|
+
self.data = data
|
|
203
|
+
elif data_path is not None:
|
|
204
|
+
if data_path.suffix != ".feather":
|
|
205
|
+
raise ValueError(
|
|
206
|
+
f"The data file {data_path} must be a .feather file"
|
|
207
|
+
)
|
|
208
|
+
self.data = pd.read_feather(data_path)
|
|
209
|
+
else:
|
|
210
|
+
raise ValueError("Provide data or data_path")
|
|
211
|
+
|
|
212
|
+
if series is None:
|
|
213
|
+
raise ValueError("series is required")
|
|
203
214
|
|
|
204
215
|
self.base_100 = base_100
|
|
205
216
|
self.annotate = annotate # unused for the moment
|
|
@@ -254,7 +265,7 @@ class LinePlot:
|
|
|
254
265
|
pass
|
|
255
266
|
|
|
256
267
|
assert self.format is not None
|
|
257
|
-
|
|
268
|
+
style_spines( # maybe make this function accept a Format object
|
|
258
269
|
ax,
|
|
259
270
|
decimals=self.format.decimals,
|
|
260
271
|
units=self.format.units,
|
|
@@ -262,7 +273,7 @@ class LinePlot:
|
|
|
262
273
|
)
|
|
263
274
|
if self.baseline:
|
|
264
275
|
reference = 100 if self.base_100 else 0
|
|
265
|
-
|
|
276
|
+
style_baseline(ax, reference, **AX_CONFIG["baseline"])
|
|
266
277
|
|
|
267
278
|
if self.legend is not None:
|
|
268
279
|
ax.legend( # type: ignore[reportUnknownMemberType]
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Self
|
|
5
|
+
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from matplotlib.axes import Axes
|
|
10
|
+
from matplotlib.figure import Figure
|
|
11
|
+
from yaml.nodes import MappingNode
|
|
12
|
+
|
|
13
|
+
from tesorotools.artists.line_plot import (
|
|
14
|
+
AX_CONFIG,
|
|
15
|
+
FIG_CONFIG,
|
|
16
|
+
Format,
|
|
17
|
+
Legend,
|
|
18
|
+
style_baseline,
|
|
19
|
+
style_spines,
|
|
20
|
+
)
|
|
21
|
+
from tesorotools.utils.config import TemplateLoader
|
|
22
|
+
|
|
23
|
+
_DEFAULT_NCOL = 5
|
|
24
|
+
_DEFAULT_SEP = -0.125
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StackedAreaPlot:
|
|
28
|
+
"""Stacked area chart with the tesorotools visual style.
|
|
29
|
+
|
|
30
|
+
Parameters match ``LinePlot`` where applicable so that
|
|
31
|
+
chart configs can switch between types by changing a
|
|
32
|
+
single field.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
out_path: Path,
|
|
38
|
+
data: pd.DataFrame,
|
|
39
|
+
series: dict[str, str],
|
|
40
|
+
*,
|
|
41
|
+
scale: float = 1,
|
|
42
|
+
start_date: str | None = None,
|
|
43
|
+
end_date: str | None = None,
|
|
44
|
+
baseline: bool = False,
|
|
45
|
+
format: Format | None = None,
|
|
46
|
+
legend: Legend | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
if out_path.suffix != ".png":
|
|
49
|
+
raise ValueError(f"out_path must be .png: {out_path}")
|
|
50
|
+
self.out_path = out_path
|
|
51
|
+
self.data = data
|
|
52
|
+
self.series = series
|
|
53
|
+
self.scale = scale
|
|
54
|
+
self.start_date = start_date
|
|
55
|
+
self.end_date = end_date
|
|
56
|
+
self.baseline = baseline
|
|
57
|
+
self.format = format or Format()
|
|
58
|
+
self.legend = legend
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_yaml(cls, loader: TemplateLoader, node: MappingNode) -> Self:
|
|
62
|
+
cfg: dict[str, Any] = loader.construct_mapping( # type: ignore[assignment]
|
|
63
|
+
node, deep=True
|
|
64
|
+
)
|
|
65
|
+
cfg.pop("id")
|
|
66
|
+
cfg["out_path"] = Path(cfg["out_path"])
|
|
67
|
+
cfg["data"] = pd.read_feather(cfg.pop("data_path"))
|
|
68
|
+
return cls(**cfg)
|
|
69
|
+
|
|
70
|
+
def plot(self) -> Axes:
|
|
71
|
+
start = (
|
|
72
|
+
pd.Timestamp(self.start_date)
|
|
73
|
+
if self.start_date
|
|
74
|
+
else self.data.index.min()
|
|
75
|
+
)
|
|
76
|
+
end = (
|
|
77
|
+
pd.Timestamp(self.end_date)
|
|
78
|
+
if self.end_date
|
|
79
|
+
else self.data.index.max()
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
plot_data = self.data.loc[start:end, list(self.series.keys())].dropna()
|
|
83
|
+
plot_data = plot_data * self.scale
|
|
84
|
+
|
|
85
|
+
fig: Figure = plt.figure( # type: ignore[reportUnknownMemberType]
|
|
86
|
+
**FIG_CONFIG
|
|
87
|
+
)
|
|
88
|
+
ax: Axes = fig.add_subplot()
|
|
89
|
+
|
|
90
|
+
labels = list(self.series.values())
|
|
91
|
+
arrays: list[np.ndarray[tuple[int], np.dtype[np.float64]]] = [
|
|
92
|
+
plot_data[col].to_numpy(dtype=np.float64) for col in self.series
|
|
93
|
+
]
|
|
94
|
+
ax.stackplot( # type: ignore[reportUnknownMemberType]
|
|
95
|
+
plot_data.index,
|
|
96
|
+
*arrays,
|
|
97
|
+
labels=labels,
|
|
98
|
+
alpha=0.85,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
style_spines(
|
|
102
|
+
ax,
|
|
103
|
+
decimals=self.format.decimals,
|
|
104
|
+
units=self.format.units,
|
|
105
|
+
**AX_CONFIG["spines"],
|
|
106
|
+
)
|
|
107
|
+
if self.baseline:
|
|
108
|
+
style_baseline(ax, 0, **AX_CONFIG["baseline"])
|
|
109
|
+
|
|
110
|
+
ncol = self.legend.ncol if self.legend else _DEFAULT_NCOL
|
|
111
|
+
sep = self.legend.sep if self.legend else _DEFAULT_SEP
|
|
112
|
+
ax.legend( # type: ignore[reportUnknownMemberType]
|
|
113
|
+
loc="upper center",
|
|
114
|
+
bbox_to_anchor=(0.5, sep),
|
|
115
|
+
ncol=ncol,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
fig.savefig( # type: ignore[reportUnknownMemberType]
|
|
119
|
+
self.out_path
|
|
120
|
+
)
|
|
121
|
+
plt.close(fig)
|
|
122
|
+
return ax
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class StackedBarPlot:
|
|
126
|
+
"""Stacked bar chart with the tesorotools visual style.
|
|
127
|
+
|
|
128
|
+
Positive and negative values are stacked separately so
|
|
129
|
+
that bars extend in both directions from the baseline.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def __init__(
|
|
133
|
+
self,
|
|
134
|
+
out_path: Path,
|
|
135
|
+
data: pd.DataFrame,
|
|
136
|
+
series: dict[str, str],
|
|
137
|
+
*,
|
|
138
|
+
scale: float = 1,
|
|
139
|
+
start_date: str | None = None,
|
|
140
|
+
end_date: str | None = None,
|
|
141
|
+
baseline: bool = True,
|
|
142
|
+
format: Format | None = None,
|
|
143
|
+
legend: Legend | None = None,
|
|
144
|
+
) -> None:
|
|
145
|
+
if out_path.suffix != ".png":
|
|
146
|
+
raise ValueError(f"out_path must be .png: {out_path}")
|
|
147
|
+
self.out_path = out_path
|
|
148
|
+
self.data = data
|
|
149
|
+
self.series = series
|
|
150
|
+
self.scale = scale
|
|
151
|
+
self.start_date = start_date
|
|
152
|
+
self.end_date = end_date
|
|
153
|
+
self.baseline = baseline
|
|
154
|
+
self.format = format or Format()
|
|
155
|
+
self.legend = legend
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def from_yaml(cls, loader: TemplateLoader, node: MappingNode) -> Self:
|
|
159
|
+
cfg: dict[str, Any] = loader.construct_mapping( # type: ignore[assignment]
|
|
160
|
+
node, deep=True
|
|
161
|
+
)
|
|
162
|
+
cfg.pop("id")
|
|
163
|
+
cfg["out_path"] = Path(cfg["out_path"])
|
|
164
|
+
cfg["data"] = pd.read_feather(cfg.pop("data_path"))
|
|
165
|
+
return cls(**cfg)
|
|
166
|
+
|
|
167
|
+
def plot(self) -> Axes:
|
|
168
|
+
start = (
|
|
169
|
+
pd.Timestamp(self.start_date)
|
|
170
|
+
if self.start_date
|
|
171
|
+
else self.data.index.min()
|
|
172
|
+
)
|
|
173
|
+
end = (
|
|
174
|
+
pd.Timestamp(self.end_date)
|
|
175
|
+
if self.end_date
|
|
176
|
+
else self.data.index.max()
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
plot_data = self.data.loc[start:end, list(self.series.keys())].dropna()
|
|
180
|
+
plot_data = plot_data * self.scale
|
|
181
|
+
|
|
182
|
+
fig: Figure = plt.figure( # type: ignore[reportUnknownMemberType]
|
|
183
|
+
figsize=(12, 6), **FIG_CONFIG
|
|
184
|
+
)
|
|
185
|
+
ax: Axes = fig.add_subplot()
|
|
186
|
+
|
|
187
|
+
cols = list(self.series.keys())
|
|
188
|
+
labels = list(self.series.values())
|
|
189
|
+
|
|
190
|
+
x = np.arange(len(plot_data))
|
|
191
|
+
bar_width = 0.7
|
|
192
|
+
|
|
193
|
+
pos_bottom: np.ndarray[tuple[int], np.dtype[np.float64]] = np.zeros(
|
|
194
|
+
len(plot_data)
|
|
195
|
+
)
|
|
196
|
+
neg_bottom: np.ndarray[tuple[int], np.dtype[np.float64]] = np.zeros(
|
|
197
|
+
len(plot_data)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
for col, label in zip(cols, labels):
|
|
201
|
+
values: np.ndarray[tuple[int], np.dtype[np.float64]] = plot_data[
|
|
202
|
+
col
|
|
203
|
+
].to_numpy(dtype=np.float64)
|
|
204
|
+
pos: np.ndarray[tuple[int], np.dtype[np.float64]] = np.where(
|
|
205
|
+
values >= 0, values, 0.0
|
|
206
|
+
)
|
|
207
|
+
neg: np.ndarray[tuple[int], np.dtype[np.float64]] = np.where(
|
|
208
|
+
values < 0, values, 0.0
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
color = (
|
|
212
|
+
ax.bar( # type: ignore[reportUnknownMemberType]
|
|
213
|
+
x,
|
|
214
|
+
pos,
|
|
215
|
+
bottom=pos_bottom,
|
|
216
|
+
width=bar_width,
|
|
217
|
+
label=label,
|
|
218
|
+
)
|
|
219
|
+
.patches[0]
|
|
220
|
+
.get_facecolor()
|
|
221
|
+
)
|
|
222
|
+
ax.bar( # type: ignore[reportUnknownMemberType]
|
|
223
|
+
x,
|
|
224
|
+
neg,
|
|
225
|
+
bottom=neg_bottom,
|
|
226
|
+
width=bar_width,
|
|
227
|
+
color=color,
|
|
228
|
+
)
|
|
229
|
+
pos_bottom = pos_bottom + pos
|
|
230
|
+
neg_bottom = neg_bottom + neg
|
|
231
|
+
|
|
232
|
+
dates = plot_data.index
|
|
233
|
+
step = max(1, len(dates) // 12)
|
|
234
|
+
tick_pos = list(range(0, len(dates), step))
|
|
235
|
+
tick_labels = [dates[i].strftime("%Y") for i in tick_pos]
|
|
236
|
+
ax.set_xticks( # type: ignore[reportUnknownMemberType]
|
|
237
|
+
tick_pos
|
|
238
|
+
)
|
|
239
|
+
ax.set_xticklabels( # type: ignore[reportUnknownMemberType]
|
|
240
|
+
tick_labels
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
style_spines(
|
|
244
|
+
ax,
|
|
245
|
+
decimals=self.format.decimals,
|
|
246
|
+
units=self.format.units,
|
|
247
|
+
**AX_CONFIG["spines"],
|
|
248
|
+
)
|
|
249
|
+
if self.baseline:
|
|
250
|
+
style_baseline(ax, 0, **AX_CONFIG["baseline"])
|
|
251
|
+
|
|
252
|
+
ncol = self.legend.ncol if self.legend else _DEFAULT_NCOL
|
|
253
|
+
sep = self.legend.sep if self.legend else _DEFAULT_SEP
|
|
254
|
+
ax.legend( # type: ignore[reportUnknownMemberType]
|
|
255
|
+
loc="upper center",
|
|
256
|
+
bbox_to_anchor=(0.5, sep),
|
|
257
|
+
ncol=ncol,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
fig.savefig( # type: ignore[reportUnknownMemberType]
|
|
261
|
+
self.out_path
|
|
262
|
+
)
|
|
263
|
+
plt.close(fig)
|
|
264
|
+
return ax
|
{tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/artists/type_curve.py
RENAMED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
# pending to assure stylesheet data and fonts are only loaded once
|
|
4
|
-
|
|
5
3
|
from pathlib import Path
|
|
6
4
|
from typing import Any
|
|
7
5
|
|
|
@@ -9,9 +7,12 @@ import matplotlib.pyplot as plt
|
|
|
9
7
|
import pandas as pd
|
|
10
8
|
from matplotlib.axes import Axes
|
|
11
9
|
from matplotlib.figure import Figure
|
|
12
|
-
from matplotlib.ticker import FuncFormatter
|
|
13
10
|
from pandas import Timestamp
|
|
14
11
|
|
|
12
|
+
from tesorotools.artists.line_plot import (
|
|
13
|
+
style_baseline,
|
|
14
|
+
style_spines,
|
|
15
|
+
)
|
|
15
16
|
from tesorotools.utils.config import merge
|
|
16
17
|
from tesorotools.utils.matplotlib import (
|
|
17
18
|
PLOT_CONFIG,
|
|
@@ -26,54 +27,13 @@ FIG_CONFIG: dict[str, Any] = PLOT_CONFIG["figure"]
|
|
|
26
27
|
load_fonts()
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
def
|
|
30
|
-
ax: Axes,
|
|
31
|
-
decimals: int,
|
|
32
|
-
units: str,
|
|
33
|
-
*,
|
|
34
|
-
color: str,
|
|
35
|
-
linewidth: float,
|
|
36
|
-
) -> None:
|
|
37
|
-
ax.grid( # type: ignore[reportUnknownMemberType]
|
|
38
|
-
visible=True, axis="y"
|
|
39
|
-
)
|
|
40
|
-
for spine in ax.spines.values():
|
|
41
|
-
spine.set_color(color)
|
|
42
|
-
spine.set_linewidth(linewidth)
|
|
43
|
-
ax.yaxis.tick_right()
|
|
44
|
-
|
|
45
|
-
def _fmt_tick(y: float, _pos: int) -> str:
|
|
46
|
-
return format_annotation(y, decimals, units)
|
|
47
|
-
|
|
48
|
-
ax.yaxis.set_major_formatter(FuncFormatter(_fmt_tick))
|
|
49
|
-
ax.tick_params( # type: ignore[reportUnknownMemberType]
|
|
50
|
-
axis="both", which="major"
|
|
51
|
-
)
|
|
30
|
+
def _rotate_xticks(ax: Axes) -> None:
|
|
52
31
|
ax.set_xticks( # type: ignore[reportUnknownMemberType]
|
|
53
32
|
ax.get_xticks(),
|
|
54
33
|
ax.get_xticklabels(), # type: ignore[reportArgumentType]
|
|
55
34
|
rotation=45,
|
|
56
35
|
ha="right",
|
|
57
36
|
)
|
|
58
|
-
for tick in ax.get_xticklines():
|
|
59
|
-
tick.set_markeredgecolor(color)
|
|
60
|
-
for tick in ax.get_yticklines():
|
|
61
|
-
tick.set_markeredgecolor(color)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _style_baseline(ax: Axes, **baseline_config: Any) -> None:
|
|
65
|
-
color: str = baseline_config["color"]
|
|
66
|
-
bottom_lim, top_lim = ax.get_ylim()
|
|
67
|
-
ax.set_ylim(bottom=min(0, bottom_lim), top=max(0, top_lim))
|
|
68
|
-
bottom_lim, top_lim = ax.get_ylim()
|
|
69
|
-
if bottom_lim == 0:
|
|
70
|
-
ax.spines["bottom"].set_edgecolor(color)
|
|
71
|
-
elif top_lim == 0:
|
|
72
|
-
ax.spines["top"].set_edgecolor(color)
|
|
73
|
-
else:
|
|
74
|
-
ax.axhline( # type: ignore[reportUnknownMemberType]
|
|
75
|
-
y=0, **baseline_config
|
|
76
|
-
)
|
|
77
37
|
|
|
78
38
|
|
|
79
39
|
def _format_data(data: pd.DataFrame) -> dict[str, Any]:
|
|
@@ -204,8 +164,9 @@ def plot_type_curve(
|
|
|
204
164
|
formatted_assets["current_date"],
|
|
205
165
|
**merged_config["line"],
|
|
206
166
|
)
|
|
207
|
-
|
|
208
|
-
|
|
167
|
+
style_spines(ax, **merged_config["yaxis"], **AX_CONFIG["spines"])
|
|
168
|
+
_rotate_xticks(ax)
|
|
169
|
+
style_baseline(ax, 0, **AX_CONFIG["baseline"])
|
|
209
170
|
ax.legend( # type: ignore[reportUnknownMemberType]
|
|
210
171
|
loc="upper center",
|
|
211
172
|
bbox_to_anchor=(0.5, merged_config["legend_sep"]),
|
|
@@ -90,7 +90,7 @@ if __name__ == "__main__":
|
|
|
90
90
|
]
|
|
91
91
|
independent_trimmed_df: pd.DataFrame = trim(independent_full_df) # type: ignore[assignment]
|
|
92
92
|
dependent_trimmed_df: pd.DataFrame = compute_derivate_series(
|
|
93
|
-
resolved_dict["
|
|
93
|
+
resolved_dict["rules"], # type: ignore[arg-type]
|
|
94
94
|
independent_trimmed_df,
|
|
95
95
|
)
|
|
96
96
|
offsets_config: Any = read_config(Path("examples") / "offsets.yaml")
|
{tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/data_sources/lseg.py
RENAMED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import time
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
import lseg.data as ld
|
|
5
6
|
import pandas as pd
|
|
6
7
|
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
MAX_RETRIES = 150
|
|
11
|
+
|
|
7
12
|
|
|
8
13
|
def get_series(
|
|
9
14
|
api_key: str,
|
|
@@ -16,7 +21,7 @@ def get_series(
|
|
|
16
21
|
datapoint_limit: int = 2_000,
|
|
17
22
|
cache_path: Path | None = None,
|
|
18
23
|
) -> pd.DataFrame:
|
|
19
|
-
"""Downloads data from LSEG given
|
|
24
|
+
"""Downloads data from LSEG given a valid API key."""
|
|
20
25
|
ld.open_session(app_key=api_key)
|
|
21
26
|
fields = ["TIMESTAMP", "CLOSE"] if fields is None else fields
|
|
22
27
|
|
|
@@ -53,7 +58,10 @@ def get_series(
|
|
|
53
58
|
if cache_file_path is None:
|
|
54
59
|
partial_data.append(data)
|
|
55
60
|
if downloaded_dates + download_step < len(dates_list):
|
|
56
|
-
|
|
61
|
+
logger.info(
|
|
62
|
+
"Waiting %d seconds for LSEG cooldown...",
|
|
63
|
+
cooldown,
|
|
64
|
+
)
|
|
57
65
|
time.sleep(cooldown)
|
|
58
66
|
downloaded_dates += download_step
|
|
59
67
|
return data
|
|
@@ -67,10 +75,11 @@ def block_download(
|
|
|
67
75
|
fields: list[str] | None = None,
|
|
68
76
|
cooldown: int = 60,
|
|
69
77
|
file_path: Path | None = None,
|
|
78
|
+
max_retries: int = MAX_RETRIES,
|
|
70
79
|
) -> pd.DataFrame:
|
|
71
80
|
interval = "daily" if freq == "B" else freq
|
|
72
81
|
|
|
73
|
-
|
|
82
|
+
for attempt in range(1, max_retries + 1):
|
|
74
83
|
try:
|
|
75
84
|
data: pd.DataFrame | None = ld.get_history( # type: ignore[reportUnknownMemberType]
|
|
76
85
|
universe=series_id_list,
|
|
@@ -92,11 +101,22 @@ def block_download(
|
|
|
92
101
|
data.to_csv(file_path)
|
|
93
102
|
return data
|
|
94
103
|
except ld.errors.LDError as e:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
logger.warning(
|
|
105
|
+
"LSEG error (attempt %d/%d): %s",
|
|
106
|
+
attempt,
|
|
107
|
+
max_retries,
|
|
108
|
+
e,
|
|
109
|
+
)
|
|
110
|
+
if attempt == max_retries:
|
|
111
|
+
raise
|
|
112
|
+
logger.info(
|
|
113
|
+
"Waiting %d seconds for LSEG cooldown...",
|
|
114
|
+
cooldown,
|
|
115
|
+
)
|
|
98
116
|
time.sleep(cooldown)
|
|
99
117
|
|
|
118
|
+
raise RuntimeError("Unreachable")
|
|
119
|
+
|
|
100
120
|
|
|
101
121
|
def concat_partial_data(
|
|
102
122
|
cache_path: Path | None,
|
|
@@ -44,14 +44,9 @@ def flash_to_database_format(data: pd.DataFrame) -> pd.DataFrame:
|
|
|
44
44
|
columns=["offset", "difference_type", "stat"]
|
|
45
45
|
)
|
|
46
46
|
database_data["value_meta"] = pd.NA
|
|
47
|
-
print(database_data)
|
|
48
47
|
return database_data
|
|
49
48
|
|
|
50
49
|
|
|
51
|
-
def database_to_flash_format() -> None:
|
|
52
|
-
pass
|
|
53
|
-
|
|
54
|
-
|
|
55
50
|
def push_to_database(data: pd.DataFrame, conn_string: str, table: str) -> None:
|
|
56
51
|
engine: Any = create_engine(url=conn_string)
|
|
57
52
|
data.to_sql(
|
|
@@ -61,9 +56,3 @@ def push_to_database(data: pd.DataFrame, conn_string: str, table: str) -> None:
|
|
|
61
56
|
chunksize=1000,
|
|
62
57
|
index=False,
|
|
63
58
|
)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def pull_from_database(
|
|
67
|
-
conn_string: str, start: str, end: str, series: list[str]
|
|
68
|
-
) -> None:
|
|
69
|
-
_ = conn_string, start, end, series
|