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/__init__.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
import stackprinter
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from qubx.core.lookups import FeesLookup, GlobalLookup, InstrumentsLookup
|
|
9
|
+
from qubx.utils import runtime_env, set_mpl_theme
|
|
10
|
+
from qubx.utils.misc import install_pyx_recompiler_for_dev
|
|
11
|
+
|
|
12
|
+
# - TODO: import some main methods from packages
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def formatter(record):
|
|
16
|
+
end = record["extra"].get("end", "\n")
|
|
17
|
+
fmt = "<lvl>{message}</lvl>%s" % end
|
|
18
|
+
if record["level"].name in {"WARNING", "SNAKY"}:
|
|
19
|
+
fmt = "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - %s" % fmt
|
|
20
|
+
|
|
21
|
+
prefix = (
|
|
22
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> [ <level>%s</level> ] <cyan>({module})</cyan> "
|
|
23
|
+
% record["level"].icon
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if record["exception"] is not None:
|
|
27
|
+
# stackprinter.set_excepthook(style='darkbg2')
|
|
28
|
+
record["extra"]["stack"] = stackprinter.format(record["exception"], style="darkbg3")
|
|
29
|
+
fmt += "\n{extra[stack]}\n"
|
|
30
|
+
|
|
31
|
+
if record["level"].name in {"TEXT"}:
|
|
32
|
+
prefix = ""
|
|
33
|
+
|
|
34
|
+
return prefix + fmt
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class QubxLogConfig:
|
|
38
|
+
@staticmethod
|
|
39
|
+
def get_log_level():
|
|
40
|
+
return os.getenv("QUBX_LOG_LEVEL", "WARNING")
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def set_log_level(level: str):
|
|
44
|
+
os.environ["QUBX_LOG_LEVEL"] = level
|
|
45
|
+
QubxLogConfig.setup_logger(level)
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def setup_logger(level: str | None = None, custom_formatter: Callable | None = None):
|
|
49
|
+
global logger
|
|
50
|
+
config = {
|
|
51
|
+
"handlers": [
|
|
52
|
+
{"sink": sys.stdout, "format": "{time} - {message}"},
|
|
53
|
+
],
|
|
54
|
+
"extra": {"user": "someone"},
|
|
55
|
+
}
|
|
56
|
+
logger.configure(**config)
|
|
57
|
+
logger.remove(None)
|
|
58
|
+
level = level or QubxLogConfig.get_log_level()
|
|
59
|
+
logger.add(sys.stdout, format=custom_formatter or formatter, colorize=True, level=level, enqueue=True)
|
|
60
|
+
logger = logger.opt(colors=True)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
QubxLogConfig.setup_logger()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# - global lookup helper
|
|
67
|
+
lookup = GlobalLookup(InstrumentsLookup(), FeesLookup())
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# registering magic for jupyter notebook
|
|
71
|
+
if runtime_env() in ["notebook", "shell"]:
|
|
72
|
+
from IPython.core.getipython import get_ipython
|
|
73
|
+
from IPython.core.magic import Magics, line_cell_magic, line_magic, magics_class
|
|
74
|
+
|
|
75
|
+
@magics_class
|
|
76
|
+
class QubxMagics(Magics):
|
|
77
|
+
# process data manager
|
|
78
|
+
__manager = None
|
|
79
|
+
|
|
80
|
+
@line_magic
|
|
81
|
+
def qubx(self, line: str):
|
|
82
|
+
self.qubx_setup("dark" + " " + line)
|
|
83
|
+
|
|
84
|
+
@line_magic
|
|
85
|
+
def qubxd(self, line: str):
|
|
86
|
+
self.qubx_setup("dark" + " " + line)
|
|
87
|
+
|
|
88
|
+
@line_magic
|
|
89
|
+
def qubxl(self, line: str):
|
|
90
|
+
self.qubx_setup("light" + " " + line)
|
|
91
|
+
|
|
92
|
+
@line_magic
|
|
93
|
+
def qubx_setup(self, line: str):
|
|
94
|
+
"""
|
|
95
|
+
QUBX framework initialization
|
|
96
|
+
"""
|
|
97
|
+
import os
|
|
98
|
+
|
|
99
|
+
args = [x.strip() for x in line.split(" ")]
|
|
100
|
+
|
|
101
|
+
# setup cython dev hooks - only if 'dev' is passed as argument
|
|
102
|
+
if line and "dev" in args:
|
|
103
|
+
install_pyx_recompiler_for_dev()
|
|
104
|
+
|
|
105
|
+
tpl_path = os.path.join(os.path.dirname(__file__), "_nb_magic.py")
|
|
106
|
+
with open(tpl_path, "r", encoding="utf8") as myfile:
|
|
107
|
+
s = myfile.read()
|
|
108
|
+
|
|
109
|
+
exec(s, self.shell.user_ns)
|
|
110
|
+
|
|
111
|
+
# setup more funcy mpl theme instead of ugly default
|
|
112
|
+
if line:
|
|
113
|
+
if "dark" in line.lower():
|
|
114
|
+
set_mpl_theme("dark")
|
|
115
|
+
# - temporary workaround for vscode - dark theme not applying to ipywidgets in notebook
|
|
116
|
+
# - see https://github.com/microsoft/vscode-jupyter/issues/7161
|
|
117
|
+
if runtime_env() == "notebook":
|
|
118
|
+
_vscode_clr_trick = """from IPython.display import display, HTML; display(HTML("<style> .cell-output-ipywidget-background { background-color: transparent !important; } :root { --jp-widgets-color: var(--vscode-editor-foreground); --jp-widgets-font-size: var(--vscode-editor-font-size); } .widget-hprogress, .jupyter-widget-hprogress { height: 16px; align-self: center; kj} table.dataframe, .dataframe td, .dataframe tr { border: 1px solid #55554a85; border-collapse: collapse; color: #859548d9 !important; } .dataframe th { border: 1px solid #55554a85; border-collapse: collapse; background-color: #010101 !important; color: #177 !important; } </style>"))"""
|
|
119
|
+
exec(_vscode_clr_trick, self.shell.user_ns)
|
|
120
|
+
|
|
121
|
+
elif "light" in line.lower():
|
|
122
|
+
set_mpl_theme("light")
|
|
123
|
+
|
|
124
|
+
def _get_manager(self):
|
|
125
|
+
if self.__manager is None:
|
|
126
|
+
import multiprocessing as m
|
|
127
|
+
|
|
128
|
+
self.__manager = m.Manager()
|
|
129
|
+
return self.__manager
|
|
130
|
+
|
|
131
|
+
@line_cell_magic
|
|
132
|
+
def proc(self, line, cell=None):
|
|
133
|
+
"""
|
|
134
|
+
Run cell in separate process
|
|
135
|
+
|
|
136
|
+
>>> %%proc x, y as MyProc1
|
|
137
|
+
>>> x.set('Hello')
|
|
138
|
+
>>> y.set([1,2,3,4])
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
import multiprocessing as m
|
|
142
|
+
import re
|
|
143
|
+
import time
|
|
144
|
+
|
|
145
|
+
# create ext args
|
|
146
|
+
name = None
|
|
147
|
+
if line:
|
|
148
|
+
# check if custom process name was provided
|
|
149
|
+
if " as " in line:
|
|
150
|
+
line, name = line.split("as")
|
|
151
|
+
if not name.isspace():
|
|
152
|
+
name = name.strip()
|
|
153
|
+
else:
|
|
154
|
+
print('>>> Process name must be specified afer "as" keyword !')
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
ipy = get_ipython()
|
|
158
|
+
for a in [x for x in re.split(r"[\ ,;]", line.strip()) if x]:
|
|
159
|
+
ipy.push({a: self._get_manager().Value(None, None)})
|
|
160
|
+
|
|
161
|
+
# code to run
|
|
162
|
+
lines = "\n".join([" %s" % x for x in cell.split("\n")])
|
|
163
|
+
|
|
164
|
+
def fn():
|
|
165
|
+
result = get_ipython().run_cell(lines)
|
|
166
|
+
|
|
167
|
+
# send errors to parent
|
|
168
|
+
if result.error_before_exec:
|
|
169
|
+
raise result.error_before_exec
|
|
170
|
+
|
|
171
|
+
if result.error_in_exec:
|
|
172
|
+
raise result.error_in_exec
|
|
173
|
+
|
|
174
|
+
t_start = str(time.time()).replace(".", "_")
|
|
175
|
+
f_id = f"proc_{t_start}" if name is None else name
|
|
176
|
+
if self._is_task_name_already_used(f_id):
|
|
177
|
+
f_id = f"{f_id}_{t_start}"
|
|
178
|
+
|
|
179
|
+
task = m.Process(target=fn, name=f_id)
|
|
180
|
+
task.start()
|
|
181
|
+
print(" -> Task %s is started" % f_id)
|
|
182
|
+
|
|
183
|
+
def _is_task_name_already_used(self, name):
|
|
184
|
+
import multiprocessing as m
|
|
185
|
+
|
|
186
|
+
for p in m.active_children():
|
|
187
|
+
if p.name == name:
|
|
188
|
+
return True
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
@line_magic
|
|
192
|
+
def list_proc(self, line):
|
|
193
|
+
import multiprocessing as m
|
|
194
|
+
|
|
195
|
+
for p in m.active_children():
|
|
196
|
+
print(p.name)
|
|
197
|
+
|
|
198
|
+
@line_magic
|
|
199
|
+
def kill_proc(self, line):
|
|
200
|
+
import multiprocessing as m
|
|
201
|
+
|
|
202
|
+
for p in m.active_children():
|
|
203
|
+
if line and p.name.startswith(line):
|
|
204
|
+
p.terminate()
|
|
205
|
+
|
|
206
|
+
# - registering magic here
|
|
207
|
+
get_ipython().register_magics(QubxMagics) # type: ignore
|
qubx/_nb_magic.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
""" "
|
|
2
|
+
Here stuff we want to have in every Jupyter notebook after calling %qubx magic
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import qubx
|
|
6
|
+
from qubx.utils import runtime_env
|
|
7
|
+
from qubx.utils.misc import add_project_to_system_path, logo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def np_fmt_short():
|
|
11
|
+
# default np output is 75 columns so extend it a bit and suppress scientific fmt for small floats
|
|
12
|
+
np.set_printoptions(linewidth=240, suppress=True)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def np_fmt_reset():
|
|
16
|
+
# reset default np printing options
|
|
17
|
+
np.set_printoptions(
|
|
18
|
+
edgeitems=3,
|
|
19
|
+
infstr="inf",
|
|
20
|
+
linewidth=75,
|
|
21
|
+
nanstr="nan",
|
|
22
|
+
precision=8,
|
|
23
|
+
suppress=False,
|
|
24
|
+
threshold=1000,
|
|
25
|
+
formatter=None,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if runtime_env() in ["notebook", "shell"]:
|
|
30
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
31
|
+
# -- all imports below will appear in notebook after calling %%qubx magic ---
|
|
32
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
33
|
+
|
|
34
|
+
# - - - - Common stuff - - - -
|
|
35
|
+
from datetime import time, timedelta # noqa: F401
|
|
36
|
+
|
|
37
|
+
import numpy as np # noqa: F401
|
|
38
|
+
import pandas as pd # noqa: F401
|
|
39
|
+
|
|
40
|
+
# - - - - Charting stuff - - - -
|
|
41
|
+
from matplotlib import pyplot as plt # noqa: F401
|
|
42
|
+
from tqdm.auto import tqdm # noqa: F401
|
|
43
|
+
|
|
44
|
+
# - - - - TA stuff and indicators - - - -
|
|
45
|
+
import qubx.pandaz.ta as pta # noqa: F401
|
|
46
|
+
import qubx.ta.indicators as ta # noqa: F401
|
|
47
|
+
from qubx.backtester.optimization import variate # noqa: F401
|
|
48
|
+
|
|
49
|
+
# - - - - Simulator stuff - - - -
|
|
50
|
+
from qubx.backtester.simulator import simulate # noqa: F401
|
|
51
|
+
|
|
52
|
+
# - - - - Portfolio analysis - - - -
|
|
53
|
+
from qubx.core.metrics import ( # noqa: F401
|
|
54
|
+
chart_signals,
|
|
55
|
+
drop_symbols,
|
|
56
|
+
get_symbol_pnls,
|
|
57
|
+
pick_symbols,
|
|
58
|
+
pnl,
|
|
59
|
+
portfolio_metrics,
|
|
60
|
+
tearsheet,
|
|
61
|
+
)
|
|
62
|
+
from qubx.data.helpers import loader # noqa: F401
|
|
63
|
+
|
|
64
|
+
# - - - - Data reading - - - -
|
|
65
|
+
from qubx.data.readers import ( # noqa: F401
|
|
66
|
+
AsOhlcvSeries,
|
|
67
|
+
AsPandasFrame,
|
|
68
|
+
AsQuotes,
|
|
69
|
+
AsTimestampedRecords,
|
|
70
|
+
CsvStorageDataReader,
|
|
71
|
+
MultiQdbConnector,
|
|
72
|
+
QuestDBConnector,
|
|
73
|
+
RestoreTicksFromOHLC,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# - - - - Utils - - - -
|
|
77
|
+
from qubx.pandaz.utils import ( # noqa: F401
|
|
78
|
+
continuous_periods,
|
|
79
|
+
drop_duplicated_indexes,
|
|
80
|
+
generate_equal_date_ranges,
|
|
81
|
+
ohlc_resample,
|
|
82
|
+
retain_columns_and_join,
|
|
83
|
+
rolling_forward_test_split,
|
|
84
|
+
scols,
|
|
85
|
+
srows,
|
|
86
|
+
)
|
|
87
|
+
from qubx.utils.charting.lookinglass import LookingGlass # noqa: F401
|
|
88
|
+
from qubx.utils.charting.mpl_helpers import fig, ohlc_plot, plot_trends, sbp, subplot # noqa: F401
|
|
89
|
+
from qubx.utils.misc import this_project_root # noqa: F401
|
|
90
|
+
|
|
91
|
+
# - setup short numpy output format
|
|
92
|
+
np_fmt_short()
|
|
93
|
+
|
|
94
|
+
# - add project home to system path
|
|
95
|
+
add_project_to_system_path()
|
|
96
|
+
|
|
97
|
+
# show logo first time
|
|
98
|
+
if not hasattr(qubx.QubxMagics, "__already_initialized__"):
|
|
99
|
+
setattr(qubx.QubxMagics, "__already_initialized__", True)
|
|
100
|
+
logo()
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from qubx import logger
|
|
2
|
+
from qubx.backtester.ome import OrdersManagementEngine
|
|
3
|
+
from qubx.core.account import BasicAccountProcessor
|
|
4
|
+
from qubx.core.basics import (
|
|
5
|
+
ZERO_COSTS,
|
|
6
|
+
CtrlChannel,
|
|
7
|
+
Instrument,
|
|
8
|
+
Order,
|
|
9
|
+
Position,
|
|
10
|
+
Timestamped,
|
|
11
|
+
TransactionCostsCalculator,
|
|
12
|
+
dt_64,
|
|
13
|
+
)
|
|
14
|
+
from qubx.core.interfaces import ITimeProvider
|
|
15
|
+
from qubx.core.series import Bar, OrderBook, Quote, Trade
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
19
|
+
ome: dict[Instrument, OrdersManagementEngine]
|
|
20
|
+
order_to_instrument: dict[str, Instrument]
|
|
21
|
+
|
|
22
|
+
_channel: CtrlChannel
|
|
23
|
+
_fill_stop_order_at_price: bool
|
|
24
|
+
_half_tick_size: dict[Instrument, float]
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
account_id: str,
|
|
29
|
+
channel: CtrlChannel,
|
|
30
|
+
base_currency: str,
|
|
31
|
+
initial_capital: float,
|
|
32
|
+
time_provider: ITimeProvider,
|
|
33
|
+
tcc: TransactionCostsCalculator = ZERO_COSTS,
|
|
34
|
+
accurate_stop_orders_execution: bool = False,
|
|
35
|
+
) -> None:
|
|
36
|
+
super().__init__(
|
|
37
|
+
account_id=account_id,
|
|
38
|
+
time_provider=time_provider,
|
|
39
|
+
base_currency=base_currency,
|
|
40
|
+
tcc=tcc,
|
|
41
|
+
initial_capital=initial_capital,
|
|
42
|
+
)
|
|
43
|
+
self.ome = {}
|
|
44
|
+
self.order_to_instrument = {}
|
|
45
|
+
self._channel = channel
|
|
46
|
+
self._half_tick_size = {}
|
|
47
|
+
self._fill_stop_order_at_price = accurate_stop_orders_execution
|
|
48
|
+
if self._fill_stop_order_at_price:
|
|
49
|
+
logger.info(f"[<y>{self.__class__.__name__}</y>] :: emulates stop orders executions at exact price")
|
|
50
|
+
|
|
51
|
+
def get_orders(self, instrument: Instrument | None = None) -> dict[str, Order]:
|
|
52
|
+
if instrument is not None:
|
|
53
|
+
ome = self.ome.get(instrument)
|
|
54
|
+
if ome is None:
|
|
55
|
+
raise ValueError(f"ExchangeService:get_orders :: No OME configured for '{instrument}'!")
|
|
56
|
+
|
|
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()}
|
|
60
|
+
|
|
61
|
+
def get_position(self, instrument: Instrument) -> Position:
|
|
62
|
+
if instrument in self.positions:
|
|
63
|
+
return self.positions[instrument]
|
|
64
|
+
|
|
65
|
+
# - initiolize OME for this instrument
|
|
66
|
+
self.ome[instrument] = OrdersManagementEngine(
|
|
67
|
+
instrument=instrument,
|
|
68
|
+
time_provider=self.time_provider,
|
|
69
|
+
tcc=self._tcc, # type: ignore
|
|
70
|
+
fill_stop_order_at_price=self._fill_stop_order_at_price,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# - initiolize empty position
|
|
74
|
+
position = Position(instrument) # type: ignore
|
|
75
|
+
self._half_tick_size[instrument] = instrument.tick_size / 2 # type: ignore
|
|
76
|
+
self.attach_positions(position)
|
|
77
|
+
return self.positions[instrument]
|
|
78
|
+
|
|
79
|
+
def update_position_price(self, time: dt_64, instrument: Instrument, price: float) -> None:
|
|
80
|
+
super().update_position_price(time, instrument, price)
|
|
81
|
+
|
|
82
|
+
# - first we need to update OME with new quote.
|
|
83
|
+
# - if update is not a quote we need 'emulate' it.
|
|
84
|
+
# - actually if SimulatedExchangeService is used in backtesting mode it will recieve only quotes
|
|
85
|
+
# - case when we need that - SimulatedExchangeService is used for paper trading and data provider configured to listen to OHLC or TAS.
|
|
86
|
+
# - probably we need to subscribe to quotes in real data provider in any case and then this emulation won't be needed.
|
|
87
|
+
quote = price if isinstance(price, Quote) else self.emulate_quote_from_data(instrument, time, price)
|
|
88
|
+
if quote is None:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
# - process new quote
|
|
92
|
+
self._process_new_quote(instrument, quote)
|
|
93
|
+
|
|
94
|
+
def process_order(self, order: Order, update_locked_value: bool = True) -> None:
|
|
95
|
+
_new = order.status == "NEW"
|
|
96
|
+
_open = order.status == "OPEN"
|
|
97
|
+
_cancel = order.status == "CANCELED"
|
|
98
|
+
_closed = order.status == "CLOSED"
|
|
99
|
+
if _new or _open:
|
|
100
|
+
self.order_to_instrument[order.id] = order.instrument
|
|
101
|
+
if (_cancel or _closed) and order.id in self.order_to_instrument:
|
|
102
|
+
self.order_to_instrument.pop(order.id)
|
|
103
|
+
return super().process_order(order, update_locked_value)
|
|
104
|
+
|
|
105
|
+
def emulate_quote_from_data(
|
|
106
|
+
self, instrument: Instrument, timestamp: dt_64, data: float | Timestamped
|
|
107
|
+
) -> Quote | None:
|
|
108
|
+
if instrument not in self._half_tick_size:
|
|
109
|
+
_ = self.get_position(instrument)
|
|
110
|
+
|
|
111
|
+
if isinstance(data, Quote):
|
|
112
|
+
return data
|
|
113
|
+
|
|
114
|
+
elif isinstance(data, Trade):
|
|
115
|
+
_ts2 = self._half_tick_size[instrument]
|
|
116
|
+
if data.taker: # type: ignore
|
|
117
|
+
return Quote(timestamp, data.price - _ts2 * 2, data.price, 0, 0) # type: ignore
|
|
118
|
+
else:
|
|
119
|
+
return Quote(timestamp, data.price, data.price + _ts2 * 2, 0, 0) # type: ignore
|
|
120
|
+
|
|
121
|
+
elif isinstance(data, Bar):
|
|
122
|
+
_ts2 = self._half_tick_size[instrument]
|
|
123
|
+
return Quote(timestamp, data.close - _ts2, data.close + _ts2, 0, 0) # type: ignore
|
|
124
|
+
|
|
125
|
+
elif isinstance(data, OrderBook):
|
|
126
|
+
return data.to_quote()
|
|
127
|
+
|
|
128
|
+
elif isinstance(data, float):
|
|
129
|
+
_ts2 = self._half_tick_size[instrument]
|
|
130
|
+
return Quote(timestamp, data - _ts2, data + _ts2, 0, 0)
|
|
131
|
+
|
|
132
|
+
else:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
def _process_new_quote(self, instrument: Instrument, data: Quote) -> None:
|
|
136
|
+
ome = self.ome.get(instrument)
|
|
137
|
+
if ome is None:
|
|
138
|
+
logger.warning("ExchangeService:update :: No OME configured for '{symbol}' yet !")
|
|
139
|
+
return
|
|
140
|
+
for r in ome.update_bbo(data):
|
|
141
|
+
if r.exec is not None:
|
|
142
|
+
self.order_to_instrument.pop(r.order.id)
|
|
143
|
+
# - process methods will be called from stg context
|
|
144
|
+
self._channel.send((instrument, "order", r.order, False))
|
|
145
|
+
self._channel.send((instrument, "deals", [r.exec], False))
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from qubx.backtester.ome import OmeReport
|
|
2
|
+
from qubx.core.basics import (
|
|
3
|
+
CtrlChannel,
|
|
4
|
+
Instrument,
|
|
5
|
+
Order,
|
|
6
|
+
)
|
|
7
|
+
from qubx.core.interfaces import IBroker
|
|
8
|
+
|
|
9
|
+
from .account import SimulatedAccountProcessor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SimulatedBroker(IBroker):
|
|
13
|
+
channel: CtrlChannel
|
|
14
|
+
|
|
15
|
+
_account: SimulatedAccountProcessor
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
channel: CtrlChannel,
|
|
20
|
+
account: SimulatedAccountProcessor,
|
|
21
|
+
exchange_id: str = "simulated",
|
|
22
|
+
) -> None:
|
|
23
|
+
self.channel = channel
|
|
24
|
+
self._account = account
|
|
25
|
+
self._exchange_id = exchange_id
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def is_simulated_trading(self) -> bool:
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
def send_order(
|
|
32
|
+
self,
|
|
33
|
+
instrument: Instrument,
|
|
34
|
+
order_side: str,
|
|
35
|
+
order_type: str,
|
|
36
|
+
amount: float,
|
|
37
|
+
price: float | None = None,
|
|
38
|
+
client_id: str | None = None,
|
|
39
|
+
time_in_force: str = "gtc",
|
|
40
|
+
**options,
|
|
41
|
+
) -> Order:
|
|
42
|
+
ome = self._account.ome.get(instrument)
|
|
43
|
+
if ome is None:
|
|
44
|
+
raise ValueError(f"ExchangeService:send_order :: No OME configured for '{instrument.symbol}'!")
|
|
45
|
+
|
|
46
|
+
# - try to place order in OME
|
|
47
|
+
report = ome.place_order(
|
|
48
|
+
order_side.upper(), # type: ignore
|
|
49
|
+
order_type.upper(), # type: ignore
|
|
50
|
+
amount,
|
|
51
|
+
price,
|
|
52
|
+
client_id,
|
|
53
|
+
time_in_force,
|
|
54
|
+
**options,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
self._send_exec_report(instrument, report)
|
|
58
|
+
return report.order
|
|
59
|
+
|
|
60
|
+
def cancel_order(self, order_id: str) -> Order | None:
|
|
61
|
+
instrument = self._account.order_to_instrument.get(order_id)
|
|
62
|
+
if instrument is None:
|
|
63
|
+
raise ValueError(f"ExchangeService:cancel_order :: can't find order with id = '{order_id}'!")
|
|
64
|
+
|
|
65
|
+
ome = self._account.ome.get(instrument)
|
|
66
|
+
if ome is None:
|
|
67
|
+
raise ValueError(f"ExchangeService:send_order :: No OME configured for '{instrument}'!")
|
|
68
|
+
|
|
69
|
+
# - cancel order in OME and remove from the map to free memory
|
|
70
|
+
order_update = ome.cancel_order(order_id)
|
|
71
|
+
self._send_exec_report(instrument, order_update)
|
|
72
|
+
|
|
73
|
+
return order_update.order
|
|
74
|
+
|
|
75
|
+
def cancel_orders(self, instrument: Instrument) -> None:
|
|
76
|
+
raise NotImplementedError("Not implemented yet")
|
|
77
|
+
|
|
78
|
+
def update_order(self, order_id: str, price: float | None = None, amount: float | None = None) -> Order:
|
|
79
|
+
raise NotImplementedError("Not implemented yet")
|
|
80
|
+
|
|
81
|
+
def _send_exec_report(self, instrument: Instrument, report: OmeReport):
|
|
82
|
+
self.channel.send((instrument, "order", report.order, False))
|
|
83
|
+
if report.exec is not None:
|
|
84
|
+
self.channel.send((instrument, "deals", [report.exec], False))
|
|
85
|
+
|
|
86
|
+
def exchange(self) -> str:
|
|
87
|
+
return self._exchange_id.upper()
|