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.
- {pycharting-0.2.8 → pycharting-0.2.9}/PKG-INFO +1 -1
- {pycharting-0.2.8 → pycharting-0.2.9}/pyproject.toml +6 -1
- {pycharting-0.2.8 → pycharting-0.2.9}/src/pycharting/__init__.py +1 -1
- pycharting-0.2.9/src/pycharting/data/__init__.py +1 -0
- pycharting-0.2.9/src/pycharting/data/ingestion.py +307 -0
- {pycharting-0.2.8 → pycharting-0.2.9}/README.md +0 -0
- {pycharting-0.2.8 → pycharting-0.2.9}/src/pycharting/api/__init__.py +0 -0
- {pycharting-0.2.8 → pycharting-0.2.9}/src/pycharting/api/interface.py +0 -0
- {pycharting-0.2.8 → pycharting-0.2.9}/src/pycharting/api/routes.py +0 -0
- {pycharting-0.2.8 → pycharting-0.2.9}/src/pycharting/core/__init__.py +0 -0
- {pycharting-0.2.8 → pycharting-0.2.9}/src/pycharting/core/lifecycle.py +0 -0
- {pycharting-0.2.8 → pycharting-0.2.9}/src/pycharting/core/server.py +0 -0
- {pycharting-0.2.8 → pycharting-0.2.9}/src/pycharting/web/__init__.py +0 -0
- {pycharting-0.2.8 → pycharting-0.2.9}/src/pycharting/web/static/demo.html +0 -0
- {pycharting-0.2.8 → pycharting-0.2.9}/src/pycharting/web/static/js/chart.js +0 -0
- {pycharting-0.2.8 → pycharting-0.2.9}/src/pycharting/web/static/js/sync.js +0 -0
- {pycharting-0.2.8 → pycharting-0.2.9}/src/pycharting/web/static/js/viewport.js +0 -0
- {pycharting-0.2.8 → pycharting-0.2.9}/src/pycharting/web/static/multi-chart-demo.html +0 -0
- {pycharting-0.2.8 → pycharting-0.2.9}/src/pycharting/web/static/viewport-demo.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "pycharting"
|
|
3
|
-
version = "0.2.
|
|
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"
|
|
@@ -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
|
|
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
|