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.
- PyAlgoEngine-0.7.4.dist-info/LICENSE +21 -0
- PyAlgoEngine-0.7.4.dist-info/METADATA +27 -0
- PyAlgoEngine-0.7.4.dist-info/RECORD +43 -0
- PyAlgoEngine-0.7.4.dist-info/WHEEL +5 -0
- PyAlgoEngine-0.7.4.dist-info/top_level.txt +1 -0
- algo_engine/__init__.py +41 -0
- algo_engine/apps/__init__.py +17 -0
- algo_engine/apps/backtest/__init__.py +20 -0
- algo_engine/apps/backtest/doc_server.py +331 -0
- algo_engine/apps/backtest/tester.py +254 -0
- algo_engine/apps/backtest/web_app.py +127 -0
- algo_engine/apps/bokeh_server.py +205 -0
- algo_engine/apps/demo/__init__.py +0 -0
- algo_engine/apps/demo/test.py +39 -0
- algo_engine/backtest/__init__.py +19 -0
- algo_engine/backtest/__main__.py +51 -0
- algo_engine/backtest/metrics.py +179 -0
- algo_engine/backtest/replay.py +261 -0
- algo_engine/backtest/sim_match.py +295 -0
- algo_engine/base/__init__.py +40 -0
- algo_engine/base/console_utils.py +1070 -0
- algo_engine/base/finance_decimal.py +258 -0
- algo_engine/base/market_buffer.py +571 -0
- algo_engine/base/market_utils.py +3092 -0
- algo_engine/base/market_utils_nt.py +188 -0
- algo_engine/base/market_utils_posix.py +3004 -0
- algo_engine/base/technical_analysis.py +406 -0
- algo_engine/base/telemetrics.py +78 -0
- algo_engine/base/trade_utils.py +709 -0
- algo_engine/engine/__init__.py +28 -0
- algo_engine/engine/algo_engine.py +901 -0
- algo_engine/engine/event_engine.py +53 -0
- algo_engine/engine/market_engine.py +370 -0
- algo_engine/engine/trade_engine.py +2037 -0
- algo_engine/monitor/__init__.py +15 -0
- algo_engine/monitor/advanced_data_interface.py +239 -0
- algo_engine/profile/__init__.py +121 -0
- algo_engine/profile/cn.py +175 -0
- algo_engine/strategy/__init__.py +44 -0
- algo_engine/strategy/strategy_engine.py +440 -0
- algo_engine/utils/__init__.py +3 -0
- algo_engine/utils/commit_regularizer.py +49 -0
- 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']
|