Qubx 0.5.3__cp311-cp311-manylinux_2_35_x86_64.whl → 0.5.5__cp311-cp311-manylinux_2_35_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/backtester/management.py +23 -1
- qubx/backtester/simulator.py +4 -1
- qubx/core/interfaces.py +34 -2
- qubx/core/metrics.py +85 -7
- qubx/core/series.cpython-311-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.cpython-311-x86_64-linux-gnu.so +0 -0
- qubx/ta/indicators.cpython-311-x86_64-linux-gnu.so +0 -0
- qubx/trackers/sizers.py +54 -0
- qubx/utils/runner/configs.py +2 -0
- qubx/utils/runner/runner.py +47 -9
- {qubx-0.5.3.dist-info → qubx-0.5.5.dist-info}/METADATA +1 -1
- {qubx-0.5.3.dist-info → qubx-0.5.5.dist-info}/RECORD +14 -14
- {qubx-0.5.3.dist-info → qubx-0.5.5.dist-info}/WHEEL +0 -0
- {qubx-0.5.3.dist-info → qubx-0.5.5.dist-info}/entry_points.txt +0 -0
qubx/backtester/management.py
CHANGED
|
@@ -90,8 +90,12 @@ class BacktestsResultsManager:
|
|
|
90
90
|
stop = pd.Timestamp(info.get("stop", "")).round("1s")
|
|
91
91
|
dscr = info.get("description", "")
|
|
92
92
|
_s = f"{yellow(str(info.get('idx')))} - {red(name)} ::: {magenta(pd.Timestamp(info.get('creation_time', '')).round('1s'))} by {cyan(info.get('author', ''))}"
|
|
93
|
+
|
|
93
94
|
if dscr:
|
|
94
|
-
|
|
95
|
+
dscr = dscr.split("\n")
|
|
96
|
+
for _d in dscr:
|
|
97
|
+
_s += f"\n\t{magenta('# ' + _d)}"
|
|
98
|
+
|
|
95
99
|
_s += f"\n\tstrategy: {green(s_cls)}"
|
|
96
100
|
_s += f"\n\tinterval: {blue(start)} - {blue(stop)}"
|
|
97
101
|
_s += f"\n\tcapital: {blue(info.get('capital', ''))} {info.get('base_currency', '')} ({info.get('commissions', '')})"
|
|
@@ -117,3 +121,21 @@ class BacktestsResultsManager:
|
|
|
117
121
|
for i in _m_repr:
|
|
118
122
|
print("\t " + cyan(i))
|
|
119
123
|
print()
|
|
124
|
+
|
|
125
|
+
def delete(self, name: str | int):
|
|
126
|
+
print(red(f" -> Danger zone - you are about to delete {name} ..."))
|
|
127
|
+
for info in self.results.values():
|
|
128
|
+
match name:
|
|
129
|
+
case int():
|
|
130
|
+
if info.get("idx", -1) == name:
|
|
131
|
+
Path(info["path"]).unlink()
|
|
132
|
+
print(f" -> Deleted {red(name)} ...")
|
|
133
|
+
self.reload()
|
|
134
|
+
return
|
|
135
|
+
case str():
|
|
136
|
+
if info.get("name", "") == name:
|
|
137
|
+
Path(info["path"]).unlink()
|
|
138
|
+
print(f" -> Deleted {red(name)} ...")
|
|
139
|
+
self.reload()
|
|
140
|
+
return
|
|
141
|
+
print(f" -> No results found for {red(name)} !")
|
qubx/backtester/simulator.py
CHANGED
|
@@ -57,6 +57,7 @@ def simulate(
|
|
|
57
57
|
open_close_time_indent_secs=1,
|
|
58
58
|
debug: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = "WARNING",
|
|
59
59
|
show_latency_report: bool = False,
|
|
60
|
+
parallel_backend: Literal["loky", "multiprocessing"] = "multiprocessing",
|
|
60
61
|
) -> list[TradingSessionResult]:
|
|
61
62
|
"""
|
|
62
63
|
Backtest utility for trading strategies or signals using historical data.
|
|
@@ -149,6 +150,7 @@ def simulate(
|
|
|
149
150
|
n_jobs=n_jobs,
|
|
150
151
|
silent=silent,
|
|
151
152
|
show_latency_report=show_latency_report,
|
|
153
|
+
parallel_backend=parallel_backend,
|
|
152
154
|
)
|
|
153
155
|
|
|
154
156
|
|
|
@@ -160,6 +162,7 @@ def _run_setups(
|
|
|
160
162
|
n_jobs: int = -1,
|
|
161
163
|
silent: bool = False,
|
|
162
164
|
show_latency_report: bool = False,
|
|
165
|
+
parallel_backend: Literal["loky", "multiprocessing"] = "multiprocessing",
|
|
163
166
|
) -> list[TradingSessionResult]:
|
|
164
167
|
# loggers don't work well with joblib and multiprocessing in general because they contain
|
|
165
168
|
# open file handlers that cannot be pickled. I found a solution which requires the usage of enqueue=True
|
|
@@ -170,7 +173,7 @@ def _run_setups(
|
|
|
170
173
|
n_jobs = 1 if _main_loop_silent else n_jobs
|
|
171
174
|
|
|
172
175
|
reports = ProgressParallel(
|
|
173
|
-
n_jobs=n_jobs, total=len(strategies_setups), silent=_main_loop_silent, backend=
|
|
176
|
+
n_jobs=n_jobs, total=len(strategies_setups), silent=_main_loop_silent, backend=parallel_backend
|
|
174
177
|
)(
|
|
175
178
|
delayed(_run_setup)(id, f"Simulated-{id}", setup, data_setup, start, stop, silent, show_latency_report)
|
|
176
179
|
for id, setup in enumerate(strategies_setups)
|
qubx/core/interfaces.py
CHANGED
|
@@ -1003,6 +1003,18 @@ class PositionsTracker:
|
|
|
1003
1003
|
...
|
|
1004
1004
|
|
|
1005
1005
|
|
|
1006
|
+
def _unpickle_instance(chain: tuple[type], state: dict):
|
|
1007
|
+
"""
|
|
1008
|
+
chain is a tuple of the *original* classes, e.g. (A, B, C).
|
|
1009
|
+
Reconstruct a new ephemeral class that inherits from them.
|
|
1010
|
+
"""
|
|
1011
|
+
name = "_".join(cls.__name__ for cls in chain)
|
|
1012
|
+
# Reverse the chain to respect the typical left-to-right MRO
|
|
1013
|
+
inst = type(name, chain[::-1], {"__module__": "__main__"})()
|
|
1014
|
+
inst.__dict__.update(state)
|
|
1015
|
+
return inst
|
|
1016
|
+
|
|
1017
|
+
|
|
1006
1018
|
class Mixable(type):
|
|
1007
1019
|
"""
|
|
1008
1020
|
It's possible to create composite strategies dynamically by adding mixins with functionality.
|
|
@@ -1011,8 +1023,28 @@ class Mixable(type):
|
|
|
1011
1023
|
NewStrategy(....) can be used in simulation or live trading.
|
|
1012
1024
|
"""
|
|
1013
1025
|
|
|
1014
|
-
def __add__(cls
|
|
1015
|
-
|
|
1026
|
+
def __add__(cls, other_cls):
|
|
1027
|
+
# If we already have a _composition, combine them;
|
|
1028
|
+
# else treat cls itself as the start of the chain
|
|
1029
|
+
cls_chain = getattr(cls, "__composition__", (cls,))
|
|
1030
|
+
other_chain = getattr(other_cls, "__composition__", (other_cls,))
|
|
1031
|
+
|
|
1032
|
+
# Combine them into one chain. You can define your own order rules:
|
|
1033
|
+
new_chain = cls_chain + other_chain
|
|
1034
|
+
|
|
1035
|
+
# Create ephemeral class
|
|
1036
|
+
name = "_".join(c.__name__ for c in new_chain)
|
|
1037
|
+
|
|
1038
|
+
def __reduce__(self):
|
|
1039
|
+
# Just return the chain of *original real classes*
|
|
1040
|
+
return _unpickle_instance, (new_chain, self.__dict__)
|
|
1041
|
+
|
|
1042
|
+
new_cls = type(
|
|
1043
|
+
name,
|
|
1044
|
+
new_chain[::-1],
|
|
1045
|
+
{"__module__": cls.__module__, "__composition__": new_chain, "__reduce__": __reduce__},
|
|
1046
|
+
)
|
|
1047
|
+
return new_cls
|
|
1016
1048
|
|
|
1017
1049
|
|
|
1018
1050
|
class IStrategy(metaclass=Mixable):
|
qubx/core/metrics.py
CHANGED
|
@@ -609,6 +609,7 @@ class TradingSessionResult:
|
|
|
609
609
|
creation_time: pd.Timestamp | None = None # when result was created
|
|
610
610
|
author: str | None = None # who created the result
|
|
611
611
|
qubx_version: str | None = None # Qubx version used to create the result
|
|
612
|
+
_metrics: dict[str, float] | None = None # performance metrics
|
|
612
613
|
# fmt: on
|
|
613
614
|
|
|
614
615
|
def __init__(
|
|
@@ -649,6 +650,39 @@ class TradingSessionResult:
|
|
|
649
650
|
self.creation_time = pd.Timestamp(creation_time) if creation_time else pd.Timestamp.now()
|
|
650
651
|
self.author = author
|
|
651
652
|
self.qubx_version = version()
|
|
653
|
+
self._metrics = None
|
|
654
|
+
|
|
655
|
+
def performance(self) -> dict[str, float]:
|
|
656
|
+
"""
|
|
657
|
+
Calculate performance metrics for the trading session
|
|
658
|
+
"""
|
|
659
|
+
if not self._metrics:
|
|
660
|
+
# - caluclate short statistics
|
|
661
|
+
self._metrics = portfolio_metrics(
|
|
662
|
+
self.portfolio_log,
|
|
663
|
+
self.executions_log,
|
|
664
|
+
self.capital,
|
|
665
|
+
performance_statistics_period=DAILY_365,
|
|
666
|
+
account_transactions=True,
|
|
667
|
+
commission_factor=1,
|
|
668
|
+
)
|
|
669
|
+
# - convert timestamps to isoformat
|
|
670
|
+
for k, v in self._metrics.items():
|
|
671
|
+
match v:
|
|
672
|
+
case pd.Timestamp():
|
|
673
|
+
self._metrics[k] = v.isoformat()
|
|
674
|
+
case np.float64():
|
|
675
|
+
self._metrics[k] = float(v)
|
|
676
|
+
# fmt: off
|
|
677
|
+
for k in [
|
|
678
|
+
"equity", "drawdown_usd", "drawdown_pct",
|
|
679
|
+
"compound_returns", "returns_daily", "returns", "monthly_returns",
|
|
680
|
+
"rolling_sharpe", "long_value", "short_value",
|
|
681
|
+
]:
|
|
682
|
+
self._metrics.pop(k, None)
|
|
683
|
+
# fmt: on
|
|
684
|
+
|
|
685
|
+
return self._metrics
|
|
652
686
|
|
|
653
687
|
@property
|
|
654
688
|
def symbols(self) -> list[str]:
|
|
@@ -690,6 +724,7 @@ class TradingSessionResult:
|
|
|
690
724
|
"author": self.author,
|
|
691
725
|
"qubx_version": self.qubx_version,
|
|
692
726
|
"symbols": self.symbols,
|
|
727
|
+
"performance": dict(self.performance()),
|
|
693
728
|
}
|
|
694
729
|
|
|
695
730
|
def to_html(self, compound=True) -> HTML:
|
|
@@ -743,8 +778,42 @@ class TradingSessionResult:
|
|
|
743
778
|
"""
|
|
744
779
|
return HTML(_tmpl)
|
|
745
780
|
|
|
746
|
-
def to_file(
|
|
747
|
-
|
|
781
|
+
def to_file(
|
|
782
|
+
self,
|
|
783
|
+
name: str,
|
|
784
|
+
description: str | None = None,
|
|
785
|
+
compound=True,
|
|
786
|
+
archive=True,
|
|
787
|
+
suffix: str | None = None,
|
|
788
|
+
attachments: list[str] | None = None,
|
|
789
|
+
):
|
|
790
|
+
"""
|
|
791
|
+
Save the trading session results to files.
|
|
792
|
+
|
|
793
|
+
Args:
|
|
794
|
+
name (str): Base name/path for saving the files
|
|
795
|
+
description (str | None, optional): Description to include in info file. Defaults to None.
|
|
796
|
+
compound (bool, optional): Whether to use compound returns in report. Defaults to True.
|
|
797
|
+
archive (bool, optional): Whether to zip the output files. Defaults to True.
|
|
798
|
+
suffix (str | None, optional): Optional suffix to append to filename. Defaults to None.
|
|
799
|
+
attachments (list[str] | None, optional): Additional files to include. Defaults to None.
|
|
800
|
+
|
|
801
|
+
The following files are saved:
|
|
802
|
+
- info.yml: Contains strategy configuration and metadata
|
|
803
|
+
- portfolio.csv: Portfolio state log
|
|
804
|
+
- executions.csv: Trade execution log
|
|
805
|
+
- signals.csv: Strategy signals log
|
|
806
|
+
- report.html: HTML performance report
|
|
807
|
+
- Any provided attachment files
|
|
808
|
+
|
|
809
|
+
If archive=True, all files are zipped into a single archive and the directory is removed.
|
|
810
|
+
"""
|
|
811
|
+
import shutil
|
|
812
|
+
|
|
813
|
+
if suffix is not None:
|
|
814
|
+
name = f"{name}{suffix}"
|
|
815
|
+
else:
|
|
816
|
+
name = (name + self.creation_time.strftime("%Y%m%d%H%M%S")) if self.creation_time else name
|
|
748
817
|
p = Path(makedirs(name))
|
|
749
818
|
with open(p / "info.yml", "w") as f:
|
|
750
819
|
info = self.info()
|
|
@@ -761,9 +830,13 @@ class TradingSessionResult:
|
|
|
761
830
|
with open(p / "report.html", "w") as f:
|
|
762
831
|
f.write(self.to_html(compound=compound).data)
|
|
763
832
|
|
|
764
|
-
|
|
765
|
-
|
|
833
|
+
# - save attachments
|
|
834
|
+
if attachments:
|
|
835
|
+
for a in attachments:
|
|
836
|
+
if (af := Path(a)).is_file():
|
|
837
|
+
shutil.copy(af, p / af.name)
|
|
766
838
|
|
|
839
|
+
if archive:
|
|
767
840
|
shutil.make_archive(name, "zip", p) # type: ignore
|
|
768
841
|
shutil.rmtree(p) # type: ignore
|
|
769
842
|
|
|
@@ -784,9 +857,11 @@ class TradingSessionResult:
|
|
|
784
857
|
# load result
|
|
785
858
|
_qbx_version = info.pop("qubx_version")
|
|
786
859
|
_decr = info.pop("description", None)
|
|
860
|
+
_perf = info.pop("performance", None)
|
|
787
861
|
info["instruments"] = info.pop("symbols")
|
|
788
862
|
tsr = TradingSessionResult(**info, portfolio_log=portfolio, executions_log=executions, signals_log=signals)
|
|
789
863
|
tsr.qubx_version = _qbx_version
|
|
864
|
+
tsr._metrics = _perf
|
|
790
865
|
return tsr
|
|
791
866
|
|
|
792
867
|
def __repr__(self) -> str:
|
|
@@ -796,10 +871,13 @@ class TradingSessionResult:
|
|
|
796
871
|
: QUBX: {self.qubx_version}
|
|
797
872
|
: Capital: {self.capital} {self.base_currency} ({self.commissions} @ {self.exchange})
|
|
798
873
|
: Instruments: [{",".join(self.symbols)}]
|
|
799
|
-
: Generated: {len(self.signals_log)} signals, {len(self.executions_log)} executions
|
|
800
|
-
: Strategy: {self.config(False)}
|
|
801
874
|
: Created: {self.creation_time} by {self.author}
|
|
802
|
-
|
|
875
|
+
: Strategy: {self.config(False)}
|
|
876
|
+
: Generated: {len(self.signals_log)} signals, {len(self.executions_log)} executions
|
|
877
|
+
"""
|
|
878
|
+
_perf = pd.DataFrame.from_dict(self.performance(), orient="index").T.to_string(index=None)
|
|
879
|
+
for _i, s in enumerate(_perf.split("\n")):
|
|
880
|
+
r += f" : {s}\n" if _i > 0 else f" `----: {s}\n"
|
|
803
881
|
return r
|
|
804
882
|
|
|
805
883
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
qubx/trackers/sizers.py
CHANGED
|
@@ -173,3 +173,57 @@ class LongShortRatioPortfolioSizer(IPositionSizer):
|
|
|
173
173
|
t_pos.append(TargetPosition.create(ctx, signal, _p * _p_q))
|
|
174
174
|
|
|
175
175
|
return t_pos
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class FixedRiskSizerWithConstantCapital(IPositionSizer):
|
|
179
|
+
def __init__(
|
|
180
|
+
self,
|
|
181
|
+
capital: float,
|
|
182
|
+
max_cap_in_risk: float,
|
|
183
|
+
max_allowed_position=np.inf,
|
|
184
|
+
divide_by_symbols: bool = True,
|
|
185
|
+
):
|
|
186
|
+
"""
|
|
187
|
+
Create fixed risk sizer calculator instance.
|
|
188
|
+
:param max_cap_in_risk: maximal risked capital (in percentage)
|
|
189
|
+
:param max_allowed_position: limitation for max position size in quoted currency (i.e. max 5000 in USDT)
|
|
190
|
+
:param reinvest_profit: if true use profit to reinvest
|
|
191
|
+
"""
|
|
192
|
+
self.capital = capital
|
|
193
|
+
assert self.capital > 0, f" >> {self.__class__.__name__}: Capital must be positive, got {self.capital}"
|
|
194
|
+
self.max_cap_in_risk = max_cap_in_risk / 100
|
|
195
|
+
self.max_allowed_position_quoted = max_allowed_position
|
|
196
|
+
self.divide_by_symbols = divide_by_symbols
|
|
197
|
+
|
|
198
|
+
def calculate_target_positions(self, ctx: IStrategyContext, signals: List[Signal]) -> List[TargetPosition]:
|
|
199
|
+
t_pos = []
|
|
200
|
+
for signal in signals:
|
|
201
|
+
target_position_size = 0
|
|
202
|
+
if signal.signal != 0:
|
|
203
|
+
if signal.stop and signal.stop > 0:
|
|
204
|
+
# - get signal entry price
|
|
205
|
+
if (_entry := self.get_signal_entry_price(ctx, signal)) is None:
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
# - just use same fixed capital
|
|
209
|
+
_cap = self.capital / (len(ctx.instruments) if self.divide_by_symbols else 1)
|
|
210
|
+
|
|
211
|
+
# fmt: off
|
|
212
|
+
_direction = np.sign(signal.signal)
|
|
213
|
+
target_position_size = (
|
|
214
|
+
_direction * min(
|
|
215
|
+
(_cap * self.max_cap_in_risk) / abs(signal.stop / _entry - 1),
|
|
216
|
+
self.max_allowed_position_quoted
|
|
217
|
+
) / _entry
|
|
218
|
+
)
|
|
219
|
+
# fmt: on
|
|
220
|
+
|
|
221
|
+
else:
|
|
222
|
+
logger.warning(
|
|
223
|
+
f" >>> {self.__class__.__name__}: stop is not specified for {str(signal)} - can't calculate position !"
|
|
224
|
+
)
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
t_pos.append(TargetPosition.create(ctx, signal, target_position_size))
|
|
228
|
+
|
|
229
|
+
return t_pos
|
qubx/utils/runner/configs.py
CHANGED
|
@@ -55,6 +55,8 @@ class StrategySimulationConfig(BaseModel):
|
|
|
55
55
|
parameters: dict = Field(default_factory=dict)
|
|
56
56
|
data: dict = Field(default_factory=dict)
|
|
57
57
|
simulation: dict = Field(default_factory=dict)
|
|
58
|
+
description: str | list[str] | None = None
|
|
59
|
+
variate: dict = Field(default_factory=dict)
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
def load_simulation_config_from_yaml(path: Path | str) -> StrategySimulationConfig:
|
qubx/utils/runner/runner.py
CHANGED
|
@@ -4,8 +4,11 @@ import time
|
|
|
4
4
|
from functools import reduce
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
7
9
|
from qubx import formatter, logger, lookup
|
|
8
10
|
from qubx.backtester.account import SimulatedAccountProcessor
|
|
11
|
+
from qubx.backtester.optimization import variate
|
|
9
12
|
from qubx.backtester.simulator import SimulatedBroker, simulate
|
|
10
13
|
from qubx.connectors.ccxt.account import CcxtAccountProcessor
|
|
11
14
|
from qubx.connectors.ccxt.broker import CcxtBroker
|
|
@@ -18,7 +21,7 @@ from qubx.core.helpers import BasicScheduler
|
|
|
18
21
|
from qubx.core.interfaces import IAccountProcessor, IBroker, IDataProvider, IStrategyContext
|
|
19
22
|
from qubx.core.loggers import StrategyLogging
|
|
20
23
|
from qubx.data import DataReader
|
|
21
|
-
from qubx.utils.misc import class_import, makedirs
|
|
24
|
+
from qubx.utils.misc import blue, class_import, cyan, green, magenta, makedirs, red, yellow
|
|
22
25
|
from qubx.utils.runner.configs import ExchangeConfig, load_simulation_config_from_yaml, load_strategy_config_from_yaml
|
|
23
26
|
|
|
24
27
|
from .accounts import AccountConfigurationManager
|
|
@@ -379,6 +382,8 @@ def simulate_strategy(
|
|
|
379
382
|
|
|
380
383
|
cfg = load_simulation_config_from_yaml(config_file)
|
|
381
384
|
stg = cfg.strategy
|
|
385
|
+
simulation_name = config_file.stem
|
|
386
|
+
_v_id = pd.Timestamp("now").strftime("%Y%m%d%H%M%S")
|
|
382
387
|
|
|
383
388
|
match stg:
|
|
384
389
|
case list():
|
|
@@ -388,8 +393,24 @@ def simulate_strategy(
|
|
|
388
393
|
case _:
|
|
389
394
|
raise SimulationConfigError(f"Invalid strategy type: {stg}")
|
|
390
395
|
|
|
391
|
-
|
|
392
|
-
|
|
396
|
+
# - create simulation setup
|
|
397
|
+
if cfg.variate:
|
|
398
|
+
# - get conditions for variations if exists
|
|
399
|
+
cond = cfg.variate.pop("with", None)
|
|
400
|
+
conditions = []
|
|
401
|
+
dict2lambda = lambda a, d: eval(f"lambda {a}: {d}") # noqa: E731
|
|
402
|
+
if cond:
|
|
403
|
+
for a, c in cond.items():
|
|
404
|
+
conditions.append(dict2lambda(a, c))
|
|
405
|
+
|
|
406
|
+
experiments = variate(stg_cls, **(cfg.parameters | cfg.variate), conditions=conditions)
|
|
407
|
+
experiments = {f"{simulation_name}.{_v_id}.[{k}]": v for k, v in experiments.items()}
|
|
408
|
+
print(f"Variation is enabled. There are {len(experiments)} simualtions to run.")
|
|
409
|
+
_n_jobs = -1
|
|
410
|
+
else:
|
|
411
|
+
strategy = stg_cls(**cfg.parameters)
|
|
412
|
+
experiments = {simulation_name: strategy}
|
|
413
|
+
_n_jobs = 1
|
|
393
414
|
|
|
394
415
|
data_i = {}
|
|
395
416
|
|
|
@@ -409,13 +430,30 @@ def simulate_strategy(
|
|
|
409
430
|
sim_params["stop"] = stop
|
|
410
431
|
logger.info(f"Stop date set to {stop}")
|
|
411
432
|
|
|
412
|
-
|
|
413
|
-
|
|
433
|
+
# - run simulation
|
|
434
|
+
print(f" > Run simulation for [{red(simulation_name)}] ::: {sim_params['start']} - {sim_params['stop']}")
|
|
435
|
+
sim_params["n_jobs"] = sim_params.get("n_jobs", _n_jobs)
|
|
436
|
+
test_res = simulate(experiments, data=data_i, **sim_params)
|
|
414
437
|
|
|
415
438
|
_where_to_save = save_path if save_path is not None else Path("results/")
|
|
416
|
-
s_path = Path(makedirs(str(_where_to_save))) /
|
|
417
|
-
|
|
418
|
-
logger.info(f"Saving results to <g>{s_path}</g> ...")
|
|
419
|
-
|
|
439
|
+
s_path = Path(makedirs(str(_where_to_save))) / simulation_name
|
|
440
|
+
|
|
441
|
+
# logger.info(f"Saving simulation results to <g>{s_path}</g> ...")
|
|
442
|
+
if cfg.description is not None:
|
|
443
|
+
_descr = cfg.description
|
|
444
|
+
if isinstance(cfg.description, list):
|
|
445
|
+
_descr = "\n".join(cfg.description)
|
|
446
|
+
else:
|
|
447
|
+
_descr = str(cfg.description)
|
|
448
|
+
|
|
449
|
+
if len(test_res) > 1:
|
|
450
|
+
# - TODO: think how to deal with variations !
|
|
451
|
+
s_path = s_path / f"variations.{_v_id}"
|
|
452
|
+
print(f" > Saving variations results to <g>{s_path}</g> ...")
|
|
453
|
+
for k, t in enumerate(test_res):
|
|
454
|
+
t.to_file(str(s_path), description=_descr, suffix=f".{k}", attachments=[str(config_file)])
|
|
455
|
+
else:
|
|
456
|
+
print(f" > Saving simulation results to <g>{s_path}</g> ...")
|
|
457
|
+
test_res[0].to_file(str(s_path), description=_descr, attachments=[str(config_file)])
|
|
420
458
|
|
|
421
459
|
return test_res
|
|
@@ -4,11 +4,11 @@ qubx/backtester/__init__.py,sha256=2VP3RCc5y542IBYynV33rsZ4PD-zG-lijTbD3udD9Sg,1
|
|
|
4
4
|
qubx/backtester/account.py,sha256=5lnLJq4i12vb8F2R-vuyXUs_UbapFSxLTTAL2IT1qGo,5807
|
|
5
5
|
qubx/backtester/broker.py,sha256=9Xm85OyLf-1hc2G1CcIPnatTMvFcdUTZSClJWc4quKU,2759
|
|
6
6
|
qubx/backtester/data.py,sha256=5t3e8PHPf2ZJ7ZsswvyOznyJx8PtqaOzaAy85aIENU0,11826
|
|
7
|
-
qubx/backtester/management.py,sha256=
|
|
7
|
+
qubx/backtester/management.py,sha256=qprAmiyu23bhtC1UEU45V0GpfrMzZSMQ8pcfwvu3iUo,5510
|
|
8
8
|
qubx/backtester/ome.py,sha256=zveRUXBKnjR3fqKD44NOKpJPAgPDjMXzdwRTFTbXOCw,11066
|
|
9
9
|
qubx/backtester/optimization.py,sha256=Bl-7zLpmpnRnc12iPxxQeKjITsoUegis5DgipQqF_4o,7618
|
|
10
10
|
qubx/backtester/simulated_data.py,sha256=MVAjmj6ocXr8gPMXDfFfYljI7HVnShTUnAaayl7TbG4,22183
|
|
11
|
-
qubx/backtester/simulator.py,sha256=
|
|
11
|
+
qubx/backtester/simulator.py,sha256=nkoCD3JQ6eUPxyo1e1lB7lLpd5_IwtB4oR7VpKaE0sI,14380
|
|
12
12
|
qubx/backtester/utils.py,sha256=V8ethHIUhs3o85q1ssWwJj1s6NplwVpdzOFIjluUd3A,30483
|
|
13
13
|
qubx/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
qubx/cli/commands.py,sha256=SMl7Zax3OiWjB5M4fFh5w0bIVI4OLJrYUDsamjCe7_w,2276
|
|
@@ -26,21 +26,21 @@ qubx/core/basics.py,sha256=pFfNYRGroQ1K0x54oZC7503sJA5r1GlENKfKq4PFIdM,27789
|
|
|
26
26
|
qubx/core/context.py,sha256=IGNLwS28OUdNjbeWyugy_e0ut-XwnXwpd791LKjyBFc,15461
|
|
27
27
|
qubx/core/exceptions.py,sha256=Jidp6v8rF6bCGB4SDNPt5CMHltkd9tbVkHzOvM29KdU,477
|
|
28
28
|
qubx/core/helpers.py,sha256=h9sFpl_o0SZuringIik0oDWBO07graHDKvB9tvSLQts,17058
|
|
29
|
-
qubx/core/interfaces.py,sha256=
|
|
29
|
+
qubx/core/interfaces.py,sha256=kkwf3Zy0LjKc2-WiZQ9dAR4OrEFVWAyI0j7bBo48O70,32663
|
|
30
30
|
qubx/core/loggers.py,sha256=ytUJh7k2npS8XY3chM7p-j32qJpsBzTuoIbXEvr2YiE,17639
|
|
31
31
|
qubx/core/lookups.py,sha256=PNOym2sxRRa0xQHTso8YDTqlHQ_dbZ3ohoJrUMNuba8,14779
|
|
32
|
-
qubx/core/metrics.py,sha256=
|
|
32
|
+
qubx/core/metrics.py,sha256=TOu3XkSd-RaR4QfuMeRioNFBn7-URTXDTkCKzPbzSkA,56577
|
|
33
33
|
qubx/core/mixins/__init__.py,sha256=zdoxocPyKvdvs4N6HCDTfwli5n-f4MD5sDnoChboj7k,196
|
|
34
34
|
qubx/core/mixins/market.py,sha256=s1NQDUjex7LR_ShnbSA3VnPMZpP7NmCgax5cmHdTmh4,3251
|
|
35
35
|
qubx/core/mixins/processing.py,sha256=_QmlPvB_cU3i98KayzybabqYPGhP_B21jFUPl6Cjmgc,17560
|
|
36
36
|
qubx/core/mixins/subscription.py,sha256=J_SX0CNw2bPy4bhxe0vswvDXY4LCkwXSaj_1PepKRLY,8540
|
|
37
37
|
qubx/core/mixins/trading.py,sha256=CQQIp1t1LJiFph5CiHQR4k4vxTymjFqrkA0awKYn4Dw,3224
|
|
38
38
|
qubx/core/mixins/universe.py,sha256=2N6wPoVjACQMK3D477KiE4UBg8cuVxFJzqblvBSMjWc,5749
|
|
39
|
-
qubx/core/series.cpython-311-x86_64-linux-gnu.so,sha256=
|
|
39
|
+
qubx/core/series.cpython-311-x86_64-linux-gnu.so,sha256=Dx11oQrgo1io5s_eceAyrrRPh3fvwzpEFQg4FMzuwoM,816968
|
|
40
40
|
qubx/core/series.pxd,sha256=EqgYT41FrpVB274mDG3jpLCSqK_ykkL-d-1IH8DE1ik,3301
|
|
41
41
|
qubx/core/series.pyi,sha256=zBt8DQCiIdTU3MLJz_9MlrONo7UCVYh2xYUltAcAj6c,3247
|
|
42
42
|
qubx/core/series.pyx,sha256=4XCRdH3otXsU8EJ-g4_zLQfhqR8TVjtEq_e4oDz5mZ4,33836
|
|
43
|
-
qubx/core/utils.cpython-311-x86_64-linux-gnu.so,sha256=
|
|
43
|
+
qubx/core/utils.cpython-311-x86_64-linux-gnu.so,sha256=ogh740NY72T9NMfjgT1aVrdx7QuHivdJW9ggukrVR00,82504
|
|
44
44
|
qubx/core/utils.pyi,sha256=DAjyRVPJSxK4Em-9wui2F0yYHfP5tI5DjKavXNOnHa8,276
|
|
45
45
|
qubx/core/utils.pyx,sha256=k5QHfEFvqhqWfCob89ANiJDKNG8gGbOh-O4CVoneZ8M,1696
|
|
46
46
|
qubx/data/__init__.py,sha256=ZBIOlciDTD44xyCYAJOngxwqxrKSgwJYDpMQdecPUIQ,245
|
|
@@ -61,7 +61,7 @@ qubx/resources/instruments/symbols-bitfinex.json,sha256=CpzoVgWzGZRN6RpUNhtJVxa3
|
|
|
61
61
|
qubx/resources/instruments/symbols-kraken.f.json,sha256=lwNqml3H7lNUl1h3siySSyE1MRcGfqfhb6BcxLsiKr0,212258
|
|
62
62
|
qubx/resources/instruments/symbols-kraken.json,sha256=RjUTvkQuuu7V1HfSQREvnA4qqkdkB3-rzykDaQds2rQ,456544
|
|
63
63
|
qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
64
|
-
qubx/ta/indicators.cpython-311-x86_64-linux-gnu.so,sha256=
|
|
64
|
+
qubx/ta/indicators.cpython-311-x86_64-linux-gnu.so,sha256=bvad4VoqwSg4l10caAqMib0TBDUMDvmC4djUeIaLQ7c,609640
|
|
65
65
|
qubx/ta/indicators.pxd,sha256=eCJ9paOxtxbDFx4U5CUhcgB1jjCQAfVqMF2FnbJ03Lo,4222
|
|
66
66
|
qubx/ta/indicators.pyi,sha256=NJlvN_774UV1U3_lvaYYbCEikLR8sOUo0TdcUGR5GBM,1940
|
|
67
67
|
qubx/ta/indicators.pyx,sha256=FVkv5ld04TpZMT3a_kR1MU3IUuWfijzjJnh_lG78JxM,26029
|
|
@@ -70,7 +70,7 @@ qubx/trackers/abvanced.py,sha256=vo4DuX6sYzsXLcp5z1UYuGowlJEE47vzmSoKsMLBPu4,103
|
|
|
70
70
|
qubx/trackers/composite.py,sha256=W-n1vd4l-RZEoojj6lICqvJ8EgTV2kE6JUUmZUkZ1cI,6339
|
|
71
71
|
qubx/trackers/rebalancers.py,sha256=5Dx39QZ67iZVx-cfpYx4IoMgDd7-fCHvGkwtezL7ofY,5269
|
|
72
72
|
qubx/trackers/riskctrl.py,sha256=CawDn6x3cEyJFbhbLl4yg3pnwzJbRT9UCy2L-W6wXek,26085
|
|
73
|
-
qubx/trackers/sizers.py,sha256=
|
|
73
|
+
qubx/trackers/sizers.py,sha256=tIo3kOvfdv9vG4v4_Cq1VfunFOghn08BhI8F2QPvJd0,9250
|
|
74
74
|
qubx/utils/__init__.py,sha256=pIS1ulI6Hj8btZlPd5P9To7DlyEY20bEVvFREAZkR0A,384
|
|
75
75
|
qubx/utils/_pyxreloader.py,sha256=FyqGzfSpZGYziB8JYS5AP3cLRAvJSIPAKgwQn0E4YQ0,12017
|
|
76
76
|
qubx/utils/charting/lookinglass.py,sha256=m7lWU8c0E8tXzGbkN0GB8CL-kd92MnH_wD8cATX067k,39232
|
|
@@ -91,10 +91,10 @@ qubx/utils/plotting/renderers/plotly.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
|
|
|
91
91
|
qubx/utils/runner/__init__.py,sha256=axs9MF78BYk30jhHBu0gSXIr-IN5ZOzoprlJ_N85yN8,77
|
|
92
92
|
qubx/utils/runner/_jupyter_runner.pyt,sha256=0SSc9F6caok_uRy9Qzy3L7hEuebZykH6U5QEM9YnhZU,2321
|
|
93
93
|
qubx/utils/runner/accounts.py,sha256=3D9bqqG4MWVRw2YJ5iT1RgmyGRdTEBr7BDk1UephIUo,3237
|
|
94
|
-
qubx/utils/runner/configs.py,sha256=
|
|
95
|
-
qubx/utils/runner/runner.py,sha256=
|
|
94
|
+
qubx/utils/runner/configs.py,sha256=nQXU1oqtSSGpGHw4cqk1dVpcojibj7bzjWZbDAHRxNc,1741
|
|
95
|
+
qubx/utils/runner/runner.py,sha256=0Dp2piBStIYwx3BTCGBjnynZaXj_F8JfmP0Z1964mbE,17444
|
|
96
96
|
qubx/utils/time.py,sha256=yYYAZvfXn79xE32nyoyJqBTdBbQum7-jzIiZf5xOi50,6612
|
|
97
|
-
qubx-0.5.
|
|
98
|
-
qubx-0.5.
|
|
99
|
-
qubx-0.5.
|
|
100
|
-
qubx-0.5.
|
|
97
|
+
qubx-0.5.5.dist-info/METADATA,sha256=-Jd0-kM0OkoUsZLEc2MY152y2_4tWiNNZX9S4_uhQAY,3575
|
|
98
|
+
qubx-0.5.5.dist-info/WHEEL,sha256=MLOa6LysROdjgj4FVxsHitAnIh8Be2D_c9ZSBHKrz2M,110
|
|
99
|
+
qubx-0.5.5.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
|
|
100
|
+
qubx-0.5.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|