pycharting 0.2.8__tar.gz → 0.2.9__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pycharting
3
- Version: 0.2.8
3
+ Version: 0.2.9
4
4
  Summary: High-performance financial charting library for OHLC data visualization with technical indicators
5
5
  Home-page: https://github.com/alihaskar/pycharting
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pycharting"
3
- version = "0.2.8"
3
+ version = "0.2.9"
4
4
  description = "High-performance financial charting library for OHLC data visualization with technical indicators"
5
5
  authors = ["ali askar <26202651+alihaskar@users.noreply.github.com>"]
6
6
  readme = "README.md"
@@ -24,6 +24,11 @@ packages = [
24
24
  { include = "pycharting", from = "src" }
25
25
  ]
26
26
 
27
+ # Include package data (web static files)
28
+ include = [
29
+ { path = "src/pycharting/web/static/**/*", format = ["sdist", "wheel"] }
30
+ ]
31
+
27
32
  [tool.poetry.dependencies]
28
33
  python = "^3.12"
29
34
  fastapi = ">=0.115.0,<1.0.0"
@@ -46,4 +46,4 @@ from .api.interface import plot, stop_server, get_server_status # type: ignore
46
46
  __all__ = ["plot", "stop_server", "get_server_status", "__version__"]
47
47
 
48
48
  # Keep this in sync with pyproject.toml
