PyAlgoEngine 0.7.4__py3-none-any.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 (43) hide show
  1. PyAlgoEngine-0.7.4.dist-info/LICENSE +21 -0
  2. PyAlgoEngine-0.7.4.dist-info/METADATA +27 -0
  3. PyAlgoEngine-0.7.4.dist-info/RECORD +43 -0
  4. PyAlgoEngine-0.7.4.dist-info/WHEEL +5 -0
  5. PyAlgoEngine-0.7.4.dist-info/top_level.txt +1 -0
  6. algo_engine/__init__.py +41 -0
  7. algo_engine/apps/__init__.py +17 -0
  8. algo_engine/apps/backtest/__init__.py +20 -0
  9. algo_engine/apps/backtest/doc_server.py +331 -0
  10. algo_engine/apps/backtest/tester.py +254 -0
  11. algo_engine/apps/backtest/web_app.py +127 -0
  12. algo_engine/apps/bokeh_server.py +205 -0
  13. algo_engine/apps/demo/__init__.py +0 -0
  14. algo_engine/apps/demo/test.py +39 -0
  15. algo_engine/backtest/__init__.py +19 -0
  16. algo_engine/backtest/__main__.py +51 -0
  17. algo_engine/backtest/metrics.py +179 -0
  18. algo_engine/backtest/replay.py +261 -0
  19. algo_engine/backtest/sim_match.py +295 -0
  20. algo_engine/base/__init__.py +40 -0
  21. algo_engine/base/console_utils.py +1070 -0
  22. algo_engine/base/finance_decimal.py +258 -0
  23. algo_engine/base/market_buffer.py +571 -0
  24. algo_engine/base/market_utils.py +3092 -0
  25. algo_engine/base/market_utils_nt.py +188 -0
  26. algo_engine/base/market_utils_posix.py +3004 -0
  27. algo_engine/base/technical_analysis.py +406 -0
  28. algo_engine/base/telemetrics.py +78 -0
  29. algo_engine/base/trade_utils.py +709 -0
  30. algo_engine/engine/__init__.py +28 -0
  31. algo_engine/engine/algo_engine.py +901 -0
  32. algo_engine/engine/event_engine.py +53 -0
  33. algo_engine/engine/market_engine.py +370 -0
  34. algo_engine/engine/trade_engine.py +2037 -0
  35. algo_engine/monitor/__init__.py +15 -0
  36. algo_engine/monitor/advanced_data_interface.py +239 -0
  37. algo_engine/profile/__init__.py +121 -0
  38. algo_engine/profile/cn.py +175 -0
  39. algo_engine/strategy/__init__.py +44 -0
  40. algo_engine/strategy/strategy_engine.py +440 -0
  41. algo_engine/utils/__init__.py +3 -0
  42. algo_engine/utils/commit_regularizer.py +49 -0
  43. algo_engine/utils/data_utils.py +251 -0
