tesorotools-python 0.0.49__tar.gz → 0.0.50__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.49 → tesorotools_python-0.0.50}/PKG-INFO +1 -1
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/__init__.py +13 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/artists/__init__.py +2 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/artists/line_plot.py +36 -7
- tesorotools_python-0.0.50/src/tesorotools/artists/shock_plot.py +333 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/artists/type_curve.py +13 -2
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/.gitignore +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/pyproject.toml +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/_build_context.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/_registry.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/artists/_common.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/artists/barh_plot.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/artists/compact.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/artists/matrix.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/artists/stacked.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/artists/vector_plot.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/artists/waterfall.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/assets/README.md +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/assets/fonts/README.md +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/assets/plots.yaml +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/assets/template.docx +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/assets/tesoro.mplstyle +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/data_sources/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/data_sources/debug.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/database/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/database/local.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/database/push.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/database/shared.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/dependencies/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/dependencies/node.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/dependencies/resolution.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/driver.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/manifest.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/offsets/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/offsets/offsets.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/offsets/outliers.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/orchestration.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/pipeline/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/pipeline/diagnose.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/pipeline/engine.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/pipeline/rules.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/providers/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/providers/base.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/providers/bde.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/providers/ecb.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/providers/imf_irfcl.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/providers/lseg.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/py.typed +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/render/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/render/content/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/render/content/content.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/render/content/images.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/render/content/section.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/render/content/subtitle.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/render/content/table.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/render/content/text.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/render/content/title.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/render/document.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/render/report.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/testing/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/testing/compare.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/utils/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/utils/config.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/utils/format.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/utils/globals.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/utils/matplotlib.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/utils/series.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/utils/shortcuts.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/utils/template.py +0 -0
|
@@ -17,8 +17,17 @@ plural variants). See ``docs/extending.md`` for the
|
|
|
17
17
|
recommended idioms.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
20
21
|
from typing import TYPE_CHECKING, Any
|
|
21
22
|
|
|
23
|
+
try:
|
|
24
|
+
#: Installed distribution version (set by hatch-vcs from the git tag).
|
|
25
|
+
#: Falls back to ``"0.0.0"`` when the package is not installed (e.g.
|
|
26
|
+
#: running straight from a source checkout with no metadata).
|
|
27
|
+
__version__ = version("tesorotools-python")
|
|
28
|
+
except PackageNotFoundError: # pragma: no cover - source-only checkout
|
|
29
|
+
__version__ = "0.0.0"
|
|
30
|
+
|
|
22
31
|
if TYPE_CHECKING:
|
|
23
32
|
from tesorotools.providers.bde import BdeProvider
|
|
24
33
|
from tesorotools.providers.ecb import EcbProvider
|
|
@@ -50,6 +59,7 @@ from tesorotools.artists import (
|
|
|
50
59
|
Legend,
|
|
51
60
|
LinePlot,
|
|
52
61
|
MatrixChart,
|
|
62
|
+
ShockChart,
|
|
53
63
|
StackedAreaPlot,
|
|
54
64
|
StackedBarPlot,
|
|
55
65
|
TypeCurve,
|
|
@@ -85,6 +95,7 @@ def _register_builtins() -> None:
|
|
|
85
95
|
register_artist("type_curve", TypeCurve)
|
|
86
96
|
register_artist("vector_plot", VectorPlot)
|
|
87
97
|
register_artist("matrix", MatrixChart)
|
|
98
|
+
register_artist("shock_plot", ShockChart)
|
|
88
99
|
register_artist("waterfall", Waterfall)
|
|
89
100
|
|
|
90
101
|
register_tag("format", Format)
|
|
@@ -103,6 +114,7 @@ _register_builtins()
|
|
|
103
114
|
|
|
104
115
|
|
|
105
116
|
__all__ = [
|
|
117
|
+
"__version__",
|
|
106
118
|
"Artist",
|
|
107
119
|
"BdeProvider",
|
|
108
120
|
"BuildContext",
|
|
@@ -122,6 +134,7 @@ __all__ = [
|
|
|
122
134
|
"RegistryProtocol",
|
|
123
135
|
"Report",
|
|
124
136
|
"Section",
|
|
137
|
+
"ShockChart",
|
|
125
138
|
"StackedAreaPlot",
|
|
126
139
|
"StackedBarPlot",
|
|
127
140
|
"Subtitle",
|
|
@@ -29,6 +29,7 @@ from tesorotools.artists._common import Artist, Format, Legend
|
|
|
29
29
|
from tesorotools.artists.barh_plot import GroupedBarChart, HorizontalBarChart
|
|
30
30
|
from tesorotools.artists.line_plot import LinePlot
|
|
31
31
|
from tesorotools.artists.matrix import MatrixChart
|
|
32
|
+
from tesorotools.artists.shock_plot import ShockChart
|
|
32
33
|
from tesorotools.artists.stacked import StackedAreaPlot, StackedBarPlot
|
|
33
34
|
from tesorotools.artists.type_curve import TypeCurve
|
|
34
35
|
from tesorotools.artists.vector_plot import VectorPlot
|
|
@@ -42,6 +43,7 @@ __all__ = [
|
|
|
42
43
|
"Legend",
|
|
43
44
|
"LinePlot",
|
|
44
45
|
"MatrixChart",
|
|
46
|
+
"ShockChart",
|
|
45
47
|
"StackedAreaPlot",
|
|
46
48
|
"StackedBarPlot",
|
|
47
49
|
"TypeCurve",
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/artists/line_plot.py
RENAMED
|
@@ -57,8 +57,16 @@ class LinePlot:
|
|
|
57
57
|
``plot_size``). Line-specific knobs:
|
|
58
58
|
|
|
59
59
|
* ``base_100`` / ``base_100_date`` -- rebase every series
|
|
60
|
-
to 100 at the first row, or at the
|
|
61
|
-
given date.
|
|
60
|
+
*multiplicatively* to 100 at the first row, or at the
|
|
61
|
+
row nearest the given date (the index form).
|
|
62
|
+
* ``base_0`` / ``base_0_date`` -- rebase every series
|
|
63
|
+
*subtractively* to 0: the plotted value is the change
|
|
64
|
+
since the anchor row (first row, or the row nearest the
|
|
65
|
+
given date), ``(value - anchor)``. ``scale`` still maps
|
|
66
|
+
the raw change to the display unit (e.g. ``scale: 100``
|
|
67
|
+
with ``units: "p.b."`` for the move in basis points
|
|
68
|
+
since an event). Mutually exclusive with ``base_100``;
|
|
69
|
+
pair with ``baseline: true`` to draw the zero line.
|
|
62
70
|
* ``annotate`` / ``annotate_color`` -- label the last
|
|
63
71
|
non-NaN value next to each line; with packing so
|
|
64
72
|
labels do not overlap.
|
|
@@ -118,6 +126,8 @@ class LinePlot:
|
|
|
118
126
|
end_date: datetime.datetime | None = None,
|
|
119
127
|
base_100: bool = False,
|
|
120
128
|
base_100_date: datetime.datetime | str | None = None,
|
|
129
|
+
base_0: bool = False,
|
|
130
|
+
base_0_date: datetime.datetime | str | None = None,
|
|
121
131
|
annotate: bool = False,
|
|
122
132
|
annotate_color: str | None = None,
|
|
123
133
|
baseline: bool = False,
|
|
@@ -136,6 +146,12 @@ class LinePlot:
|
|
|
136
146
|
) -> None:
|
|
137
147
|
if out_path.suffix != ".png":
|
|
138
148
|
raise ValueError(f"out_path must be .png: {out_path}")
|
|
149
|
+
if base_100 and base_0:
|
|
150
|
+
raise ValueError(
|
|
151
|
+
"base_100 and base_0 are mutually exclusive: a series is "
|
|
152
|
+
"either rebased multiplicatively (index) or subtractively "
|
|
153
|
+
"(change-since), not both"
|
|
154
|
+
)
|
|
139
155
|
self.out_path = out_path
|
|
140
156
|
self.data = resolve_data(data, data_path)
|
|
141
157
|
self.series = series
|
|
@@ -145,6 +161,8 @@ class LinePlot:
|
|
|
145
161
|
self.end_date = end_date
|
|
146
162
|
self.base_100 = base_100
|
|
147
163
|
self.base_100_date = base_100_date
|
|
164
|
+
self.base_0 = base_0
|
|
165
|
+
self.base_0_date = base_0_date
|
|
148
166
|
self.annotate = annotate
|
|
149
167
|
self.annotate_color = annotate_color
|
|
150
168
|
self.baseline = baseline
|
|
@@ -179,7 +197,8 @@ class LinePlot:
|
|
|
179
197
|
Optional keys (forwarded as-is to ``__init__``)
|
|
180
198
|
----------------------------------------------
|
|
181
199
|
``scale``, ``start_date``, ``end_date``, ``base_100``,
|
|
182
|
-
``base_100_date``, ``
|
|
200
|
+
``base_100_date``, ``base_0``, ``base_0_date``,
|
|
201
|
+
``annotate``, ``annotate_color``,
|
|
183
202
|
``baseline``, ``fmt`` (``!format``), ``legend``
|
|
184
203
|
(``!legend``), ``figsize``, ``series_styles``,
|
|
185
204
|
``plot_size``, ``vlines``, ``compact``, ``font_pt``,
|
|
@@ -236,11 +255,18 @@ class LinePlot:
|
|
|
236
255
|
|
|
237
256
|
plot_data = plot_data * self.scale
|
|
238
257
|
|
|
239
|
-
if self.base_100:
|
|
240
|
-
|
|
258
|
+
if self.base_100 or self.base_0:
|
|
259
|
+
# shared anchor lookup: the first row, or the row at the nearest
|
|
260
|
+
# index date to the requested anchor date. base_100 rebases
|
|
261
|
+
# multiplicatively (ratio -> index 100); base_0 subtractively
|
|
262
|
+
# (difference -> change since the anchor, baseline 0).
|
|
263
|
+
anchor_date = (
|
|
264
|
+
self.base_100_date if self.base_100 else self.base_0_date
|
|
265
|
+
)
|
|
266
|
+
if anchor_date is None:
|
|
241
267
|
anchor: pd.Series[float] = plot_data.iloc[0, :]
|
|
242
268
|
else:
|
|
243
|
-
anchor_ts: pd.Timestamp = pd.to_datetime(
|
|
269
|
+
anchor_ts: pd.Timestamp = pd.to_datetime(anchor_date)
|
|
244
270
|
idx_pos: int = self.data.index.get_indexer(
|
|
245
271
|
pd.Index([anchor_ts]), method="nearest"
|
|
246
272
|
)[0]
|
|
@@ -248,7 +274,10 @@ class LinePlot:
|
|
|
248
274
|
self.data.iloc[idx_pos, :].loc[list(self.series.keys())]
|
|
249
275
|
* self.scale
|
|
250
276
|
)
|
|
251
|
-
|
|
277
|
+
if self.base_100:
|
|
278
|
+
plot_data = plot_data / anchor * 100
|
|
279
|
+
else:
|
|
280
|
+
plot_data = plot_data - anchor
|
|
252
281
|
|
|
253
282
|
fig: Figure = plt.figure( # type: ignore[reportUnknownMemberType]
|
|
254
283
|
**compact_figure_kwargs(self.compact, self.figsize)
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""Per-shock aftermath chart artist, registered as ``!shock_plot``.
|
|
2
|
+
|
|
3
|
+
Plots **one** asset across **N** shock events, each window reindexed to
|
|
4
|
+
days-relative-to-the-event so the aftermaths overlay on a common x-axis
|
|
5
|
+
(0 = the shock day). Historical shocks draw in ``C2`` with rotating
|
|
6
|
+
linestyles; the most recent (last in dict order) is highlighted in
|
|
7
|
+
``C0`` (solid).
|
|
8
|
+
|
|
9
|
+
Two rebases, mirroring :class:`LinePlot`:
|
|
10
|
+
|
|
11
|
+
* ``base100`` (the default) -- normalise each window *multiplicatively*
|
|
12
|
+
to 100 at its anchor, so every aftermath reads as an index.
|
|
13
|
+
* ``base100: false`` -- the *subtractive* form: the plotted value is the
|
|
14
|
+
change since the anchor, ``(value - anchor)``, with ``scale`` mapping
|
|
15
|
+
the raw move to the display unit (e.g. ``scale: 100`` + ``units:
|
|
16
|
+
"p.b."`` for the move in basis points since the event). The 0 baseline
|
|
17
|
+
is drawn here.
|
|
18
|
+
|
|
19
|
+
Layout and styling come entirely from
|
|
20
|
+
:mod:`tesorotools.artists._common`, so the chart matches the other image
|
|
21
|
+
artists (``style_spines`` / ``style_baseline`` for the frame, the compact
|
|
22
|
+
toolkit for the two-per-row size, ``place_legend`` for legend packing).
|
|
23
|
+
|
|
24
|
+
Render lifecycle mirrors :class:`LinePlot`: ``build()`` returns
|
|
25
|
+
``(Figure, Axes)`` in memory; ``save(fig)`` persists to ``out_path``;
|
|
26
|
+
``plot()`` does both.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any, Self, cast
|
|
34
|
+
|
|
35
|
+
import matplotlib.pyplot as plt
|
|
36
|
+
import pandas as pd
|
|
37
|
+
from matplotlib.axes import Axes
|
|
38
|
+
from matplotlib.figure import Figure
|
|
39
|
+
from pandas.tseries.offsets import BDay
|
|
40
|
+
from yaml.nodes import MappingNode
|
|
41
|
+
|
|
42
|
+
from tesorotools.artists._common import (
|
|
43
|
+
AX_CONFIG,
|
|
44
|
+
Format,
|
|
45
|
+
Legend,
|
|
46
|
+
compact_figure_kwargs,
|
|
47
|
+
compact_font_pt,
|
|
48
|
+
dynamic_dpi,
|
|
49
|
+
place_legend,
|
|
50
|
+
resolve_data,
|
|
51
|
+
style_baseline,
|
|
52
|
+
style_compact_axes,
|
|
53
|
+
style_spines,
|
|
54
|
+
)
|
|
55
|
+
from tesorotools.utils.template import TemplateLoader
|
|
56
|
+
|
|
57
|
+
__all__ = ["ShockChart"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class _ShockEvent:
|
|
62
|
+
date: str
|
|
63
|
+
days_before: int
|
|
64
|
+
days_after: int
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _normalize_shocks(
|
|
68
|
+
shocks: dict[str, Any],
|
|
69
|
+
default_before: int,
|
|
70
|
+
default_after: int,
|
|
71
|
+
) -> dict[str, _ShockEvent]:
|
|
72
|
+
"""Translate the YAML ``shocks`` dict into per-event records.
|
|
73
|
+
|
|
74
|
+
Each entry maps a label to either the simple string form
|
|
75
|
+
(``"YYYY-MM-DD"``) -- in which case the chart-level ``shock_pre`` /
|
|
76
|
+
``shock_cooldown`` apply -- or the extended dict form
|
|
77
|
+
(``{date, days_before, days_after}``) for per-shock window overrides.
|
|
78
|
+
"""
|
|
79
|
+
out: dict[str, _ShockEvent] = {}
|
|
80
|
+
for name, spec in shocks.items():
|
|
81
|
+
if isinstance(spec, str):
|
|
82
|
+
out[name] = _ShockEvent(
|
|
83
|
+
date=spec,
|
|
84
|
+
days_before=default_before,
|
|
85
|
+
days_after=default_after,
|
|
86
|
+
)
|
|
87
|
+
elif isinstance(spec, dict):
|
|
88
|
+
spec_dict = cast("dict[str, Any]", spec)
|
|
89
|
+
if "date" not in spec_dict:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"Shock {name!r}: extended form requires a 'date' key"
|
|
92
|
+
)
|
|
93
|
+
out[name] = _ShockEvent(
|
|
94
|
+
date=str(spec_dict["date"]),
|
|
95
|
+
days_before=int(spec_dict.get("days_before", default_before)),
|
|
96
|
+
days_after=int(spec_dict.get("days_after", default_after)),
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
raise TypeError(
|
|
100
|
+
f"Shock {name!r}: spec must be a date string or a dict, "
|
|
101
|
+
f"got {type(spec).__name__}"
|
|
102
|
+
)
|
|
103
|
+
return out
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ShockChart:
|
|
107
|
+
"""One asset reindexed across N shocks; x-axis = days from event.
|
|
108
|
+
|
|
109
|
+
Parameters mirror the other image artists where they overlap
|
|
110
|
+
(``out_path``, ``data`` / ``data_path``, ``scale``, ``fmt``,
|
|
111
|
+
``legend``, ``figsize``, ``compact``). Shock-specific knobs:
|
|
112
|
+
|
|
113
|
+
* ``asset`` -- the single column of ``data`` to follow.
|
|
114
|
+
* ``shocks`` -- ``label -> spec`` mapping; each spec is either a
|
|
115
|
+
``"YYYY-MM-DD"`` string (using the chart-level window) or a
|
|
116
|
+
``{date, days_before, days_after}`` dict for per-shock overrides.
|
|
117
|
+
* ``shock_pre`` / ``shock_cooldown`` -- default window (business
|
|
118
|
+
days before / after the event) for the string-form shocks.
|
|
119
|
+
* ``base100`` -- ``True`` (default) rebases each window to 100
|
|
120
|
+
multiplicatively; ``False`` plots ``(value - anchor) * scale`` and
|
|
121
|
+
draws the 0 baseline (e.g. p.b. since the event).
|
|
122
|
+
* ``common_anchor`` -- rebase every window against one shock's anchor
|
|
123
|
+
value instead of each window's own anchor; must be a shock label.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def __init__(
|
|
127
|
+
self,
|
|
128
|
+
out_path: Path,
|
|
129
|
+
*,
|
|
130
|
+
data: pd.DataFrame | None = None,
|
|
131
|
+
data_path: Path | None = None,
|
|
132
|
+
asset: str,
|
|
133
|
+
shocks: dict[str, Any],
|
|
134
|
+
scale: float = 1,
|
|
135
|
+
shock_cooldown: int = 60,
|
|
136
|
+
shock_pre: int = 30,
|
|
137
|
+
base100: bool = True,
|
|
138
|
+
common_anchor: str | None = None,
|
|
139
|
+
fmt: Format | None = None,
|
|
140
|
+
legend: Legend | None = None,
|
|
141
|
+
figsize: tuple[float, float] | None = None,
|
|
142
|
+
compact: bool = False,
|
|
143
|
+
) -> None:
|
|
144
|
+
if out_path.suffix != ".png":
|
|
145
|
+
raise ValueError(f"out_path must be .png: {out_path}")
|
|
146
|
+
self.out_path = out_path
|
|
147
|
+
self.data = resolve_data(data, data_path)
|
|
148
|
+
self.asset = asset
|
|
149
|
+
self.scale = scale
|
|
150
|
+
self.shock_cooldown = shock_cooldown
|
|
151
|
+
self.shock_pre = shock_pre
|
|
152
|
+
self.base100 = base100
|
|
153
|
+
self.fmt = fmt or Format()
|
|
154
|
+
self.legend = legend
|
|
155
|
+
self.figsize = figsize
|
|
156
|
+
self.compact = compact
|
|
157
|
+
self._events = _normalize_shocks(shocks, shock_pre, shock_cooldown)
|
|
158
|
+
if common_anchor is not None and common_anchor not in self._events:
|
|
159
|
+
raise ValueError(
|
|
160
|
+
f"common_anchor {common_anchor!r} must match one of the "
|
|
161
|
+
f"shock labels: {list(self._events)}"
|
|
162
|
+
)
|
|
163
|
+
self.common_anchor = common_anchor
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def from_yaml(cls, loader: TemplateLoader, node: MappingNode) -> Self:
|
|
167
|
+
"""Build a :class:`ShockChart` from the ``!shock_plot`` YAML tag.
|
|
168
|
+
|
|
169
|
+
Required keys
|
|
170
|
+
-------------
|
|
171
|
+
``out_path``
|
|
172
|
+
``.png`` destination path.
|
|
173
|
+
``data_path``
|
|
174
|
+
``.feather`` file with daily observations
|
|
175
|
+
(DatetimeIndex x asset columns).
|
|
176
|
+
``asset``
|
|
177
|
+
column of the loaded DataFrame to follow.
|
|
178
|
+
``shocks``
|
|
179
|
+
``label -> spec`` mapping (string date or
|
|
180
|
+
``{date, days_before, days_after}`` dict).
|
|
181
|
+
|
|
182
|
+
Optional keys (forwarded as-is to ``__init__``)
|
|
183
|
+
----------------------------------------------
|
|
184
|
+
``scale``, ``shock_pre``, ``shock_cooldown``,
|
|
185
|
+
``base100``, ``common_anchor``, ``fmt`` (``!format``),
|
|
186
|
+
``legend`` (``!legend``), ``figsize``, ``compact``.
|
|
187
|
+
|
|
188
|
+
Example
|
|
189
|
+
-------
|
|
190
|
+
.. code-block:: yaml
|
|
191
|
+
|
|
192
|
+
ust_10y_shocks: !shock_plot
|
|
193
|
+
out_path: out/ust_shocks.png
|
|
194
|
+
data_path: data/rates.feather
|
|
195
|
+
asset: UST10Y
|
|
196
|
+
base100: false
|
|
197
|
+
scale: 100
|
|
198
|
+
fmt: !format {decimals: 0, units: "p.b."}
|
|
199
|
+
legend: !legend {}
|
|
200
|
+
shocks:
|
|
201
|
+
"Elecciones EE.UU.": "2024-11-05"
|
|
202
|
+
"Liberation Day": {date: "2025-04-02", days_after: 90}
|
|
203
|
+
"""
|
|
204
|
+
cfg: dict[str, Any] = loader.construct_mapping( # type: ignore[assignment]
|
|
205
|
+
node, deep=True
|
|
206
|
+
)
|
|
207
|
+
cfg.pop("id")
|
|
208
|
+
cfg["out_path"] = Path(cfg["out_path"])
|
|
209
|
+
if "data_path" in cfg:
|
|
210
|
+
cfg["data_path"] = Path(cfg["data_path"])
|
|
211
|
+
return cls(**cfg)
|
|
212
|
+
|
|
213
|
+
def build(self) -> tuple[Figure, Axes]:
|
|
214
|
+
"""Render the chart in memory; return ``(fig, ax)`` without saving."""
|
|
215
|
+
selected: pd.Series[float] = self.data.loc[:, self.asset] * self.scale
|
|
216
|
+
|
|
217
|
+
if self.common_anchor is not None:
|
|
218
|
+
anchor_value = selected.loc[self._events[self.common_anchor].date]
|
|
219
|
+
else:
|
|
220
|
+
anchor_value = None
|
|
221
|
+
|
|
222
|
+
new_data: list[pd.Series[float]] = []
|
|
223
|
+
for shock_name, ev in self._events.items():
|
|
224
|
+
shock_dt = pd.to_datetime(ev.date, format="%Y-%m-%d")
|
|
225
|
+
date_range = pd.bdate_range(
|
|
226
|
+
start=shock_dt - BDay(ev.days_before),
|
|
227
|
+
periods=ev.days_before + ev.days_after + 1,
|
|
228
|
+
)
|
|
229
|
+
aftermath = selected.reindex(date_range)
|
|
230
|
+
anchor = (
|
|
231
|
+
anchor_value
|
|
232
|
+
if anchor_value is not None
|
|
233
|
+
else aftermath.loc[ev.date]
|
|
234
|
+
)
|
|
235
|
+
# base100: multiplicative index (scale cancels). Otherwise the
|
|
236
|
+
# subtractive form -- the change since the anchor -- where scale
|
|
237
|
+
# maps the raw move to the display unit (e.g. x100 for p.b.).
|
|
238
|
+
if self.base100:
|
|
239
|
+
aftermath = aftermath / anchor * 100
|
|
240
|
+
else:
|
|
241
|
+
aftermath = aftermath - anchor
|
|
242
|
+
aftermath = aftermath.reset_index(drop=True)
|
|
243
|
+
aftermath.index = aftermath.index - ev.days_before
|
|
244
|
+
aftermath.name = shock_name
|
|
245
|
+
new_data.append(aftermath)
|
|
246
|
+
|
|
247
|
+
consolidated = pd.concat(new_data, axis=1)
|
|
248
|
+
linestyles = ["dotted", "dashed", "dashdot", "solid"]
|
|
249
|
+
|
|
250
|
+
font_pt = compact_font_pt(None) if self.compact else None
|
|
251
|
+
fig: Figure = plt.figure( # type: ignore[reportUnknownMemberType]
|
|
252
|
+
**compact_figure_kwargs(self.compact, self.figsize)
|
|
253
|
+
)
|
|
254
|
+
ax: Axes = fig.add_subplot()
|
|
255
|
+
|
|
256
|
+
# historical shocks in C2 with rotating linestyles; the most recent
|
|
257
|
+
# (last in dict order) highlighted in C0 solid.
|
|
258
|
+
for i, col in enumerate(consolidated.columns[:-1]):
|
|
259
|
+
ax.plot( # type: ignore[reportUnknownMemberType]
|
|
260
|
+
consolidated.index,
|
|
261
|
+
consolidated[col],
|
|
262
|
+
color="C2",
|
|
263
|
+
linestyle=linestyles[i % len(linestyles)],
|
|
264
|
+
label=col,
|
|
265
|
+
)
|
|
266
|
+
last_col = consolidated.columns[-1]
|
|
267
|
+
ax.plot( # type: ignore[reportUnknownMemberType]
|
|
268
|
+
consolidated.index,
|
|
269
|
+
consolidated[last_col],
|
|
270
|
+
color="C0",
|
|
271
|
+
label=last_col,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
ax.axvline( # type: ignore[reportUnknownMemberType]
|
|
275
|
+
x=0, color="gray", linestyle="solid", alpha=0.2, label="_nolabel_"
|
|
276
|
+
)
|
|
277
|
+
xmin = -max(ev.days_before for ev in self._events.values())
|
|
278
|
+
xmax = max(ev.days_after for ev in self._events.values())
|
|
279
|
+
ax.set_xlim(xmin=xmin, xmax=xmax)
|
|
280
|
+
ax.set_xlabel( # type: ignore[reportUnknownMemberType]
|
|
281
|
+
"Días hábiles tras el evento",
|
|
282
|
+
labelpad=6 if self.compact else 15,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
style_spines(
|
|
286
|
+
ax,
|
|
287
|
+
decimals=self.fmt.decimals,
|
|
288
|
+
units=self.fmt.units,
|
|
289
|
+
**AX_CONFIG["spines"],
|
|
290
|
+
)
|
|
291
|
+
# base100 anchors at 100; the subtractive form at 0.
|
|
292
|
+
style_baseline(
|
|
293
|
+
ax,
|
|
294
|
+
reference=100 if self.base100 else 0,
|
|
295
|
+
**AX_CONFIG["baseline"],
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if self.compact:
|
|
299
|
+
style_compact_axes(ax, font_pt)
|
|
300
|
+
|
|
301
|
+
if self.legend is not None:
|
|
302
|
+
place_legend(fig, ax, compact=self.compact, ncol=self.legend.ncol)
|
|
303
|
+
|
|
304
|
+
return fig, ax
|
|
305
|
+
|
|
306
|
+
def save(
|
|
307
|
+
self,
|
|
308
|
+
fig: Figure,
|
|
309
|
+
*,
|
|
310
|
+
path: Path | None = None,
|
|
311
|
+
dpi: int | None = None,
|
|
312
|
+
) -> Path:
|
|
313
|
+
"""Persist *fig* as a PNG and return the path written.
|
|
314
|
+
|
|
315
|
+
Mirrors :meth:`LinePlot.save`: defaults to ``self.out_path``;
|
|
316
|
+
``dpi`` overrides the save resolution, derived from the figure
|
|
317
|
+
size via :func:`dynamic_dpi` when omitted.
|
|
318
|
+
"""
|
|
319
|
+
target: Path = path if path is not None else self.out_path
|
|
320
|
+
save_dpi: int = dpi if dpi is not None else dynamic_dpi(fig)
|
|
321
|
+
fig.savefig( # type: ignore[reportUnknownMemberType]
|
|
322
|
+
target, dpi=save_dpi
|
|
323
|
+
)
|
|
324
|
+
return target
|
|
325
|
+
|
|
326
|
+
def plot(self) -> Axes:
|
|
327
|
+
"""Build the chart and persist it to ``self.out_path``."""
|
|
328
|
+
fig, ax = self.build()
|
|
329
|
+
try:
|
|
330
|
+
self.save(fig)
|
|
331
|
+
finally:
|
|
332
|
+
plt.close(fig)
|
|
333
|
+
return ax
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/artists/type_curve.py
RENAMED
|
@@ -120,6 +120,7 @@ class TypeCurve:
|
|
|
120
120
|
legend: Legend | None = None,
|
|
121
121
|
figsize: tuple[float, float] | None = None,
|
|
122
122
|
points_to_mark: list[str] | None = None,
|
|
123
|
+
baseline: bool = True,
|
|
123
124
|
compact: bool = False,
|
|
124
125
|
font_pt: float | None = None,
|
|
125
126
|
) -> None:
|
|
@@ -138,6 +139,7 @@ class TypeCurve:
|
|
|
138
139
|
self.legend = legend
|
|
139
140
|
self.figsize = figsize
|
|
140
141
|
self.points_to_mark = points_to_mark or []
|
|
142
|
+
self.baseline = baseline
|
|
141
143
|
self.compact = compact
|
|
142
144
|
self.font_pt = font_pt
|
|
143
145
|
|
|
@@ -165,7 +167,11 @@ class TypeCurve:
|
|
|
165
167
|
``scale``, ``fmt`` (``!format``), ``legend``
|
|
166
168
|
(``!legend``), ``figsize``, ``points_to_mark``
|
|
167
169
|
(list of ``due_period_code`` to mark on the current
|
|
168
|
-
line; codes must appear in ``series``), ``
|
|
170
|
+
line; codes must appear in ``series``), ``baseline``
|
|
171
|
+
(``True`` -- the default -- draws the 0 line and pins
|
|
172
|
+
the y-axis to 0, right for a spread curve; ``False``
|
|
173
|
+
skips it so the y-axis auto-frames an all-positive
|
|
174
|
+
yield/level curve), ``compact``
|
|
169
175
|
(fixed physical size for two charts per row; thins the
|
|
170
176
|
45-degree tenor labels to the marked points -- or every
|
|
171
177
|
other one -- so they do not collide on the narrow
|
|
@@ -284,7 +290,12 @@ class TypeCurve:
|
|
|
284
290
|
style_compact_axes(ax, font_pt, side="right")
|
|
285
291
|
else:
|
|
286
292
|
_rotate_xticks(ax)
|
|
287
|
-
|
|
293
|
+
# the 0 baseline both draws the zero line and extends the y-limits
|
|
294
|
+
# down to 0: right for a spread curve (centred on 0), wrong for an
|
|
295
|
+
# all-positive yield/level curve (it squashes the curve into the top
|
|
296
|
+
# half). baseline=False skips it so the y-axis auto-frames the data.
|
|
297
|
+
if self.baseline:
|
|
298
|
+
style_baseline(ax, 0, **AX_CONFIG["baseline"])
|
|
288
299
|
|
|
289
300
|
if self.compact:
|
|
290
301
|
legend_ncol = self.legend.ncol if self.legend else None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/artists/barh_plot.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/artists/vector_plot.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/artists/waterfall.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/assets/fonts/README.md
RENAMED
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/assets/template.docx
RENAMED
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/assets/tesoro.mplstyle
RENAMED
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/data_sources/__init__.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/data_sources/debug.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/database/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/dependencies/__init__.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/dependencies/node.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/dependencies/resolution.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/pipeline/__init__.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/pipeline/diagnose.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/providers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/providers/imf_irfcl.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/render/content/__init__.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/render/content/content.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/render/content/images.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/render/content/section.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/render/content/subtitle.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/render/content/table.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/render/content/text.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.50}/src/tesorotools/render/content/title.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|