49
- __version__ = "0.2.8"
49
+ __version__ = "0.2.9"
@@ -0,0 +1 @@
1
+ """Data processing and management for PyCharting."""
@@ -0,0 +1,307 @@
1
+ """
2
+ Data Ingestion and Validation Module.
3
+
4
+ This module is responsible for:
5
+ 1. Validating input data for integrity and consistency (e.g., ensuring arrays are the same length).
6
+ 2. Enforcing financial data constraints (e.g., High must be >= Low).
7
+ 3. Normalizing various input formats (lists, pandas Series/DataFrames) into optimized NumPy arrays.
8
+ 4. Providing efficient, sliced access to large datasets for the API.
9
+
10
+ The `DataManager` class is the core component here, acting as the optimized data store for a chart session.
11
+ """
12
+
13
+ from typing import Optional, Union, List, Dict, Any
14
+ import pandas as pd
15
+ import numpy as np
16
+
17
+
18
+ class DataValidationError(Exception):
19
+ """Exception raised when input data fails validation checks."""
20
+ pass
21
+
22
+
23
+ def validate_input(
24
+ index: Union[pd.Index, np.ndarray],
25
+ open_data: Union[pd.Series, np.ndarray],
26
+ high: Union[pd.Series, np.ndarray],
27
+ low: Union[pd.Series, np.ndarray],
28
+ close: Union[pd.Series, np.ndarray],
29
+ overlays: Optional[Dict[str, Union[pd.Series, np.ndarray]]] = None,
30
+ subplots: Optional[Dict[str, Union[pd.Series, np.ndarray]]] = None,
31
+ ) -> Dict[str, Any]:
32
+ """
33
+ Validate and normalize input data for OHLC charting.
34
+
35
+ This function performs rigorous checks to ensure data integrity:
36
+ - **Type Checking:** Ensures inputs are converted to NumPy arrays.
37
+ - **Length Consistency:** Verifies that all price arrays and the index have the exact same length.
38
+ - **Logic Validation:** Checks that `High >= max(Open, Close)` and `Low <= min(Open, Close)` for all points.
39
+ - **Overlay/Subplot Validation:** Ensures additional series match the length of the main data.
40
+
41
+ Args:
42
+ index (Union[pd.Index, np.ndarray]): The x-axis data.
43
+ open_data (Union[pd.Series, np.ndarray]): Opening prices.
44
+ high (Union[pd.Series, np.ndarray]): High prices.
45
+ low (Union[pd.Series, np.ndarray]): Low prices.
46
+ close (Union[pd.Series, np.ndarray]): Closing prices.
47
+ overlays (Optional[Dict[str, Union[pd.Series, np.ndarray]]]): Dictionary of overlay series.
48
+ subplots (Optional[Dict[str, Union[pd.Series, np.ndarray]]]): Dictionary of subplot series.
49
+
50
+ Returns:
51
+ Dict[str, Any]: A dictionary containing normalized `numpy.ndarray` objects for all inputs.
52
+
53
+ Raises:
54
+ DataValidationError: If any validation check fails (e.g., mismatched lengths, invalid OHLC logic).
55
+ """
56
+ # Convert index to numpy array if needed
57
+ if isinstance(index, pd.Index):
58
+ index_array = index.to_numpy()
59
+ elif isinstance(index, np.ndarray):
60
+ index_array = index
61
+ else:
62
+ raise DataValidationError(f"Index must be pd.Index or np.ndarray, got {type(index)}")
63
+
64
+ # Helper function to convert to numpy array
65
+ def to_array(data: Union[pd.Series, np.ndarray], name: str) -> np.ndarray:
66
+ if isinstance(data, pd.Series):
67
+ return data.to_numpy()
68
+ elif isinstance(data, np.ndarray):
69
+ return data
70
+ else:
71
+ raise DataValidationError(f"{name} must be pd.Series or np.ndarray, got {type(data)}")
72
+
73
+ # Convert OHLC data to arrays
74
+ open_array = to_array(open_data, "Open")
75
+ high_array = to_array(high, "High")
76
+ low_array = to_array(low, "Low")
77
+ close_array = to_array(close, "Close")
78
+
79
+ # Validate shapes
80
+ n = len(index_array)
81
+ for name, arr in [("Open", open_array), ("High", high_array),
82
+ ("Low", low_array), ("Close", close_array)]:
83
+ if len(arr) != n:
84
+ raise DataValidationError(
85
+ f"{name} length ({len(arr)}) does not match index length ({n})"
86
+ )
87
+
88
+ # Validate OHLC constraints
89
+ # High should be >= max(open, close)
90
+ max_oc = np.maximum(open_array, close_array)
91
+ if not np.all(high_array >= max_oc):
92
+ invalid_indices = np.where(high_array < max_oc)[0]
93
+ raise DataValidationError(
94
+ f"High must be >= max(Open, Close). Violations at indices: {invalid_indices[:5]}"
95
+ )
96
+
97
+ # Low should be <= min(open, close)
98
+ min_oc = np.minimum(open_array, close_array)
99
+ if not np.all(low_array <= min_oc):
100
+ invalid_indices = np.where(low_array > min_oc)[0]
101
+ raise DataValidationError(
102
+ f"Low must be <= min(Open, Close). Violations at indices: {invalid_indices[:5]}"
103
+ )
104
+
105
+ result = {
106
+ "index": index_array,
107
+ "open": open_array,
108
+ "high": high_array,
109
+ "low": low_array,
110
+ "close": close_array,
111
+ "overlays": {},
112
+ "subplots": {},
113
+ }
114
+
115
+ # Validate and convert overlays
116
+ if overlays:
117
+ for name, data in overlays.items():
118
+ arr = to_array(data, f"Overlay '{name}'")
119
+ if len(arr) != n:
120
+ raise DataValidationError(
121
+ f"Overlay '{name}' length ({len(arr)}) does not match index length ({n})"
122
+ )
123
+ result["overlays"][name] = arr
124
+
125
+ # Validate and convert subplots
126
+ if subplots:
127
+ for name, data in subplots.items():
128
+ arr = to_array(data, f"Subplot '{name}'")
129
+ if len(arr) != n:
130
+ raise DataValidationError(
131
+ f"Subplot '{name}' length ({len(arr)}) does not match index length ({n})"
132
+ )
133
+ result["subplots"][name] = arr
134
+
135
+ return result
136
+
137
+
138
+ class DataManager:
139
+ """
140
+ High-performance data container and manager.
141
+
142
+ This class holds the financial data in memory as optimized NumPy arrays. It provides
143
+ methods to slice and access this data efficiently for the API. It ensures that
144
+ the data served to the frontend is always consistent and valid.
145
+
146
+ Attributes:
147
+ index (np.ndarray): The x-axis values.
148
+ open (np.ndarray): Opening prices.
149
+ high (np.ndarray): High prices.
150
+ low (np.ndarray): Low prices.
151
+ close (np.ndarray): Closing prices.
152
+ overlays (Dict[str, np.ndarray]): Additional series overlaying the main chart.
153
+ subplots (Dict[str, np.ndarray]): Additional series in separate panels.
154
+ length (int): The total number of data points.
155
+ """
156
+
157
+ def __init__(
158
+ self,
159
+ index: Union[pd.Index, np.ndarray],
160
+ open: Union[pd.Series, np.ndarray],
161
+ high: Union[pd.Series, np.ndarray],
162
+ low: Union[pd.Series, np.ndarray],
163
+ close: Union[pd.Series, np.ndarray],
164
+ overlays: Optional[Dict[str, Union[pd.Series, np.ndarray]]] = None,
165
+ subplots: Optional[Dict[str, Union[pd.Series, np.ndarray]]] = None,
166
+ ):
167
+ """
168
+ Initialize the DataManager with validated OHLC data.
169
+
170
+ Args:
171
+ index (Union[pd.Index, np.ndarray]): Time/Index data.
172
+ open (Union[pd.Series, np.ndarray]): Open prices.
173
+ high (Union[pd.Series, np.ndarray]): High prices.
174
+ low (Union[pd.Series, np.ndarray]): Low prices.
175
+ close (Union[pd.Series, np.ndarray]): Close prices.
176
+ overlays (Optional[Dict[str, Union[pd.Series, np.ndarray]]]): Overlay data series.
177
+ subplots (Optional[Dict[str, Union[pd.Series, np.ndarray]]]): Subplot data series.
178
+
179
+ Raises:
180
+ DataValidationError: If the input data fails validation checks.
181
+ """
182
+ # Validate input and get normalized arrays
183
+ validated = validate_input(index, open, high, low, close, overlays, subplots)
184
+
185
+ # Store references (numpy arrays are views where possible, avoiding duplication)
186
+ self._index = validated["index"]
187
+ self._open = validated["open"]
188
+ self._high = validated["high"]
189
+ self._low = validated["low"]
190
+ self._close = validated["close"]
191
+ self._overlays = validated["overlays"]
192
+ self._subplots = validated["subplots"]
193
+
194
+ self._length = len(self._index)
195
+
196
+ @property
197
+ def index(self) -> np.ndarray:
198
+ """Get the index array."""
199
+ return self._index
200
+
201
+ @property
202
+ def open(self) -> np.ndarray:
203
+ """Get the open prices array."""
204
+ return self._open
205
+
206
+ @property
207
+ def high(self) -> np.ndarray:
208
+ """Get the high prices array."""
209
+ return self._high
210
+
211
+ @property
212
+ def low(self) -> np.ndarray:
213
+ """Get the low prices array."""
214
+ return self._low
215
+
216
+ @property
217
+ def close(self) -> np.ndarray:
218
+ """Get the close prices array."""
219
+ return self._close
220
+
221
+ @property
222
+ def overlays(self) -> Dict[str, np.ndarray]:
223
+ """Get the overlays dictionary."""
224
+ return self._overlays
225
+
226
+ @property
227
+ def subplots(self) -> Dict[str, np.ndarray]:
228
+ """Get the subplots dictionary."""
229
+ return self._subplots
230
+
231
+ @property
232
+ def length(self) -> int:
233
+ """Get the number of data points."""
234
+ return self._length
235
+
236
+ def __len__(self) -> int:
237
+ """Return the number of data points."""
238
+ return self._length
239
+
240
+ def __repr__(self) -> str:
241
+ """String representation of DataManager."""
242
+ overlay_info = f", {len(self._overlays)} overlays" if self._overlays else ""
243
+ subplot_info = f", {len(self._subplots)} subplots" if self._subplots else ""
244
+ return f"DataManager({self._length} points{overlay_info}{subplot_info})"
245
+
246
+ def get_chunk(
247
+ self,
248
+ start_index: Optional[int] = None,
249
+ end_index: Optional[int] = None,
250
+ ) -> Dict[str, Any]:
251
+ """
252
+ Retrieve a slice of the dataset for a specific range.
253
+
254
+ This method is critical for performance. It extracts a specific window of data
255
+ (e.g., what is currently visible on the screen) to send to the client.
256
+ It converts NumPy arrays to standard Python lists for JSON serialization.
257
+
258
+ Args:
259
+ start_index (Optional[int]): The starting index (inclusive). Defaults to 0.
260
+ end_index (Optional[int]): The ending index (exclusive). Defaults to total length.
261
+
262
+ Returns:
263
+ Dict[str, Any]: A dictionary containing sliced data lists for:
264
+ - `index`
265
+ - `open`, `high`, `low`, `close`
266
+ - `overlays` (all keys)
267
+ - `subplots` (all keys)
268
+
269
+ Example:
270
+ ```python
271
+ # Get the first 100 data points
272
+ chunk = dm.get_chunk(0, 100)
273
+
274
+ # Get data from index 500 to the end
275
+ chunk = dm.get_chunk(500, None)
276
+ ```
277
+ """
278
+ # Handle default values
279
+ if start_index is None:
280
+ start_index = 0
281
+ if end_index is None:
282
+ end_index = self._length
283
+
284
+ # Clamp indices to valid range
285
+ start_index = max(0, min(start_index, self._length))
286
+ end_index = max(start_index, min(end_index, self._length))
287
+
288
+ # Slice arrays (views, not copies - very efficient)
289
+ result = {
290
+ "index": self._index[start_index:end_index].tolist(),
291
+ "open": self._open[start_index:end_index].tolist(),
292
+ "high": self._high[start_index:end_index].tolist(),
293
+ "low": self._low[start_index:end_index].tolist(),
294
+ "close": self._close[start_index:end_index].tolist(),
295
+ "overlays": {},
296
+ "subplots": {},
297
+ }
298
+
299
+ # Include overlays
300
+ for name, data in self._overlays.items():
301
+ result["overlays"][name] = data[start_index:end_index].tolist()
302
+
303
+ # Include subplots
304
+ for name, data in self._subplots.items():
305
+ result["subplots"][name] = data[start_index:end_index].tolist()
306
+
307
+ return result
File without changes