maidr 1.6.1__py3-none-any.whl → 1.7.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/plot/candlestick.py +58 -56
- maidr/core/plot/mplfinance_barplot.py +18 -1
- maidr/core/plot/mplfinance_lineplot.py +11 -3
- maidr/patch/mplfinance.py +32 -2
- maidr/util/datetime_conversion.py +406 -0
- {maidr-1.6.1.dist-info → maidr-1.7.0.dist-info}/METADATA +1 -1
- {maidr-1.6.1.dist-info → maidr-1.7.0.dist-info}/RECORD +10 -9
- {maidr-1.6.1.dist-info → maidr-1.7.0.dist-info}/WHEEL +0 -0
- {maidr-1.6.1.dist-info → maidr-1.7.0.dist-info}/licenses/LICENSE +0 -0
maidr/__init__.py
CHANGED
maidr/core/plot/candlestick.py
CHANGED
|
@@ -44,6 +44,7 @@ class CandlestickPlot(MaidrPlot):
|
|
|
44
44
|
self._maidr_body_collection = kwargs.get("_maidr_body_collection", None)
|
|
45
45
|
self._maidr_date_nums = kwargs.get("_maidr_date_nums", None)
|
|
46
46
|
self._maidr_original_data = kwargs.get("_maidr_original_data", None) # Store original data
|
|
47
|
+
self._maidr_datetime_converter = kwargs.get("_maidr_datetime_converter", None)
|
|
47
48
|
|
|
48
49
|
# Store the GID for proper selector generation (legacy/shared)
|
|
49
50
|
self._maidr_gid = None
|
|
@@ -91,63 +92,64 @@ class CandlestickPlot(MaidrPlot):
|
|
|
91
92
|
# Use the original collections for highlighting
|
|
92
93
|
self._elements = [body_collection, wick_collection]
|
|
93
94
|
|
|
94
|
-
# Use
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
self._maidr_gid
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
rect
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
95
|
+
# Use datetime converter for enhanced data extraction
|
|
96
|
+
if self._maidr_datetime_converter is not None:
|
|
97
|
+
data = self._maidr_datetime_converter.extract_candlestick_data(
|
|
98
|
+
self.axes[0], wick_collection, body_collection
|
|
99
|
+
)
|
|
100
|
+
return data
|
|
101
|
+
|
|
102
|
+
# Fallback to original detection method
|
|
103
|
+
if not self.axes:
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
ax_ohlc = self.axes[0]
|
|
107
|
+
|
|
108
|
+
# Look for Rectangle patches (original_flavor candlestick)
|
|
109
|
+
body_rectangles = []
|
|
110
|
+
for patch in ax_ohlc.patches:
|
|
111
|
+
if isinstance(patch, Rectangle):
|
|
112
|
+
body_rectangles.append(patch)
|
|
113
|
+
|
|
114
|
+
if body_rectangles:
|
|
115
|
+
# Set elements for highlighting
|
|
116
|
+
self._elements = body_rectangles
|
|
117
|
+
|
|
118
|
+
# Generate a GID for highlighting if none exists
|
|
119
|
+
if not self._maidr_gid:
|
|
120
|
+
self._maidr_gid = f"maidr-{uuid.uuid4()}"
|
|
121
|
+
# Set GID on all rectangles
|
|
122
|
+
for rect in body_rectangles:
|
|
123
|
+
rect.set_gid(self._maidr_gid)
|
|
124
|
+
# Keep a dedicated body gid for legacy dict selectors
|
|
125
|
+
self._maidr_body_gid = getattr(self, "_maidr_body_gid", None) or self._maidr_gid
|
|
126
|
+
|
|
127
|
+
# Assign a shared gid to wick Line2D (vertical 2-point lines) on the same axis
|
|
128
|
+
wick_lines = []
|
|
129
|
+
for line in ax_ohlc.get_lines():
|
|
130
|
+
try:
|
|
131
|
+
xydata = line.get_xydata()
|
|
132
|
+
if xydata is None:
|
|
133
|
+
continue
|
|
134
|
+
xy_arr = np.asarray(xydata)
|
|
135
|
+
if xy_arr.ndim == 2 and xy_arr.shape[0] == 2 and xy_arr.shape[1] >= 2:
|
|
136
|
+
x0 = float(xy_arr[0, 0])
|
|
137
|
+
x1 = float(xy_arr[1, 0])
|
|
138
|
+
if abs(x0 - x1) < 1e-10:
|
|
139
|
+
wick_lines.append(line)
|
|
140
|
+
except Exception:
|
|
131
141
|
continue
|
|
132
|
-
|
|
133
|
-
if
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
for line in wick_lines:
|
|
144
|
-
line.set_gid(self._maidr_wick_gid)
|
|
145
|
-
|
|
146
|
-
# Use the utility class to extract data
|
|
147
|
-
data = MplfinanceDataExtractor.extract_rectangle_candlestick_data(
|
|
148
|
-
body_rectangles, self._maidr_date_nums, self._maidr_original_data
|
|
149
|
-
)
|
|
150
|
-
return data
|
|
142
|
+
if wick_lines:
|
|
143
|
+
if not getattr(self, "_maidr_wick_gid", None):
|
|
144
|
+
self._maidr_wick_gid = f"maidr-{uuid.uuid4()}"
|
|
145
|
+
for line in wick_lines:
|
|
146
|
+
line.set_gid(self._maidr_wick_gid)
|
|
147
|
+
|
|
148
|
+
# Use the utility class to extract data
|
|
149
|
+
data = MplfinanceDataExtractor.extract_rectangle_candlestick_data(
|
|
150
|
+
body_rectangles, self._maidr_date_nums, self._maidr_original_data
|
|
151
|
+
)
|
|
152
|
+
return data
|
|
151
153
|
|
|
152
154
|
return []
|
|
153
155
|
|
|
@@ -31,6 +31,10 @@ class MplfinanceBarPlot(
|
|
|
31
31
|
self._custom_patches = kwargs.get("_maidr_patches", None)
|
|
32
32
|
# Store date numbers for volume bars (from mplfinance)
|
|
33
33
|
self._maidr_date_nums = kwargs.get("_maidr_date_nums", None)
|
|
34
|
+
|
|
35
|
+
# Store datetime converter if available
|
|
36
|
+
self._maidr_datetime_converter = kwargs.get("_maidr_datetime_converter", None)
|
|
37
|
+
|
|
34
38
|
# Store custom title
|
|
35
39
|
|
|
36
40
|
def set_title(self, title: str) -> None:
|
|
@@ -48,6 +52,13 @@ class MplfinanceBarPlot(
|
|
|
48
52
|
)
|
|
49
53
|
data = self._extract_bar_container_data(plot)
|
|
50
54
|
levels = self.extract_level(self.ax)
|
|
55
|
+
|
|
56
|
+
# Ensure we have valid data and levels before processing
|
|
57
|
+
if data is None or levels is None:
|
|
58
|
+
if data is None:
|
|
59
|
+
raise ExtractionError(self.type, plot)
|
|
60
|
+
return []
|
|
61
|
+
|
|
51
62
|
formatted_data = []
|
|
52
63
|
combined_data = list(
|
|
53
64
|
zip(levels, data)
|
|
@@ -78,7 +89,13 @@ class MplfinanceBarPlot(
|
|
|
78
89
|
# Set elements for highlighting (use the patches directly)
|
|
79
90
|
self._elements = sorted_patches
|
|
80
91
|
|
|
81
|
-
# Use
|
|
92
|
+
# Use datetime converter for enhanced data extraction
|
|
93
|
+
if self._maidr_datetime_converter is not None:
|
|
94
|
+
data = self._maidr_datetime_converter.extract_volume_data(self.ax)
|
|
95
|
+
if data: # Only use if successful
|
|
96
|
+
return [{"x": item[0], "y": item[1]} for item in data]
|
|
97
|
+
|
|
98
|
+
# Fallback to existing logic
|
|
82
99
|
return MplfinanceDataExtractor.extract_volume_data(
|
|
83
100
|
sorted_patches, self._maidr_date_nums
|
|
84
101
|
)
|
|
@@ -23,6 +23,9 @@ class MplfinanceLinePlot(MaidrPlot, LineExtractorMixin):
|
|
|
23
23
|
def __init__(self, ax: Axes, **kwargs):
|
|
24
24
|
super().__init__(ax, PlotType.LINE)
|
|
25
25
|
|
|
26
|
+
# Store datetime converter if available
|
|
27
|
+
self._maidr_datetime_converter = kwargs.get("_maidr_datetime_converter", None)
|
|
28
|
+
|
|
26
29
|
def _get_selector(self) -> Union[str, List[str]]:
|
|
27
30
|
"""Return selectors for all lines that have data."""
|
|
28
31
|
all_lines = self.ax.get_lines()
|
|
@@ -101,10 +104,15 @@ class MplfinanceLinePlot(MaidrPlot, LineExtractorMixin):
|
|
|
101
104
|
if np.isnan(x) or np.isnan(y) or np.isinf(x) or np.isinf(y):
|
|
102
105
|
continue
|
|
103
106
|
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
# Use datetime converter for enhanced data extraction
|
|
108
|
+
datetime_converter = getattr(line, "_maidr_datetime_converter", None) or self._maidr_datetime_converter
|
|
109
|
+
if datetime_converter is not None:
|
|
110
|
+
# Convert x-coordinate (matplotlib index) to formatted datetime
|
|
111
|
+
x_value = datetime_converter.get_formatted_datetime(int(round(x)))
|
|
112
|
+
if x_value is None:
|
|
113
|
+
x_value = float(x) # Fallback to numeric
|
|
107
114
|
else:
|
|
115
|
+
# Fallback to existing logic
|
|
108
116
|
# Check if we have date numbers from mplfinance
|
|
109
117
|
if date_nums is not None and i < len(date_nums):
|
|
110
118
|
# Use the date number to convert to date string
|
maidr/patch/mplfinance.py
CHANGED
|
@@ -8,6 +8,7 @@ from matplotlib.lines import Line2D
|
|
|
8
8
|
from maidr.core.enum import PlotType
|
|
9
9
|
from maidr.patch.common import common
|
|
10
10
|
from maidr.core.context_manager import ContextManager
|
|
11
|
+
from maidr.util.datetime_conversion import create_datetime_converter
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def mplfinance_plot_patch(wrapped, instance, args, kwargs):
|
|
@@ -53,26 +54,37 @@ def mplfinance_plot_patch(wrapped, instance, args, kwargs):
|
|
|
53
54
|
elif volume_ax is None and "volume" in ax.get_ylabel().lower():
|
|
54
55
|
volume_ax = ax
|
|
55
56
|
|
|
56
|
-
# Try to extract date numbers from the data
|
|
57
|
+
# Try to extract date numbers from the data (existing logic preserved)
|
|
57
58
|
date_nums = None
|
|
58
59
|
data = None
|
|
60
|
+
datetime_converter = None
|
|
61
|
+
|
|
59
62
|
if len(args) > 0:
|
|
60
63
|
data = args[0]
|
|
61
64
|
elif "data" in kwargs:
|
|
62
65
|
data = kwargs["data"]
|
|
63
66
|
|
|
64
67
|
if data is not None:
|
|
68
|
+
# Existing date_nums logic (preserved)
|
|
65
69
|
if hasattr(data, "Date_num"):
|
|
66
70
|
date_nums = list(data["Date_num"])
|
|
67
71
|
elif hasattr(data, "index"):
|
|
68
72
|
# fallback: use index if it's a DatetimeIndex
|
|
69
73
|
try:
|
|
70
74
|
import matplotlib.dates as mdates
|
|
71
|
-
|
|
72
75
|
date_nums = [mdates.date2num(d) for d in data.index]
|
|
73
76
|
except Exception:
|
|
74
77
|
pass
|
|
75
78
|
|
|
79
|
+
# Create datetime converter for DatetimeIndex data
|
|
80
|
+
if hasattr(data, "index") and hasattr(data.index, "dtype"):
|
|
81
|
+
if "datetime" in str(data.index.dtype).lower():
|
|
82
|
+
datetime_converter = create_datetime_converter(data)
|
|
83
|
+
|
|
84
|
+
# Use enhanced converter's date_nums for mplfinance compatibility
|
|
85
|
+
if date_nums is None and hasattr(datetime_converter, 'date_nums'):
|
|
86
|
+
date_nums = datetime_converter.date_nums
|
|
87
|
+
|
|
76
88
|
# Process and register the Candlestick plot
|
|
77
89
|
if price_ax:
|
|
78
90
|
wick_collection = next(
|
|
@@ -97,6 +109,10 @@ def mplfinance_plot_patch(wrapped, instance, args, kwargs):
|
|
|
97
109
|
_maidr_wick_gid=wick_gid,
|
|
98
110
|
_maidr_body_gid=body_gid,
|
|
99
111
|
)
|
|
112
|
+
|
|
113
|
+
# Add datetime converter
|
|
114
|
+
if datetime_converter is not None:
|
|
115
|
+
candlestick_kwargs["_maidr_datetime_converter"] = datetime_converter
|
|
100
116
|
common(
|
|
101
117
|
PlotType.CANDLESTICK,
|
|
102
118
|
lambda *a, **k: price_ax,
|
|
@@ -129,6 +145,11 @@ def mplfinance_plot_patch(wrapped, instance, args, kwargs):
|
|
|
129
145
|
_maidr_patches=volume_patches,
|
|
130
146
|
_maidr_date_nums=date_nums,
|
|
131
147
|
)
|
|
148
|
+
|
|
149
|
+
# Add datetime converter
|
|
150
|
+
if datetime_converter is not None:
|
|
151
|
+
bar_kwargs["_maidr_datetime_converter"] = datetime_converter # type: ignore
|
|
152
|
+
|
|
132
153
|
common(PlotType.BAR, lambda *a, **k: volume_ax, instance, args, bar_kwargs)
|
|
133
154
|
|
|
134
155
|
# Process and register Moving Averages as LINE plots
|
|
@@ -196,6 +217,10 @@ def mplfinance_plot_patch(wrapped, instance, args, kwargs):
|
|
|
196
217
|
if date_nums is not None:
|
|
197
218
|
setattr(line, "_maidr_date_nums", date_nums)
|
|
198
219
|
|
|
220
|
+
# Store datetime converter
|
|
221
|
+
if datetime_converter is not None:
|
|
222
|
+
setattr(line, "_maidr_datetime_converter", datetime_converter)
|
|
223
|
+
|
|
199
224
|
# Ensure GID is set for highlighting
|
|
200
225
|
if line.get_gid() is None:
|
|
201
226
|
gid = f"maidr-{uuid.uuid4()}"
|
|
@@ -207,6 +232,11 @@ def mplfinance_plot_patch(wrapped, instance, args, kwargs):
|
|
|
207
232
|
# Register all valid lines as a single LINE plot
|
|
208
233
|
if valid_lines:
|
|
209
234
|
line_kwargs = dict(kwargs)
|
|
235
|
+
|
|
236
|
+
# Add datetime converter
|
|
237
|
+
if datetime_converter is not None:
|
|
238
|
+
line_kwargs["_maidr_datetime_converter"] = datetime_converter
|
|
239
|
+
|
|
210
240
|
common(PlotType.LINE, lambda *a, **k: price_ax, instance, args, line_kwargs)
|
|
211
241
|
|
|
212
242
|
if not original_returnfig:
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
from typing import Optional, Dict, Any, List, Tuple
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
class DatetimeConverter:
|
|
7
|
+
"""
|
|
8
|
+
Enhanced datetime converter that automatically detects time periods
|
|
9
|
+
and provides intelligent date/time formatting for mplfinance plots.
|
|
10
|
+
|
|
11
|
+
This utility automatically detects the time period of financial data and formats
|
|
12
|
+
datetime values consistently for screen reader accessibility and visual clarity.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
data : pd.DataFrame
|
|
17
|
+
DataFrame with DatetimeIndex containing financial data.
|
|
18
|
+
datetime_format : str, optional
|
|
19
|
+
Custom datetime format string. If None, automatic format detection is used.
|
|
20
|
+
|
|
21
|
+
Attributes
|
|
22
|
+
----------
|
|
23
|
+
data : pd.DataFrame
|
|
24
|
+
The input DataFrame with DatetimeIndex.
|
|
25
|
+
datetime_format : str or None
|
|
26
|
+
Custom datetime format string if provided.
|
|
27
|
+
date_mapping : Dict[int, datetime]
|
|
28
|
+
Mapping from integer index to datetime objects.
|
|
29
|
+
time_period : str
|
|
30
|
+
Detected time period ('minute', 'intraday', 'hour', 'day', 'week', 'month').
|
|
31
|
+
|
|
32
|
+
Raises
|
|
33
|
+
------
|
|
34
|
+
ValueError
|
|
35
|
+
If the input data does not have a DatetimeIndex.
|
|
36
|
+
|
|
37
|
+
Examples
|
|
38
|
+
--------
|
|
39
|
+
>>> import pandas as pd
|
|
40
|
+
>>> from maidr.util.datetime_conversion import create_datetime_converter
|
|
41
|
+
>>>
|
|
42
|
+
>>> # Create sample data with DatetimeIndex
|
|
43
|
+
>>> dates = pd.date_range('2024-01-15', periods=5, freq='D')
|
|
44
|
+
>>> df = pd.DataFrame({'Open': [3050, 3078, 3080, 3075, 3087]}, index=dates)
|
|
45
|
+
>>>
|
|
46
|
+
>>> # Create converter
|
|
47
|
+
>>> converter = create_datetime_converter(df)
|
|
48
|
+
>>>
|
|
49
|
+
>>> # Get formatted datetime
|
|
50
|
+
>>> formatted = converter.get_formatted_datetime(0)
|
|
51
|
+
>>> print(formatted) # Output: "Jan 15 2024"
|
|
52
|
+
>>>
|
|
53
|
+
>>> # For time-based data
|
|
54
|
+
>>> hourly_dates = pd.date_range('2024-01-15 09:00:00', periods=3, freq='H')
|
|
55
|
+
>>> df_hourly = pd.DataFrame({'Open': [3050, 3078, 3080]}, index=hourly_dates)
|
|
56
|
+
>>> converter_hourly = create_datetime_converter(df_hourly)
|
|
57
|
+
>>> formatted_hourly = converter_hourly.get_formatted_datetime(0)
|
|
58
|
+
>>> print(formatted_hourly) # Output: "Jan 15 2024 09:00"
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, data: pd.DataFrame, datetime_format: Optional[str] = None) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Initialize the DatetimeConverter.
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
data : pd.DataFrame
|
|
68
|
+
DataFrame with DatetimeIndex containing financial data.
|
|
69
|
+
datetime_format : str, optional
|
|
70
|
+
Custom datetime format string. If None, automatic format detection is used.
|
|
71
|
+
|
|
72
|
+
Raises
|
|
73
|
+
------
|
|
74
|
+
ValueError
|
|
75
|
+
If the input data does not have a DatetimeIndex.
|
|
76
|
+
|
|
77
|
+
Notes
|
|
78
|
+
-----
|
|
79
|
+
The converter automatically detects the time period of the data based on
|
|
80
|
+
average time differences between consecutive data points.
|
|
81
|
+
"""
|
|
82
|
+
self.data = data
|
|
83
|
+
self.datetime_format = datetime_format
|
|
84
|
+
|
|
85
|
+
if not isinstance(data.index, pd.DatetimeIndex):
|
|
86
|
+
raise ValueError("Data must have a DatetimeIndex")
|
|
87
|
+
|
|
88
|
+
self.date_mapping = self._create_date_mapping()
|
|
89
|
+
self.time_period = self._detect_time_period()
|
|
90
|
+
|
|
91
|
+
def _create_date_mapping(self) -> Dict[int, datetime]:
|
|
92
|
+
"""
|
|
93
|
+
Create mapping from integer index to datetime objects.
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
Dict[int, datetime]
|
|
98
|
+
Dictionary mapping integer indices to corresponding datetime objects
|
|
99
|
+
from the DataFrame index.
|
|
100
|
+
"""
|
|
101
|
+
return {i: date for i, date in enumerate(self.data.index)}
|
|
102
|
+
|
|
103
|
+
def _detect_time_period(self) -> str:
|
|
104
|
+
"""
|
|
105
|
+
Detect the time period of the data based on average time differences.
|
|
106
|
+
|
|
107
|
+
Returns
|
|
108
|
+
-------
|
|
109
|
+
str
|
|
110
|
+
Detected time period: 'minute', 'intraday', 'hour', 'day', 'week', 'month', or 'unknown'.
|
|
111
|
+
|
|
112
|
+
Notes
|
|
113
|
+
-----
|
|
114
|
+
Time period detection is based on average time differences between consecutive
|
|
115
|
+
data points in the DatetimeIndex.
|
|
116
|
+
"""
|
|
117
|
+
if len(self.data) < 2:
|
|
118
|
+
return "unknown"
|
|
119
|
+
|
|
120
|
+
# Calculate average time difference between consecutive data points
|
|
121
|
+
time_diffs = []
|
|
122
|
+
for i in range(1, len(self.data)):
|
|
123
|
+
diff = self.data.index[i] - self.data.index[i-1]
|
|
124
|
+
time_diffs.append(diff.total_seconds())
|
|
125
|
+
|
|
126
|
+
avg_diff_seconds = np.mean(time_diffs)
|
|
127
|
+
|
|
128
|
+
# Determine time period based on average difference
|
|
129
|
+
if avg_diff_seconds < 60: # Less than 1 minute
|
|
130
|
+
return "minute"
|
|
131
|
+
elif avg_diff_seconds < 3600: # Less than 1 hour
|
|
132
|
+
return "intraday"
|
|
133
|
+
elif avg_diff_seconds < 86400: # Less than 1 day
|
|
134
|
+
return "hour"
|
|
135
|
+
elif avg_diff_seconds < 604800: # Less than 1 week
|
|
136
|
+
return "day"
|
|
137
|
+
elif avg_diff_seconds < 2592000: # Less than 1 month
|
|
138
|
+
return "week"
|
|
139
|
+
else:
|
|
140
|
+
return "month"
|
|
141
|
+
|
|
142
|
+
def get_time_period_description(self) -> str:
|
|
143
|
+
"""
|
|
144
|
+
Get human-readable description of detected time period.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
str
|
|
149
|
+
Human-readable description of the detected time period.
|
|
150
|
+
"""
|
|
151
|
+
period_descriptions = {
|
|
152
|
+
"minute": "Sub-minute data",
|
|
153
|
+
"intraday": "Intraday (minute-level) data",
|
|
154
|
+
"hour": "Hourly data",
|
|
155
|
+
"day": "Daily data",
|
|
156
|
+
"week": "Weekly data",
|
|
157
|
+
"month": "Monthly data",
|
|
158
|
+
"unknown": "Unknown time period"
|
|
159
|
+
}
|
|
160
|
+
return period_descriptions.get(self.time_period, "Unknown time period")
|
|
161
|
+
|
|
162
|
+
def get_formatted_datetime(self, index: int) -> Optional[str]:
|
|
163
|
+
"""
|
|
164
|
+
Get formatted datetime string for given index using consistent formatting.
|
|
165
|
+
|
|
166
|
+
Always includes year for screen reader accessibility.
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
index : int
|
|
171
|
+
Integer index into the DataFrame.
|
|
172
|
+
|
|
173
|
+
Returns
|
|
174
|
+
-------
|
|
175
|
+
str or None
|
|
176
|
+
Formatted datetime string or None if index is invalid.
|
|
177
|
+
|
|
178
|
+
Examples
|
|
179
|
+
--------
|
|
180
|
+
>>> converter = create_datetime_converter(df)
|
|
181
|
+
>>> formatted = converter.get_formatted_datetime(0)
|
|
182
|
+
>>> print(formatted) # "Jan 15 2024" for daily data
|
|
183
|
+
"""
|
|
184
|
+
if index not in self.date_mapping:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
dt = self.date_mapping[index]
|
|
188
|
+
return self._format_datetime_custom(dt)
|
|
189
|
+
|
|
190
|
+
def _format_datetime_custom(self, dt: datetime) -> str:
|
|
191
|
+
"""
|
|
192
|
+
Consistent datetime formatting with year always included.
|
|
193
|
+
|
|
194
|
+
Parameters
|
|
195
|
+
----------
|
|
196
|
+
dt : datetime
|
|
197
|
+
Datetime object to format.
|
|
198
|
+
|
|
199
|
+
Returns
|
|
200
|
+
-------
|
|
201
|
+
str
|
|
202
|
+
Formatted datetime string with consistent pattern.
|
|
203
|
+
|
|
204
|
+
Notes
|
|
205
|
+
-----
|
|
206
|
+
Formatting rules:
|
|
207
|
+
- Daily data: "Jan 15 2024"
|
|
208
|
+
- Time-based data: "Jan 15 2024 09:00" or "Jan 15 2024 09:00:30"
|
|
209
|
+
- Seconds are only shown when they are non-zero for cleaner display.
|
|
210
|
+
"""
|
|
211
|
+
if self.time_period in ["minute", "intraday", "hour"]:
|
|
212
|
+
# Time-based data: include time with optional seconds
|
|
213
|
+
if dt.second == 0:
|
|
214
|
+
return dt.strftime("%b %d %Y %H:%M")
|
|
215
|
+
else:
|
|
216
|
+
return dt.strftime("%b %d %Y %H:%M:%S")
|
|
217
|
+
else:
|
|
218
|
+
# Daily/weekly/monthly data: just date
|
|
219
|
+
return dt.strftime("%b %d %Y")
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def date_nums(self) -> List[float]:
|
|
223
|
+
"""
|
|
224
|
+
Convert DatetimeIndex to matplotlib date numbers for backward compatibility.
|
|
225
|
+
|
|
226
|
+
Returns
|
|
227
|
+
-------
|
|
228
|
+
List[float]
|
|
229
|
+
List of matplotlib date numbers converted from the DatetimeIndex.
|
|
230
|
+
Empty list if conversion fails.
|
|
231
|
+
|
|
232
|
+
Notes
|
|
233
|
+
-----
|
|
234
|
+
This property provides backward compatibility with existing matplotlib
|
|
235
|
+
plotting code that expects date numbers instead of datetime objects.
|
|
236
|
+
"""
|
|
237
|
+
try:
|
|
238
|
+
import matplotlib.dates as mdates
|
|
239
|
+
date_nums = []
|
|
240
|
+
for d in self.data.index:
|
|
241
|
+
try:
|
|
242
|
+
date_num = mdates.date2num(d)
|
|
243
|
+
date_nums.append(float(date_num))
|
|
244
|
+
except (ValueError, TypeError):
|
|
245
|
+
continue
|
|
246
|
+
return date_nums
|
|
247
|
+
except Exception:
|
|
248
|
+
return []
|
|
249
|
+
|
|
250
|
+
def extract_candlestick_data(self, ax, wick_collection=None, body_collection=None) -> List[Dict[str, Any]]:
|
|
251
|
+
"""
|
|
252
|
+
Extract candlestick data with proper datetime formatting using original DataFrame.
|
|
253
|
+
|
|
254
|
+
Parameters
|
|
255
|
+
----------
|
|
256
|
+
ax : matplotlib.axes.Axes
|
|
257
|
+
Matplotlib axes object (not used in current implementation).
|
|
258
|
+
wick_collection : matplotlib.collections.LineCollection, optional
|
|
259
|
+
Collection containing wick lines for candlestick plots.
|
|
260
|
+
body_collection : matplotlib.collections.PolyCollection, optional
|
|
261
|
+
Collection containing body rectangles for candlestick plots.
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
List[Dict[str, Any]]
|
|
266
|
+
List of dictionaries containing candlestick data with keys:
|
|
267
|
+
'value', 'open', 'high', 'low', 'close', 'volume'.
|
|
268
|
+
Each 'value' contains formatted datetime string.
|
|
269
|
+
|
|
270
|
+
Notes
|
|
271
|
+
-----
|
|
272
|
+
This method extracts OHLC data from the original DataFrame and formats
|
|
273
|
+
datetime values using the enhanced datetime conversion logic.
|
|
274
|
+
"""
|
|
275
|
+
candles = []
|
|
276
|
+
if not hasattr(self.data, 'Open') or not hasattr(self.data, 'High') or not hasattr(self.data, 'Low') or not hasattr(self.data, 'Close'):
|
|
277
|
+
return candles
|
|
278
|
+
|
|
279
|
+
for i in range(len(self.data)):
|
|
280
|
+
try:
|
|
281
|
+
open_price = self.data.iloc[i]['Open']
|
|
282
|
+
high_price = self.data.iloc[i]['High']
|
|
283
|
+
low_price = self.data.iloc[i]['Low']
|
|
284
|
+
close_price = self.data.iloc[i]['Close']
|
|
285
|
+
volume = self.data.iloc[i].get('Volume', 0.0)
|
|
286
|
+
|
|
287
|
+
formatted_datetime = self.get_formatted_datetime(i)
|
|
288
|
+
|
|
289
|
+
candle_data = {
|
|
290
|
+
'value': formatted_datetime or f"datetime_{i:03d}",
|
|
291
|
+
'open': float(open_price),
|
|
292
|
+
'high': float(high_price),
|
|
293
|
+
'low': float(low_price),
|
|
294
|
+
'close': float(close_price),
|
|
295
|
+
'volume': float(volume)
|
|
296
|
+
}
|
|
297
|
+
candles.append(candle_data)
|
|
298
|
+
except (KeyError, IndexError, ValueError):
|
|
299
|
+
continue
|
|
300
|
+
return candles
|
|
301
|
+
|
|
302
|
+
def extract_moving_average_data(self, ax, line_index: int = 0) -> List[Tuple[str, float]]:
|
|
303
|
+
"""
|
|
304
|
+
Extract moving average data with proper datetime formatting and NaN filtering.
|
|
305
|
+
|
|
306
|
+
Parameters
|
|
307
|
+
----------
|
|
308
|
+
ax : matplotlib.axes.Axes
|
|
309
|
+
Matplotlib axes object containing the moving average lines.
|
|
310
|
+
line_index : int, default=0
|
|
311
|
+
Index of the line to extract data from.
|
|
312
|
+
|
|
313
|
+
Returns
|
|
314
|
+
-------
|
|
315
|
+
List[Tuple[str, float]]
|
|
316
|
+
List of tuples containing (formatted_datetime, y_value) pairs.
|
|
317
|
+
NaN and infinite values are filtered out.
|
|
318
|
+
|
|
319
|
+
Notes
|
|
320
|
+
-----
|
|
321
|
+
This method filters out invalid data points and formats datetime values
|
|
322
|
+
using the enhanced datetime conversion logic.
|
|
323
|
+
"""
|
|
324
|
+
ma_data = []
|
|
325
|
+
lines = ax.get_lines() if ax else []
|
|
326
|
+
if line_index >= len(lines):
|
|
327
|
+
return ma_data
|
|
328
|
+
line = lines[line_index]
|
|
329
|
+
xydata = line.get_xydata()
|
|
330
|
+
if xydata is None or len(xydata) == 0:
|
|
331
|
+
return ma_data
|
|
332
|
+
|
|
333
|
+
for i, (x, y) in enumerate(xydata):
|
|
334
|
+
if np.isnan(y) or np.isinf(y):
|
|
335
|
+
continue
|
|
336
|
+
try:
|
|
337
|
+
df_index = int(round(x))
|
|
338
|
+
if 0 <= df_index < len(self.data):
|
|
339
|
+
formatted_datetime = self.get_formatted_datetime(df_index)
|
|
340
|
+
if formatted_datetime:
|
|
341
|
+
ma_data.append((formatted_datetime, float(y)))
|
|
342
|
+
except (ValueError, TypeError):
|
|
343
|
+
continue
|
|
344
|
+
return ma_data
|
|
345
|
+
|
|
346
|
+
def extract_volume_data(self, ax) -> List[Tuple[str, float]]:
|
|
347
|
+
"""
|
|
348
|
+
Extract volume data with proper datetime formatting using original DataFrame.
|
|
349
|
+
|
|
350
|
+
Parameters
|
|
351
|
+
----------
|
|
352
|
+
ax : matplotlib.axes.Axes
|
|
353
|
+
Matplotlib axes object (not used in current implementation).
|
|
354
|
+
|
|
355
|
+
Returns
|
|
356
|
+
-------
|
|
357
|
+
List[Tuple[str, float]]
|
|
358
|
+
List of tuples containing (formatted_datetime, volume) pairs.
|
|
359
|
+
Zero and NaN volume values are filtered out.
|
|
360
|
+
|
|
361
|
+
Notes
|
|
362
|
+
-----
|
|
363
|
+
This method extracts volume data from the original DataFrame and formats
|
|
364
|
+
datetime values using the enhanced datetime conversion logic.
|
|
365
|
+
"""
|
|
366
|
+
volume_data = []
|
|
367
|
+
if hasattr(self.data, 'Volume'):
|
|
368
|
+
for i in range(len(self.data)):
|
|
369
|
+
try:
|
|
370
|
+
volume = self.data.iloc[i]['Volume']
|
|
371
|
+
if pd.isna(volume) or volume <= 0:
|
|
372
|
+
continue
|
|
373
|
+
formatted_datetime = self.get_formatted_datetime(i)
|
|
374
|
+
volume_data.append((formatted_datetime, float(volume)))
|
|
375
|
+
except (KeyError, IndexError, ValueError):
|
|
376
|
+
continue
|
|
377
|
+
return volume_data
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def create_datetime_converter(data: pd.DataFrame, datetime_format: Optional[str] = None) -> DatetimeConverter:
|
|
381
|
+
"""
|
|
382
|
+
Factory function to create a DatetimeConverter instance.
|
|
383
|
+
|
|
384
|
+
Parameters
|
|
385
|
+
----------
|
|
386
|
+
data : pd.DataFrame
|
|
387
|
+
DataFrame with DatetimeIndex containing financial data.
|
|
388
|
+
datetime_format : str, optional
|
|
389
|
+
Custom datetime format string. If None, automatic format detection is used.
|
|
390
|
+
|
|
391
|
+
Returns
|
|
392
|
+
-------
|
|
393
|
+
DatetimeConverter
|
|
394
|
+
Configured DatetimeConverter instance for the given data.
|
|
395
|
+
|
|
396
|
+
Examples
|
|
397
|
+
--------
|
|
398
|
+
>>> import pandas as pd
|
|
399
|
+
>>> from maidr.util.datetime_conversion import create_datetime_converter
|
|
400
|
+
>>>
|
|
401
|
+
>>> dates = pd.date_range('2024-01-15', periods=5, freq='D')
|
|
402
|
+
>>> df = pd.DataFrame({'Open': [3050, 3078, 3080, 3075, 3087]}, index=dates)
|
|
403
|
+
>>> converter = create_datetime_converter(df)
|
|
404
|
+
>>> print(type(converter)) # <class 'maidr.util.datetime_conversion.DatetimeConverter'>
|
|
405
|
+
"""
|
|
406
|
+
return DatetimeConverter(data, datetime_format)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
maidr/__init__.py,sha256=
|
|
1
|
+
maidr/__init__.py,sha256=N6tFj8F07eJxaRvcZZNW0wNWTR2I2JcTgM9PTSKWGtM,415
|
|
2
2
|
maidr/api.py,sha256=gRNLXqUWpFGdD-I7Nu6J0_LeEni9KRAr0TBHwHaDAsc,1928
|
|
3
3
|
maidr/core/__init__.py,sha256=WgxLpSEYMc4k3OyEOf1shOxfEq0ASzppEIZYmE91ThQ,25
|
|
4
4
|
maidr/core/context_manager.py,sha256=6cT7ZGOApSpC-SLD2XZWWU_H08i-nfv-JUlzXOtvWYw,3374
|
|
@@ -12,15 +12,15 @@ maidr/core/enum/smooth_keywords.py,sha256=z2kVZZ-mETWWh5reWu_hj9WkJD6WFj7_2-6s1e
|
|
|
12
12
|
maidr/core/plot/__init__.py,sha256=xDIpRGM-4DfaSSL3nKcXrjdMecCHJ6en4K4nA_fPefQ,83
|
|
13
13
|
maidr/core/plot/barplot.py,sha256=0hBgp__putezvxXc9G3qmaktmAzze3cN8pQMD9iqktE,2116
|
|
14
14
|
maidr/core/plot/boxplot.py,sha256=i11GdNuz_c-hilmhydu3ah-bzyVdFoBkNvRi5lpMrrY,9946
|
|
15
|
-
maidr/core/plot/candlestick.py,sha256=
|
|
15
|
+
maidr/core/plot/candlestick.py,sha256=9R1ddSnUkGEE9WJgUPeuZhRtxXNBnOM9rbJdfJcY8bo,9886
|
|
16
16
|
maidr/core/plot/grouped_barplot.py,sha256=_zn4XMeEnSiDHtf6t4-z9ErBqg_CijhAS2CCtlHgYIQ,2077
|
|
17
17
|
maidr/core/plot/heatmap.py,sha256=yMS-31tS2GW4peds9LtZesMxmmTV_YfqYO5M_t5KasQ,2521
|
|
18
18
|
maidr/core/plot/histogram.py,sha256=QV5W-6ZJQQcZsrM91JJBX-ONktJzH7yg_et5_bBPfQQ,1525
|
|
19
19
|
maidr/core/plot/lineplot.py,sha256=48aEkx8rjZ9lYEZSHWCJcSzdVblKwcdJZDZK0vnxmPo,4291
|
|
20
20
|
maidr/core/plot/maidr_plot.py,sha256=9T0boWaonM99jggEed97-rCy_cufRMWXrXo-CbmPudE,4230
|
|
21
21
|
maidr/core/plot/maidr_plot_factory.py,sha256=NW2iFScswgXbAC9rAOo4iMkAFsjY43DAvFioGr0yzx4,2732
|
|
22
|
-
maidr/core/plot/mplfinance_barplot.py,sha256=
|
|
23
|
-
maidr/core/plot/mplfinance_lineplot.py,sha256=
|
|
22
|
+
maidr/core/plot/mplfinance_barplot.py,sha256=zhTp2i6BH0xn7vQvGTotKgu2HbzlKT4p6zA5CVUUHHc,5673
|
|
23
|
+
maidr/core/plot/mplfinance_lineplot.py,sha256=oRXFAa13wsz5i9tDN267ZSqd1-gGpOEzntmFTAz8o1w,7269
|
|
24
24
|
maidr/core/plot/regplot.py,sha256=b7u6bGTz1IxKahplNUrfwIr_OGSwMJ2BuLgFAVjL0s0,2744
|
|
25
25
|
maidr/core/plot/scatterplot.py,sha256=o0i0uS-wXK9ZrENxneoHbh3-u-2goRONp19Yu9QLsaY,1257
|
|
26
26
|
maidr/exception/__init__.py,sha256=PzaXoYBhyZxMDcJkuxJugDx7jZeseI0El6LpxIwXyG4,46
|
|
@@ -36,10 +36,11 @@ maidr/patch/highlight.py,sha256=I1dGFHJAnVd0AHVnMJzk_TE8BC8Uv-I6fTzSrJLU5QM,1155
|
|
|
36
36
|
maidr/patch/histogram.py,sha256=k3N0RUf1SQ2402pwbaY5QyS98KnLWvr9glCHQw9NTko,2378
|
|
37
37
|
maidr/patch/kdeplot.py,sha256=qv-OKzuop2aTrkZgUe2OnLxvV-KMyeXt1Td0_uZeHzE,2338
|
|
38
38
|
maidr/patch/lineplot.py,sha256=kvTAiOddy1g5GMP456Awk21NUEZfn-vWaYXy5GTVtuA,1841
|
|
39
|
-
maidr/patch/mplfinance.py,sha256=
|
|
39
|
+
maidr/patch/mplfinance.py,sha256=L_C_EsIghUVVdtOxooa1a9Lxe-UcUpKuYV2Tw_bMDBw,9490
|
|
40
40
|
maidr/patch/regplot.py,sha256=k86ekd0E4XJ_L1u85zObuDnxuXlM83z7tKtyXRTj2rI,3240
|
|
41
41
|
maidr/patch/scatterplot.py,sha256=kln6zZwjVsdQzICalo-RnBOJrx1BnIB2xYUwItHvSNY,525
|
|
42
42
|
maidr/util/__init__.py,sha256=eRJZfRpDX-n7UoV3JXw_9Lbfu_qNl_D0W1UTvLL-Iv4,81
|
|
43
|
+
maidr/util/datetime_conversion.py,sha256=fl2MnSCmz8IqYIHsiQPWKkNz3lDrFyWoStkQPIDReCA,14364
|
|
43
44
|
maidr/util/dedup_utils.py,sha256=RpgPL5p-3oULUHaTCZJaQKhPHfyPkvBLHMt8lAGpJ5A,438
|
|
44
45
|
maidr/util/environment.py,sha256=SSfxzjmY2ZVA98B-IVql8n9AX3d6h2f7_R4t3_53dYI,9452
|
|
45
46
|
maidr/util/mplfinance_utils.py,sha256=aWNRkNS2IAF4YYEF9w7CcmcKWMGg3KkYJAv0xL8KcyY,14417
|
|
@@ -51,7 +52,7 @@ maidr/util/mixin/extractor_mixin.py,sha256=oHtwpmS5kARvaLrSO3DKTPQxyFUw9nOcKN7rz
|
|
|
51
52
|
maidr/util/mixin/merger_mixin.py,sha256=V0qLw_6DUB7X6CQ3BCMpsCQX_ZuwAhoSTm_E4xAJFKM,712
|
|
52
53
|
maidr/widget/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
54
|
maidr/widget/shiny.py,sha256=wrrw2KAIpE_A6CNQGBtNHauR1DjenA_n47qlFXX9_rk,745
|
|
54
|
-
maidr-1.
|
|
55
|
-
maidr-1.
|
|
56
|
-
maidr-1.
|
|
57
|
-
maidr-1.
|
|
55
|
+
maidr-1.7.0.dist-info/METADATA,sha256=rjXKEyignRmgCOB6C6E_FhvqEkPvjGHIMuxFkZAHpos,3154
|
|
56
|
+
maidr-1.7.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
57
|
+
maidr-1.7.0.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
58
|
+
maidr-1.7.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|