Qubx 0.5.7__cp312-cp312-manylinux_2_39_x86_64.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.

Potentially problematic release.


This version of Qubx might be problematic. Click here for more details.

Files changed (100) hide show
  1. qubx/__init__.py +207 -0
  2. qubx/_nb_magic.py +100 -0
  3. qubx/backtester/__init__.py +5 -0
  4. qubx/backtester/account.py +145 -0
  5. qubx/backtester/broker.py +87 -0
  6. qubx/backtester/data.py +296 -0
  7. qubx/backtester/management.py +378 -0
  8. qubx/backtester/ome.py +296 -0
  9. qubx/backtester/optimization.py +201 -0
  10. qubx/backtester/simulated_data.py +558 -0
  11. qubx/backtester/simulator.py +362 -0
  12. qubx/backtester/utils.py +780 -0
  13. qubx/cli/__init__.py +0 -0
  14. qubx/cli/commands.py +67 -0
  15. qubx/connectors/ccxt/__init__.py +0 -0
  16. qubx/connectors/ccxt/account.py +495 -0
  17. qubx/connectors/ccxt/broker.py +132 -0
  18. qubx/connectors/ccxt/customizations.py +193 -0
  19. qubx/connectors/ccxt/data.py +612 -0
  20. qubx/connectors/ccxt/exceptions.py +17 -0
  21. qubx/connectors/ccxt/factory.py +93 -0
  22. qubx/connectors/ccxt/utils.py +307 -0
  23. qubx/core/__init__.py +0 -0
  24. qubx/core/account.py +251 -0
  25. qubx/core/basics.py +850 -0
  26. qubx/core/context.py +420 -0
  27. qubx/core/exceptions.py +38 -0
  28. qubx/core/helpers.py +480 -0
  29. qubx/core/interfaces.py +1150 -0
  30. qubx/core/loggers.py +514 -0
  31. qubx/core/lookups.py +475 -0
  32. qubx/core/metrics.py +1512 -0
  33. qubx/core/mixins/__init__.py +13 -0
  34. qubx/core/mixins/market.py +94 -0
  35. qubx/core/mixins/processing.py +428 -0
  36. qubx/core/mixins/subscription.py +203 -0
  37. qubx/core/mixins/trading.py +88 -0
  38. qubx/core/mixins/universe.py +270 -0
  39. qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
  40. qubx/core/series.pxd +125 -0
  41. qubx/core/series.pyi +118 -0
  42. qubx/core/series.pyx +988 -0
  43. qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
  44. qubx/core/utils.pyi +6 -0
  45. qubx/core/utils.pyx +62 -0
  46. qubx/data/__init__.py +25 -0
  47. qubx/data/helpers.py +416 -0
  48. qubx/data/readers.py +1562 -0
  49. qubx/data/tardis.py +100 -0
  50. qubx/gathering/simplest.py +88 -0
  51. qubx/math/__init__.py +3 -0
  52. qubx/math/stats.py +129 -0
  53. qubx/pandaz/__init__.py +23 -0
  54. qubx/pandaz/ta.py +2757 -0
  55. qubx/pandaz/utils.py +638 -0
  56. qubx/resources/instruments/symbols-binance.cm.json +1 -0
  57. qubx/resources/instruments/symbols-binance.json +1 -0
  58. qubx/resources/instruments/symbols-binance.um.json +1 -0
  59. qubx/resources/instruments/symbols-bitfinex.f.json +1 -0
  60. qubx/resources/instruments/symbols-bitfinex.json +1 -0
  61. qubx/resources/instruments/symbols-kraken.f.json +1 -0
  62. qubx/resources/instruments/symbols-kraken.json +1 -0
  63. qubx/ta/__init__.py +0 -0
  64. qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
  65. qubx/ta/indicators.pxd +149 -0
  66. qubx/ta/indicators.pyi +41 -0
  67. qubx/ta/indicators.pyx +787 -0
  68. qubx/trackers/__init__.py +3 -0
  69. qubx/trackers/abvanced.py +236 -0
  70. qubx/trackers/composite.py +146 -0
  71. qubx/trackers/rebalancers.py +129 -0
  72. qubx/trackers/riskctrl.py +641 -0
  73. qubx/trackers/sizers.py +235 -0
  74. qubx/utils/__init__.py +5 -0
  75. qubx/utils/_pyxreloader.py +281 -0
  76. qubx/utils/charting/lookinglass.py +1057 -0
  77. qubx/utils/charting/mpl_helpers.py +1183 -0
  78. qubx/utils/marketdata/binance.py +284 -0
  79. qubx/utils/marketdata/ccxt.py +90 -0
  80. qubx/utils/marketdata/dukas.py +130 -0
  81. qubx/utils/misc.py +541 -0
  82. qubx/utils/ntp.py +63 -0
  83. qubx/utils/numbers_utils.py +7 -0
  84. qubx/utils/orderbook.py +491 -0
  85. qubx/utils/plotting/__init__.py +0 -0
  86. qubx/utils/plotting/dashboard.py +150 -0
  87. qubx/utils/plotting/data.py +137 -0
  88. qubx/utils/plotting/interfaces.py +25 -0
  89. qubx/utils/plotting/renderers/__init__.py +0 -0
  90. qubx/utils/plotting/renderers/plotly.py +0 -0
  91. qubx/utils/runner/__init__.py +1 -0
  92. qubx/utils/runner/_jupyter_runner.pyt +60 -0
  93. qubx/utils/runner/accounts.py +88 -0
  94. qubx/utils/runner/configs.py +65 -0
  95. qubx/utils/runner/runner.py +470 -0
  96. qubx/utils/time.py +312 -0
  97. qubx-0.5.7.dist-info/METADATA +105 -0
  98. qubx-0.5.7.dist-info/RECORD +100 -0
  99. qubx-0.5.7.dist-info/WHEEL +4 -0
  100. qubx-0.5.7.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,235 @@
