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.
- qubx/__init__.py +207 -0
- qubx/_nb_magic.py +100 -0
- qubx/backtester/__init__.py +5 -0
- qubx/backtester/account.py +145 -0
- qubx/backtester/broker.py +87 -0
- qubx/backtester/data.py +296 -0
- qubx/backtester/management.py +378 -0
- qubx/backtester/ome.py +296 -0
- qubx/backtester/optimization.py +201 -0
- qubx/backtester/simulated_data.py +558 -0
- qubx/backtester/simulator.py +362 -0
- qubx/backtester/utils.py +780 -0
- qubx/cli/__init__.py +0 -0
- qubx/cli/commands.py +67 -0
- qubx/connectors/ccxt/__init__.py +0 -0
- qubx/connectors/ccxt/account.py +495 -0
- qubx/connectors/ccxt/broker.py +132 -0
- qubx/connectors/ccxt/customizations.py +193 -0
- qubx/connectors/ccxt/data.py +612 -0
- qubx/connectors/ccxt/exceptions.py +17 -0
- qubx/connectors/ccxt/factory.py +93 -0
- qubx/connectors/ccxt/utils.py +307 -0
- qubx/core/__init__.py +0 -0
- qubx/core/account.py +251 -0
- qubx/core/basics.py +850 -0
- qubx/core/context.py +420 -0
- qubx/core/exceptions.py +38 -0
- qubx/core/helpers.py +480 -0
- qubx/core/interfaces.py +1150 -0
- qubx/core/loggers.py +514 -0
- qubx/core/lookups.py +475 -0
- qubx/core/metrics.py +1512 -0
- qubx/core/mixins/__init__.py +13 -0
- qubx/core/mixins/market.py +94 -0
- qubx/core/mixins/processing.py +428 -0
- qubx/core/mixins/subscription.py +203 -0
- qubx/core/mixins/trading.py +88 -0
- qubx/core/mixins/universe.py +270 -0
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/series.pxd +125 -0
- qubx/core/series.pyi +118 -0
- qubx/core/series.pyx +988 -0
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.pyi +6 -0
- qubx/core/utils.pyx +62 -0
- qubx/data/__init__.py +25 -0
- qubx/data/helpers.py +416 -0
- qubx/data/readers.py +1562 -0
- qubx/data/tardis.py +100 -0
- qubx/gathering/simplest.py +88 -0
- qubx/math/__init__.py +3 -0
- qubx/math/stats.py +129 -0
- qubx/pandaz/__init__.py +23 -0
- qubx/pandaz/ta.py +2757 -0
- qubx/pandaz/utils.py +638 -0
- qubx/resources/instruments/symbols-binance.cm.json +1 -0
- qubx/resources/instruments/symbols-binance.json +1 -0
- qubx/resources/instruments/symbols-binance.um.json +1 -0
- qubx/resources/instruments/symbols-bitfinex.f.json +1 -0
- qubx/resources/instruments/symbols-bitfinex.json +1 -0
- qubx/resources/instruments/symbols-kraken.f.json +1 -0
- qubx/resources/instruments/symbols-kraken.json +1 -0
- qubx/ta/__init__.py +0 -0
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/ta/indicators.pxd +149 -0
- qubx/ta/indicators.pyi +41 -0
- qubx/ta/indicators.pyx +787 -0
- qubx/trackers/__init__.py +3 -0
- qubx/trackers/abvanced.py +236 -0
- qubx/trackers/composite.py +146 -0
- qubx/trackers/rebalancers.py +129 -0
- qubx/trackers/riskctrl.py +641 -0
- qubx/trackers/sizers.py +235 -0
- qubx/utils/__init__.py +5 -0
- qubx/utils/_pyxreloader.py +281 -0
- qubx/utils/charting/lookinglass.py +1057 -0
- qubx/utils/charting/mpl_helpers.py +1183 -0
- qubx/utils/marketdata/binance.py +284 -0
- qubx/utils/marketdata/ccxt.py +90 -0
- qubx/utils/marketdata/dukas.py +130 -0
- qubx/utils/misc.py +541 -0
- qubx/utils/ntp.py +63 -0
- qubx/utils/numbers_utils.py +7 -0
- qubx/utils/orderbook.py +491 -0
- qubx/utils/plotting/__init__.py +0 -0
- qubx/utils/plotting/dashboard.py +150 -0
- qubx/utils/plotting/data.py +137 -0
- qubx/utils/plotting/interfaces.py +25 -0
- qubx/utils/plotting/renderers/__init__.py +0 -0
- qubx/utils/plotting/renderers/plotly.py +0 -0
- qubx/utils/runner/__init__.py +1 -0
- qubx/utils/runner/_jupyter_runner.pyt +60 -0
- qubx/utils/runner/accounts.py +88 -0
- qubx/utils/runner/configs.py +65 -0
- qubx/utils/runner/runner.py +470 -0
- qubx/utils/time.py +312 -0
- qubx-0.5.7.dist-info/METADATA +105 -0
- qubx-0.5.7.dist-info/RECORD +100 -0
- qubx-0.5.7.dist-info/WHEEL +4 -0
- qubx-0.5.7.dist-info/entry_points.txt +3 -0
qubx/trackers/sizers.py
ADDED
|
@@ -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
|
+
|