maidr 1.9.0__py3-none-any.whl → 1.11.0__py3-none-any.whl
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.
- maidr/__init__.py +1 -1
- maidr/core/enum/maidr_key.py +1 -0
- maidr/core/plot/candlestick.py +73 -104
- maidr/core/plot/maidr_plot.py +11 -2
- maidr/core/plot/mplfinance_barplot.py +5 -1
- maidr/core/plot/mplfinance_lineplot.py +5 -1
- maidr/util/datetime_conversion.py +8 -18
- maidr/util/format_config.py +679 -0
- maidr/util/mixin/__init__.py +11 -0
- maidr/util/mixin/format_extractor_mixin.py +83 -0
- maidr/util/mplfinance_utils.py +5 -303
- {maidr-1.9.0.dist-info → maidr-1.11.0.dist-info}/METADATA +1 -1
- {maidr-1.9.0.dist-info → maidr-1.11.0.dist-info}/RECORD +15 -13
- {maidr-1.9.0.dist-info → maidr-1.11.0.dist-info}/WHEEL +1 -1
- {maidr-1.9.0.dist-info → maidr-1.11.0.dist-info}/licenses/LICENSE +0 -0
maidr/__init__.py
CHANGED
maidr/core/enum/maidr_key.py
CHANGED
maidr/core/plot/candlestick.py
CHANGED
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import uuid
|
|
4
3
|
from typing import Union, Dict
|
|
5
4
|
from matplotlib.axes import Axes
|
|
6
|
-
|
|
7
|
-
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
8
6
|
|
|
9
7
|
from maidr.core.enum import PlotType
|
|
10
8
|
from maidr.core.plot import MaidrPlot
|
|
11
9
|
from maidr.core.enum.maidr_key import MaidrKey
|
|
12
10
|
from maidr.exception import ExtractionError
|
|
13
|
-
from maidr.util.mplfinance_utils import MplfinanceDataExtractor
|
|
14
11
|
|
|
15
12
|
|
|
16
13
|
class CandlestickPlot(MaidrPlot):
|
|
17
14
|
"""
|
|
18
15
|
Specialized candlestick plot class for mplfinance OHLC data.
|
|
19
16
|
|
|
20
|
-
This class
|
|
21
|
-
|
|
17
|
+
This class extracts candlestick data directly from the original DataFrame
|
|
18
|
+
without any formatting or transformation.
|
|
22
19
|
"""
|
|
23
20
|
|
|
24
21
|
def __init__(self, axes: list[Axes], **kwargs) -> None:
|
|
@@ -34,133 +31,106 @@ class CandlestickPlot(MaidrPlot):
|
|
|
34
31
|
Additional keyword arguments.
|
|
35
32
|
"""
|
|
36
33
|
self.axes = axes
|
|
37
|
-
# Ensure there's at least one axis for the superclass init
|
|
38
34
|
if not axes:
|
|
39
35
|
raise ValueError("Axes list cannot be empty.")
|
|
40
36
|
super().__init__(axes[0], PlotType.CANDLESTICK)
|
|
41
37
|
|
|
42
|
-
# Store
|
|
38
|
+
# Store collections passed from mplfinance patch
|
|
43
39
|
self._maidr_wick_collection = kwargs.get("_maidr_wick_collection", None)
|
|
44
40
|
self._maidr_body_collection = kwargs.get("_maidr_body_collection", None)
|
|
45
|
-
self.
|
|
46
|
-
self._maidr_original_data = kwargs.get(
|
|
47
|
-
"_maidr_original_data", None
|
|
48
|
-
) # Store original data
|
|
49
|
-
self._maidr_datetime_converter = kwargs.get("_maidr_datetime_converter", None)
|
|
41
|
+
self._maidr_original_data = kwargs.get("_maidr_original_data", None)
|
|
50
42
|
|
|
51
|
-
# Store the GID for
|
|
43
|
+
# Store the GID for selector generation
|
|
52
44
|
self._maidr_gid = None
|
|
53
|
-
# Modern-path separate gids for body and wick
|
|
54
45
|
self._maidr_body_gid = None
|
|
55
46
|
self._maidr_wick_gid = None
|
|
56
47
|
if self._maidr_body_collection:
|
|
57
48
|
self._maidr_gid = self._maidr_body_collection.get_gid()
|
|
58
49
|
self._maidr_body_gid = self._maidr_gid
|
|
59
|
-
|
|
60
|
-
self.
|
|
61
|
-
|
|
50
|
+
if self._maidr_wick_collection:
|
|
51
|
+
self._maidr_wick_gid = self._maidr_wick_collection.get_gid()
|
|
52
|
+
if not self._maidr_gid:
|
|
53
|
+
self._maidr_gid = self._maidr_wick_gid
|
|
62
54
|
|
|
63
55
|
def _extract_plot_data(self) -> list[dict]:
|
|
64
56
|
"""
|
|
65
|
-
Extract candlestick data from the
|
|
66
|
-
|
|
67
|
-
This method processes candlestick plots from both modern (mplfinance.plot) and
|
|
68
|
-
legacy (original_flavor) pipelines, extracting OHLC data and setting up
|
|
69
|
-
highlighting elements and GIDs.
|
|
57
|
+
Extract candlestick data directly from the original DataFrame.
|
|
70
58
|
|
|
71
59
|
Returns
|
|
72
60
|
-------
|
|
73
61
|
list[dict]
|
|
74
62
|
List of dictionaries containing candlestick data with keys:
|
|
75
|
-
- 'value': Date string
|
|
63
|
+
- 'value': Date string (raw from DataFrame index)
|
|
76
64
|
- 'open': Opening price (float)
|
|
77
65
|
- 'high': High price (float)
|
|
78
66
|
- 'low': Low price (float)
|
|
79
67
|
- 'close': Closing price (float)
|
|
80
|
-
- 'volume': Volume (float
|
|
68
|
+
- 'volume': Volume (float)
|
|
81
69
|
"""
|
|
82
|
-
|
|
83
|
-
# Get the custom collections from kwargs
|
|
84
70
|
body_collection = self._maidr_body_collection
|
|
85
71
|
wick_collection = self._maidr_wick_collection
|
|
86
72
|
|
|
87
73
|
if body_collection and wick_collection:
|
|
88
|
-
# Store the GIDs from the collections
|
|
74
|
+
# Store the GIDs from the collections
|
|
89
75
|
self._maidr_body_gid = body_collection.get_gid()
|
|
90
76
|
self._maidr_wick_gid = wick_collection.get_gid()
|
|
91
|
-
# Keep legacy gid filled for backward compatibility
|
|
92
77
|
self._maidr_gid = self._maidr_body_gid or self._maidr_wick_gid
|
|
93
78
|
|
|
94
79
|
# Use the original collections for highlighting
|
|
95
80
|
self._elements = [body_collection, wick_collection]
|
|
96
81
|
|
|
97
|
-
#
|
|
98
|
-
if self.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
)
|
|
102
|
-
return data
|
|
103
|
-
|
|
104
|
-
# Fallback to original detection method
|
|
105
|
-
if not self.axes:
|
|
106
|
-
return []
|
|
107
|
-
|
|
108
|
-
ax_ohlc = self.axes[0]
|
|
109
|
-
|
|
110
|
-
# Look for Rectangle patches (original_flavor candlestick)
|
|
111
|
-
body_rectangles = []
|
|
112
|
-
for patch in ax_ohlc.patches:
|
|
113
|
-
if isinstance(patch, Rectangle):
|
|
114
|
-
body_rectangles.append(patch)
|
|
115
|
-
|
|
116
|
-
if body_rectangles:
|
|
117
|
-
# Set elements for highlighting
|
|
118
|
-
self._elements = body_rectangles
|
|
119
|
-
|
|
120
|
-
# Generate a GID for highlighting if none exists
|
|
121
|
-
if not self._maidr_gid:
|
|
122
|
-
self._maidr_gid = f"maidr-{uuid.uuid4()}"
|
|
123
|
-
# Set GID on all rectangles
|
|
124
|
-
for rect in body_rectangles:
|
|
125
|
-
rect.set_gid(self._maidr_gid)
|
|
126
|
-
# Keep a dedicated body gid for legacy dict selectors
|
|
127
|
-
self._maidr_body_gid = (
|
|
128
|
-
getattr(self, "_maidr_body_gid", None) or self._maidr_gid
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
# Assign a shared gid to wick Line2D (vertical 2-point lines) on the same axis
|
|
132
|
-
wick_lines = []
|
|
133
|
-
for line in ax_ohlc.get_lines():
|
|
134
|
-
try:
|
|
135
|
-
xydata = line.get_xydata()
|
|
136
|
-
if xydata is None:
|
|
137
|
-
continue
|
|
138
|
-
xy_arr = np.asarray(xydata)
|
|
139
|
-
if (
|
|
140
|
-
xy_arr.ndim == 2
|
|
141
|
-
and xy_arr.shape[0] == 2
|
|
142
|
-
and xy_arr.shape[1] >= 2
|
|
143
|
-
):
|
|
144
|
-
x0 = float(xy_arr[0, 0])
|
|
145
|
-
x1 = float(xy_arr[1, 0])
|
|
146
|
-
if abs(x0 - x1) < 1e-10:
|
|
147
|
-
wick_lines.append(line)
|
|
148
|
-
except Exception:
|
|
149
|
-
continue
|
|
150
|
-
if wick_lines:
|
|
151
|
-
if not getattr(self, "_maidr_wick_gid", None):
|
|
152
|
-
self._maidr_wick_gid = f"maidr-{uuid.uuid4()}"
|
|
153
|
-
for line in wick_lines:
|
|
154
|
-
line.set_gid(self._maidr_wick_gid)
|
|
155
|
-
|
|
156
|
-
# Use the utility class to extract data
|
|
157
|
-
data = MplfinanceDataExtractor.extract_rectangle_candlestick_data(
|
|
158
|
-
body_rectangles, self._maidr_date_nums, self._maidr_original_data
|
|
159
|
-
)
|
|
160
|
-
return data
|
|
82
|
+
# Extract data directly from DataFrame
|
|
83
|
+
if self._maidr_original_data is not None and isinstance(
|
|
84
|
+
self._maidr_original_data, pd.DataFrame
|
|
85
|
+
):
|
|
86
|
+
return self._extract_from_dataframe(self._maidr_original_data)
|
|
161
87
|
|
|
162
88
|
return []
|
|
163
89
|
|
|
90
|
+
def _extract_from_dataframe(self, df: pd.DataFrame) -> list[dict]:
|
|
91
|
+
"""
|
|
92
|
+
Extract candlestick data directly from DataFrame without any formatting.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
df : pd.DataFrame
|
|
97
|
+
DataFrame with OHLC data and DatetimeIndex.
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
list[dict]
|
|
102
|
+
List of candlestick data dictionaries with raw values.
|
|
103
|
+
"""
|
|
104
|
+
candles = []
|
|
105
|
+
|
|
106
|
+
for i in range(len(df)):
|
|
107
|
+
try:
|
|
108
|
+
# Get date directly from index - raw representation
|
|
109
|
+
date_value = str(df.index[i])
|
|
110
|
+
|
|
111
|
+
# Get OHLC values directly from DataFrame columns
|
|
112
|
+
open_price = float(df.iloc[i]["Open"])
|
|
113
|
+
high_price = float(df.iloc[i]["High"])
|
|
114
|
+
low_price = float(df.iloc[i]["Low"])
|
|
115
|
+
close_price = float(df.iloc[i]["Close"])
|
|
116
|
+
|
|
117
|
+
# Get volume if available, otherwise 0
|
|
118
|
+
volume = float(df.iloc[i].get("Volume", 0.0))
|
|
119
|
+
|
|
120
|
+
candle_data = {
|
|
121
|
+
"value": date_value,
|
|
122
|
+
"open": open_price,
|
|
123
|
+
"high": high_price,
|
|
124
|
+
"low": low_price,
|
|
125
|
+
"close": close_price,
|
|
126
|
+
"volume": volume,
|
|
127
|
+
}
|
|
128
|
+
candles.append(candle_data)
|
|
129
|
+
except (KeyError, IndexError, ValueError, TypeError):
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
return candles
|
|
133
|
+
|
|
164
134
|
def _extract_axes_data(self) -> dict:
|
|
165
135
|
"""
|
|
166
136
|
Extract the plot's axes data including labels.
|
|
@@ -178,11 +148,7 @@ class CandlestickPlot(MaidrPlot):
|
|
|
178
148
|
return {MaidrKey.X: x_labels, MaidrKey.Y: self.ax.get_ylabel()}
|
|
179
149
|
|
|
180
150
|
def _get_selector(self) -> Union[str, Dict[str, str]]:
|
|
181
|
-
"""Return selectors for highlighting candlestick elements.
|
|
182
|
-
|
|
183
|
-
- Modern path (collections present): return a dict with separate selectors for body, wickLow, wickHigh
|
|
184
|
-
- Legacy path: return a dict with body and shared wick selectors (no open/close keys)
|
|
185
|
-
"""
|
|
151
|
+
"""Return selectors for highlighting candlestick elements."""
|
|
186
152
|
# Modern path: build structured selectors using separate gids
|
|
187
153
|
if (
|
|
188
154
|
self._maidr_body_collection
|
|
@@ -221,12 +187,12 @@ class CandlestickPlot(MaidrPlot):
|
|
|
221
187
|
}
|
|
222
188
|
return selectors
|
|
223
189
|
|
|
224
|
-
# Legacy path
|
|
190
|
+
# Legacy path
|
|
225
191
|
legacy_selectors = {}
|
|
226
|
-
if
|
|
227
|
-
body_gid =
|
|
192
|
+
if self._maidr_body_gid or self._maidr_gid:
|
|
193
|
+
body_gid = self._maidr_body_gid or self._maidr_gid
|
|
228
194
|
legacy_selectors["body"] = f"g[id='{body_gid}'] > path"
|
|
229
|
-
if
|
|
195
|
+
if self._maidr_wick_gid:
|
|
230
196
|
legacy_selectors["wick"] = f"g[id='{self._maidr_wick_gid}'] > path"
|
|
231
197
|
if legacy_selectors:
|
|
232
198
|
return legacy_selectors
|
|
@@ -238,9 +204,12 @@ class CandlestickPlot(MaidrPlot):
|
|
|
238
204
|
"""Initialize the MAIDR schema dictionary with basic plot information."""
|
|
239
205
|
base_schema = super().render()
|
|
240
206
|
base_schema[MaidrKey.TITLE] = "Candlestick Chart"
|
|
241
|
-
|
|
207
|
+
# Update axes labels while preserving format from parent
|
|
208
|
+
axes_data = self._extract_axes_data()
|
|
209
|
+
if MaidrKey.AXES in base_schema and MaidrKey.FORMAT in base_schema[MaidrKey.AXES]:
|
|
210
|
+
axes_data[MaidrKey.FORMAT] = base_schema[MaidrKey.AXES][MaidrKey.FORMAT]
|
|
211
|
+
base_schema[MaidrKey.AXES] = axes_data
|
|
242
212
|
base_schema[MaidrKey.DATA] = self._extract_plot_data()
|
|
243
|
-
# Include selector only if the plot supports highlighting.
|
|
244
213
|
if self._support_highlighting:
|
|
245
214
|
base_schema[MaidrKey.SELECTOR] = self._get_selector()
|
|
246
215
|
return base_schema
|
maidr/core/plot/maidr_plot.py
CHANGED
|
@@ -5,12 +5,13 @@ from abc import ABC, abstractmethod
|
|
|
5
5
|
from matplotlib.axes import Axes
|
|
6
6
|
|
|
7
7
|
from maidr.core.enum import MaidrKey, PlotType
|
|
8
|
+
from maidr.util.mixin import FormatExtractorMixin
|
|
8
9
|
|
|
9
10
|
# uuid is used to generate unique identifiers for each plot layer in the MAIDR schema.
|
|
10
11
|
import uuid
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
class MaidrPlot(ABC):
|
|
14
|
+
class MaidrPlot(ABC, FormatExtractorMixin):
|
|
14
15
|
"""
|
|
15
16
|
Abstract base class for plots managed by the MAIDR system.
|
|
16
17
|
|
|
@@ -61,13 +62,21 @@ class MaidrPlot(ABC):
|
|
|
61
62
|
"""
|
|
62
63
|
Generate the MAIDR schema for this plot layer, including a unique id for layer identification.
|
|
63
64
|
"""
|
|
65
|
+
# Extract axes data first
|
|
66
|
+
axes_data = self._extract_axes_data()
|
|
67
|
+
|
|
68
|
+
# Extract and include format configuration inside axes if available.
|
|
69
|
+
format_config = self.extract_format(self.ax)
|
|
70
|
+
if format_config:
|
|
71
|
+
axes_data[MaidrKey.FORMAT] = format_config
|
|
72
|
+
|
|
64
73
|
# Generate a unique UUID for this layer to ensure each plot layer can be distinctly identified
|
|
65
74
|
# in the MAIDR frontend. This supports robust layer switching.
|
|
66
75
|
maidr_schema = {
|
|
67
76
|
MaidrKey.ID: str(uuid.uuid4()),
|
|
68
77
|
MaidrKey.TYPE: self.type,
|
|
69
78
|
MaidrKey.TITLE: self.ax.get_title(),
|
|
70
|
-
MaidrKey.AXES:
|
|
79
|
+
MaidrKey.AXES: axes_data,
|
|
71
80
|
MaidrKey.DATA: self._extract_plot_data(),
|
|
72
81
|
}
|
|
73
82
|
|
|
@@ -142,7 +142,11 @@ class MplfinanceBarPlot(
|
|
|
142
142
|
def render(self) -> dict:
|
|
143
143
|
base_schema = super().render()
|
|
144
144
|
base_schema[MaidrKey.TITLE] = "Volume Bar Plot"
|
|
145
|
-
|
|
145
|
+
# Update axes labels while preserving format from parent
|
|
146
|
+
axes_data = self._extract_axes_data()
|
|
147
|
+
if MaidrKey.AXES in base_schema and MaidrKey.FORMAT in base_schema[MaidrKey.AXES]:
|
|
148
|
+
axes_data[MaidrKey.FORMAT] = base_schema[MaidrKey.AXES][MaidrKey.FORMAT]
|
|
149
|
+
base_schema[MaidrKey.AXES] = axes_data
|
|
146
150
|
base_schema[MaidrKey.DATA] = self._extract_plot_data()
|
|
147
151
|
if self._support_highlighting:
|
|
148
152
|
base_schema[MaidrKey.SELECTOR] = self._get_selector()
|
|
@@ -192,7 +192,11 @@ class MplfinanceLinePlot(MaidrPlot, LineExtractorMixin):
|
|
|
192
192
|
def render(self) -> dict:
|
|
193
193
|
base_schema = super().render()
|
|
194
194
|
base_schema[MaidrKey.TITLE] = "Moving Average Line Plot"
|
|
195
|
-
|
|
195
|
+
# Update axes labels while preserving format from parent
|
|
196
|
+
axes_data = self._extract_axes_data()
|
|
197
|
+
if MaidrKey.AXES in base_schema and MaidrKey.FORMAT in base_schema[MaidrKey.AXES]:
|
|
198
|
+
axes_data[MaidrKey.FORMAT] = base_schema[MaidrKey.AXES][MaidrKey.FORMAT]
|
|
199
|
+
base_schema[MaidrKey.AXES] = axes_data
|
|
196
200
|
base_schema[MaidrKey.DATA] = self._extract_plot_data()
|
|
197
201
|
if self._support_highlighting:
|
|
198
202
|
base_schema[MaidrKey.SELECTOR] = self._get_selector()
|
|
@@ -49,14 +49,14 @@ class DatetimeConverter:
|
|
|
49
49
|
>>>
|
|
50
50
|
>>> # Get formatted datetime
|
|
51
51
|
>>> formatted = converter.get_formatted_datetime(0)
|
|
52
|
-
>>> print(formatted) # Output: "
|
|
52
|
+
>>> print(formatted) # Output: "2024-01-15 00:00:00"
|
|
53
53
|
>>>
|
|
54
54
|
>>> # For time-based data
|
|
55
55
|
>>> hourly_dates = pd.date_range('2024-01-15 09:00:00', periods=3, freq='H')
|
|
56
56
|
>>> df_hourly = pd.DataFrame({'Open': [3050, 3078, 3080]}, index=hourly_dates)
|
|
57
57
|
>>> converter_hourly = create_datetime_converter(df_hourly)
|
|
58
58
|
>>> formatted_hourly = converter_hourly.get_formatted_datetime(0)
|
|
59
|
-
>>> print(formatted_hourly) # Output: "
|
|
59
|
+
>>> print(formatted_hourly) # Output: "2024-01-15 09:00:00"
|
|
60
60
|
"""
|
|
61
61
|
|
|
62
62
|
def __init__(
|
|
@@ -182,7 +182,7 @@ class DatetimeConverter:
|
|
|
182
182
|
--------
|
|
183
183
|
>>> converter = create_datetime_converter(df)
|
|
184
184
|
>>> formatted = converter.get_formatted_datetime(0)
|
|
185
|
-
>>> print(formatted) # "
|
|
185
|
+
>>> print(formatted) # "2024-01-15 00:00:00" (plain datetime string)
|
|
186
186
|
"""
|
|
187
187
|
if index not in self.date_mapping:
|
|
188
188
|
return None
|
|
@@ -192,7 +192,7 @@ class DatetimeConverter:
|
|
|
192
192
|
|
|
193
193
|
def _format_datetime_custom(self, dt: datetime) -> str:
|
|
194
194
|
"""
|
|
195
|
-
|
|
195
|
+
Return plain datetime string representation.
|
|
196
196
|
|
|
197
197
|
Parameters
|
|
198
198
|
----------
|
|
@@ -202,24 +202,14 @@ class DatetimeConverter:
|
|
|
202
202
|
Returns
|
|
203
203
|
-------
|
|
204
204
|
str
|
|
205
|
-
|
|
205
|
+
Plain string representation of datetime (ISO format).
|
|
206
206
|
|
|
207
207
|
Notes
|
|
208
208
|
-----
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
- Time-based data: "Jan 15 2024 09:00" or "Jan 15 2024 09:00:30"
|
|
212
|
-
- Seconds are only shown when they are non-zero for cleaner display.
|
|
209
|
+
Returns the raw string representation of the datetime object,
|
|
210
|
+
allowing the frontend to handle formatting as needed.
|
|
213
211
|
"""
|
|
214
|
-
|
|
215
|
-
# Time-based data: include time with optional seconds
|
|
216
|
-
if dt.second == 0:
|
|
217
|
-
return dt.strftime("%b %d %Y %H:%M")
|
|
218
|
-
else:
|
|
219
|
-
return dt.strftime("%b %d %Y %H:%M:%S")
|
|
220
|
-
else:
|
|
221
|
-
# Daily/weekly/monthly data: just date
|
|
222
|
-
return dt.strftime("%b %d %Y")
|
|
212
|
+
return str(dt)
|
|
223
213
|
|
|
224
214
|
@property
|
|
225
215
|
def date_nums(self) -> List[float]:
|