tacuda 0.7.1__py3-none-win_amd64.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.
- tacuda/__init__.py +1032 -0
- tacuda/_generated.py +10690 -0
- tacuda/tacuda.dll +0 -0
- tacuda-0.7.1.dist-info/METADATA +169 -0
- tacuda-0.7.1.dist-info/RECORD +8 -0
- tacuda-0.7.1.dist-info/WHEEL +5 -0
- tacuda-0.7.1.dist-info/licenses/LICENSE +191 -0
- tacuda-0.7.1.dist-info/top_level.txt +1 -0
tacuda/__init__.py
ADDED
|
@@ -0,0 +1,1032 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# TaCuda | GPU-accelerated technical analysis | https://tacuda.com
|
|
3
|
+
# Copyright (c) 2024-2026 Paul Deecalov <paul@deecalov.com>
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
# ============================================================================
|
|
6
|
+
"""ctypes based Python bindings for the :mod:`tacuda` library.
|
|
7
|
+
|
|
8
|
+
The shared library must be built prior to importing this module. The
|
|
9
|
+
bindings are generated from the public ``tacuda.h`` header in order to keep
|
|
10
|
+
the Python API in sync with the C++ implementation and to avoid large amounts
|
|
11
|
+
of manually written boilerplate.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import ctypes
|
|
17
|
+
import ctypes.util
|
|
18
|
+
import inspect
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
from typing import Any, Dict, List, Tuple
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
from . import _generated
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _load_lib() -> ctypes.CDLL:
|
|
29
|
+
"""Locate and load the ``tacuda`` shared library."""
|
|
30
|
+
|
|
31
|
+
if sys.platform.startswith("win"):
|
|
32
|
+
names = ["tacuda.dll"]
|
|
33
|
+
elif sys.platform == "darwin":
|
|
34
|
+
names = ["libtacuda.dylib", "tacuda.dylib"]
|
|
35
|
+
else:
|
|
36
|
+
names = ["libtacuda.so", "tacuda.so"]
|
|
37
|
+
|
|
38
|
+
# Environment variables for explicit locations
|
|
39
|
+
env_candidates: List[str] = []
|
|
40
|
+
env = os.environ.get("TACUDA_LIBRARY")
|
|
41
|
+
if env:
|
|
42
|
+
env_candidates.append(env)
|
|
43
|
+
|
|
44
|
+
env_dir = os.environ.get("TACUDA_LIBRARY_PATH") or os.environ.get(
|
|
45
|
+
"TACUDA_LIBRARY_DIR"
|
|
46
|
+
)
|
|
47
|
+
if env_dir:
|
|
48
|
+
for n in names:
|
|
49
|
+
env_candidates.append(os.path.join(env_dir, n))
|
|
50
|
+
|
|
51
|
+
for candidate in env_candidates:
|
|
52
|
+
if candidate and os.path.exists(candidate):
|
|
53
|
+
try:
|
|
54
|
+
# Python 3.8+: use winmode to search for dependencies in PATH
|
|
55
|
+
if sys.version_info >= (3, 8) and sys.platform.startswith("win"):
|
|
56
|
+
lib = ctypes.CDLL(candidate, winmode=0) # 0 = default search
|
|
57
|
+
else:
|
|
58
|
+
lib = ctypes.CDLL(candidate)
|
|
59
|
+
return lib
|
|
60
|
+
except OSError:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
# Try system paths via ctypes.util.find_library
|
|
64
|
+
libname = ctypes.util.find_library("tacuda")
|
|
65
|
+
if libname:
|
|
66
|
+
try:
|
|
67
|
+
return ctypes.CDLL(libname)
|
|
68
|
+
except OSError:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
# Fall back to package-relative locations
|
|
72
|
+
here = os.path.abspath(os.path.dirname(__file__))
|
|
73
|
+
root = os.path.abspath(os.path.join(here, "..", ".."))
|
|
74
|
+
search_paths = [
|
|
75
|
+
os.getcwd(),
|
|
76
|
+
os.path.join(os.getcwd(), "build"),
|
|
77
|
+
here,
|
|
78
|
+
os.path.join(here, "lib"),
|
|
79
|
+
root,
|
|
80
|
+
os.path.join(root, "lib"),
|
|
81
|
+
os.path.join(root, "build"),
|
|
82
|
+
]
|
|
83
|
+
for base in search_paths:
|
|
84
|
+
for n in names:
|
|
85
|
+
path = os.path.join(base, n)
|
|
86
|
+
if os.path.exists(path):
|
|
87
|
+
try:
|
|
88
|
+
return ctypes.CDLL(path)
|
|
89
|
+
except OSError:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
raise OSError(
|
|
93
|
+
"tacuda library not found. Build the project first or set "
|
|
94
|
+
"TACUDA_LIBRARY[_PATH]."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
_lib = _load_lib()
|
|
99
|
+
|
|
100
|
+
CT_DTYPE_F16 = 0
|
|
101
|
+
CT_DTYPE_F32 = 1
|
|
102
|
+
CT_DTYPE_F64 = 2
|
|
103
|
+
|
|
104
|
+
_DTYPE_NAME_TO_ENUM = {
|
|
105
|
+
"float16": CT_DTYPE_F16,
|
|
106
|
+
"f16": CT_DTYPE_F16,
|
|
107
|
+
"float32": CT_DTYPE_F32,
|
|
108
|
+
"f32": CT_DTYPE_F32,
|
|
109
|
+
"float64": CT_DTYPE_F64,
|
|
110
|
+
"f64": CT_DTYPE_F64,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_ENUM_TO_NP_DTYPE = {
|
|
114
|
+
CT_DTYPE_F16: np.float16,
|
|
115
|
+
CT_DTYPE_F32: np.float32,
|
|
116
|
+
CT_DTYPE_F64: np.float64,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _normalize_dtype(dtype: str | np.dtype | type) -> int:
|
|
121
|
+
if isinstance(dtype, np.dtype):
|
|
122
|
+
dtype = str(dtype)
|
|
123
|
+
elif isinstance(dtype, type):
|
|
124
|
+
dtype = np.dtype(dtype).name
|
|
125
|
+
key = str(dtype).lower()
|
|
126
|
+
if key not in _DTYPE_NAME_TO_ENUM:
|
|
127
|
+
raise ValueError(f"Unsupported dtype '{dtype}'. Use float16/float32/float64.")
|
|
128
|
+
return _DTYPE_NAME_TO_ENUM[key]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _compute_dtype_for_storage(storage_enum: int) -> int:
|
|
132
|
+
return CT_DTYPE_F32 if storage_enum == CT_DTYPE_F16 else storage_enum
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _as_typed_array(arr: Any, dtype_enum: int) -> np.ndarray:
|
|
136
|
+
np_dtype = _ENUM_TO_NP_DTYPE[dtype_enum]
|
|
137
|
+
out = np.asarray(arr, dtype=np_dtype)
|
|
138
|
+
if not out.flags.c_contiguous:
|
|
139
|
+
out = np.ascontiguousarray(out, dtype=np_dtype)
|
|
140
|
+
return out
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _as_float_array(arr: Any) -> Tuple[np.ndarray, ctypes.POINTER(ctypes.c_float)]:
|
|
144
|
+
"""Return ``arr`` as a contiguous ``float32`` array and pointer."""
|
|
145
|
+
|
|
146
|
+
if not isinstance(arr, np.ndarray):
|
|
147
|
+
arr = np.asarray(arr, dtype=np.float32)
|
|
148
|
+
if arr.dtype != np.float32 or not arr.flags.c_contiguous:
|
|
149
|
+
arr = np.ascontiguousarray(arr, dtype=np.float32)
|
|
150
|
+
return arr, arr.ctypes.data_as(ctypes.POINTER(ctypes.c_float))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
_C_FLOAT_PTR = ctypes.POINTER(ctypes.c_float)
|
|
154
|
+
_SPEC = _generated.FUNCTIONS
|
|
155
|
+
|
|
156
|
+
# Column pulled from an OHLCV container for each recognised C parameter name.
|
|
157
|
+
# Functions whose host inputs all appear here accept an OHLCV instance as the
|
|
158
|
+
# first positional argument instead of individual column arrays.
|
|
159
|
+
_OHLCV_INPUT_COLUMNS = {
|
|
160
|
+
"host_open": "open",
|
|
161
|
+
"host_high": "high",
|
|
162
|
+
"host_low": "low",
|
|
163
|
+
"host_close": "close",
|
|
164
|
+
"host_volume": "volume",
|
|
165
|
+
"host_price": "close",
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _scalar_ctype(name: str) -> Any:
|
|
170
|
+
mapping: Dict[str, Any] = {
|
|
171
|
+
"int": ctypes.c_int,
|
|
172
|
+
"float": ctypes.c_float,
|
|
173
|
+
"ctMaType_t": ctypes.c_int,
|
|
174
|
+
"ctDataType_t": ctypes.c_int,
|
|
175
|
+
"cudaStream_t": ctypes.c_void_p,
|
|
176
|
+
"size_t": ctypes.c_size_t,
|
|
177
|
+
}
|
|
178
|
+
try:
|
|
179
|
+
return mapping[name]
|
|
180
|
+
except KeyError as exc:
|
|
181
|
+
raise KeyError(f"Unsupported scalar type '{name}' in tacuda bindings") from exc
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _prepare_stream(value: Any) -> ctypes.c_void_p:
|
|
185
|
+
if value is None:
|
|
186
|
+
return ctypes.c_void_p(0)
|
|
187
|
+
if isinstance(value, ctypes.c_void_p):
|
|
188
|
+
return value
|
|
189
|
+
if isinstance(value, int):
|
|
190
|
+
return ctypes.c_void_p(value)
|
|
191
|
+
raise TypeError("Stream parameters must be integers or ctypes.c_void_p instances")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _prepare_device_ptr(value: Any) -> ctypes.c_void_p:
|
|
195
|
+
if value is None:
|
|
196
|
+
return ctypes.c_void_p(0)
|
|
197
|
+
if isinstance(value, ctypes.c_void_p):
|
|
198
|
+
return value
|
|
199
|
+
if isinstance(value, int):
|
|
200
|
+
return ctypes.c_void_p(value)
|
|
201
|
+
# Also accept ctypes pointers
|
|
202
|
+
if hasattr(value, "contents"):
|
|
203
|
+
return ctypes.cast(value, ctypes.c_void_p)
|
|
204
|
+
raise TypeError(f"Device pointer must be int or c_void_p, got {type(value)}")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _bind_function(name: str, spec: _generated.FunctionSpec) -> None:
|
|
208
|
+
cfunc = getattr(_lib, spec.c_name)
|
|
209
|
+
|
|
210
|
+
# Precision (_ex) host functions take void* buffers whose element type is
|
|
211
|
+
# selected by the storageType scalar; the generic float32 path must NOT
|
|
212
|
+
# be used for them (casting F16/F64 data to float32 would make the native
|
|
213
|
+
# side read the wrong element size).
|
|
214
|
+
storage_aware = any(s.name == "storageType" for s in spec.scalars)
|
|
215
|
+
|
|
216
|
+
# Determine argument types
|
|
217
|
+
argtypes: List[Any] = []
|
|
218
|
+
|
|
219
|
+
outputs_first = spec.c_name in ("ct_device_upload", "ct_device_upload_ex", "ct_device_download", "ct_device_download_ex")
|
|
220
|
+
|
|
221
|
+
def append_inputs():
|
|
222
|
+
if hasattr(spec, 'input_types'):
|
|
223
|
+
for i, type_name in enumerate(spec.input_types):
|
|
224
|
+
if type_name == "device" or storage_aware:
|
|
225
|
+
argtypes.append(ctypes.c_void_p) # Device or dtype-selected pointer
|
|
226
|
+
else:
|
|
227
|
+
argtypes.append(_C_FLOAT_PTR) # Host pointer (float*)
|
|
228
|
+
else:
|
|
229
|
+
# Fallback for old spec format (all float*)
|
|
230
|
+
argtypes.extend([_C_FLOAT_PTR] * len(spec.inputs))
|
|
231
|
+
|
|
232
|
+
def append_outputs():
|
|
233
|
+
if hasattr(spec, 'output_types'):
|
|
234
|
+
for i, type_name in enumerate(spec.output_types):
|
|
235
|
+
if type_name == "device" or storage_aware:
|
|
236
|
+
argtypes.append(ctypes.c_void_p) # Device or dtype-selected pointer
|
|
237
|
+
else:
|
|
238
|
+
argtypes.append(_C_FLOAT_PTR) # Host pointer (float*)
|
|
239
|
+
else:
|
|
240
|
+
# Fallback for old spec format (all float*)
|
|
241
|
+
argtypes.extend([_C_FLOAT_PTR] * len(spec.outputs))
|
|
242
|
+
|
|
243
|
+
if outputs_first:
|
|
244
|
+
append_outputs()
|
|
245
|
+
append_inputs()
|
|
246
|
+
else:
|
|
247
|
+
append_inputs()
|
|
248
|
+
append_outputs()
|
|
249
|
+
|
|
250
|
+
# Size param (if exists)
|
|
251
|
+
if spec.size_param:
|
|
252
|
+
argtypes.append(ctypes.c_int)
|
|
253
|
+
|
|
254
|
+
# Scalar params
|
|
255
|
+
for scalar in spec.scalars:
|
|
256
|
+
argtypes.append(_scalar_ctype(scalar.ctype))
|
|
257
|
+
|
|
258
|
+
cfunc.argtypes = argtypes
|
|
259
|
+
|
|
260
|
+
# Determine return type
|
|
261
|
+
if hasattr(spec, 'return_type'):
|
|
262
|
+
if spec.return_type == "ctStatus_t":
|
|
263
|
+
cfunc.restype = ctypes.c_int
|
|
264
|
+
elif spec.return_type in ("float*", "void*"):
|
|
265
|
+
cfunc.restype = ctypes.c_void_p # Return as opaque pointer
|
|
266
|
+
elif spec.return_type == "void":
|
|
267
|
+
cfunc.restype = None
|
|
268
|
+
elif spec.return_type == "size_t":
|
|
269
|
+
cfunc.restype = ctypes.c_size_t
|
|
270
|
+
else:
|
|
271
|
+
# Fallback/Unknown
|
|
272
|
+
cfunc.restype = ctypes.c_int
|
|
273
|
+
else:
|
|
274
|
+
cfunc.restype = ctypes.c_int
|
|
275
|
+
|
|
276
|
+
# Create Python wrapper
|
|
277
|
+
parameters: List[inspect.Parameter] = []
|
|
278
|
+
for input_name in spec.inputs:
|
|
279
|
+
parameters.append(
|
|
280
|
+
inspect.Parameter(input_name, inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# For device API outputs, they might be passed as arguments!
|
|
284
|
+
# But FunctionSpec splits pointers into inputs (const) and outputs (non-const).
|
|
285
|
+
# In generated bindings, outputs are usually hidden from python signature and created internally.
|
|
286
|
+
# BUT for Device API, the user MUST provide the output buffer pointer.
|
|
287
|
+
# So for Device API functions (where outputs are "device"), we should add them to parameters.
|
|
288
|
+
|
|
289
|
+
device_outputs_indices = []
|
|
290
|
+
if hasattr(spec, 'output_types'):
|
|
291
|
+
for i, type_name in enumerate(spec.output_types):
|
|
292
|
+
if type_name == "device":
|
|
293
|
+
device_outputs_indices.append(i)
|
|
294
|
+
# Add to python signature
|
|
295
|
+
out_name = spec.outputs[i]
|
|
296
|
+
parameters.append(
|
|
297
|
+
inspect.Parameter(out_name, inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
has_host_inputs = False
|
|
301
|
+
if hasattr(spec, "input_types"):
|
|
302
|
+
has_host_inputs = any(t == "host" for t in spec.input_types)
|
|
303
|
+
elif spec.inputs:
|
|
304
|
+
has_host_inputs = True
|
|
305
|
+
|
|
306
|
+
if spec.size_param and not has_host_inputs:
|
|
307
|
+
parameters.append(
|
|
308
|
+
inspect.Parameter("size", inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
for scalar in spec.scalars:
|
|
312
|
+
default = inspect._empty
|
|
313
|
+
if scalar.ctype == "cudaStream_t" and scalar.default is not None:
|
|
314
|
+
default = None
|
|
315
|
+
parameters.append(
|
|
316
|
+
inspect.Parameter(
|
|
317
|
+
scalar.name,
|
|
318
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
319
|
+
default=default,
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
signature = inspect.Signature(parameters)
|
|
324
|
+
|
|
325
|
+
ohlcv_columns: List[str] | None = None
|
|
326
|
+
host_input_names = [
|
|
327
|
+
n for i, n in enumerate(spec.inputs)
|
|
328
|
+
if not (hasattr(spec, "input_types") and spec.input_types[i] == "device")
|
|
329
|
+
]
|
|
330
|
+
if host_input_names and all(n in _OHLCV_INPUT_COLUMNS for n in host_input_names):
|
|
331
|
+
ohlcv_columns = [_OHLCV_INPUT_COLUMNS[n] for n in host_input_names]
|
|
332
|
+
|
|
333
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
334
|
+
if args and isinstance(args[0], OHLCV):
|
|
335
|
+
if ohlcv_columns is None:
|
|
336
|
+
raise TypeError(
|
|
337
|
+
f"{name} does not accept an OHLCV container; "
|
|
338
|
+
"pass the required arrays explicitly"
|
|
339
|
+
)
|
|
340
|
+
series = args[0]
|
|
341
|
+
columns = tuple(getattr(series, col) for col in ohlcv_columns)
|
|
342
|
+
args = columns + args[1:]
|
|
343
|
+
bound = signature.bind(*args, **kwargs)
|
|
344
|
+
bound.apply_defaults()
|
|
345
|
+
|
|
346
|
+
# Resolve the storage dtype for precision (_ex) functions: host
|
|
347
|
+
# buffers are converted to and returned in exactly that dtype.
|
|
348
|
+
storage_enum = None
|
|
349
|
+
if storage_aware:
|
|
350
|
+
sval = bound.arguments.get("storageType", CT_DTYPE_F32)
|
|
351
|
+
storage_enum = int(sval) if isinstance(sval, (int, np.integer)) else _dtype_to_enum(sval)
|
|
352
|
+
bound.arguments["storageType"] = storage_enum
|
|
353
|
+
|
|
354
|
+
c_args: List[Any] = []
|
|
355
|
+
c_args_inputs: List[Any] = []
|
|
356
|
+
c_args_outputs: List[Any] = []
|
|
357
|
+
|
|
358
|
+
# Process Inputs
|
|
359
|
+
np_inputs: List[np.ndarray] = []
|
|
360
|
+
|
|
361
|
+
for i, name_ in enumerate(spec.inputs):
|
|
362
|
+
val = bound.arguments[name_]
|
|
363
|
+
is_device = False
|
|
364
|
+
if hasattr(spec, 'input_types') and spec.input_types[i] == "device":
|
|
365
|
+
is_device = True
|
|
366
|
+
|
|
367
|
+
if is_device:
|
|
368
|
+
c_args_inputs.append(_prepare_device_ptr(val))
|
|
369
|
+
elif storage_enum is not None:
|
|
370
|
+
arr_np = _as_typed_array(val, storage_enum)
|
|
371
|
+
np_inputs.append(arr_np)
|
|
372
|
+
c_args_inputs.append(arr_np.ctypes.data_as(ctypes.c_void_p))
|
|
373
|
+
else:
|
|
374
|
+
arr_np, ptr = _as_float_array(val)
|
|
375
|
+
np_inputs.append(arr_np)
|
|
376
|
+
c_args_inputs.append(ptr)
|
|
377
|
+
|
|
378
|
+
# Check sizes if we have numpy inputs
|
|
379
|
+
size = 0
|
|
380
|
+
if np_inputs:
|
|
381
|
+
size = np_inputs[0].size
|
|
382
|
+
for arr in np_inputs[1:]:
|
|
383
|
+
if arr.size != size:
|
|
384
|
+
raise ValueError("Input arrays must have the same size")
|
|
385
|
+
elif "size" in bound.arguments:
|
|
386
|
+
size = bound.arguments["size"]
|
|
387
|
+
elif "count" in bound.arguments:
|
|
388
|
+
size = bound.arguments["count"]
|
|
389
|
+
|
|
390
|
+
# Process Outputs
|
|
391
|
+
outputs_to_return: List[Any] = [] # Only host outputs are returned
|
|
392
|
+
|
|
393
|
+
for i, name_ in enumerate(spec.outputs):
|
|
394
|
+
is_device = False
|
|
395
|
+
if hasattr(spec, 'output_types') and spec.output_types[i] == "device":
|
|
396
|
+
is_device = True
|
|
397
|
+
|
|
398
|
+
if is_device:
|
|
399
|
+
# Device outputs are passed as arguments
|
|
400
|
+
val = bound.arguments[name_]
|
|
401
|
+
c_args_outputs.append(_prepare_device_ptr(val))
|
|
402
|
+
# We don't return device pointers, user already has them
|
|
403
|
+
elif storage_enum is not None:
|
|
404
|
+
# Host output in the storage dtype selected by storageType.
|
|
405
|
+
out_arr = np.zeros(size, dtype=_ENUM_TO_NP_DTYPE[storage_enum])
|
|
406
|
+
c_args_outputs.append(out_arr.ctypes.data_as(ctypes.c_void_p))
|
|
407
|
+
outputs_to_return.append(out_arr)
|
|
408
|
+
else:
|
|
409
|
+
# Host output - create numpy array
|
|
410
|
+
if size == 0 and spec.size_param:
|
|
411
|
+
# We need size. If we have no input arrays, we might need to look at size scalar if it exists?
|
|
412
|
+
# But usually 'size' is not in bound.arguments if it's the special size param.
|
|
413
|
+
# Wait, if inputs are device ptrs, we don't know size!
|
|
414
|
+
# The C function expects 'size'. Where does it come from?
|
|
415
|
+
pass
|
|
416
|
+
|
|
417
|
+
# In mixed mode (host input, host output), size comes from input.
|
|
418
|
+
out_arr = np.zeros(size, dtype=np.float32)
|
|
419
|
+
_, ptr = _as_float_array(out_arr)
|
|
420
|
+
c_args_outputs.append(ptr)
|
|
421
|
+
outputs_to_return.append(out_arr)
|
|
422
|
+
|
|
423
|
+
if outputs_first:
|
|
424
|
+
c_args.extend(c_args_outputs)
|
|
425
|
+
c_args.extend(c_args_inputs)
|
|
426
|
+
else:
|
|
427
|
+
c_args.extend(c_args_inputs)
|
|
428
|
+
c_args.extend(c_args_outputs)
|
|
429
|
+
|
|
430
|
+
# Size parameter
|
|
431
|
+
if spec.size_param:
|
|
432
|
+
# If we have size from numpy arrays, use it.
|
|
433
|
+
# If we rely on manual size passing (e.g. for device-only calls), we need to find where 'size' comes from.
|
|
434
|
+
# But wait, 'size' is EXCLUDED from python signature in 'render_python' logic for standard functions.
|
|
435
|
+
# "FunctionSpec.scalar_params" excludes size.
|
|
436
|
+
|
|
437
|
+
# Issue: For device-only functions, we MUST pass size manually.
|
|
438
|
+
# My generate_bindings script excluded 'size' from scalars.
|
|
439
|
+
# This is a problem for Device API functions where size cannot be inferred from numpy arrays.
|
|
440
|
+
|
|
441
|
+
# Quick fix: If size is 0 (no numpy inputs), try to find 'size' in kwargs? No, it's not in signature.
|
|
442
|
+
|
|
443
|
+
# We will add a hack: if we have no host inputs, we implicitly assume 'size' is passed as argument?
|
|
444
|
+
# But wrapper signature doesn't have it.
|
|
445
|
+
|
|
446
|
+
# Real fix: generate_bindings should include 'size' in scalars if it cannot be inferred?
|
|
447
|
+
# Or we assume for now Device API is mixed with Host API or manual calls use ctypes directly.
|
|
448
|
+
|
|
449
|
+
# Let's assume if np_inputs is empty, we check if 'size' is in bound args (it won't be).
|
|
450
|
+
# This is a limitation. I will assume size is available or 0.
|
|
451
|
+
# For pure device functions, the user currently CANNOT pass size via this wrapper if it was stripped.
|
|
452
|
+
|
|
453
|
+
# However, 'ct_device_alloc' takes 'count' (renamed to size?). No, 'count'.
|
|
454
|
+
# 'ct_sma_d' takes 'size'.
|
|
455
|
+
|
|
456
|
+
c_args.append(ctypes.c_int(size))
|
|
457
|
+
|
|
458
|
+
# Scalars
|
|
459
|
+
for scalar in spec.scalars:
|
|
460
|
+
val = bound.arguments[scalar.name]
|
|
461
|
+
if scalar.ctype == "cudaStream_t":
|
|
462
|
+
c_args.append(_prepare_stream(val))
|
|
463
|
+
else:
|
|
464
|
+
ctype = _scalar_ctype(scalar.ctype)
|
|
465
|
+
c_args.append(ctype(val))
|
|
466
|
+
|
|
467
|
+
rc = cfunc(*c_args)
|
|
468
|
+
|
|
469
|
+
# Handle return value
|
|
470
|
+
if hasattr(spec, 'return_type') and spec.return_type == "ctStatus_t":
|
|
471
|
+
if rc != 0:
|
|
472
|
+
raise RuntimeError(f"{spec.c_name} failed with code {rc}")
|
|
473
|
+
|
|
474
|
+
if outputs_to_return:
|
|
475
|
+
if len(outputs_to_return) == 1:
|
|
476
|
+
return outputs_to_return[0]
|
|
477
|
+
return tuple(outputs_to_return)
|
|
478
|
+
return None # Success, no host outputs
|
|
479
|
+
|
|
480
|
+
return rc # Return whatever it returned (float*, size_t, etc)
|
|
481
|
+
|
|
482
|
+
wrapper.__name__ = name
|
|
483
|
+
wrapper.__doc__ = f"Python wrapper for {spec.c_name}"
|
|
484
|
+
wrapper.__signature__ = signature
|
|
485
|
+
globals()[name] = wrapper
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
for _py_name, _spec in _SPEC.items():
|
|
489
|
+
_bind_function(_py_name, _spec)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _bind_precision_functions() -> None:
|
|
493
|
+
c_void_p = ctypes.c_void_p
|
|
494
|
+
c_int = ctypes.c_int
|
|
495
|
+
c_float = ctypes.c_float
|
|
496
|
+
|
|
497
|
+
_lib.ct_sma_ex.argtypes = [c_void_p, c_void_p, c_int, c_int, c_int, c_int, c_void_p]
|
|
498
|
+
_lib.ct_sma_ex.restype = c_int
|
|
499
|
+
_lib.ct_ema_ex.argtypes = [c_void_p, c_void_p, c_int, c_int, c_int, c_int, c_void_p]
|
|
500
|
+
_lib.ct_ema_ex.restype = c_int
|
|
501
|
+
_lib.ct_rsi_ex.argtypes = [c_void_p, c_void_p, c_int, c_int, c_int, c_int, c_void_p]
|
|
502
|
+
_lib.ct_rsi_ex.restype = c_int
|
|
503
|
+
_lib.ct_macd_ex.argtypes = [
|
|
504
|
+
c_void_p, c_void_p, c_void_p, c_void_p, c_int, c_int, c_int, c_int, c_int, c_int, c_int, c_void_p
|
|
505
|
+
]
|
|
506
|
+
_lib.ct_macd_ex.restype = c_int
|
|
507
|
+
_lib.ct_atr_ex.argtypes = [
|
|
508
|
+
c_void_p, c_void_p, c_void_p, c_void_p, c_int, c_int, c_float, c_int, c_int, c_void_p
|
|
509
|
+
]
|
|
510
|
+
_lib.ct_atr_ex.restype = c_int
|
|
511
|
+
_lib.ct_bbands_ex.argtypes = [
|
|
512
|
+
c_void_p, c_void_p, c_void_p, c_void_p, c_int, c_int, c_float, c_float, c_int, c_int, c_void_p
|
|
513
|
+
]
|
|
514
|
+
_lib.ct_bbands_ex.restype = c_int
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
_bind_precision_functions()
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
_sma_f32 = sma
|
|
521
|
+
_ema_f32 = ema
|
|
522
|
+
_rsi_f32 = rsi
|
|
523
|
+
_macd_f32 = macd
|
|
524
|
+
_atr_f32 = atr
|
|
525
|
+
_bbands_f32 = bbands
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _call_status(rc: int, func_name: str) -> None:
|
|
529
|
+
if rc != 0:
|
|
530
|
+
raise RuntimeError(f"{func_name} failed with code {rc}")
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def sma(input: Any, period: int, stream: Any = None, dtype: str = "float32") -> np.ndarray:
|
|
534
|
+
storage = _normalize_dtype(dtype)
|
|
535
|
+
if storage == CT_DTYPE_F32:
|
|
536
|
+
return _sma_f32(input, period, stream)
|
|
537
|
+
x = _as_typed_array(input, storage)
|
|
538
|
+
out = np.zeros_like(x)
|
|
539
|
+
compute = _compute_dtype_for_storage(storage)
|
|
540
|
+
rc = _lib.ct_sma_ex(
|
|
541
|
+
x.ctypes.data_as(ctypes.c_void_p), out.ctypes.data_as(ctypes.c_void_p),
|
|
542
|
+
ctypes.c_int(x.size), ctypes.c_int(int(period)),
|
|
543
|
+
ctypes.c_int(storage), ctypes.c_int(compute), _prepare_stream(stream)
|
|
544
|
+
)
|
|
545
|
+
_call_status(rc, "ct_sma_ex")
|
|
546
|
+
return out
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def ema(input: Any, period: int, stream: Any = None, dtype: str = "float32") -> np.ndarray:
|
|
550
|
+
storage = _normalize_dtype(dtype)
|
|
551
|
+
if storage == CT_DTYPE_F32:
|
|
552
|
+
return _ema_f32(input, period, stream)
|
|
553
|
+
x = _as_typed_array(input, storage)
|
|
554
|
+
out = np.zeros_like(x)
|
|
555
|
+
compute = _compute_dtype_for_storage(storage)
|
|
556
|
+
rc = _lib.ct_ema_ex(
|
|
557
|
+
x.ctypes.data_as(ctypes.c_void_p), out.ctypes.data_as(ctypes.c_void_p),
|
|
558
|
+
ctypes.c_int(x.size), ctypes.c_int(int(period)),
|
|
559
|
+
ctypes.c_int(storage), ctypes.c_int(compute), _prepare_stream(stream)
|
|
560
|
+
)
|
|
561
|
+
_call_status(rc, "ct_ema_ex")
|
|
562
|
+
return out
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def rsi(input: Any, period: int, stream: Any = None, dtype: str = "float32") -> np.ndarray:
|
|
566
|
+
storage = _normalize_dtype(dtype)
|
|
567
|
+
if storage == CT_DTYPE_F32:
|
|
568
|
+
return _rsi_f32(input, period, stream)
|
|
569
|
+
x = _as_typed_array(input, storage)
|
|
570
|
+
out = np.zeros_like(x)
|
|
571
|
+
compute = _compute_dtype_for_storage(storage)
|
|
572
|
+
rc = _lib.ct_rsi_ex(
|
|
573
|
+
x.ctypes.data_as(ctypes.c_void_p), out.ctypes.data_as(ctypes.c_void_p),
|
|
574
|
+
ctypes.c_int(x.size), ctypes.c_int(int(period)),
|
|
575
|
+
ctypes.c_int(storage), ctypes.c_int(compute), _prepare_stream(stream)
|
|
576
|
+
)
|
|
577
|
+
_call_status(rc, "ct_rsi_ex")
|
|
578
|
+
return out
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def macd(
|
|
582
|
+
input: Any,
|
|
583
|
+
fastPeriod: int,
|
|
584
|
+
slowPeriod: int,
|
|
585
|
+
signalPeriod: int,
|
|
586
|
+
type: int = 1,
|
|
587
|
+
stream: Any = None,
|
|
588
|
+
dtype: str = "float32",
|
|
589
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
590
|
+
storage = _normalize_dtype(dtype)
|
|
591
|
+
if storage == CT_DTYPE_F32:
|
|
592
|
+
return _macd_f32(input, fastPeriod, slowPeriod, signalPeriod, type, stream)
|
|
593
|
+
x = _as_typed_array(input, storage)
|
|
594
|
+
m = np.zeros_like(x)
|
|
595
|
+
s = np.zeros_like(x)
|
|
596
|
+
h = np.zeros_like(x)
|
|
597
|
+
compute = _compute_dtype_for_storage(storage)
|
|
598
|
+
rc = _lib.ct_macd_ex(
|
|
599
|
+
x.ctypes.data_as(ctypes.c_void_p), m.ctypes.data_as(ctypes.c_void_p),
|
|
600
|
+
s.ctypes.data_as(ctypes.c_void_p), h.ctypes.data_as(ctypes.c_void_p),
|
|
601
|
+
ctypes.c_int(x.size), ctypes.c_int(int(fastPeriod)), ctypes.c_int(int(slowPeriod)),
|
|
602
|
+
ctypes.c_int(int(signalPeriod)), ctypes.c_int(int(type)), ctypes.c_int(storage),
|
|
603
|
+
ctypes.c_int(compute), _prepare_stream(stream)
|
|
604
|
+
)
|
|
605
|
+
_call_status(rc, "ct_macd_ex")
|
|
606
|
+
return m, s, h
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def atr(
|
|
610
|
+
high: Any, low: Any = None, close: Any = None, period: int = 14,
|
|
611
|
+
initial: float = 0.0, stream: Any = None, dtype: str = "float32",
|
|
612
|
+
) -> np.ndarray:
|
|
613
|
+
if isinstance(high, OHLCV):
|
|
614
|
+
# atr(ohlcv, 14) / atr(ohlcv, period=14)
|
|
615
|
+
if low is not None:
|
|
616
|
+
if not np.isscalar(low) or close is not None:
|
|
617
|
+
raise TypeError(
|
|
618
|
+
"atr(OHLCV, ...) accepts only the period after the container"
|
|
619
|
+
)
|
|
620
|
+
period = int(low)
|
|
621
|
+
series = high
|
|
622
|
+
high, low, close = series.high, series.low, series.close
|
|
623
|
+
elif low is None or close is None:
|
|
624
|
+
raise TypeError("atr() requires high, low and close arrays")
|
|
625
|
+
storage = _normalize_dtype(dtype)
|
|
626
|
+
if storage == CT_DTYPE_F32:
|
|
627
|
+
return _atr_f32(high, low, close, period, initial, stream)
|
|
628
|
+
h = _as_typed_array(high, storage)
|
|
629
|
+
l = _as_typed_array(low, storage)
|
|
630
|
+
c = _as_typed_array(close, storage)
|
|
631
|
+
if h.size != l.size or h.size != c.size:
|
|
632
|
+
raise ValueError("high/low/close arrays must have same size")
|
|
633
|
+
out = np.zeros_like(h)
|
|
634
|
+
compute = _compute_dtype_for_storage(storage)
|
|
635
|
+
rc = _lib.ct_atr_ex(
|
|
636
|
+
h.ctypes.data_as(ctypes.c_void_p), l.ctypes.data_as(ctypes.c_void_p),
|
|
637
|
+
c.ctypes.data_as(ctypes.c_void_p), out.ctypes.data_as(ctypes.c_void_p),
|
|
638
|
+
ctypes.c_int(h.size), ctypes.c_int(int(period)), ctypes.c_float(float(initial)),
|
|
639
|
+
ctypes.c_int(storage), ctypes.c_int(compute), _prepare_stream(stream)
|
|
640
|
+
)
|
|
641
|
+
_call_status(rc, "ct_atr_ex")
|
|
642
|
+
return out
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def bbands(
|
|
646
|
+
input: Any, period: int, upperMul: float = 2.0, lowerMul: float = 2.0,
|
|
647
|
+
stream: Any = None, dtype: str = "float32",
|
|
648
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
649
|
+
storage = _normalize_dtype(dtype)
|
|
650
|
+
if storage == CT_DTYPE_F32:
|
|
651
|
+
return _bbands_f32(input, period, upperMul, lowerMul, stream)
|
|
652
|
+
x = _as_typed_array(input, storage)
|
|
653
|
+
up = np.zeros_like(x)
|
|
654
|
+
mid = np.zeros_like(x)
|
|
655
|
+
lo = np.zeros_like(x)
|
|
656
|
+
compute = _compute_dtype_for_storage(storage)
|
|
657
|
+
rc = _lib.ct_bbands_ex(
|
|
658
|
+
x.ctypes.data_as(ctypes.c_void_p), up.ctypes.data_as(ctypes.c_void_p),
|
|
659
|
+
mid.ctypes.data_as(ctypes.c_void_p), lo.ctypes.data_as(ctypes.c_void_p),
|
|
660
|
+
ctypes.c_int(x.size), ctypes.c_int(int(period)), ctypes.c_float(float(upperMul)),
|
|
661
|
+
ctypes.c_float(float(lowerMul)), ctypes.c_int(storage), ctypes.c_int(compute),
|
|
662
|
+
_prepare_stream(stream)
|
|
663
|
+
)
|
|
664
|
+
_call_status(rc, "ct_bbands_ex")
|
|
665
|
+
return up, mid, lo
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
_lib.ct_version.argtypes = []
|
|
669
|
+
_lib.ct_version.restype = ctypes.c_char_p
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def version() -> str:
|
|
673
|
+
"""Return the native library version string (MAJOR.MINOR.PATCH)."""
|
|
674
|
+
return _lib.ct_version().decode("ascii")
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _as_batch_matrix(arr: Any) -> np.ndarray:
|
|
678
|
+
a = np.asarray(arr, dtype=np.float32)
|
|
679
|
+
if a.ndim != 2:
|
|
680
|
+
raise ValueError("batch input must be a 2D array (numSeries, seriesLen)")
|
|
681
|
+
return np.ascontiguousarray(a)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _batch_ptr(a: np.ndarray) -> Any:
|
|
685
|
+
return a.ctypes.data_as(_C_FLOAT_PTR)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def sma_batch(inputs: Any, period: int, max_concurrency: int = 0,
|
|
689
|
+
stream: Any = None) -> np.ndarray:
|
|
690
|
+
"""SMA over a (numSeries, seriesLen) matrix; one row per symbol."""
|
|
691
|
+
a = _as_batch_matrix(inputs)
|
|
692
|
+
out = np.zeros_like(a)
|
|
693
|
+
rc = _lib.ct_sma_batch(_batch_ptr(a), _batch_ptr(out),
|
|
694
|
+
ctypes.c_int(a.shape[1]), ctypes.c_int(a.shape[0]),
|
|
695
|
+
ctypes.c_int(int(period)),
|
|
696
|
+
ctypes.c_int(int(max_concurrency)),
|
|
697
|
+
_prepare_stream(stream))
|
|
698
|
+
_call_status(rc, "ct_sma_batch")
|
|
699
|
+
return out
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def ema_batch(inputs: Any, period: int, max_concurrency: int = 0,
|
|
703
|
+
stream: Any = None) -> np.ndarray:
|
|
704
|
+
"""EMA over a (numSeries, seriesLen) matrix; one row per symbol."""
|
|
705
|
+
a = _as_batch_matrix(inputs)
|
|
706
|
+
out = np.zeros_like(a)
|
|
707
|
+
rc = _lib.ct_ema_batch(_batch_ptr(a), _batch_ptr(out),
|
|
708
|
+
ctypes.c_int(a.shape[1]), ctypes.c_int(a.shape[0]),
|
|
709
|
+
ctypes.c_int(int(period)),
|
|
710
|
+
ctypes.c_int(int(max_concurrency)),
|
|
711
|
+
_prepare_stream(stream))
|
|
712
|
+
_call_status(rc, "ct_ema_batch")
|
|
713
|
+
return out
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def rsi_batch(inputs: Any, period: int, max_concurrency: int = 0,
|
|
717
|
+
stream: Any = None) -> np.ndarray:
|
|
718
|
+
"""RSI over a (numSeries, seriesLen) matrix; one row per symbol."""
|
|
719
|
+
a = _as_batch_matrix(inputs)
|
|
720
|
+
out = np.zeros_like(a)
|
|
721
|
+
rc = _lib.ct_rsi_batch(_batch_ptr(a), _batch_ptr(out),
|
|
722
|
+
ctypes.c_int(a.shape[1]), ctypes.c_int(a.shape[0]),
|
|
723
|
+
ctypes.c_int(int(period)),
|
|
724
|
+
ctypes.c_int(int(max_concurrency)),
|
|
725
|
+
_prepare_stream(stream))
|
|
726
|
+
_call_status(rc, "ct_rsi_batch")
|
|
727
|
+
return out
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def atr_batch(high: Any, low: Any, close: Any, period: int,
|
|
731
|
+
initial: float = 0.0, max_concurrency: int = 0,
|
|
732
|
+
stream: Any = None) -> np.ndarray:
|
|
733
|
+
"""ATR over (numSeries, seriesLen) high/low/close matrices."""
|
|
734
|
+
h = _as_batch_matrix(high)
|
|
735
|
+
l = _as_batch_matrix(low)
|
|
736
|
+
c = _as_batch_matrix(close)
|
|
737
|
+
if h.shape != l.shape or h.shape != c.shape:
|
|
738
|
+
raise ValueError("high/low/close matrices must have the same shape")
|
|
739
|
+
out = np.zeros_like(h)
|
|
740
|
+
rc = _lib.ct_atr_batch(_batch_ptr(h), _batch_ptr(l), _batch_ptr(c),
|
|
741
|
+
_batch_ptr(out),
|
|
742
|
+
ctypes.c_int(h.shape[1]), ctypes.c_int(h.shape[0]),
|
|
743
|
+
ctypes.c_int(int(period)),
|
|
744
|
+
ctypes.c_float(float(initial)),
|
|
745
|
+
ctypes.c_int(int(max_concurrency)),
|
|
746
|
+
_prepare_stream(stream))
|
|
747
|
+
_call_status(rc, "ct_atr_batch")
|
|
748
|
+
return out
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def macd_batch(inputs: Any, fastPeriod: int = 12, slowPeriod: int = 26,
|
|
752
|
+
signalPeriod: int = 9, type: int = 1, max_concurrency: int = 0,
|
|
753
|
+
stream: Any = None) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
754
|
+
"""MACD over a (numSeries, seriesLen) matrix; returns (macd, signal, hist)."""
|
|
755
|
+
a = _as_batch_matrix(inputs)
|
|
756
|
+
m = np.zeros_like(a)
|
|
757
|
+
s = np.zeros_like(a)
|
|
758
|
+
h = np.zeros_like(a)
|
|
759
|
+
rc = _lib.ct_macd_batch(_batch_ptr(a), _batch_ptr(m), _batch_ptr(s),
|
|
760
|
+
_batch_ptr(h),
|
|
761
|
+
ctypes.c_int(a.shape[1]), ctypes.c_int(a.shape[0]),
|
|
762
|
+
ctypes.c_int(int(fastPeriod)),
|
|
763
|
+
ctypes.c_int(int(slowPeriod)),
|
|
764
|
+
ctypes.c_int(int(signalPeriod)),
|
|
765
|
+
ctypes.c_int(int(type)),
|
|
766
|
+
ctypes.c_int(int(max_concurrency)),
|
|
767
|
+
_prepare_stream(stream))
|
|
768
|
+
_call_status(rc, "ct_macd_batch")
|
|
769
|
+
return m, s, h
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def bbands_batch(inputs: Any, period: int, upperMul: float = 2.0,
|
|
773
|
+
lowerMul: float = 2.0, max_concurrency: int = 0,
|
|
774
|
+
stream: Any = None) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
775
|
+
"""Bollinger Bands over a (numSeries, seriesLen) matrix."""
|
|
776
|
+
a = _as_batch_matrix(inputs)
|
|
777
|
+
up = np.zeros_like(a)
|
|
778
|
+
mid = np.zeros_like(a)
|
|
779
|
+
lo = np.zeros_like(a)
|
|
780
|
+
rc = _lib.ct_bbands_batch(_batch_ptr(a), _batch_ptr(up), _batch_ptr(mid),
|
|
781
|
+
_batch_ptr(lo),
|
|
782
|
+
ctypes.c_int(a.shape[1]), ctypes.c_int(a.shape[0]),
|
|
783
|
+
ctypes.c_int(int(period)),
|
|
784
|
+
ctypes.c_float(float(upperMul)),
|
|
785
|
+
ctypes.c_float(float(lowerMul)),
|
|
786
|
+
ctypes.c_int(int(max_concurrency)),
|
|
787
|
+
_prepare_stream(stream))
|
|
788
|
+
_call_status(rc, "ct_bbands_batch")
|
|
789
|
+
return up, mid, lo
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def stochastic_batch(high: Any, low: Any, close: Any, kPeriod: int = 14,
|
|
793
|
+
dPeriod: int = 3, max_concurrency: int = 0,
|
|
794
|
+
stream: Any = None) -> Tuple[np.ndarray, np.ndarray]:
|
|
795
|
+
"""Stochastic %K/%D over (numSeries, seriesLen) high/low/close matrices."""
|
|
796
|
+
h = _as_batch_matrix(high)
|
|
797
|
+
l = _as_batch_matrix(low)
|
|
798
|
+
c = _as_batch_matrix(close)
|
|
799
|
+
if h.shape != l.shape or h.shape != c.shape:
|
|
800
|
+
raise ValueError("high/low/close matrices must have the same shape")
|
|
801
|
+
k = np.zeros_like(h)
|
|
802
|
+
d = np.zeros_like(h)
|
|
803
|
+
rc = _lib.ct_stochastic_batch(_batch_ptr(h), _batch_ptr(l), _batch_ptr(c),
|
|
804
|
+
_batch_ptr(k), _batch_ptr(d),
|
|
805
|
+
ctypes.c_int(h.shape[1]),
|
|
806
|
+
ctypes.c_int(h.shape[0]),
|
|
807
|
+
ctypes.c_int(int(kPeriod)),
|
|
808
|
+
ctypes.c_int(int(dPeriod)),
|
|
809
|
+
ctypes.c_int(int(max_concurrency)),
|
|
810
|
+
_prepare_stream(stream))
|
|
811
|
+
_call_status(rc, "ct_stochastic_batch")
|
|
812
|
+
return k, d
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
# ---------------------------------------------------------------------------
|
|
816
|
+
# Generated batch families: friendly (numSeries, seriesLen)-matrix wrappers
|
|
817
|
+
# built from the _generated specs for every remaining ct_*_batch symbol.
|
|
818
|
+
# ---------------------------------------------------------------------------
|
|
819
|
+
|
|
820
|
+
_MANUAL_BATCH = {
|
|
821
|
+
"sma_batch", "ema_batch", "rsi_batch", "atr_batch",
|
|
822
|
+
"macd_batch", "bbands_batch", "stochastic_batch",
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def _make_batch_wrapper(py_name: str, spec: _generated.FunctionSpec):
|
|
827
|
+
cfunc = getattr(_lib, spec.c_name)
|
|
828
|
+
n_in = len(spec.inputs)
|
|
829
|
+
n_out = len(spec.outputs)
|
|
830
|
+
family_scalars = [
|
|
831
|
+
s for s in spec.scalars
|
|
832
|
+
if s.name not in ("seriesLen", "numSeries", "maxConcurrency")
|
|
833
|
+
and s.ctype != "cudaStream_t"
|
|
834
|
+
]
|
|
835
|
+
|
|
836
|
+
def wrapper(*args: Any, max_concurrency: int = 0, stream: Any = None,
|
|
837
|
+
**kwargs: Any) -> Any:
|
|
838
|
+
if len(args) < n_in or len(args) > n_in + len(family_scalars):
|
|
839
|
+
expected = list(spec.inputs) + [s.name for s in family_scalars]
|
|
840
|
+
raise TypeError(
|
|
841
|
+
f"{py_name} expects arguments {expected} "
|
|
842
|
+
"(plus keyword-only max_concurrency/stream)")
|
|
843
|
+
mats = [_as_batch_matrix(a) for a in args[:n_in]]
|
|
844
|
+
for m in mats[1:]:
|
|
845
|
+
if m.shape != mats[0].shape:
|
|
846
|
+
raise ValueError("input matrices must have the same shape")
|
|
847
|
+
scalar_values = list(args[n_in:])
|
|
848
|
+
for s in family_scalars[len(scalar_values):]:
|
|
849
|
+
if s.name not in kwargs:
|
|
850
|
+
raise TypeError(f"{py_name} missing argument '{s.name}'")
|
|
851
|
+
scalar_values.append(kwargs.pop(s.name))
|
|
852
|
+
if kwargs:
|
|
853
|
+
raise TypeError(
|
|
854
|
+
f"{py_name} got unexpected arguments {sorted(kwargs)}")
|
|
855
|
+
outs = [np.zeros_like(mats[0]) for _ in range(n_out)]
|
|
856
|
+
|
|
857
|
+
c_args: list[Any] = [_batch_ptr(m) for m in mats]
|
|
858
|
+
c_args += [_batch_ptr(o) for o in outs]
|
|
859
|
+
c_args += [ctypes.c_int(mats[0].shape[1]),
|
|
860
|
+
ctypes.c_int(mats[0].shape[0])]
|
|
861
|
+
for s, v in zip(family_scalars, scalar_values):
|
|
862
|
+
c_args.append(ctypes.c_float(float(v)) if s.ctype == "float"
|
|
863
|
+
else ctypes.c_int(int(v)))
|
|
864
|
+
c_args.append(ctypes.c_int(int(max_concurrency)))
|
|
865
|
+
c_args.append(_prepare_stream(stream))
|
|
866
|
+
|
|
867
|
+
_call_status(cfunc(*c_args), spec.c_name)
|
|
868
|
+
return outs[0] if n_out == 1 else tuple(outs)
|
|
869
|
+
|
|
870
|
+
wrapper.__name__ = py_name
|
|
871
|
+
param_names = list(spec.inputs) + [s.name for s in family_scalars]
|
|
872
|
+
wrapper.__doc__ = (
|
|
873
|
+
f"Batched wrapper for {spec.c_name}: arguments {param_names}; "
|
|
874
|
+
"inputs are (numSeries, seriesLen) matrices, one row per symbol.")
|
|
875
|
+
return wrapper
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
for _py_name, _spec in _SPEC.items():
|
|
879
|
+
if (_py_name.endswith("_batch") and _py_name not in _MANUAL_BATCH):
|
|
880
|
+
globals()[_py_name] = _make_batch_wrapper(_py_name, _spec)
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
class OHLCV:
|
|
884
|
+
"""Columnar OHLCV container backed by NumPy arrays."""
|
|
885
|
+
|
|
886
|
+
__slots__ = ("_data",)
|
|
887
|
+
|
|
888
|
+
def __init__(self, size: int = 0) -> None:
|
|
889
|
+
size = int(size)
|
|
890
|
+
if size < 0:
|
|
891
|
+
raise ValueError("OHLCV size must be non-negative")
|
|
892
|
+
self._data = np.zeros((5, size), dtype=np.float32)
|
|
893
|
+
|
|
894
|
+
@property
|
|
895
|
+
def size(self) -> int:
|
|
896
|
+
"""Number of rows contained in the series."""
|
|
897
|
+
|
|
898
|
+
return int(self._data.shape[1])
|
|
899
|
+
|
|
900
|
+
def __len__(self) -> int: # pragma: no cover - trivial
|
|
901
|
+
return self.size
|
|
902
|
+
|
|
903
|
+
@classmethod
|
|
904
|
+
def from_columns(
|
|
905
|
+
cls,
|
|
906
|
+
open: Any,
|
|
907
|
+
high: Any,
|
|
908
|
+
low: Any,
|
|
909
|
+
close: Any,
|
|
910
|
+
volume: Any | None = None,
|
|
911
|
+
) -> "OHLCV":
|
|
912
|
+
"""Create an :class:`OHLCV` series from column vectors."""
|
|
913
|
+
|
|
914
|
+
arrays = []
|
|
915
|
+
size: int | None = None
|
|
916
|
+
for name, source in zip(
|
|
917
|
+
("open", "high", "low", "close"), (open, high, low, close)
|
|
918
|
+
):
|
|
919
|
+
arr = np.asarray(source, dtype=np.float32)
|
|
920
|
+
if arr.ndim == 0:
|
|
921
|
+
arr = arr.reshape(1)
|
|
922
|
+
arr = np.ascontiguousarray(arr.reshape(-1), dtype=np.float32)
|
|
923
|
+
if size is None:
|
|
924
|
+
size = int(arr.size)
|
|
925
|
+
elif int(arr.size) != size:
|
|
926
|
+
raise ValueError(f"{name} column must have length {size}")
|
|
927
|
+
arrays.append(arr)
|
|
928
|
+
|
|
929
|
+
if size is None:
|
|
930
|
+
size = 0
|
|
931
|
+
|
|
932
|
+
instance = cls(size)
|
|
933
|
+
for idx, arr in enumerate(arrays):
|
|
934
|
+
instance._data[idx, : size] = arr
|
|
935
|
+
|
|
936
|
+
if volume is None:
|
|
937
|
+
instance._data[4, : size].fill(0.0)
|
|
938
|
+
else:
|
|
939
|
+
vol = np.asarray(volume, dtype=np.float32)
|
|
940
|
+
if vol.ndim == 0:
|
|
941
|
+
vol = vol.reshape(1)
|
|
942
|
+
vol = np.ascontiguousarray(vol.reshape(-1), dtype=np.float32)
|
|
943
|
+
if int(vol.size) != size:
|
|
944
|
+
raise ValueError("volume column must match other columns")
|
|
945
|
+
instance._data[4, : size] = vol
|
|
946
|
+
|
|
947
|
+
return instance
|
|
948
|
+
|
|
949
|
+
@classmethod
|
|
950
|
+
def from_rows(cls, rows: Any) -> "OHLCV":
|
|
951
|
+
"""Create an :class:`OHLCV` series from row-wise data."""
|
|
952
|
+
|
|
953
|
+
arr = np.asarray(rows, dtype=np.float32)
|
|
954
|
+
if arr.ndim != 2:
|
|
955
|
+
raise ValueError("rows input must be a 2D array")
|
|
956
|
+
if arr.shape[1] not in (4, 5):
|
|
957
|
+
raise ValueError("rows must have 4 (OHLC) or 5 (OHLCV) columns")
|
|
958
|
+
|
|
959
|
+
arr = np.ascontiguousarray(arr, dtype=np.float32)
|
|
960
|
+
columns = [arr[:, i] for i in range(4)]
|
|
961
|
+
volume = arr[:, 4] if arr.shape[1] == 5 else np.zeros(arr.shape[0], dtype=np.float32)
|
|
962
|
+
return cls.from_columns(columns[0], columns[1], columns[2], columns[3], volume)
|
|
963
|
+
|
|
964
|
+
def _column_view(self, idx: int) -> np.ndarray:
|
|
965
|
+
return self._data[idx]
|
|
966
|
+
|
|
967
|
+
@property
|
|
968
|
+
def open(self) -> np.ndarray:
|
|
969
|
+
return self._column_view(0)
|
|
970
|
+
|
|
971
|
+
@property
|
|
972
|
+
def high(self) -> np.ndarray:
|
|
973
|
+
return self._column_view(1)
|
|
974
|
+
|
|
975
|
+
@property
|
|
976
|
+
def low(self) -> np.ndarray:
|
|
977
|
+
return self._column_view(2)
|
|
978
|
+
|
|
979
|
+
@property
|
|
980
|
+
def close(self) -> np.ndarray:
|
|
981
|
+
return self._column_view(3)
|
|
982
|
+
|
|
983
|
+
@property
|
|
984
|
+
def volume(self) -> np.ndarray:
|
|
985
|
+
return self._column_view(4)
|
|
986
|
+
|
|
987
|
+
def column_major(self, include_volume: bool = True) -> np.ndarray:
|
|
988
|
+
"""Return a view of the data in column-major (SoA) layout."""
|
|
989
|
+
|
|
990
|
+
if include_volume:
|
|
991
|
+
return self._data.reshape(-1)
|
|
992
|
+
return self._data[:4].reshape(-1)
|
|
993
|
+
|
|
994
|
+
def copy_column_major(self, include_volume: bool = True) -> np.ndarray:
|
|
995
|
+
"""Return a copy of the data in column-major layout."""
|
|
996
|
+
|
|
997
|
+
return np.array(self.column_major(include_volume), copy=True)
|
|
998
|
+
|
|
999
|
+
def to_numpy(self, include_volume: bool = True, copy: bool = True) -> np.ndarray:
|
|
1000
|
+
"""Return a 2D NumPy array of shape (rows, columns)."""
|
|
1001
|
+
|
|
1002
|
+
data = self._data.T if include_volume else self._data[:4].T
|
|
1003
|
+
return np.array(data, copy=copy)
|
|
1004
|
+
|
|
1005
|
+
def set(
|
|
1006
|
+
self,
|
|
1007
|
+
index: int,
|
|
1008
|
+
open: float,
|
|
1009
|
+
high: float,
|
|
1010
|
+
low: float,
|
|
1011
|
+
close: float,
|
|
1012
|
+
volume: float = 0.0,
|
|
1013
|
+
) -> None:
|
|
1014
|
+
if index < 0 or index >= self.size:
|
|
1015
|
+
raise IndexError("OHLCV row index out of range")
|
|
1016
|
+
self.open[index] = float(open)
|
|
1017
|
+
self.high[index] = float(high)
|
|
1018
|
+
self.low[index] = float(low)
|
|
1019
|
+
self.close[index] = float(close)
|
|
1020
|
+
self.volume[index] = float(volume)
|
|
1021
|
+
|
|
1022
|
+
def __array__(self, dtype: Any | None = None) -> np.ndarray: # pragma: no cover - numpy protocol
|
|
1023
|
+
if dtype is None or dtype == self._data.dtype:
|
|
1024
|
+
return self._data
|
|
1025
|
+
return self._data.astype(dtype, copy=False)
|
|
1026
|
+
|
|
1027
|
+
def __repr__(self) -> str: # pragma: no cover - debugging helper
|
|
1028
|
+
return f"OHLCV(size={self.size})"
|
|
1029
|
+
|
|
1030
|
+
__all_list = sorted(_SPEC)
|
|
1031
|
+
__all_list.extend(("OHLCV", "version"))
|
|
1032
|
+
__all__ = tuple(__all_list)
|