Qubx 0.5.5__tar.gz → 0.5.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of Qubx might be problematic. Click here for more details.
- {qubx-0.5.5 → qubx-0.5.6}/PKG-INFO +1 -1
- {qubx-0.5.5 → qubx-0.5.6}/pyproject.toml +1 -1
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/backtester/account.py +4 -3
- qubx-0.5.6/src/qubx/backtester/management.py +237 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/context.py +6 -2
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/helpers.py +29 -15
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/interfaces.py +29 -3
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/mixins/processing.py +21 -5
- qubx-0.5.6/src/qubx/core/mixins/universe.py +272 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/runner/runner.py +10 -1
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/time.py +85 -0
- qubx-0.5.5/src/qubx/backtester/management.py +0 -141
- qubx-0.5.5/src/qubx/core/mixins/universe.py +0 -155
- {qubx-0.5.5 → qubx-0.5.6}/README.md +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/build.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/backtester/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/backtester/broker.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/backtester/data.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/backtester/ome.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/backtester/optimization.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/backtester/simulator.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/backtester/utils.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/cli/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/cli/commands.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/connectors/ccxt/customizations.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/account.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/basics.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/exceptions.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/loggers.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/lookups.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/metrics.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/mixins/market.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/series.pxd +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/series.pyi +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/series.pyx +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/data/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/data/helpers.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/data/readers.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/data/tardis.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/gathering/simplest.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/math/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/math/stats.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/pandaz/ta.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/pandaz/utils.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/resources/instruments/symbols-binance.cm.json +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/resources/instruments/symbols-binance.json +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/resources/instruments/symbols-binance.um.json +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/resources/instruments/symbols-bitfinex.f.json +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/resources/instruments/symbols-bitfinex.json +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/resources/instruments/symbols-kraken.f.json +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/resources/instruments/symbols-kraken.json +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/trackers/abvanced.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/trackers/sizers.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/misc.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/orderbook.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-0.5.5 → qubx-0.5.6}/src/qubx/utils/runner/configs.py +0 -0
|
@@ -48,14 +48,15 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
48
48
|
if self._fill_stop_order_at_price:
|
|
49
49
|
logger.info(f"[<y>{self.__class__.__name__}</y>] :: emulates stop orders executions at exact price")
|
|
50
50
|
|
|
51
|
-
def get_orders(self, instrument: Instrument | None = None) ->
|
|
51
|
+
def get_orders(self, instrument: Instrument | None = None) -> dict[str, Order]:
|
|
52
52
|
if instrument is not None:
|
|
53
53
|
ome = self.ome.get(instrument)
|
|
54
54
|
if ome is None:
|
|
55
55
|
raise ValueError(f"ExchangeService:get_orders :: No OME configured for '{instrument}'!")
|
|
56
|
-
return ome.get_open_orders()
|
|
57
56
|
|
|
58
|
-
|
|
57
|
+
return {o.id: o for o in ome.get_open_orders()}
|
|
58
|
+
|
|
59
|
+
return {o.id: o for ome in self.ome.values() for o in ome.get_open_orders()}
|
|
59
60
|
|
|
60
61
|
def get_position(self, instrument: Instrument) -> Position:
|
|
61
62
|
if instrument in self.positions:
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import zipfile
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from qubx.core.metrics import TradingSessionResult, _pfl_metrics_prepare
|
|
10
|
+
from qubx.utils.misc import blue, cyan, green, magenta, red, yellow
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BacktestsResultsManager:
|
|
14
|
+
"""
|
|
15
|
+
Manager class for handling backtesting results.
|
|
16
|
+
|
|
17
|
+
This class provides functionality to load, list and manage backtesting results stored in zip files.
|
|
18
|
+
Each result contains trading session information and metrics that can be loaded and analyzed.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
path : str
|
|
23
|
+
Path to directory containing backtesting result zip files
|
|
24
|
+
|
|
25
|
+
Methods
|
|
26
|
+
-------
|
|
27
|
+
reload()
|
|
28
|
+
Reloads all backtesting results from the specified path
|
|
29
|
+
list(regex="", with_metrics=False)
|
|
30
|
+
Lists all backtesting results, optionally filtered by regex and including metrics
|
|
31
|
+
load(name)
|
|
32
|
+
Loads a specific backtesting result by name
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, path: str):
|
|
36
|
+
self.path = path
|
|
37
|
+
self.reload()
|
|
38
|
+
|
|
39
|
+
def reload(self) -> "BacktestsResultsManager":
|
|
40
|
+
self.results = {}
|
|
41
|
+
names = defaultdict(lambda: 0)
|
|
42
|
+
for p in Path(self.path).glob("**/*.zip"):
|
|
43
|
+
with zipfile.ZipFile(p, "r") as zip_ref:
|
|
44
|
+
try:
|
|
45
|
+
info = yaml.safe_load(zip_ref.read("info.yml"))
|
|
46
|
+
info["path"] = str(p)
|
|
47
|
+
n = info.get("name", "")
|
|
48
|
+
_new_name = n if names[n] == 0 else f"{n}.{names[n]}"
|
|
49
|
+
names[n] += 1
|
|
50
|
+
info["name"] = _new_name
|
|
51
|
+
self.results[_new_name] = info
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
# - reindex
|
|
56
|
+
_idx = 1
|
|
57
|
+
for n in sorted(self.results.keys()):
|
|
58
|
+
self.results[n]["idx"] = _idx
|
|
59
|
+
_idx += 1
|
|
60
|
+
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def __getitem__(
|
|
64
|
+
self, name: str | int | list[int] | list[str] | slice
|
|
65
|
+
) -> TradingSessionResult | list[TradingSessionResult]:
|
|
66
|
+
return self.load(name)
|
|
67
|
+
|
|
68
|
+
def load(
|
|
69
|
+
self, name: str | int | list[int] | list[str] | slice
|
|
70
|
+
) -> TradingSessionResult | list[TradingSessionResult]:
|
|
71
|
+
match name:
|
|
72
|
+
case list():
|
|
73
|
+
return [self.load(i) for i in name]
|
|
74
|
+
case slice():
|
|
75
|
+
return [self.load(i) for i in range(name.start, name.stop, name.step if name.step else 1)]
|
|
76
|
+
|
|
77
|
+
for info in self.results.values():
|
|
78
|
+
match name:
|
|
79
|
+
case int():
|
|
80
|
+
if info.get("idx", -1) == name:
|
|
81
|
+
return TradingSessionResult.from_file(info["path"])
|
|
82
|
+
case str():
|
|
83
|
+
if info.get("name", "") == name:
|
|
84
|
+
return TradingSessionResult.from_file(info["path"])
|
|
85
|
+
raise ValueError(f"No result found for {name}")
|
|
86
|
+
|
|
87
|
+
def delete(self, name: str | int | list[int] | list[str] | slice):
|
|
88
|
+
def _del_idx(idx):
|
|
89
|
+
for info in self.results.values():
|
|
90
|
+
if info.get("idx", -1) == idx:
|
|
91
|
+
Path(info["path"]).unlink()
|
|
92
|
+
return info.get("name", idx)
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
match name:
|
|
96
|
+
case str():
|
|
97
|
+
nms = [_del_idx(i) for i in self._find_indices(name)]
|
|
98
|
+
self.reload()
|
|
99
|
+
print(f" -> Deleted {red(', '.join(nms))} ...")
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
case list():
|
|
103
|
+
nms = [_del_idx(i) for i in name]
|
|
104
|
+
self.reload()
|
|
105
|
+
print(f" -> Deleted {red(', '.join(nms))} ...")
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
case slice():
|
|
109
|
+
nms = [_del_idx(i) for i in range(name.start, name.stop, name.step if name.step else 1)]
|
|
110
|
+
self.reload()
|
|
111
|
+
print(f" -> Deleted {red(', '.join(nms))} ...")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
for info in self.results.values():
|
|
115
|
+
match name:
|
|
116
|
+
case int():
|
|
117
|
+
if info.get("idx", -1) == name:
|
|
118
|
+
Path(info["path"]).unlink()
|
|
119
|
+
print(f" -> Deleted {red(info.get('name', name))} ...")
|
|
120
|
+
self.reload()
|
|
121
|
+
return
|
|
122
|
+
case str():
|
|
123
|
+
if info.get("name", "") == name:
|
|
124
|
+
Path(info["path"]).unlink()
|
|
125
|
+
print(f" -> Deleted {red(info.get('name', name))} ...")
|
|
126
|
+
self.reload()
|
|
127
|
+
return
|
|
128
|
+
print(f" -> No results found for {red(name)} !")
|
|
129
|
+
|
|
130
|
+
def _find_indices(self, regex: str):
|
|
131
|
+
for n in sorted(self.results.keys()):
|
|
132
|
+
info = self.results[n]
|
|
133
|
+
s_cls = info.get("strategy_class", "").split(".")[-1]
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
if not re.match(regex, n, re.IGNORECASE):
|
|
137
|
+
if not re.match(regex, s_cls, re.IGNORECASE):
|
|
138
|
+
continue
|
|
139
|
+
except Exception:
|
|
140
|
+
if regex.lower() != n.lower() and regex.lower() != s_cls.lower():
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
yield info.get("idx", -1)
|
|
144
|
+
|
|
145
|
+
def list(self, regex: str = "", with_metrics=True, params=False, as_table=False):
|
|
146
|
+
"""List backtesting results with optional filtering and formatting.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
- regex (str, optional): Regular expression pattern to filter results by strategy name or class. Defaults to "".
|
|
150
|
+
- with_metrics (bool, optional): Whether to include performance metrics in output. Defaults to True.
|
|
151
|
+
- params (bool, optional): Whether to display strategy parameters. Defaults to False.
|
|
152
|
+
- as_table (bool, optional): Return results as a pandas DataFrame instead of printing. Defaults to False.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
- Optional[pd.DataFrame]: If as_table=True, returns a DataFrame containing the results sorted by creation time.
|
|
156
|
+
- Otherwise prints formatted results to console.
|
|
157
|
+
"""
|
|
158
|
+
_t_rep = []
|
|
159
|
+
for n in sorted(self.results.keys()):
|
|
160
|
+
info = self.results[n]
|
|
161
|
+
s_cls = info.get("strategy_class", "").split(".")[-1]
|
|
162
|
+
|
|
163
|
+
if regex:
|
|
164
|
+
if not re.match(regex, n, re.IGNORECASE):
|
|
165
|
+
if not re.match(regex, s_cls, re.IGNORECASE):
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
name = info.get("name", "")
|
|
169
|
+
smbs = ", ".join(info.get("symbols", list()))
|
|
170
|
+
start = pd.Timestamp(info.get("start", "")).round("1s")
|
|
171
|
+
stop = pd.Timestamp(info.get("stop", "")).round("1s")
|
|
172
|
+
dscr = info.get("description", "")
|
|
173
|
+
created = pd.Timestamp(info.get("creation_time", "")).round("1s")
|
|
174
|
+
metrics = info.get("performance", {})
|
|
175
|
+
author = info.get("author", "")
|
|
176
|
+
_s = f"{yellow(str(info.get('idx')))} - {red(name)} ::: {magenta(created)} by {cyan(author)}"
|
|
177
|
+
|
|
178
|
+
_one_line_dscr = ""
|
|
179
|
+
if dscr:
|
|
180
|
+
dscr = dscr.split("\n")
|
|
181
|
+
for _d in dscr:
|
|
182
|
+
_s += f"\n\t{magenta('# ' + _d)}"
|
|
183
|
+
_one_line_dscr += " " + _d
|
|
184
|
+
|
|
185
|
+
_s += f"\n\tstrategy: {green(s_cls)}"
|
|
186
|
+
_s += f"\n\tinterval: {blue(start)} - {blue(stop)}"
|
|
187
|
+
_s += f"\n\tcapital: {blue(info.get('capital', ''))} {info.get('base_currency', '')} ({info.get('commissions', '')})"
|
|
188
|
+
_s += f"\n\tinstruments: {blue(smbs)}"
|
|
189
|
+
if params:
|
|
190
|
+
formats = ["{" + f":<{i}" + "}" for i in [50]]
|
|
191
|
+
_p = pd.DataFrame.from_dict(info.get("parameters", {}), orient="index")
|
|
192
|
+
for i in _p.to_string(
|
|
193
|
+
max_colwidth=30,
|
|
194
|
+
header=False,
|
|
195
|
+
formatters=[(lambda x: cyan(fmt.format(str(x)))) for fmt in formats],
|
|
196
|
+
justify="left",
|
|
197
|
+
).split("\n"):
|
|
198
|
+
_s += f"\n\t | {yellow(i)}"
|
|
199
|
+
|
|
200
|
+
if not as_table:
|
|
201
|
+
print(_s)
|
|
202
|
+
|
|
203
|
+
if with_metrics:
|
|
204
|
+
_m_repr = (
|
|
205
|
+
pd.DataFrame.from_dict(metrics, orient="index")
|
|
206
|
+
.T[["gain", "cagr", "sharpe", "qr", "max_dd_pct", "mdd_usd", "fees", "execs"]]
|
|
207
|
+
.astype(float)
|
|
208
|
+
)
|
|
209
|
+
_m_repr = _m_repr.round(3).to_string(index=False)
|
|
210
|
+
_h, _v = _m_repr.split("\n")
|
|
211
|
+
if not as_table:
|
|
212
|
+
print("\t " + red(_h))
|
|
213
|
+
print("\t " + cyan(_v))
|
|
214
|
+
|
|
215
|
+
if not as_table:
|
|
216
|
+
print()
|
|
217
|
+
else:
|
|
218
|
+
metrics = {
|
|
219
|
+
m: round(v, 3)
|
|
220
|
+
for m, v in metrics.items()
|
|
221
|
+
if m in ["gain", "cagr", "sharpe", "qr", "max_dd_pct", "mdd_usd", "fees", "execs"]
|
|
222
|
+
}
|
|
223
|
+
_t_rep.append(
|
|
224
|
+
{"Index": info.get("idx", ""), "Strategy": name}
|
|
225
|
+
| metrics
|
|
226
|
+
| {
|
|
227
|
+
"start": start,
|
|
228
|
+
"stop": stop,
|
|
229
|
+
"Created": created,
|
|
230
|
+
"Author": author,
|
|
231
|
+
"Description": _one_line_dscr,
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if as_table:
|
|
236
|
+
_df = pd.DataFrame.from_records(_t_rep, index="Index")
|
|
237
|
+
return _df.sort_values(by="Created", ascending=False)
|
|
@@ -34,6 +34,7 @@ from qubx.core.interfaces import (
|
|
|
34
34
|
ITradingManager,
|
|
35
35
|
IUniverseManager,
|
|
36
36
|
PositionsTracker,
|
|
37
|
+
RemovalPolicy,
|
|
37
38
|
)
|
|
38
39
|
from qubx.core.loggers import StrategyLogging
|
|
39
40
|
from qubx.data.readers import DataReader
|
|
@@ -144,6 +145,7 @@ class StrategyContext(IStrategyContext):
|
|
|
144
145
|
account=self.account,
|
|
145
146
|
position_tracker=__position_tracker,
|
|
146
147
|
position_gathering=__position_gathering,
|
|
148
|
+
universe_manager=self._universe_manager,
|
|
147
149
|
cache=self._cache,
|
|
148
150
|
scheduler=self._scheduler,
|
|
149
151
|
is_simulation=self._data_provider.is_simulation,
|
|
@@ -325,8 +327,10 @@ class StrategyContext(IStrategyContext):
|
|
|
325
327
|
return self._trading_manager.cancel_orders(instrument)
|
|
326
328
|
|
|
327
329
|
# IUniverseManager delegation
|
|
328
|
-
def set_universe(
|
|
329
|
-
|
|
330
|
+
def set_universe(
|
|
331
|
+
self, instruments: list[Instrument], skip_callback: bool = False, if_has_position_then: RemovalPolicy = "close"
|
|
332
|
+
):
|
|
333
|
+
return self._universe_manager.set_universe(instruments, skip_callback, if_has_position_then)
|
|
330
334
|
|
|
331
335
|
def add_instruments(self, instruments: list[Instrument]):
|
|
332
336
|
return self._universe_manager.add_instruments(instruments)
|
|
@@ -14,7 +14,7 @@ from croniter import croniter
|
|
|
14
14
|
from qubx import logger
|
|
15
15
|
from qubx.core.basics import SW, CtrlChannel, DataType, Instrument, Timestamped
|
|
16
16
|
from qubx.core.series import OHLCV, Bar, OrderBook, Quote, Trade
|
|
17
|
-
from qubx.utils.time import convert_seconds_to_str, convert_tf_str_td64
|
|
17
|
+
from qubx.utils.time import convert_seconds_to_str, convert_tf_str_td64, interval_to_cron
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class CachedMarketDataHolder:
|
|
@@ -204,7 +204,7 @@ def _make_shift(_b, _w, _d, _h, _m, _s):
|
|
|
204
204
|
|
|
205
205
|
# return AS_TD(f'{_b*4}W') + AS_TD(f'{_w}W') + AS_TD(f'{_d}D') + AS_TD(f'{_h}h') + AS_TD(f'{_m}Min') + AS_TD(f'{_s}Sec')
|
|
206
206
|
for t in [
|
|
207
|
-
AS_TD(f"{_b*4}W"),
|
|
207
|
+
AS_TD(f"{_b * 4}W"),
|
|
208
208
|
AS_TD(f"{_w}W"),
|
|
209
209
|
AS_TD(f"{_d}D"),
|
|
210
210
|
AS_TD(f"{_h}h"),
|
|
@@ -218,12 +218,12 @@ def _make_shift(_b, _w, _d, _h, _m, _s):
|
|
|
218
218
|
return P, N
|
|
219
219
|
|
|
220
220
|
|
|
221
|
-
def _parse_schedule_spec(schedule: str) ->
|
|
221
|
+
def _parse_schedule_spec(schedule: str) -> dict[str, str]:
|
|
222
222
|
m = SPEC_REGEX.match(schedule)
|
|
223
223
|
return {k: v for k, v in m.groupdict().items() if v} if m else {}
|
|
224
224
|
|
|
225
225
|
|
|
226
|
-
def process_schedule_spec(spec_str: str | None) ->
|
|
226
|
+
def process_schedule_spec(spec_str: str | None) -> dict[str, Any]:
|
|
227
227
|
AS_INT = lambda d, k: int(d.get(k, 0)) # noqa: E731
|
|
228
228
|
S = lambda s: [x for x in re.split(r"[, ]", s) if x] # noqa: E731
|
|
229
229
|
config = {}
|
|
@@ -246,10 +246,16 @@ def process_schedule_spec(spec_str: str | None) -> Dict[str, Any]:
|
|
|
246
246
|
|
|
247
247
|
match _T:
|
|
248
248
|
case "cron":
|
|
249
|
-
if not _S
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
249
|
+
if not _S:
|
|
250
|
+
raise ValueError(f"Empty specification for cron: {spec_str}")
|
|
251
|
+
|
|
252
|
+
if not croniter.is_valid(_S):
|
|
253
|
+
_S = interval_to_cron(_S)
|
|
254
|
+
|
|
255
|
+
if not croniter.is_valid(_S):
|
|
256
|
+
raise ValueError(f"Wrong specification for cron: {spec_str}")
|
|
257
|
+
|
|
258
|
+
config = dict(type="cron", schedule=_S, spec=_S)
|
|
253
259
|
|
|
254
260
|
case "time":
|
|
255
261
|
for t in _t:
|
|
@@ -265,13 +271,20 @@ def process_schedule_spec(spec_str: str | None) -> Dict[str, Any]:
|
|
|
265
271
|
if croniter.is_valid(_S):
|
|
266
272
|
config = dict(type="cron", schedule=_S, spec=_S)
|
|
267
273
|
else:
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
274
|
+
# - try convert to cron
|
|
275
|
+
_S = interval_to_cron(_S)
|
|
276
|
+
if croniter.is_valid(_S):
|
|
277
|
+
config = dict(type="cron", schedule=_S, spec=_S)
|
|
278
|
+
else:
|
|
279
|
+
if _has_intervals:
|
|
280
|
+
_F = (
|
|
281
|
+
convert_seconds_to_str(
|
|
282
|
+
int(_s_pos.as_unit("s").to_timedelta64().item().total_seconds())
|
|
283
|
+
)
|
|
284
|
+
if not _F
|
|
285
|
+
else _F
|
|
286
|
+
)
|
|
287
|
+
config = dict(type="bar", schedule=None, timeframe=_F, delay=_s_neg, spec=_S)
|
|
275
288
|
case _:
|
|
276
289
|
config = dict(type=_T, schedule=None, timeframe=_F, delay=_shift, spec=_S)
|
|
277
290
|
|
|
@@ -348,6 +361,7 @@ class BasicScheduler:
|
|
|
348
361
|
# - update next nearest time
|
|
349
362
|
self._next_times[event] = next_time
|
|
350
363
|
self._next_nearest_time = np.datetime64(int(min(self._next_times.values()) * 1000000000), "ns")
|
|
364
|
+
# logger.debug(f" >>> ({event}) task is scheduled at {self._next_nearest_time}")
|
|
351
365
|
|
|
352
366
|
return True
|
|
353
367
|
logger.debug(f"({event}) task is not scheduled")
|
|
@@ -10,7 +10,7 @@ This module includes:
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import traceback
|
|
13
|
-
from typing import Any, Dict, List, Set, Tuple
|
|
13
|
+
from typing import Any, Dict, List, Literal, Set, Tuple
|
|
14
14
|
|
|
15
15
|
import numpy as np
|
|
16
16
|
import pandas as pd
|
|
@@ -36,6 +36,8 @@ from qubx.core.basics import (
|
|
|
36
36
|
from qubx.core.helpers import set_parameters_to_object
|
|
37
37
|
from qubx.core.series import OHLCV, Bar, Quote
|
|
38
38
|
|
|
39
|
+
RemovalPolicy = Literal["close", "wait_for_close", "wait_for_change"]
|
|
40
|
+
|
|
39
41
|
|
|
40
42
|
class IAccountViewer:
|
|
41
43
|
account_id: str
|
|
@@ -568,11 +570,18 @@ class ITradingManager:
|
|
|
568
570
|
class IUniverseManager:
|
|
569
571
|
"""Manages universe updates."""
|
|
570
572
|
|
|
571
|
-
def set_universe(
|
|
573
|
+
def set_universe(
|
|
574
|
+
self, instruments: list[Instrument], skip_callback: bool = False, if_has_position_then: RemovalPolicy = "close"
|
|
575
|
+
):
|
|
572
576
|
"""Set the trading universe.
|
|
573
577
|
|
|
574
578
|
Args:
|
|
575
579
|
instruments: List of instruments in the universe
|
|
580
|
+
skip_callback: Skip callback to the strategy
|
|
581
|
+
if_has_position_then: What to do if the instrument has a position
|
|
582
|
+
- “close” (default) - close position immediatelly and remove (unsubscribe) instrument from strategy
|
|
583
|
+
- “wait_for_close” - keep instrument and it’s position until it’s closed from strategy (or risk management), then remove instrument from strategy
|
|
584
|
+
- “wait_for_change” - keep instrument and position until strategy would try to change it - then close position and remove instrument
|
|
576
585
|
"""
|
|
577
586
|
...
|
|
578
587
|
|
|
@@ -584,11 +593,15 @@ class IUniverseManager:
|
|
|
584
593
|
"""
|
|
585
594
|
...
|
|
586
595
|
|
|
587
|
-
def remove_instruments(self, instruments: list[Instrument]):
|
|
596
|
+
def remove_instruments(self, instruments: list[Instrument], if_has_position_then: RemovalPolicy = "close"):
|
|
588
597
|
"""Remove instruments from the trading universe.
|
|
589
598
|
|
|
590
599
|
Args:
|
|
591
600
|
instruments: List of instruments to remove
|
|
601
|
+
if_has_position_then: What to do if the instrument has a position
|
|
602
|
+
- “close” (default) - close position immediatelly and remove (unsubscribe) instrument from strategy
|
|
603
|
+
- “wait_for_close” - keep instrument and it’s position until it’s closed from strategy (or risk management), then remove instrument from strategy
|
|
604
|
+
- “wait_for_change” - keep instrument and position until strategy would try to change it - then close position and remove instrument
|
|
592
605
|
"""
|
|
593
606
|
...
|
|
594
607
|
|
|
@@ -599,6 +612,19 @@ class IUniverseManager:
|
|
|
599
612
|
"""
|
|
600
613
|
...
|
|
601
614
|
|
|
615
|
+
def on_alter_position(self, instrument: Instrument) -> None:
|
|
616
|
+
"""
|
|
617
|
+
Called when the position of an instrument changes.
|
|
618
|
+
It can be used for postponed unsubscribed events
|
|
619
|
+
"""
|
|
620
|
+
...
|
|
621
|
+
|
|
622
|
+
def is_trading_allowed(self, instrument: Instrument) -> bool:
|
|
623
|
+
"""
|
|
624
|
+
Check if trading is allowed for an instrument because of the instrument's trading policy.
|
|
625
|
+
"""
|
|
626
|
+
...
|
|
627
|
+
|
|
602
628
|
|
|
603
629
|
class ISubscriptionManager:
|
|
604
630
|
"""Manages subscriptions."""
|
|
@@ -28,6 +28,7 @@ from qubx.core.interfaces import (
|
|
|
28
28
|
IStrategyContext,
|
|
29
29
|
ISubscriptionManager,
|
|
30
30
|
ITimeProvider,
|
|
31
|
+
IUniverseManager,
|
|
31
32
|
PositionsTracker,
|
|
32
33
|
)
|
|
33
34
|
from qubx.core.loggers import StrategyLogging
|
|
@@ -48,6 +49,7 @@ class ProcessingManager(IProcessingManager):
|
|
|
48
49
|
_position_gathering: IPositionGathering
|
|
49
50
|
_cache: CachedMarketDataHolder
|
|
50
51
|
_scheduler: BasicScheduler
|
|
52
|
+
_universe_manager: IUniverseManager
|
|
51
53
|
|
|
52
54
|
_handlers: dict[str, Callable[["ProcessingManager", Instrument, str, Any], TriggerEvent | None]]
|
|
53
55
|
_strategy_name: str
|
|
@@ -72,6 +74,7 @@ class ProcessingManager(IProcessingManager):
|
|
|
72
74
|
account: IAccountProcessor,
|
|
73
75
|
position_tracker: PositionsTracker,
|
|
74
76
|
position_gathering: IPositionGathering,
|
|
77
|
+
universe_manager: IUniverseManager,
|
|
75
78
|
cache: CachedMarketDataHolder,
|
|
76
79
|
scheduler: BasicScheduler,
|
|
77
80
|
is_simulation: bool,
|
|
@@ -86,6 +89,7 @@ class ProcessingManager(IProcessingManager):
|
|
|
86
89
|
self._is_simulation = is_simulation
|
|
87
90
|
self._position_gathering = position_gathering
|
|
88
91
|
self._position_tracker = position_tracker
|
|
92
|
+
self._universe_manager = universe_manager
|
|
89
93
|
self._cache = cache
|
|
90
94
|
self._scheduler = scheduler
|
|
91
95
|
|
|
@@ -100,7 +104,7 @@ class ProcessingManager(IProcessingManager):
|
|
|
100
104
|
|
|
101
105
|
def set_fit_schedule(self, schedule: str) -> None:
|
|
102
106
|
rule = process_schedule_spec(schedule)
|
|
103
|
-
if rule
|
|
107
|
+
if rule.get("type") != "cron":
|
|
104
108
|
raise ValueError("Only cron type is supported for fit schedule")
|
|
105
109
|
self._scheduler.schedule_event(rule["schedule"], "fit")
|
|
106
110
|
|
|
@@ -228,8 +232,13 @@ class ProcessingManager(IProcessingManager):
|
|
|
228
232
|
) -> list[TargetPosition]:
|
|
229
233
|
if target_positions is None:
|
|
230
234
|
return []
|
|
235
|
+
|
|
231
236
|
if isinstance(target_positions, TargetPosition):
|
|
232
237
|
target_positions = [target_positions]
|
|
238
|
+
|
|
239
|
+
# - check if trading is allowed for each target position
|
|
240
|
+
target_positions = [t for t in target_positions if self._universe_manager.is_trading_allowed(t.instrument)]
|
|
241
|
+
|
|
233
242
|
self._logging.save_signals_targets(target_positions)
|
|
234
243
|
return target_positions
|
|
235
244
|
|
|
@@ -394,14 +403,17 @@ class ProcessingManager(IProcessingManager):
|
|
|
394
403
|
return order
|
|
395
404
|
|
|
396
405
|
@SW.watch("StrategyContext")
|
|
397
|
-
def _handle_deals(self, instrument: Instrument, event_type: str, deals: list[Deal]) -> TriggerEvent | None:
|
|
398
|
-
self._account.process_deals(instrument, deals)
|
|
399
|
-
self._logging.save_deals(instrument, deals)
|
|
406
|
+
def _handle_deals(self, instrument: Instrument | None, event_type: str, deals: list[Deal]) -> TriggerEvent | None:
|
|
400
407
|
if instrument is None:
|
|
401
408
|
logger.debug(
|
|
402
409
|
f"[<y>{self.__class__.__name__}</y>] :: Execution report for unknown instrument <r>{instrument}</r>"
|
|
403
410
|
)
|
|
404
|
-
return
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
# - process deals only for subscribed instruments
|
|
414
|
+
self._account.process_deals(instrument, deals)
|
|
415
|
+
self._logging.save_deals(instrument, deals)
|
|
416
|
+
|
|
405
417
|
for d in deals:
|
|
406
418
|
# - notify position gatherer and tracker
|
|
407
419
|
self._position_gathering.on_execution_report(self._context, instrument, d)
|
|
@@ -409,4 +421,8 @@ class ProcessingManager(IProcessingManager):
|
|
|
409
421
|
logger.debug(
|
|
410
422
|
f"[<y>{self.__class__.__name__}</y>(<g>{instrument}</g>)] :: executed <r>{d.order_id}</r> | {d.amount} @ {d.price}"
|
|
411
423
|
)
|
|
424
|
+
|
|
425
|
+
# - notify universe manager about position change
|
|
426
|
+
self._universe_manager.on_alter_position(instrument)
|
|
427
|
+
|
|
412
428
|
return None
|