Qubx 0.5.7__cp312-cp312-manylinux_2_39_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (100) hide show
  1. qubx/__init__.py +207 -0
  2. qubx/_nb_magic.py +100 -0
  3. qubx/backtester/__init__.py +5 -0
  4. qubx/backtester/account.py +145 -0
  5. qubx/backtester/broker.py +87 -0
  6. qubx/backtester/data.py +296 -0
  7. qubx/backtester/management.py +378 -0
  8. qubx/backtester/ome.py +296 -0
  9. qubx/backtester/optimization.py +201 -0
  10. qubx/backtester/simulated_data.py +558 -0
  11. qubx/backtester/simulator.py +362 -0
  12. qubx/backtester/utils.py +780 -0
  13. qubx/cli/__init__.py +0 -0
  14. qubx/cli/commands.py +67 -0
  15. qubx/connectors/ccxt/__init__.py +0 -0
  16. qubx/connectors/ccxt/account.py +495 -0
  17. qubx/connectors/ccxt/broker.py +132 -0
  18. qubx/connectors/ccxt/customizations.py +193 -0
  19. qubx/connectors/ccxt/data.py +612 -0
  20. qubx/connectors/ccxt/exceptions.py +17 -0
  21. qubx/connectors/ccxt/factory.py +93 -0
  22. qubx/connectors/ccxt/utils.py +307 -0
  23. qubx/core/__init__.py +0 -0
  24. qubx/core/account.py +251 -0
  25. qubx/core/basics.py +850 -0
  26. qubx/core/context.py +420 -0
  27. qubx/core/exceptions.py +38 -0
  28. qubx/core/helpers.py +480 -0
  29. qubx/core/interfaces.py +1150 -0
  30. qubx/core/loggers.py +514 -0
  31. qubx/core/lookups.py +475 -0
  32. qubx/core/metrics.py +1512 -0
  33. qubx/core/mixins/__init__.py +13 -0
  34. qubx/core/mixins/market.py +94 -0
  35. qubx/core/mixins/processing.py +428 -0
  36. qubx/core/mixins/subscription.py +203 -0
  37. qubx/core/mixins/trading.py +88 -0
  38. qubx/core/mixins/universe.py +270 -0
  39. qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
  40. qubx/core/series.pxd +125 -0
  41. qubx/core/series.pyi +118 -0
  42. qubx/core/series.pyx +988 -0
  43. qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
  44. qubx/core/utils.pyi +6 -0
  45. qubx/core/utils.pyx +62 -0
  46. qubx/data/__init__.py +25 -0
  47. qubx/data/helpers.py +416 -0
  48. qubx/data/readers.py +1562 -0
  49. qubx/data/tardis.py +100 -0
  50. qubx/gathering/simplest.py +88 -0
  51. qubx/math/__init__.py +3 -0
  52. qubx/math/stats.py +129 -0
  53. qubx/pandaz/__init__.py +23 -0
  54. qubx/pandaz/ta.py +2757 -0
  55. qubx/pandaz/utils.py +638 -0
  56. qubx/resources/instruments/symbols-binance.cm.json +1 -0
  57. qubx/resources/instruments/symbols-binance.json +1 -0
  58. qubx/resources/instruments/symbols-binance.um.json +1 -0
  59. qubx/resources/instruments/symbols-bitfinex.f.json +1 -0
  60. qubx/resources/instruments/symbols-bitfinex.json +1 -0
  61. qubx/resources/instruments/symbols-kraken.f.json +1 -0
  62. qubx/resources/instruments/symbols-kraken.json +1 -0
  63. qubx/ta/__init__.py +0 -0
  64. qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
  65. qubx/ta/indicators.pxd +149 -0
  66. qubx/ta/indicators.pyi +41 -0
  67. qubx/ta/indicators.pyx +787 -0
  68. qubx/trackers/__init__.py +3 -0
  69. qubx/trackers/abvanced.py +236 -0
  70. qubx/trackers/composite.py +146 -0
  71. qubx/trackers/rebalancers.py +129 -0
  72. qubx/trackers/riskctrl.py +641 -0
  73. qubx/trackers/sizers.py +235 -0
  74. qubx/utils/__init__.py +5 -0
  75. qubx/utils/_pyxreloader.py +281 -0
  76. qubx/utils/charting/lookinglass.py +1057 -0
  77. qubx/utils/charting/mpl_helpers.py +1183 -0
  78. qubx/utils/marketdata/binance.py +284 -0
  79. qubx/utils/marketdata/ccxt.py +90 -0
  80. qubx/utils/marketdata/dukas.py +130 -0
  81. qubx/utils/misc.py +541 -0
  82. qubx/utils/ntp.py +63 -0
  83. qubx/utils/numbers_utils.py +7 -0
  84. qubx/utils/orderbook.py +491 -0
  85. qubx/utils/plotting/__init__.py +0 -0
  86. qubx/utils/plotting/dashboard.py +150 -0
  87. qubx/utils/plotting/data.py +137 -0
  88. qubx/utils/plotting/interfaces.py +25 -0
  89. qubx/utils/plotting/renderers/__init__.py +0 -0
  90. qubx/utils/plotting/renderers/plotly.py +0 -0
  91. qubx/utils/runner/__init__.py +1 -0
  92. qubx/utils/runner/_jupyter_runner.pyt +60 -0
  93. qubx/utils/runner/accounts.py +88 -0
  94. qubx/utils/runner/configs.py +65 -0
  95. qubx/utils/runner/runner.py +470 -0
  96. qubx/utils/time.py +312 -0
  97. qubx-0.5.7.dist-info/METADATA +105 -0
  98. qubx-0.5.7.dist-info/RECORD +100 -0
  99. qubx-0.5.7.dist-info/WHEEL +4 -0
  100. qubx-0.5.7.dist-info/entry_points.txt +3 -0
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,5 @@
1
+ __all__ = ["BacktestsResultsManager", "variate", "simulate"]
2
+
3
+ from .management import BacktestsResultsManager
4
+ from .optimization import variate
5
+ from .simulator import simulate
@@ -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()