PyAlgoEngine 0.12.3__cp315-cp315-win_amd64.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.
Files changed (156) hide show
  1. algo_engine/__infra__.pxd +3 -0
  2. algo_engine/__init__.pxd +3 -0
  3. algo_engine/__init__.py +107 -0
  4. algo_engine/apps/__init__.py +17 -0
  5. algo_engine/apps/backtest/__init__.py +20 -0
  6. algo_engine/apps/backtest/doc_server.py +328 -0
  7. algo_engine/apps/backtest/static/styles/dash.css +48 -0
  8. algo_engine/apps/backtest/templates/dash.html +20 -0
  9. algo_engine/apps/backtest/templates/index.html +40 -0
  10. algo_engine/apps/backtest/tester.py +252 -0
  11. algo_engine/apps/backtest/web_app.py +125 -0
  12. algo_engine/apps/bokeh_server.py +245 -0
  13. algo_engine/apps/demo/__init__.py +0 -0
  14. algo_engine/apps/demo/test.py +40 -0
  15. algo_engine/apps/sim_input/__init__.py +23 -0
  16. algo_engine/apps/sim_input/client.py +412 -0
  17. algo_engine/apps/sim_input/sim_keyboard.py +88 -0
  18. algo_engine/apps/sim_input/sim_mouse.py +137 -0
  19. algo_engine/apps/sim_input/window.py +162 -0
  20. algo_engine/backtest/__init__.py +19 -0
  21. algo_engine/backtest/__main__.py +51 -0
  22. algo_engine/backtest/metrics.py +179 -0
  23. algo_engine/backtest/replay.py +635 -0
  24. algo_engine/backtest/sim_match.py +506 -0
  25. algo_engine/base/__infra__.pxd +3 -0
  26. algo_engine/base/__init__.pxd +3 -0
  27. algo_engine/base/__init__.py +60 -0
  28. algo_engine/base/c_allocator_protocol.c +11608 -0
  29. algo_engine/base/c_allocator_protocol.cp315-win_amd64.pyd +0 -0
  30. algo_engine/base/c_allocator_protocol.pxd +24 -0
  31. algo_engine/base/c_allocator_protocol.pyi +68 -0
  32. algo_engine/base/c_allocator_protocol.pyx +111 -0
  33. algo_engine/base/c_intern_string.c +5908 -0
  34. algo_engine/base/c_intern_string.cp315-win_amd64.pyd +0 -0
  35. algo_engine/base/c_intern_string.pxd +14 -0
  36. algo_engine/base/c_intern_string.pyi +22 -0
  37. algo_engine/base/c_intern_string.pyx +17 -0
  38. algo_engine/base/c_market_data/__infra__.pxd +196 -0
  39. algo_engine/base/c_market_data/__init__.pxd +196 -0
  40. algo_engine/base/c_market_data/__init__.py +24 -0
  41. algo_engine/base/c_market_data/c_candlestick.c +18996 -0
  42. algo_engine/base/c_market_data/c_candlestick.cp315-win_amd64.pyd +0 -0
  43. algo_engine/base/c_market_data/c_candlestick.pxd +27 -0
  44. algo_engine/base/c_market_data/c_candlestick.pyi +217 -0
  45. algo_engine/base/c_market_data/c_candlestick.pyx +255 -0
  46. algo_engine/base/c_market_data/c_internal.c +14059 -0
  47. algo_engine/base/c_market_data/c_internal.cp315-win_amd64.pyd +0 -0
  48. algo_engine/base/c_market_data/c_internal.pxd +14 -0
  49. algo_engine/base/c_market_data/c_internal.pyi +47 -0
  50. algo_engine/base/c_market_data/c_internal.pyx +41 -0
  51. algo_engine/base/c_market_data/c_market_data.c +30420 -0
  52. algo_engine/base/c_market_data/c_market_data.cp315-win_amd64.pyd +0 -0
  53. algo_engine/base/c_market_data/c_market_data.h +1464 -0
  54. algo_engine/base/c_market_data/c_market_data.pxd +414 -0
  55. algo_engine/base/c_market_data/c_market_data.pyi +550 -0
  56. algo_engine/base/c_market_data/c_market_data.pyx +701 -0
  57. algo_engine/base/c_market_data/c_market_data_buffer.c +29407 -0
  58. algo_engine/base/c_market_data/c_market_data_buffer.cp315-win_amd64.pyd +0 -0
  59. algo_engine/base/c_market_data/c_market_data_buffer.h +941 -0
  60. algo_engine/base/c_market_data/c_market_data_buffer.pxd +143 -0
  61. algo_engine/base/c_market_data/c_market_data_buffer.pyi +355 -0
  62. algo_engine/base/c_market_data/c_market_data_buffer.pyx +571 -0
  63. algo_engine/base/c_market_data/c_market_data_config.h +29 -0
  64. algo_engine/base/c_market_data/c_tick.c +44303 -0
  65. algo_engine/base/c_market_data/c_tick.cp315-win_amd64.pyd +0 -0
  66. algo_engine/base/c_market_data/c_tick.pxd +53 -0
  67. algo_engine/base/c_market_data/c_tick.pyi +466 -0
  68. algo_engine/base/c_market_data/c_tick.pyx +673 -0
  69. algo_engine/base/c_market_data/c_trade_utils.c +28702 -0
  70. algo_engine/base/c_market_data/c_trade_utils.cp315-win_amd64.pyd +0 -0
  71. algo_engine/base/c_market_data/c_trade_utils.pxd +53 -0
  72. algo_engine/base/c_market_data/c_trade_utils.pyi +602 -0
  73. algo_engine/base/c_market_data/c_trade_utils.pyx +609 -0
  74. algo_engine/base/c_market_data/c_transaction.c +23558 -0
  75. algo_engine/base/c_market_data/c_transaction.cp315-win_amd64.pyd +0 -0
  76. algo_engine/base/c_market_data/c_transaction.pxd +27 -0
  77. algo_engine/base/c_market_data/c_transaction.pyi +433 -0
  78. algo_engine/base/c_market_data/c_transaction.pyx +460 -0
  79. algo_engine/base/console_utils.py +1070 -0
  80. algo_engine/base/finance_decimal.py +258 -0
  81. algo_engine/base/telemetrics.py +18 -0
  82. algo_engine/engine/__infra__.pxd +10 -0
  83. algo_engine/engine/__init__.pxd +10 -0
  84. algo_engine/engine/__init__.py +40 -0
  85. algo_engine/engine/algo_engine.py +904 -0
  86. algo_engine/engine/c_event_engine.c +16325 -0
  87. algo_engine/engine/c_event_engine.cp315-win_amd64.pyd +0 -0
  88. algo_engine/engine/c_event_engine.pxd +25 -0
  89. algo_engine/engine/c_event_engine.pyi +68 -0
  90. algo_engine/engine/c_market_engine.c +24258 -0
  91. algo_engine/engine/c_market_engine.cp315-win_amd64.pyd +0 -0
  92. algo_engine/engine/c_market_engine.pxd +87 -0
  93. algo_engine/engine/c_market_engine.pyi +357 -0
  94. algo_engine/engine/event_engine.py +53 -0
  95. algo_engine/engine/trade_engine.py +2037 -0
  96. algo_engine/exchange_profile/__infra__.pxd +100 -0
  97. algo_engine/exchange_profile/__init__.pxd +100 -0
  98. algo_engine/exchange_profile/__init__.py +53 -0
  99. algo_engine/exchange_profile/c_ex_profile_base.c +87 -0
  100. algo_engine/exchange_profile/c_ex_profile_base.h +1204 -0
  101. algo_engine/exchange_profile/c_ex_profile_cn.c +968 -0
  102. algo_engine/exchange_profile/c_ex_profile_cn.h +39 -0
  103. algo_engine/exchange_profile/c_exchange_profile.c +52375 -0
  104. algo_engine/exchange_profile/c_exchange_profile.cp315-win_amd64.pyd +0 -0
  105. algo_engine/exchange_profile/c_exchange_profile.pxd +336 -0
  106. algo_engine/exchange_profile/c_exchange_profile.pyi +883 -0
  107. algo_engine/exchange_profile/c_exchange_profile.pyx +1495 -0
  108. algo_engine/exchange_profile/c_profile_cn.c +7798 -0
  109. algo_engine/exchange_profile/c_profile_cn.cp315-win_amd64.pyd +0 -0
  110. algo_engine/exchange_profile/c_profile_cn.pxd +1 -0
  111. algo_engine/exchange_profile/c_profile_cn.pyi +3 -0
  112. algo_engine/exchange_profile/c_profile_cn.pyx +1 -0
  113. algo_engine/exchange_profile/c_profile_default.c +7798 -0
  114. algo_engine/exchange_profile/c_profile_default.cp315-win_amd64.pyd +0 -0
  115. algo_engine/exchange_profile/c_profile_default.pxd +1 -0
  116. algo_engine/exchange_profile/c_profile_default.pyi +3 -0
  117. algo_engine/exchange_profile/c_profile_default.pyx +1 -0
  118. algo_engine/exchange_profile/c_profile_dispatcher.c +7798 -0
  119. algo_engine/exchange_profile/c_profile_dispatcher.cp315-win_amd64.pyd +0 -0
  120. algo_engine/exchange_profile/c_profile_dispatcher.pxd +1 -0
  121. algo_engine/exchange_profile/c_profile_dispatcher.pyi +3 -0
  122. algo_engine/exchange_profile/c_profile_dispatcher.pyx +1 -0
  123. algo_engine/includes/algo_engine/base/c_allocator_protocol.c +11608 -0
  124. algo_engine/includes/algo_engine/base/c_intern_string.c +5908 -0
  125. algo_engine/includes/algo_engine/base/c_market_data/c_candlestick.c +18996 -0
  126. algo_engine/includes/algo_engine/base/c_market_data/c_internal.c +14059 -0
  127. algo_engine/includes/algo_engine/base/c_market_data/c_market_data.c +30420 -0
  128. algo_engine/includes/algo_engine/base/c_market_data/c_market_data.h +1464 -0
  129. algo_engine/includes/algo_engine/base/c_market_data/c_market_data_buffer.c +29407 -0
  130. algo_engine/includes/algo_engine/base/c_market_data/c_market_data_buffer.h +941 -0
  131. algo_engine/includes/algo_engine/base/c_market_data/c_market_data_config.h +29 -0
  132. algo_engine/includes/algo_engine/base/c_market_data/c_tick.c +44303 -0
  133. algo_engine/includes/algo_engine/base/c_market_data/c_trade_utils.c +28702 -0
  134. algo_engine/includes/algo_engine/base/c_market_data/c_transaction.c +23558 -0
  135. algo_engine/includes/algo_engine/engine/c_event_engine.c +16325 -0
  136. algo_engine/includes/algo_engine/engine/c_market_engine.c +24258 -0
  137. algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_base.c +87 -0
  138. algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_base.h +1204 -0
  139. algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_cn.c +968 -0
  140. algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_cn.h +39 -0
  141. algo_engine/includes/algo_engine/exchange_profile/c_exchange_profile.c +52375 -0
  142. algo_engine/includes/algo_engine/exchange_profile/c_profile_cn.c +7798 -0
  143. algo_engine/includes/algo_engine/exchange_profile/c_profile_default.c +7798 -0
  144. algo_engine/includes/algo_engine/exchange_profile/c_profile_dispatcher.c +7798 -0
  145. algo_engine/monitor/__init__.py +15 -0
  146. algo_engine/monitor/advanced_data_interface.py +334 -0
  147. algo_engine/strategy/__init__.py +44 -0
  148. algo_engine/strategy/strategy_engine.py +441 -0
  149. algo_engine/utils/__init__.py +3 -0
  150. algo_engine/utils/commit_regularizer.py +49 -0
  151. algo_engine/utils/data_utils.py +296 -0
  152. pyalgoengine-0.12.3.dist-info/METADATA +142 -0
  153. pyalgoengine-0.12.3.dist-info/RECORD +156 -0
  154. pyalgoengine-0.12.3.dist-info/WHEEL +5 -0
  155. pyalgoengine-0.12.3.dist-info/licenses/LICENSE +21 -0
  156. pyalgoengine-0.12.3.dist-info/top_level.txt +2 -0