@@ -0,0 +1,295 @@
1
+ import datetime
2
+
3
+ from . import LOGGER
4
+ from ..base import OrderType, MarketData, BarData, TradeData, TickData, OrderState, OrderBook, TradeReport, TradeInstruction
5
+ from ..engine.event_engine import TOPIC, EVENT_ENGINE
6
+ from ..profile import PROFILE
7
+
8
+ LOGGER = LOGGER.getChild('SimMatch')
9
+
10
+
11
+ class SimMatch(object):
12
+ def __init__(self, ticker, instant_fill: bool = False, event_engine=None, topic_set=None, fee_rate: float = 0.):
13
+ self.ticker = ticker
14
+ self.instant_fill = instant_fill
15
+ self.event_engine = event_engine if event_engine is not None else EVENT_ENGINE
16
+ self.topic_set = topic_set if topic_set is not None else TOPIC
17
+ self.fee_rate = fee_rate
18
+
19
+ self.working: dict[str, TradeInstruction] = {}
20
+ self.history: dict[str, TradeInstruction] = {}
21
+
22
+ self.timestamp = 0.
23
+
24
+ def __call__(self, **kwargs):
25
+ order: TradeInstruction | None = kwargs.pop('order', None)
26
+ market_data: MarketData | None = kwargs.pop('market_data', None)
27
+
28
+ if order is not None:
29
+ if order.order_type == OrderType.LimitOrder:
30
+ self.launch_order(order=order)
31
+ elif order.order_type == OrderType.CancelOrder:
32
+ self.cancel_order(order=order)
33
+ else:
34
+ raise ValueError(f'Invalid order {order}')
35
+
36
+ if market_data is not None:
37
+ self.timestamp = market_data.timestamp
38
+
39
+ if isinstance(market_data, BarData):
40
+ self._check_bar_data(market_data=market_data)
41
+ elif isinstance(market_data, TickData):
42
+ self._check_tick_data(market_data=market_data)
43
+ elif isinstance(market_data, TradeData):
44
+ self._check_trade_data(market_data=market_data)
45
+ elif isinstance(market_data, OrderBook):
46
+ self._check_order_book(market_data=market_data)
47
+
48
+ def register(self, topic_set=None, event_engine=None):
49
+ if topic_set is not None:
50
+ self.topic_set = topic_set
51
+
52
+ if event_engine is not None:
53
+ self.event_engine = event_engine
54
+
55
+ self.event_engine.register_handler(topic=self.topic_set.launch_order(ticker=self.ticker), handler=self.launch_order)
56
+ self.event_engine.register_handler(topic=self.topic_set.cancel_order(ticker=self.ticker), handler=self.cancel_order)
57
+ self.event_engine.register_handler(topic=self.topic_set.realtime(ticker=self.ticker), handler=self)
58
+
59
+ def unregister(self):
60
+ self.event_engine.unregister_handler(topic=self.topic_set.launch_order(ticker=self.ticker), handler=self.launch_order)
61
+ self.event_engine.unregister_handler(topic=self.topic_set.cancel_order(ticker=self.ticker), handler=self.cancel_order)
62
+ self.event_engine.unregister_handler(topic=self.topic_set.realtime(ticker=self.ticker), handler=self)
63
+
64
+ def launch_order(self, order: TradeInstruction, **kwargs):
65
+ if (order.order_id in self.working) or (order.order_id in self.history):
66
+ raise ValueError(f'Invalid instruction {order}, OrderId already in working or history')
67
+ elif order.limit_price is None:
68
+ LOGGER.warning(f'order {order} does not have a valid limit price!')
69
+ # raise ValueError(f'Invalid instruction {order}, instruction must have a LimitPrice')
70
+
71
+ order.set_order_state(order_state=OrderState.Placed, timestamp=self.timestamp)
72
+
73
+ if not self.instant_fill:
74
+ self.working[order.order_id] = order
75
+
76
+ self.on_order(order=order, **kwargs)
77
+
78
+ if self.instant_fill:
79
+ if limit := order.limit_price:
80
+ self._match(order=order, match_price=limit)
81
+ else:
82
+ LOGGER.warning(f'No limit price provided for {order}, instant_fill mode not available.')
83
+
84
+ def cancel_order(self, order: TradeInstruction = None, order_id: str = None, **kwargs):
85
+ if order is None and order_id is None:
86
+ raise ValueError('Must assign a order or order_id to cancel order')
87
+ elif order_id is None:
88
+ order_id = order.order_id
89
+
90
+ # if order_id not in self.working:
91
+ # raise ValueError(f'Invalid cancel order {order}, OrderId not found')
92
+
93
+ order: TradeInstruction = self.working.pop(order_id, None)
94
+ if order is None:
95
+ LOGGER.info(f'[{self.market_time:%Y-%m-%d %H:%M:%S}] failed to cancel {order_id} order!')
96
+ return
97
+
98
+ if order.order_state == OrderState.Filled:
99
+ pass
100
+ else:
101
+ order.set_order_state(order_state=OrderState.Canceled, timestamp=self.timestamp)
102
+ LOGGER.info(f'[{self.market_time:%Y-%m-%d %H:%M:%S}] Sim-canceled {order.side.name} {order.ticker} order!')
103
+
104
+ self.history[order_id] = order
105
+ self.on_order(order=order, **kwargs)
106
+
107
+ def _check_bar_data(self, market_data: BarData):
108
+ for order_id in list(self.working):
109
+ order = self.working.get(order_id)
110
+ if order is None:
111
+ pass
112
+ elif order.order_state in [OrderState.Placed, OrderState.PartFilled]:
113
+ if order.side.sign > 0:
114
+ # match order based on worst offer
115
+ if order.limit_price is None:
116
+ self._match(order=order, match_price=market_data.vwap)
117
+ elif market_data.high_price < order.limit_price:
118
+ self._match(order=order, match_price=market_data.high_price)
119
+ # match order based on limit price
120
+ elif market_data.low_price < order.limit_price:
121
+ self._match(order=order, match_price=order.limit_price)
122
+ # no match
123
+ else:
124
+ pass
125
+ elif order.side.sign < 0:
126
+ # match order based on worst offer
127
+ if order.limit_price is None:
128
+ self._match(order=order, match_price=market_data.vwap)
129
+ elif market_data.low_price > order.limit_price:
130
+ self._match(order=order, match_price=market_data.low_price)
131
+ # match order based on limit price
132
+ elif market_data.high_price > order.limit_price:
133
+ self._match(order=order, match_price=order.limit_price)
134
+ # no match
135
+ else:
136
+ pass
137
+ else:
138
+ continue
139
+ # raise ValueError(f'Invalid working order state {order}')
140
+
141
+ def _check_trade_data(self, market_data: TradeData):
142
+ for order_id in list(self.working):
143
+ order = self.working.get(order_id)
144
+ if order is None:
145
+ pass
146
+ elif order.is_working:
147
+ if order.start_time > market_data.market_time:
148
+ pass
149
+ elif order.limit_price is None:
150
+ if order.side.sign * market_data.side.sign > 0:
151
+ self._match(order=order, match_volume=market_data.volume, match_price=market_data.market_price)
152
+ elif order.side.sign > 0 and market_data.market_price < order.limit_price:
153
+ self._match(order=order, match_volume=market_data.volume, match_price=market_data.market_price)
154
+ elif order.side.sign < 0 and market_data.market_price > order.limit_price:
155
+ self._match(order=order, match_volume=market_data.volume, match_price=market_data.market_price)
156
+ else:
157
+ continue
158
+ # raise ValueError(f'Invalid working order state {order}')
159
+
160
+ def _check_order_book(self, market_data: OrderBook):
161
+ for order_id in list(self.working):
162
+ order = self.working.get(order_id)
163
+
164
+ match_volume = 0.
165
+ match_notional = 0.
166
+
167
+ if order is None:
168
+ pass
169
+ elif order.order_state in [OrderState.Placed, OrderState.PartFilled]:
170
+ if order.limit_price is None:
171
+ if order.side.sign > 0:
172
+ for entry in market_data.ask:
173
+ if match_volume < order.working_volume:
174
+ addition_volume = min(entry.volume, order.working_volume - match_volume)
175
+ match_volume += addition_volume
176
+ match_notional += addition_volume * entry.price
177
+ else:
178
+ break
179
+ else:
180
+ for entry in market_data.bid:
181
+ if match_volume < order.working_volume:
182
+ addition_volume = min(entry.volume, order.working_volume - match_volume)
183
+ match_volume += addition_volume
184
+ match_notional += addition_volume * entry.price
185
+ else:
186
+ break
187
+ elif order.side.sign > 0 and market_data.best_ask_price <= order.limit_price:
188
+ for entry in market_data.ask:
189
+ if entry.price <= order.limit_price:
190
+ if match_volume < order.working_volume:
191
+ addition_volume = min(entry.volume, order.working_volume - match_volume)
192
+ match_volume += addition_volume
193
+ match_notional += addition_volume * entry.price
194
+ else:
195
+ break
196
+ else:
197
+ break
198
+ elif order.side.sign < 0 and market_data.best_bid_price >= order.limit_price:
199
+ for entry in market_data.bid:
200
+ if entry.price >= order.limit_price:
201
+ if match_volume < order.working_volume:
202
+ addition_volume = min(entry.volume, order.working_volume - match_volume)
203
+ match_volume += addition_volume
204
+ match_notional += addition_volume * entry.price
205
+ else:
206
+ break
207
+ else:
208
+ break
209
+
210
+ if match_volume:
211
+ self._match(order=order, match_volume=match_volume, match_price=match_notional / match_volume)
212
+ else:
213
+ continue
214
+ # raise ValueError(f'Invalid working order state {order}')
215
+
216
+ def _check_tick_data(self, market_data: TickData):
217
+ for order_id in list(self.working):
218
+ order = self.working.get(order_id)
219
+
220
+ if order is None:
221
+ pass
222
+ elif order.order_state in [OrderState.Placed, OrderState.PartFilled]:
223
+ if order.limit_price is None:
224
+ self._match(order=order, match_volume=order.working_volume, match_price=market_data.market_price)
225
+ elif order.side.sign > 0 and market_data.market_price <= order.limit_price:
226
+ self._match(order=order, match_volume=order.working_volume, match_price=market_data.market_price)
227
+ elif order.side.sign < 0 and market_data.market_price >= order.limit_price:
228
+ self._match(order=order, match_volume=order.working_volume, match_price=market_data.market_price)
229
+ else:
230
+ continue
231
+ else:
232
+ continue
233
+
234
+ def _match(self, order: TradeInstruction, match_volume: float = None, match_price: float = None):
235
+ if match_volume is None:
236
+ match_volume = order.working_volume
237
+ elif match_volume > order.working_volume:
238
+ match_volume = order.working_volume
239
+
240
+ if order.limit_price is None:
241
+ pass
242
+ elif match_price is None:
243
+ match_price = order.limit_price
244
+ elif order.side.sign > 0 and match_price > order.limit_price:
245
+ LOGGER.warning(f'match price greater than limit price for bid order {order}')
246
+ match_price = order.limit_price
247
+ elif order.side.sign < 0 and match_price < order.limit_price:
248
+ match_price = order.limit_price
249
+ LOGGER.warning(f'match price less than limit price for ask order {order}')
250
+
251
+ if match_volume:
252
+ report = TradeReport(
253
+ ticker=order.ticker,
254
+ side=order.side,
255
+ volume=match_volume,
256
+ notional=match_volume * match_price * order.multiplier,
257
+ timestamp=self.timestamp,
258
+ order_id=order.order_id,
259
+ price=match_price,
260
+ multiplier=order.multiplier,
261
+ fee=self.fee_rate * match_volume * match_price * order.multiplier
262
+ )
263
+
264
+ LOGGER.info(f'[{self.market_time:%Y-%m-%d %H:%M:%S}] Sim-filled {order.ticker} {order.side.name} {report.volume:,.2f} @ {report.price:.2f}')
265
+ order.fill(trade_report=report)
266
+
267
+ if order.order_state == OrderState.Filled:
268
+ self.working.pop(order.order_id, None)
269
+ self.history[order.order_id] = order
270
+
271
+ self.on_report(report=report)
272
+ self.on_order(order=order)
273
+ return report
274
+ else:
275
+ return None
276
+
277
+ def on_order(self, order, **kwargs):
278
+ self.event_engine.put(topic=self.topic_set.on_order, order=order)
279
+
280
+ def on_report(self, report, **kwargs):
281
+ self.event_engine.put(topic=self.topic_set.on_report, report=report, **kwargs)
282
+
283
+ def eod(self):
284
+ for order_id in list(self.working):
285
+ self.cancel_order(order_id=order_id)
286
+
287
+ def clear(self):
288
+ # self.fee_rate = 0.
289
+ self.working.clear()
290
+ self.history.clear()
291
+ self.timestamp = 0.
292
+
293
+ @property
294
+ def market_time(self) -> datetime.datetime:
295
+ return datetime.datetime.fromtimestamp(self.timestamp, tz=PROFILE.time_zone)
@@ -0,0 +1,40 @@
1
+ import logging
2
+ import os
3
+
4
+ from .telemetrics import LOGGER
5
+ from ..profile import PROFILE
6
+
7
+
8
+ def set_logger(logger: logging.Logger):
9
+ global LOGGER
10
+ LOGGER = logger
11
+
12
+ market_utils.LOGGER = logger.getChild('MarketUtils')
13
+ trade_utils.LOGGER = logger.getChild('TradeUtils')
14
+ technical_analysis.LOGGER = logger.getChild('TA')
15
+ console_utils.LOGGER = logger.getChild('Console')
16
+
17
+
18
+ from .finance_decimal import FinancialDecimal
19
+
20
+ if os.name == 'nt':
21
+ from .market_utils_nt import TransactionSide, OrderType, MarketData, OrderBook, BarData, DailyBar, CandleStick, TickData, TransactionData, TradeData, OrderData, MarketDataBuffer, MarketDataRingBuffer
22
+ else:
23
+ from .market_utils_posix import TransactionSide, OrderType, MarketData, OrderBook, BarData, DailyBar, CandleStick, TickData, TransactionData, TradeData, OrderData, MarketDataBuffer, MarketDataRingBuffer
24
+
25
+ # from .market_utils_posix import OrderType
26
+ # from .market_utils import TransactionSide, MarketData, OrderBook, BarData, DailyBar, CandleStick, TickData, TransactionData, TradeData
27
+ # from .market_buffer import MarketDataPointer, MarketDataMemoryBuffer, OrderBookPointer, OrderBookBuffer, BarDataPointer, BarDataBuffer, TickDataPointer, TickDataBuffer, TransactionDataPointer, TransactionDataBuffer
28
+
29
+ from .technical_analysis import TechnicalAnalysis
30
+ from .trade_utils import OrderState, TradeInstruction, TradeReport
31
+ from .console_utils import Progress, GetInput, GetArgs, count_ordinal, TerminalStyle, InteractiveShell, ShellTransfer
32
+
33
+ __all__ = ['PROFILE',
34
+ 'FinancialDecimal',
35
+ 'TransactionSide', 'OrderType', 'MarketData', 'OrderBook', 'BarData', 'DailyBar', 'CandleStick', 'TickData', 'TransactionData', 'TradeData', 'OrderData', 'MarketDataBuffer', 'MarketDataRingBuffer',
36
+ # 'MarketDataMemoryBuffer', 'OrderBookBuffer', 'BarDataBuffer', 'TickDataBuffer', 'TransactionDataBuffer',
37
+ # 'MarketDataPointer', 'OrderBookPointer', 'BarDataPointer', 'TickDataPointer', 'TransactionDataPointer',
38
+ 'TechnicalAnalysis',
39
+ 'OrderState', 'TradeInstruction', 'TradeReport',
40
+ 'Progress', 'GetInput', 'GetArgs', 'count_ordinal', 'TerminalStyle', 'InteractiveShell', 'ShellTransfer']