1
+ from typing import List
2
+
3
+ import numpy as np
4
+
5
+ from qubx import logger
6
+ from qubx.core.basics import Signal, TargetPosition
7
+ from qubx.core.interfaces import IPositionSizer, IStrategyContext
8
+
9
+
10
+ class FixedSizer(IPositionSizer):
11
+ """
12
+ Simplest fixed sizer class. It uses same fixed size for all signals.
13
+ We use it for quick backtesting of generated signals in most cases.
14
+ """
15
+
16
+ def __init__(self, fixed_size: float, amount_in_quote: bool = True):
17
+ self.amount_in_quote = amount_in_quote
18
+ self.fixed_size = abs(fixed_size)
19
+
20
+ def calculate_target_positions(self, ctx: IStrategyContext, signals: List[Signal]) -> List[TargetPosition]:
21
+ if not self.amount_in_quote:
22
+ return [TargetPosition.create(ctx, s, s.signal * self.fixed_size) for s in signals]
23
+ positions = []
24
+ for signal in signals:
25
+ if (_entry := self.get_signal_entry_price(ctx, signal)) is None:
26
+ continue
27
+ positions.append(TargetPosition.create(ctx, signal, signal.signal * self.fixed_size / _entry))
28
+ return positions
29
+
30
+
31
+ class FixedLeverageSizer(IPositionSizer):
32
+ """
33
+ Defines the leverage per each unit of signal. If leverage is 1.0, then
34
+ the position leverage will be equal to the signal value.
35
+ """
36
+
37
+ def __init__(self, leverage: float):
38
+ """
39
+ Args:
40
+ leverage (float): leverage value per a unit of signal.
41
+ split_by_symbols (bool): Should the calculated leverage by divided
42
+ by the number of symbols in the universe.
43
+ """
44
+ self.leverage = leverage
45
+
46
+ def calculate_target_positions(self, ctx: IStrategyContext, signals: List[Signal]) -> List[TargetPosition]:
47
+ total_capital = ctx.get_total_capital()
48
+ positions = []
49
+ for signal in signals:
50
+ if (_entry := self.get_signal_entry_price(ctx, signal)) is None:
51
+ continue
52
+
53
+ size = signal.signal * self.leverage * total_capital / _entry / len(ctx.instruments)
54
+ positions.append(TargetPosition.create(ctx, signal, size))
55
+ return positions
56
+
57
+
58
+ class FixedRiskSizer(IPositionSizer):
59
+ def __init__(
60
+ self,
61
+ max_cap_in_risk: float,
62
+ max_allowed_position=np.inf,
63
+ reinvest_profit: bool = True,
64
+ divide_by_symbols: bool = True,
65
+ scale_by_signal: bool = False,
66
+ ):
67
+ """
68
+ Create fixed risk sizer calculator instance.
69
+ :param max_cap_in_risk: maximal risked capital (in percentage)
70
+ :param max_allowed_position: limitation for max position size in quoted currency (i.e. max 5000 in USDT)
71
+ :param reinvest_profit: if true use profit to reinvest
72
+ :param divide_by_symbols: if true divide position size by number of symbols
73
+ :param scale_by_signal: if true scale position size by signal's value
74
+ """
75
+ self.max_cap_in_risk = max_cap_in_risk / 100
76
+ self.max_allowed_position_quoted = max_allowed_position
77
+ self.reinvest_profit = reinvest_profit
78
+ self.divide_by_symbols = divide_by_symbols
79
+ self.scale_by_signal = scale_by_signal
80
+
81
+ def calculate_target_positions(self, ctx: IStrategyContext, signals: List[Signal]) -> List[TargetPosition]:
82
+ t_pos = []
83
+ for signal in signals:
84
+ target_position_size = 0
85
+ if signal.signal != 0:
86
+ if signal.stop and signal.stop > 0:
87
+ # - get signal entry price
88
+ if (_entry := self.get_signal_entry_price(ctx, signal)) is None:
89
+ continue
90
+
91
+ # - hey, we can't trade using negative balance ;)
92
+ _cap = max(ctx.get_total_capital() if self.reinvest_profit else ctx.get_capital(), 0)
93
+ _scale = abs(signal.signal) if self.scale_by_signal else 1
94
+
95
+ # fmt: off
96
+ _direction = np.sign(signal.signal)
97
+ target_position_size = (
98
+ _direction
99
+ *min((_cap * self.max_cap_in_risk) / abs(signal.stop / _entry - 1), self.max_allowed_position_quoted) / _entry
100
+ / (len(ctx.instruments) if self.divide_by_symbols else 1)
101
+ * _scale
102
+ )
103
+ # fmt: on
104
+
105
+ else:
106
+ logger.warning(
107
+ f" >>> {self.__class__.__name__}: stop is not specified for {str(signal)} - can't calculate position !"
108
+ )
109
+ continue
110
+
111
+ t_pos.append(TargetPosition.create(ctx, signal, target_position_size))
112
+
113
+ return t_pos
114
+
115
+
116
+ class LongShortRatioPortfolioSizer(IPositionSizer):
117
+ """
118
+ Weighted portfolio sizer. Signals are cosidered as weigths.
119
+ It's supposed to split capital in the given ratio between longs and shorts positions.
120
+ For example if ratio is 1 capital invested in long and short positions should be the same.
121
+
122
+ So if we S_l = sum all long signals, S_s = abs sum all short signals, r (longs_shorts_ratio) given ratio
123
+
124
+ k_s * S_s + k_l * S_l = 1
125
+ k_l * S_l / k_s * S_s = r
126
+
127
+ then
128
+
129
+ k_s = 1 / S_s * (1 + r) or 0 if S_s == 0 (no short signals)
130
+ k_l = r / S_l * (1 + r) or 0 if S_l == 0 (no long signals)
131
+
132
+ and final positions:
133
+ P_i = S_i * available_capital * capital_using * (k_l if S_i > 0 else k_s)
134
+ """
135
+
136
+ _r: float
137
+
138
+ def __init__(self, capital_using: float = 1.0, longs_to_shorts_ratio: float = 1):
139
+ """
140
+ Create weighted portfolio sizer.
141
+
142
+ :param capital_using: how much of total capital to be used for positions
143
+ :param longs_shorts_ratio: ratio of longs to shorts positions
144
+ """
145
+ assert 0 < capital_using <= 1, f"Capital using factor must be between 0 and 1, got {capital_using}"
146
+ assert 0 < longs_to_shorts_ratio, f"Longs/shorts ratio must be greater 0, got {longs_to_shorts_ratio}"
147
+ self.capital_using = capital_using
148
+ self._r = longs_to_shorts_ratio
149
+
150
+ def calculate_target_positions(self, ctx: IStrategyContext, signals: List[Signal]) -> List[TargetPosition]:
151
+ """
152
+ Calculates target positions for each signal using weighted portfolio approach.
153
+
154
+ Parameters:
155
+ ctx (StrategyContext): The strategy context containing information about the current state of the strategy.
156
+ signals (List[Signal]): A list of signals generated by the strategy.
157
+
158
+ Returns:
159
+ List[TargetPosition]: A list of target positions for each signal, representing the desired size of the position
160
+ in the corresponding instrument.
161
+ """
162
+ total_capital = ctx.get_total_capital()
163
+ cap = self.capital_using * total_capital
164
+
165
+ _S_l, _S_s = 0, 0
166
+ for s in signals:
167
+ _S_l += s.signal if s.signal > 0 else 0
168
+ _S_s += abs(s.signal) if s.signal < 0 else 0
169
+ k_s = 1 / (_S_s * (1 + self._r)) if _S_s > 0 else 0
170
+ k_l = self._r / (_S_l * (1 + self._r)) if _S_l > 0 else 0
171
+
172
+ t_pos = []
173
+ for signal in signals:
174
+ if (_entry := self.get_signal_entry_price(ctx, signal)) is None:
175
+ continue
176
+
177
+ _p_q = cap / _entry
178
+ _p = k_l * signal.signal if signal.signal > 0 else k_s * signal.signal
179
+ t_pos.append(TargetPosition.create(ctx, signal, _p * _p_q))
180
+
181
+ return t_pos
182
+
183
+
184
+ class FixedRiskSizerWithConstantCapital(IPositionSizer):
185
+ def __init__(
186
+ self,
187
+ capital: float,
188
+ max_cap_in_risk: float,
189
+ max_allowed_position=np.inf,
190
+ divide_by_symbols: bool = True,
191
+ ):
192
+ """
193
+ Create fixed risk sizer calculator instance.
194
+ :param max_cap_in_risk: maximal risked capital (in percentage)
195
+ :param max_allowed_position: limitation for max position size in quoted currency (i.e. max 5000 in USDT)
196
+ :param reinvest_profit: if true use profit to reinvest
197
+ """
198
+ self.capital = capital
199
+ assert self.capital > 0, f" >> {self.__class__.__name__}: Capital must be positive, got {self.capital}"
200
+ self.max_cap_in_risk = max_cap_in_risk / 100
201
+ self.max_allowed_position_quoted = max_allowed_position
202
+ self.divide_by_symbols = divide_by_symbols
203
+
204
+ def calculate_target_positions(self, ctx: IStrategyContext, signals: List[Signal]) -> List[TargetPosition]:
205
+ t_pos = []
206
+ for signal in signals:
207
+ target_position_size = 0
208
+ if signal.signal != 0:
209
+ if signal.stop and signal.stop > 0:
210
+ # - get signal entry price
211
+ if (_entry := self.get_signal_entry_price(ctx, signal)) is None:
212
+ continue
213
+
214
+ # - just use same fixed capital
215
+ _cap = self.capital / (len(ctx.instruments) if self.divide_by_symbols else 1)
216
+
217
+ # fmt: off
218
+ _direction = np.sign(signal.signal)
219
+ target_position_size = (
220
+ _direction * min(
221
+ (_cap * self.max_cap_in_risk) / abs(signal.stop / _entry - 1),
222
+ self.max_allowed_position_quoted
223
+ ) / _entry
224
+ )
225
+ # fmt: on
226
+
227
+ else:
228
+ logger.warning(
229
+ f" >>> {self.__class__.__name__}: stop is not specified for {str(signal)} - can't calculate position !"
230
+ )
231
+ continue
232
+
233
+ t_pos.append(TargetPosition.create(ctx, signal, target_position_size))
234
+
235
+ return t_pos
qubx/utils/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .misc import Stopwatch, Struct, generate_name, runtime_env, this_project_root, version
2
+
3
+ from .charting.lookinglass import LookingGlass # isort: skip
4
+ from .charting.mpl_helpers import ellips, fig, hline, ohlc_plot, plot_trends, sbp, set_mpl_theme, vline # isort: skip
5
+ from .time import convert_seconds_to_str, convert_tf_str_td64, floor_t64, infer_series_frequency, time_to_str
@@ -0,0 +1,281 @@
1
+
2
+ import importlib, glob, os, sys
3
+ from importlib.abc import MetaPathFinder
4
+ from importlib.util import spec_from_file_location
5
+ from importlib.machinery import ExtensionFileLoader, SourceFileLoader
6
+ from typing import List
7
+
8
+
9
+ PYX_EXT = ".pyx"
10
+ PYXDEP_EXT = ".pyxdep"
11
+ PYXBLD_EXT = ".pyxbld"
12
+
13
+
14
+ def handle_dependencies(pyxfilename):
15
+ testing = '_test_files' in globals()
16
+ dependfile = os.path.splitext(pyxfilename)[0] + PYXDEP_EXT
17
+
18
+ # by default let distutils decide whether to rebuild on its own
19
+ # (it has a better idea of what the output file will be)
20
+
21
+ # but we know more about dependencies so force a rebuild if
22
+ # some of the dependencies are newer than the pyxfile.
23
+ if os.path.exists(dependfile):
24
+ with open(dependfile) as fid:
25
+ depends = fid.readlines()
26
+ depends = [depend.strip() for depend in depends]
27
+
28
+ # gather dependencies in the "files" variable
29
+ # the dependency file is itself a dependency
30
+ files = [dependfile]
31
+ for depend in depends:
32
+ fullpath = os.path.join(os.path.dirname(dependfile),
33
+ depend)
34
+ files.extend(glob.glob(fullpath))
35
+
36
+ # if any file that the pyxfile depends upon is newer than
37
+ # the pyx file, 'touch' the pyx file so that distutils will
38
+ # be tricked into rebuilding it.
39
+ for file in files:
40
+ from distutils.dep_util import newer
41
+ if newer(file, pyxfilename):
42
+ print("Rebuilding %s because of %s", pyxfilename, file)
43
+ filetime = os.path.getmtime(file)
44
+ os.utime(pyxfilename, (filetime, filetime))
45
+
46
+
47
+ def handle_special_build(modname, pyxfilename):
48
+ try:
49
+ import imp
50
+ except:
51
+ return None, None
52
+ special_build = os.path.splitext(pyxfilename)[0] + PYXBLD_EXT
53
+ ext = None
54
+ setup_args={}
55
+ if os.path.exists(special_build):
56
+ # globls = {}
57
+ # locs = {}
58
+ # execfile(special_build, globls, locs)
59
+ # ext = locs["make_ext"](modname, pyxfilename)
60
+ with open(special_build) as fid:
61
+ mod = imp.load_source("XXXX", special_build, fid)
62
+ make_ext = getattr(mod,'make_ext',None)
63
+ if make_ext:
64
+ ext = make_ext(modname, pyxfilename)
65
+ assert ext and ext.sources, "make_ext in %s did not return Extension" % special_build
66
+ make_setup_args = getattr(mod, 'make_setup_args',None)
67
+ if make_setup_args:
68
+ setup_args = make_setup_args()
69
+ assert isinstance(setup_args,dict), ("make_setup_args in %s did not return a dict"
70
+ % special_build)
71
+ assert set or setup_args, ("neither make_ext nor make_setup_args %s" % special_build)
72
+ ext.sources = [os.path.join(os.path.dirname(special_build), source) for source in ext.sources]
73
+ return ext, setup_args
74
+
75
+
76
+ def get_distutils_extension(modname, pyxfilename, language_level=None):
77
+ extension_mod, setup_args = handle_special_build(modname, pyxfilename)
78
+ if not extension_mod:
79
+ if not isinstance(pyxfilename, str):
80
+ # distutils is stupid in Py2 and requires exactly 'str'
81
+ # => encode accidentally coerced unicode strings back to str
82
+ pyxfilename = pyxfilename.encode(sys.getfilesystemencoding())
83
+ from distutils.extension import Extension
84
+ extension_mod = Extension(name = modname, sources=[pyxfilename])
85
+ if language_level is not None:
86
+ extension_mod.cython_directives = {'language_level': language_level}
87
+ return extension_mod, setup_args
88
+
89
+
90
+ def build_module(name, pyxfilename, user_setup_args, pyxbuild_dir=None, inplace=False, language_level=None,
91
+ build_in_temp=False, reload_support=True):
92
+ assert os.path.exists(pyxfilename), "Path does not exist: %s" % pyxfilename
93
+ handle_dependencies(pyxfilename)
94
+
95
+ extension_mod, setup_args = get_distutils_extension(name, pyxfilename, language_level)
96
+ build_in_temp = True
97
+ sargs = user_setup_args.copy() if user_setup_args else dict()
98
+ sargs.update(setup_args)
99
+ build_in_temp = sargs.pop('build_in_temp',build_in_temp)
100
+
101
+ from pyximport import pyxbuild
102
+ olddir = os.getcwd()
103
+ common = ''
104
+ if pyxbuild_dir:
105
+ # Windows concatenates the pyxbuild_dir to the pyxfilename when
106
+ # compiling, and then complains that the filename is too long
107
+ common = os.path.commonprefix([pyxbuild_dir, pyxfilename])
108
+ if len(common) > 30:
109
+ pyxfilename = os.path.relpath(pyxfilename)
110
+ pyxbuild_dir = os.path.relpath(pyxbuild_dir)
111
+ os.chdir(common)
112
+ try:
113
+ so_path = pyxbuild.pyx_to_dll(pyxfilename, extension_mod,
114
+ force_rebuild=1,
115
+ build_in_temp=build_in_temp,
116
+ pyxbuild_dir=pyxbuild_dir,
117
+ setup_args=sargs,
118
+ inplace=inplace,
119
+ reload_support=reload_support)
120
+ finally:
121
+ os.chdir(olddir)
122
+ so_path = os.path.join(common, so_path)
123
+ assert os.path.exists(so_path), "Cannot find: %s" % so_path
124
+
125
+ junkpath = os.path.join(os.path.dirname(so_path), name+"_*") #very dangerous with --inplace ? yes, indeed, trying to eat my files ;)
126
+ junkstuff = glob.glob(junkpath)
127
+ for path in junkstuff:
128
+ if path != so_path:
129
+ try:
130
+ os.remove(path)
131
+ except IOError:
132
+ print("Couldn't remove %s", path)
133
+
134
+ return so_path
135
+
136
+
137
+ def load_module(name, pyxfilename, pyxbuild_dir=None, is_package=False, build_inplace=False, language_level=None, so_path=None):
138
+ try:
139
+ import imp
140
+ except:
141
+ return None
142
+ try:
143
+ if so_path is None:
144
+ if is_package:
145
+ module_name = name + '.__init__'
146
+ else:
147
+ module_name = name
148
+ so_path = build_module(module_name, pyxfilename, pyxbuild_dir, inplace=build_inplace, language_level=language_level)
149
+ mod = imp.load_dynamic(name, so_path)
150
+ if is_package and not hasattr(mod, '__path__'):
151
+ mod.__path__ = [os.path.dirname(so_path)]
152
+ assert mod.__file__ == so_path, (mod.__file__, so_path)
153
+ except Exception as failure_exc:
154
+ print("Failed to load extension module: %r" % failure_exc)
155
+ # if pyxargs.load_py_module_on_import_failure and pyxfilename.endswith('.py'):
156
+ if False and pyxfilename.endswith('.py'):
157
+ # try to fall back to normal import
158
+ mod = imp.load_source(name, pyxfilename)
159
+ assert mod.__file__ in (pyxfilename, pyxfilename+'c', pyxfilename+'o'), (mod.__file__, pyxfilename)
160
+ else:
161
+ tb = sys.exc_info()[2]
162
+ import traceback
163
+ exc = ImportError("Building module %s failed: %s" % (name, traceback.format_exception_only(*sys.exc_info()[:2])))
164
+ if sys.version_info[0] >= 3:
165
+ raise exc.with_traceback(tb)
166
+ else:
167
+ exec("raise exc, None, tb", {'exc': exc, 'tb': tb})
168
+ return mod
169
+
170
+
171
+ class PyxImportLoader(ExtensionFileLoader):
172
+
173
+ def __init__(self, filename, setup_args, pyxbuild_dir, inplace, language_level, reload_support):
174
+ module_name = os.path.splitext(os.path.basename(filename))[0]
175
+ super().__init__(module_name, filename)
176
+ self._pyxbuild_dir = pyxbuild_dir
177
+ self._inplace = inplace
178
+ self._language_level = language_level
179
+ self._setup_args = setup_args
180
+ self._reload_support = reload_support
181
+
182
+ def create_module(self, spec):
183
+ try:
184
+ # print(f"CREATING MODULE: {spec.name} -> {spec.origin}")
185
+ so_path = build_module(spec.name, pyxfilename=spec.origin, user_setup_args=self._setup_args, pyxbuild_dir=self._pyxbuild_dir,
186
+ inplace=self._inplace, language_level=self._language_level, reload_support=self._reload_support)
187
+ self.path = so_path
188
+ spec.origin = so_path
189
+ return super().create_module(spec)
190
+ except Exception as failure_exc:
191
+ # print("LOADING on FAILURE MODULE")
192
+ # if pyxargs.load_py_module_on_import_failure and spec.origin.endswith('.pyx'):
193
+ if False and spec.origin.endswith(PYX_EXT):
194
+ spec = importlib.util.spec_from_file_location(spec.name, spec.origin,
195
+ loader=SourceFileLoader(spec.name, spec.origin))
196
+ mod = importlib.util.module_from_spec(spec)
197
+ assert mod.__file__ in (spec.origin, spec.origin + 'c', spec.origin + 'o'), (mod.__file__, spec.origin)
198
+ return mod
199
+ else:
200
+ tb = sys.exc_info()[2]
201
+ import traceback
202
+ exc = ImportError("Building module %s failed: %s" % (
203
+ spec.name, traceback.format_exception_only(*sys.exc_info()[:2])))
204
+ raise exc.with_traceback(tb)
205
+
206
+ def exec_module(self, module):
207
+ try:
208
+ # print(f"EXEC MODULE: {module}")
209
+ return super().exec_module(module)
210
+ except Exception as failure_exc:
211
+ import traceback
212
+ print("Failed to load extension module: %r" % failure_exc)
213
+ raise ImportError("Executing module %s failed %s" % (
214
+ module.__file__, traceback.format_exception_only(*sys.exc_info()[:2])))
215
+
216
+
217
+ class CustomPyxImportMetaFinder(MetaPathFinder):
218
+
219
+ def __init__(self, modules_to_check: List[str], extension=PYX_EXT, setup_args=None, pyxbuild_dir=None, inplace=False, language_level=None, reload_support=True):
220
+ self.valid_modules = modules_to_check
221
+ self.pyxbuild_dir = pyxbuild_dir
222
+ self.inplace = inplace
223
+ self.language_level = language_level
224
+ self.extension = extension
225
+ self.setup_args = setup_args if setup_args else dict()
226
+ self.reload_support = reload_support
227
+
228
+ def find_spec(self, fullname, path, target=None):
229
+ def _is_valid(module):
230
+ if not self.valid_modules:
231
+ return True
232
+ for m in self.valid_modules:
233
+ if module.startswith(m):
234
+ return True
235
+ return False
236
+
237
+ if not path:
238
+ path = [os.getcwd()] # top level import --
239
+ if "." in fullname:
240
+ *parents, name = fullname.split(".")
241
+ else:
242
+ name = fullname
243
+ for entry in path:
244
+ if os.path.isdir(os.path.join(entry, name)):
245
+ # this module has child modules
246
+ filename = os.path.join(entry, name, "__init__" + self.extension)
247
+ submodule_locations = [os.path.join(entry, name)]
248
+ else:
249
+ filename = os.path.join(entry, name + self.extension)
250
+ submodule_locations = None
251
+ if not os.path.exists(filename):
252
+ continue
253
+
254
+ if not _is_valid(fullname):
255
+ continue
256
+
257
+ return spec_from_file_location(
258
+ fullname, filename,
259
+ loader=PyxImportLoader(filename, self.setup_args, self.pyxbuild_dir, self.inplace, self.language_level, self.reload_support),
260
+ submodule_search_locations=submodule_locations)
261
+
262
+ return None # we don't know how to import this
263
+
264
+
265
+ __pyx_finder_installed = False
266
+
267
+ def pyx_install_loader(modules_to_check: List[str]):
268
+ import numpy as np
269
+ import pyximport
270
+ global __pyx_finder_installed
271
+
272
+ if not __pyx_finder_installed:
273
+ build_dir = os.path.expanduser("~/.pyxbld")
274
+ setup_args = {'include_dirs': np.get_include()}
275
+ sys.meta_path.insert(0, CustomPyxImportMetaFinder(
276
+ modules_to_check,
277
+ PYX_EXT, setup_args=setup_args, pyxbuild_dir=build_dir,
278
+ language_level=3, reload_support=True
279
+ ))
280
+ pyximport.install(setup_args=setup_args, build_dir=build_dir, reload_support=True, language_level=3)
281
+