@@ -0,0 +1,506 @@
1
+ import datetime
2
+ import random
3
+
4
+ import numpy as np
5
+ from event_engine.capi import EventEngine
6
+
7
+ from . import LOGGER
8
+ from ..base import BarData, MarketData, OrderData, OrderState, OrderType, TickData, TickDataLite, TradeData, TradeInstruction, TradeReport, TransactionData, TransactionDirection, TransactionSide
9
+ from ..engine import EVENT_ENGINE, TOPIC, TopicSet
10
+ from ..exchange_profile import PROFILE
11
+
12
+ LOGGER = LOGGER.getChild('SimMatch')
13
+
14
+
15
+ class SimMatch(object):
16
+ def __init__(self, ticker: str, event_engine=None, topic_set=None, seed: int = None, **kwargs):
17
+ self.ticker = ticker
18
+ self.event_engine = event_engine if event_engine is not None else EVENT_ENGINE
19
+ self.topic_set = topic_set if topic_set is not None else TOPIC
20
+
21
+ self.working: dict[str, TradeInstruction] = {}
22
+ self.history: dict[str, TradeInstruction] = {}
23
+
24
+ self.timestamp: float = 0.
25
+ self.last_price: float | None = None
26
+ self.last_transaction_count: int = 0
27
+ self.seed = seed
28
+ self.random = random.Random(self.seed)
29
+
30
+ self.matching_config = {
31
+ 'fee_rate': kwargs.get('fee_rate', 0.),
32
+ 'instant_fill': kwargs.get('instant_fill', False),
33
+ 'lag': {
34
+ 'ts': kwargs.get('lag_ts', 0.), # time lag in seconds
35
+ 'n_transaction': kwargs.get('lag_n_transaction', 0) # number of transactions lag
36
+ },
37
+ 'hit': {
38
+ 'prob': kwargs.get('hit_prob', 1.), # probability of order being filled
39
+ 'slippery': kwargs.get('slippery_rate', 0.0001) # slippage rate
40
+ }
41
+ }
42
+
43
+ def __call__(self, **kwargs):
44
+ order: TradeInstruction | None = kwargs.pop('order', None)
45
+ market_data: MarketData | None = kwargs.pop('market_data', None)
46
+
47
+ if order is not None:
48
+ if order.order_type == OrderType.ORDER_LIMIT:
49
+ self.launch_order(order=order)
50
+ elif order.order_type == OrderType.ORDER_CANCEL:
51
+ self.cancel_order(order=order)
52
+ else:
53
+ raise ValueError(f'Invalid order {order}')
54
+
55
+ if market_data is not None:
56
+ self.timestamp = market_data.timestamp
57
+ self.last_price = market_data.market_price
58
+
59
+ # Update transaction count if this is a transaction
60
+ if isinstance(market_data, (TransactionData, TradeData)):
61
+ self.last_transaction_count += 1
62
+
63
+ if isinstance(market_data, BarData):
64
+ self._check_bar_data(market_data=market_data)
65
+ elif isinstance(market_data, TickData):
66
+ self._check_tick_data(market_data=market_data)
67
+ elif isinstance(market_data, TickDataLite):
68
+ self._check_tick_data_lite(market_data=market_data)
69
+ elif isinstance(market_data, OrderData):
70
+ self._check_order_data(market_data=market_data)
71
+ elif isinstance(market_data, (TransactionData, TradeData)):
72
+ self._check_trade_data(market_data=market_data)
73
+
74
+ @staticmethod
75
+ def best_price(*price: float, side: TransactionSide | TransactionDirection) -> float:
76
+ """Get best price for the given side."""
77
+ valid_prices = [p for p in price if p is not None and np.isfinite(p)]
78
+ sign = side.sign
79
+
80
+ if not valid_prices:
81
+ raise ValueError("No valid prices provided")
82
+
83
+ if sign == 1:
84
+ return min(valid_prices)
85
+ elif sign == -1:
86
+ return max(valid_prices)
87
+ else:
88
+ raise ValueError(f'Invalid side {side}!')
89
+
90
+ @staticmethod
91
+ def worst_price(*price: float, side: TransactionSide | TransactionDirection) -> float:
92
+ """Get worst price for the given side."""
93
+ valid_prices = [p for p in price if p is not None and np.isfinite(p)]
94
+ sign = side.sign
95
+
96
+ if not valid_prices:
97
+ raise ValueError("No valid prices provided")
98
+
99
+ if sign == 1:
100
+ return max(valid_prices)
101
+ elif sign == -1:
102
+ return min(valid_prices)
103
+ else:
104
+ raise ValueError(f'Invalid side {side}!')
105
+
106
+ def _apply_lag(self, order: TradeInstruction) -> bool:
107
+ """Check if order should be processed considering lag settings."""
108
+ lag_config = self.matching_config['lag']
109
+ lag_ts = lag_config['ts']
110
+ lag_n_transaction = lag_config['n_transaction']
111
+
112
+ # No lag configured
113
+ if not lag_ts and not lag_n_transaction:
114
+ return True
115
+
116
+ # Check time lag
117
+ time_elapsed = self.timestamp - order.timestamp
118
+ if lag_ts > 0 and time_elapsed < lag_ts:
119
+ return False
120
+
121
+ # Check transaction lag
122
+ transactions_since_order = self.last_transaction_count - order._additional.get('transaction_count_at_placement', 0)
123
+ if lag_n_transaction > 0 and transactions_since_order < lag_n_transaction:
124
+ return False
125
+
126
+ return True
127
+
128
+ def _apply_hit_probability(self, order: TradeInstruction) -> bool:
129
+ """Determine if order should be filled based on hit probability."""
130
+ hit_config = self.matching_config['hit']
131
+ hit_prob = hit_config['prob']
132
+
133
+ if hit_prob >= 1.0:
134
+ return True
135
+
136
+ return random.random() < hit_prob
137
+
138
+ def _apply_slippage(self, price: float, side: TransactionSide | TransactionDirection) -> float:
139
+ """Apply slippage to the execution price."""
140
+ hit_config = self.matching_config['hit']
141
+ slippery_rate = hit_config['slippery']
142
+
143
+ slippage = price * slippery_rate
144
+ sign = side.sign
145
+
146
+ if sign == 1:
147
+ # For buy-orders, slippage increases the price
148
+ return price + slippage
149
+ elif sign == -1:
150
+ # For sell-orders, slippage decreases the price
151
+ return price - slippage
152
+ else:
153
+ return price
154
+
155
+ def _check_short_circuit(self, order: TradeInstruction) -> bool:
156
+ """Check if order should be filled immediately."""
157
+ if order.limit_price is None and self.last_price is None:
158
+ return False
159
+
160
+ # Check if instant fill is enabled and no lag is configured
161
+ if self.matching_config['instant_fill'] and all(not _ for _ in self.matching_config['lag'].values()):
162
+ return True
163
+
164
+ return False
165
+
166
+ def register(self, topic_set: TopicSet = None, event_engine: EventEngine = None):
167
+ if topic_set is not None:
168
+ self.topic_set = topic_set
169
+
170
+ if event_engine is not None:
171
+ self.event_engine = event_engine
172
+
173
+ self.event_engine.register_handler(topic=self.topic_set.launch_order(ticker=self.ticker), handler=self.launch_order)
174
+ self.event_engine.register_handler(topic=self.topic_set.cancel_order(ticker=self.ticker), handler=self.cancel_order)
175
+ self.event_engine.register_handler(topic=self.topic_set.realtime(ticker=self.ticker), handler=self)
176
+
177
+ def unregister(self):
178
+ self.event_engine.unregister_handler(topic=self.topic_set.launch_order(ticker=self.ticker), handler=self.launch_order)
179
+ self.event_engine.unregister_handler(topic=self.topic_set.cancel_order(ticker=self.ticker), handler=self.cancel_order)
180
+ self.event_engine.unregister_handler(topic=self.topic_set.realtime(ticker=self.ticker), handler=self)
181
+
182
+ def launch_order(self, order: TradeInstruction, **kwargs):
183
+ if order.order_id in self.working or order.order_id in self.history:
184
+ raise ValueError(f'Invalid instruction {order}, OrderId already in working or history')
185
+
186
+ if order.limit_price is None and order.order_type == OrderType.ORDER_LIMIT:
187
+ LOGGER.warning(f'order {order} does not have a valid limit price!')
188
+
189
+ order.set_order_state(order_state=OrderState.STATE_PLACED, timestamp=self.timestamp)
190
+ order.transaction_count_at_placement = self.last_transaction_count
191
+
192
+ # Check for immediate fill conditions
193
+ if self._check_short_circuit(order=order):
194
+ self.on_order(order=order, **kwargs)
195
+ worst_price = self.worst_price(
196
+ order.limit_price if order.limit_price is not None else self.last_price,
197
+ self.last_price,
198
+ side=order.side
199
+ )
200
+ self._match(order=order, match_price=worst_price)
201
+
202
+ self.working[order.order_id] = order
203
+ self.on_order(order=order, **kwargs)
204
+
205
+ def cancel_order(self, order: TradeInstruction = None, order_id: str = None, **kwargs):
206
+ if order is None and order_id is None:
207
+ raise ValueError('Must assign a order or order_id to cancel order')
208
+ elif order_id is None:
209
+ order_id = order.order_id
210
+
211
+ # if order_id not in self.working:
212
+ # raise ValueError(f'Invalid cancel order {order}, OrderId not found')
213
+
214
+ order: TradeInstruction = self.working.pop(order_id, None)
215
+ if order is None:
216
+ LOGGER.info(f'[{self.market_time:%Y-%m-%d %H:%M:%S}] failed to cancel {order_id} order!')
217
+ return
218
+
219
+ if order.order_state == OrderState.STATE_FILLED:
220
+ pass
221
+ else:
222
+ order.set_order_state(order_state=OrderState.STATE_CANCELED, timestamp=self.timestamp)
223
+ LOGGER.info(f'[{self.market_time:%Y-%m-%d %H:%M:%S}] Sim-canceled {order.side.name} {order.ticker} order!')
224
+
225
+ self.history[order_id] = order
226
+ self.on_order(order=order, **kwargs)
227
+
228
+ def _check_bar_data(self, market_data: BarData):
229
+ for order_id in list(self.working):
230
+ order = self.working.get(order_id)
231
+ if order is None:
232
+ continue
233
+
234
+ if not order.is_working:
235
+ continue
236
+
237
+ if order.start_time > market_data.market_time:
238
+ continue
239
+
240
+ if order.side.sign > 0:
241
+ # match order based on worst offer
242
+ if order.limit_price is None:
243
+ self._match(order=order, match_price=market_data.vwap)
244
+ elif market_data.high_price < order.limit_price:
245
+ self._match(order=order, match_price=market_data.high_price)
246
+ # match order based on limit price
247
+ elif market_data.low_price < order.limit_price:
248
+ self._match(order=order, match_price=order.limit_price)
249
+ # no match
250
+ else:
251
+ pass
252
+ elif order.side.sign < 0:
253
+ # match order based on worst offer
254
+ if order.limit_price is None:
255
+ self._match(order=order, match_price=market_data.vwap)
256
+ elif market_data.low_price > order.limit_price:
257
+ self._match(order=order, match_price=market_data.low_price)
258
+ # match order based on limit price
259
+ elif market_data.high_price > order.limit_price:
260
+ self._match(order=order, match_price=order.limit_price)
261
+ # no match
262
+ else:
263
+ pass
264
+
265
+ def _check_trade_data(self, market_data: TransactionData | TradeData):
266
+ for order_id in list(self.working):
267
+ order = self.working.get(order_id)
268
+ if order is None:
269
+ continue
270
+
271
+ if not order.is_working:
272
+ continue
273
+
274
+ if order.start_time > market_data.market_time:
275
+ continue
276
+
277
+ if order.limit_price is None:
278
+ if order.side.sign * market_data.side.sign > 0: # copy the next transaction info
279
+ self._match(order=order, match_volume=market_data.volume, match_price=market_data.price)
280
+ elif order.side.sign > 0 and market_data.market_price < order.limit_price:
281
+ self._match(order=order, match_volume=market_data.volume, match_price=market_data.price)
282
+ elif order.side.sign < 0 and market_data.market_price > order.limit_price:
283
+ self._match(order=order, match_volume=market_data.volume, match_price=market_data.price)
284
+
285
+ def _check_tick_data(self, market_data: TickData):
286
+ for order_id in list(self.working):
287
+ order = self.working.get(order_id)
288
+ if order is None:
289
+ continue
290
+
291
+ if not order.is_working:
292
+ continue
293
+
294
+ if order.start_time > market_data.market_time:
295
+ continue
296
+
297
+ match_volume = 0.
298
+ match_notional = 0.
299
+
300
+ if order.limit_price is None:
301
+ if order.side.sign > 0:
302
+ for entry in market_data.ask:
303
+ price, volume, _ = entry
304
+
305
+ if match_volume < order.working_volume:
306
+ addition_volume = min(volume, order.working_volume - match_volume)
307
+ match_volume += addition_volume
308
+ match_notional += addition_volume * price
309
+ else:
310
+ break
311
+ else:
312
+ for entry in market_data.bid:
313
+ price, volume, _ = entry
314
+
315
+ if match_volume < order.working_volume:
316
+ addition_volume = min(volume, order.working_volume - match_volume)
317
+ match_volume += addition_volume
318
+ match_notional += addition_volume * price
319
+ else:
320
+ break
321
+ elif order.side.sign > 0 and market_data.best_ask_price <= order.limit_price:
322
+ for entry in market_data.ask:
323
+ price, volume, _ = entry
324
+
325
+ if price <= order.limit_price:
326
+ if match_volume < order.working_volume:
327
+ addition_volume = min(volume, order.working_volume - match_volume)
328
+ match_volume += addition_volume
329
+ match_notional += addition_volume * price
330
+ else:
331
+ break
332
+ else:
333
+ break
334
+ elif order.side.sign < 0 and market_data.best_bid_price >= order.limit_price:
335
+ for entry in market_data.bid:
336
+ price, volume, _ = entry
337
+
338
+ if price >= order.limit_price:
339
+ if match_volume < order.working_volume:
340
+ addition_volume = min(volume, order.working_volume - match_volume)
341
+ match_volume += addition_volume
342
+ match_notional += addition_volume * price
343
+ else:
344
+ break
345
+ else:
346
+ break
347
+
348
+ if match_volume:
349
+ self._match(order=order, match_volume=match_volume, match_price=match_notional / match_volume)
350
+
351
+ def _check_order_data(self, market_data: OrderData) -> None:
352
+ """Process order data from the market.
353
+
354
+ Args:
355
+ market_data: The incoming order data from the market.
356
+ """
357
+ for order_id in list(self.working):
358
+ order = self.working.get(order_id)
359
+ if order is None:
360
+ continue
361
+
362
+ if not order.is_working:
363
+ continue
364
+
365
+ if order.start_time > market_data.market_time:
366
+ continue
367
+
368
+ # Check if this order matches our working order
369
+ match_volume = 0.
370
+ match_notional = 0.
371
+
372
+ if order.limit_price is None:
373
+ if order.side.sign > 0 and market_data.side.sign < 0:
374
+ match_volume = market_data.volume
375
+ match_notional = market_data.price * market_data.volume
376
+ elif order.side.sign < 0 and market_data.side.sign > 0:
377
+ match_volume = market_data.volume
378
+ match_notional = market_data.price * market_data.volume
379
+ elif order.side.sign > 0 and market_data.price <= order.limit_price:
380
+ match_volume = market_data.volume
381
+ match_notional = market_data.price * market_data.volume
382
+ elif order.side.sign < 0 and market_data.price >= order.limit_price:
383
+ match_volume = market_data.volume
384
+ match_notional = market_data.price * market_data.volume
385
+
386
+ if match_volume:
387
+ self._match(order=order, match_volume=match_volume, match_price=match_notional / match_volume)
388
+
389
+ def _check_tick_data_lite(self, market_data: TickDataLite) -> None:
390
+ """Process simplified tick data from the market.
391
+
392
+ Args:
393
+ market_data: The incoming tick data (lite version) from the market.
394
+ """
395
+ for order_id in list(self.working):
396
+ order = self.working.get(order_id)
397
+
398
+ if order is None:
399
+ continue
400
+
401
+ if not order.is_working:
402
+ continue
403
+
404
+ if order.start_time > market_data.market_time:
405
+ continue
406
+
407
+ # Check if this order matches our working order
408
+ match_volume = 0.
409
+ match_notional = 0.
410
+
411
+ if order.limit_price is None:
412
+ if order.side.sign > 0:
413
+ match_volume = market_data.ask_volume
414
+ match_notional = market_data.ask_price * market_data.ask_volume
415
+ elif order.side.sign < 0:
416
+ match_volume = market_data.bid_volume
417
+ match_notional = market_data.bid_price * market_data.bid_volume
418
+ elif order.side.sign > 0 and market_data.ask_price <= order.limit_price:
419
+ match_volume = market_data.ask_volume
420
+ match_notional = market_data.ask_price * market_data.ask_volume
421
+ elif order.side.sign < 0 and market_data.bid_price >= order.limit_price:
422
+ match_volume = market_data.bid_volume
423
+ match_notional = market_data.bid_price * market_data.bid_volume
424
+
425
+ if match_volume:
426
+ self._match(order=order, match_volume=match_volume, match_price=match_notional / match_volume)
427
+
428
+ def _match(self, order: TradeInstruction, match_volume: float = None, match_price: float = None) -> TradeReport | None:
429
+ """Attempt to match an order with the given volume and price."""
430
+ # Apply lag check
431
+ if not self._apply_lag(order):
432
+ return None
433
+
434
+ # Apply hit probability
435
+ if not self._apply_hit_probability(order):
436
+ return None
437
+
438
+ # Determine match volume
439
+ if match_volume is None:
440
+ match_volume = order.working_volume
441
+ else:
442
+ match_volume = min(match_volume, order.working_volume)
443
+
444
+ # Determine match price with slippage
445
+ if match_price is None and order.limit_price is not None:
446
+ match_price = order.limit_price
447
+ elif match_price is not None:
448
+ match_price = self._apply_slippage(match_price, order.side)
449
+
450
+ # Validate price against limit
451
+ if order.limit_price is not None:
452
+ if order.side.sign > 0 and match_price > order.limit_price:
453
+ LOGGER.warning(f'match price greater than limit price for bid order {order}')
454
+ match_price = order.limit_price
455
+ elif order.side.sign < 0 and match_price < order.limit_price:
456
+ LOGGER.warning(f'match price less than limit price for ask order {order}')
457
+ match_price = order.limit_price
458
+
459
+ if match_volume:
460
+ report = TradeReport(
461
+ ticker=order.ticker,
462
+ side=order.side,
463
+ volume=match_volume,
464
+ notional=match_volume * match_price * order.multiplier,
465
+ timestamp=self.timestamp,
466
+ order_id=order.order_id,
467
+ price=match_price,
468
+ multiplier=order.multiplier,
469
+ fee=self.matching_config['fee_rate'] * match_volume * match_price * order.multiplier
470
+ )
471
+
472
+ LOGGER.info(f'[{self.market_time:%Y-%m-%d %H:%M:%S}] Sim-filled {order.ticker} {order.side.side_name} {report.volume:,.2f} @ {report.price:.2f}')
473
+ order.fill(trade_report=report)
474
+
475
+ if order.order_state == OrderState.STATE_FILLED:
476
+ self.working.pop(order.order_id, None)
477
+ self.history[order.order_id] = order
478
+
479
+ self.on_report(report=report)
480
+ self.on_order(order=order)
481
+ return report
482
+
483
+ return None
484
+
485
+ def on_order(self, order, **kwargs):
486
+ self.event_engine.put(topic=self.topic_set.on_order, order=order)
487
+
488
+ def on_report(self, report, **kwargs):
489
+ self.event_engine.put(topic=self.topic_set.on_report, report=report, **kwargs)
490
+
491
+ def eod(self):
492
+ for order_id in list(self.working):
493
+ self.cancel_order(order_id=order_id)
494
+
495
+ def clear(self):
496
+ self.working.clear()
497
+ self.history.clear()
498
+
499
+ self.timestamp = 0.
500
+ self.last_price = None
501
+ self.last_transaction_count = 0
502
+ self.random = random.Random(self.seed)
503
+
504
+ @property
505
+ def market_time(self) -> datetime.datetime:
506
+ return PROFILE.timestamp_to_datetime(self.timestamp)
@@ -0,0 +1,3 @@
1
+ from .c_allocator_protocol cimport *
2
+ from .c_intern_string cimport *
3
+ from .c_market_data cimport *
@@ -0,0 +1,3 @@
1
+ from .c_allocator_protocol cimport *
2
+ from .c_intern_string cimport *
3
+ from .c_market_data cimport *
@@ -0,0 +1,60 @@
1
+ import logging
2
+ import os
3
+ import pathlib
4
+
5
+ from .telemetrics import LOGGER
6
+
7
+ USE_CYTHON = True
8
+
9
+
10
+ def set_logger(logger: logging.Logger):
11
+ global LOGGER
12
+ LOGGER = logger
13
+ from .c_market_data import c_trade_utils
14
+ c_trade_utils.LOGGER = logger.getChild('TradeUtils')
15
+ console_utils.LOGGER = logger.getChild('Console')
16
+
17
+
18
+ def check_cython_module(cython_module) -> bool:
19
+ if not USE_CYTHON:
20
+ return False
21
+
22
+ for name in cython_module:
23
+ cython_ext = '.pyd' if os.name == 'nt' else '.so'
24
+ for file in pathlib.Path(__file__).parent.glob(f'*{cython_ext}'):
25
+ if file.name.startswith(name):
26
+ break
27
+ else:
28
+ LOGGER.warning(f'Cython module {name} not found!')
29
+ return False
30
+
31
+ return True
32
+
33
+
34
+ from .finance_decimal import FinancialDecimal
35
+
36
+ from .c_allocator_protocol import MD_SHARED, MD_LOCKED, MD_LOCKFREE, MD_FREELIST
37
+
38
+ from .c_market_data.c_market_data import BookConfigContext, MD_BOOK5, MD_BOOK10, MD_BOOK20, DataType, MarketData, FilterMode, CONFIG
39
+ from .c_market_data.c_internal import InternalData
40
+ from .c_market_data.c_transaction import OrderType, TransactionDirection, TransactionOffset, TransactionSide, TransactionData, OrderData, TradeData
41
+ from .c_market_data.c_tick import TickDataLite, OrderBook, TickData
42
+ from .c_market_data.c_candlestick import BarData, DailyBar
43
+ from .c_market_data.c_trade_utils import OrderState, TradeReport, TradeInstruction
44
+ from .c_market_data.c_market_data_buffer import InvalidBufferError, NotInSharedMemoryError, BufferFull, BufferEmpty, PipeTimeoutError, BufferCorruptedError, MarketDataBuffer, MarketDataBufferCache, MarketDataRingBuffer, MarketDataConcurrentBuffer
45
+
46
+ from .console_utils import Progress, GetInput, GetArgs, count_ordinal, TerminalStyle, InteractiveShell, ShellTransfer
47
+
48
+ __all__ = [
49
+ 'FinancialDecimal',
50
+ "MD_SHARED", "MD_LOCKED", "MD_LOCKFREE", "MD_FREELIST", "MD_BOOK5", "MD_BOOK10", "MD_BOOK20",
51
+ "DataType", "MarketData", "FilterMode",
52
+ "CONFIG",
53
+ "InternalData",
54
+ "OrderType", "TransactionDirection", "TransactionOffset", "TransactionSide", "TransactionData", "OrderData", "TradeData",
55
+ "TickDataLite", "OrderBook", "TickData",
56
+ "BarData", "DailyBar",
57
+ "OrderState", "TradeReport", "TradeInstruction",
58
+ "InvalidBufferError", "NotInSharedMemoryError", "BufferFull", "BufferEmpty", "PipeTimeoutError", "BufferCorruptedError", "MarketDataBuffer", "MarketDataBufferCache", "MarketDataRingBuffer", "MarketDataConcurrentBuffer",
59
+ 'Progress', 'GetInput', 'GetArgs', 'count_ordinal', 'TerminalStyle', 'InteractiveShell', 'ShellTransfer'
60
+ ]