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.
- algo_engine/__infra__.pxd +3 -0
- algo_engine/__init__.pxd +3 -0
- algo_engine/__init__.py +107 -0
- algo_engine/apps/__init__.py +17 -0
- algo_engine/apps/backtest/__init__.py +20 -0
- algo_engine/apps/backtest/doc_server.py +328 -0
- algo_engine/apps/backtest/static/styles/dash.css +48 -0
- algo_engine/apps/backtest/templates/dash.html +20 -0
- algo_engine/apps/backtest/templates/index.html +40 -0
- algo_engine/apps/backtest/tester.py +252 -0
- algo_engine/apps/backtest/web_app.py +125 -0
- algo_engine/apps/bokeh_server.py +245 -0
- algo_engine/apps/demo/__init__.py +0 -0
- algo_engine/apps/demo/test.py +40 -0
- algo_engine/apps/sim_input/__init__.py +23 -0
- algo_engine/apps/sim_input/client.py +412 -0
- algo_engine/apps/sim_input/sim_keyboard.py +88 -0
- algo_engine/apps/sim_input/sim_mouse.py +137 -0
- algo_engine/apps/sim_input/window.py +162 -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 +635 -0
- algo_engine/backtest/sim_match.py +506 -0
- algo_engine/base/__infra__.pxd +3 -0
- algo_engine/base/__init__.pxd +3 -0
- algo_engine/base/__init__.py +60 -0
- algo_engine/base/c_allocator_protocol.c +11608 -0
- algo_engine/base/c_allocator_protocol.cp315-win_amd64.pyd +0 -0
- algo_engine/base/c_allocator_protocol.pxd +24 -0
- algo_engine/base/c_allocator_protocol.pyi +68 -0
- algo_engine/base/c_allocator_protocol.pyx +111 -0
- algo_engine/base/c_intern_string.c +5908 -0
- algo_engine/base/c_intern_string.cp315-win_amd64.pyd +0 -0
- algo_engine/base/c_intern_string.pxd +14 -0
- algo_engine/base/c_intern_string.pyi +22 -0
- algo_engine/base/c_intern_string.pyx +17 -0
- algo_engine/base/c_market_data/__infra__.pxd +196 -0
- algo_engine/base/c_market_data/__init__.pxd +196 -0
- algo_engine/base/c_market_data/__init__.py +24 -0
- algo_engine/base/c_market_data/c_candlestick.c +18996 -0
- algo_engine/base/c_market_data/c_candlestick.cp315-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_candlestick.pxd +27 -0
- algo_engine/base/c_market_data/c_candlestick.pyi +217 -0
- algo_engine/base/c_market_data/c_candlestick.pyx +255 -0
- algo_engine/base/c_market_data/c_internal.c +14059 -0
- algo_engine/base/c_market_data/c_internal.cp315-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_internal.pxd +14 -0
- algo_engine/base/c_market_data/c_internal.pyi +47 -0
- algo_engine/base/c_market_data/c_internal.pyx +41 -0
- algo_engine/base/c_market_data/c_market_data.c +30420 -0
- algo_engine/base/c_market_data/c_market_data.cp315-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_market_data.h +1464 -0
- algo_engine/base/c_market_data/c_market_data.pxd +414 -0
- algo_engine/base/c_market_data/c_market_data.pyi +550 -0
- algo_engine/base/c_market_data/c_market_data.pyx +701 -0
- algo_engine/base/c_market_data/c_market_data_buffer.c +29407 -0
- algo_engine/base/c_market_data/c_market_data_buffer.cp315-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_market_data_buffer.h +941 -0
- algo_engine/base/c_market_data/c_market_data_buffer.pxd +143 -0
- algo_engine/base/c_market_data/c_market_data_buffer.pyi +355 -0
- algo_engine/base/c_market_data/c_market_data_buffer.pyx +571 -0
- algo_engine/base/c_market_data/c_market_data_config.h +29 -0
- algo_engine/base/c_market_data/c_tick.c +44303 -0
- algo_engine/base/c_market_data/c_tick.cp315-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_tick.pxd +53 -0
- algo_engine/base/c_market_data/c_tick.pyi +466 -0
- algo_engine/base/c_market_data/c_tick.pyx +673 -0
- algo_engine/base/c_market_data/c_trade_utils.c +28702 -0
- algo_engine/base/c_market_data/c_trade_utils.cp315-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_trade_utils.pxd +53 -0
- algo_engine/base/c_market_data/c_trade_utils.pyi +602 -0
- algo_engine/base/c_market_data/c_trade_utils.pyx +609 -0
- algo_engine/base/c_market_data/c_transaction.c +23558 -0
- algo_engine/base/c_market_data/c_transaction.cp315-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_transaction.pxd +27 -0
- algo_engine/base/c_market_data/c_transaction.pyi +433 -0
- algo_engine/base/c_market_data/c_transaction.pyx +460 -0
- algo_engine/base/console_utils.py +1070 -0
- algo_engine/base/finance_decimal.py +258 -0
- algo_engine/base/telemetrics.py +18 -0
- algo_engine/engine/__infra__.pxd +10 -0
- algo_engine/engine/__init__.pxd +10 -0
- algo_engine/engine/__init__.py +40 -0
- algo_engine/engine/algo_engine.py +904 -0
- algo_engine/engine/c_event_engine.c +16325 -0
- algo_engine/engine/c_event_engine.cp315-win_amd64.pyd +0 -0
- algo_engine/engine/c_event_engine.pxd +25 -0
- algo_engine/engine/c_event_engine.pyi +68 -0
- algo_engine/engine/c_market_engine.c +24258 -0
- algo_engine/engine/c_market_engine.cp315-win_amd64.pyd +0 -0
- algo_engine/engine/c_market_engine.pxd +87 -0
- algo_engine/engine/c_market_engine.pyi +357 -0
- algo_engine/engine/event_engine.py +53 -0
- algo_engine/engine/trade_engine.py +2037 -0
- algo_engine/exchange_profile/__infra__.pxd +100 -0
- algo_engine/exchange_profile/__init__.pxd +100 -0
- algo_engine/exchange_profile/__init__.py +53 -0
- algo_engine/exchange_profile/c_ex_profile_base.c +87 -0
- algo_engine/exchange_profile/c_ex_profile_base.h +1204 -0
- algo_engine/exchange_profile/c_ex_profile_cn.c +968 -0
- algo_engine/exchange_profile/c_ex_profile_cn.h +39 -0
- algo_engine/exchange_profile/c_exchange_profile.c +52375 -0
- algo_engine/exchange_profile/c_exchange_profile.cp315-win_amd64.pyd +0 -0
- algo_engine/exchange_profile/c_exchange_profile.pxd +336 -0
- algo_engine/exchange_profile/c_exchange_profile.pyi +883 -0
- algo_engine/exchange_profile/c_exchange_profile.pyx +1495 -0
- algo_engine/exchange_profile/c_profile_cn.c +7798 -0
- algo_engine/exchange_profile/c_profile_cn.cp315-win_amd64.pyd +0 -0
- algo_engine/exchange_profile/c_profile_cn.pxd +1 -0
- algo_engine/exchange_profile/c_profile_cn.pyi +3 -0
- algo_engine/exchange_profile/c_profile_cn.pyx +1 -0
- algo_engine/exchange_profile/c_profile_default.c +7798 -0
- algo_engine/exchange_profile/c_profile_default.cp315-win_amd64.pyd +0 -0
- algo_engine/exchange_profile/c_profile_default.pxd +1 -0
- algo_engine/exchange_profile/c_profile_default.pyi +3 -0
- algo_engine/exchange_profile/c_profile_default.pyx +1 -0
- algo_engine/exchange_profile/c_profile_dispatcher.c +7798 -0
- algo_engine/exchange_profile/c_profile_dispatcher.cp315-win_amd64.pyd +0 -0
- algo_engine/exchange_profile/c_profile_dispatcher.pxd +1 -0
- algo_engine/exchange_profile/c_profile_dispatcher.pyi +3 -0
- algo_engine/exchange_profile/c_profile_dispatcher.pyx +1 -0
- algo_engine/includes/algo_engine/base/c_allocator_protocol.c +11608 -0
- algo_engine/includes/algo_engine/base/c_intern_string.c +5908 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_candlestick.c +18996 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_internal.c +14059 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_market_data.c +30420 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_market_data.h +1464 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_market_data_buffer.c +29407 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_market_data_buffer.h +941 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_market_data_config.h +29 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_tick.c +44303 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_trade_utils.c +28702 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_transaction.c +23558 -0
- algo_engine/includes/algo_engine/engine/c_event_engine.c +16325 -0
- algo_engine/includes/algo_engine/engine/c_market_engine.c +24258 -0
- algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_base.c +87 -0
- algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_base.h +1204 -0
- algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_cn.c +968 -0
- algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_cn.h +39 -0
- algo_engine/includes/algo_engine/exchange_profile/c_exchange_profile.c +52375 -0
- algo_engine/includes/algo_engine/exchange_profile/c_profile_cn.c +7798 -0
- algo_engine/includes/algo_engine/exchange_profile/c_profile_default.c +7798 -0
- algo_engine/includes/algo_engine/exchange_profile/c_profile_dispatcher.c +7798 -0
- algo_engine/monitor/__init__.py +15 -0
- algo_engine/monitor/advanced_data_interface.py +334 -0
- algo_engine/strategy/__init__.py +44 -0
- algo_engine/strategy/strategy_engine.py +441 -0
- algo_engine/utils/__init__.py +3 -0
- algo_engine/utils/commit_regularizer.py +49 -0
- algo_engine/utils/data_utils.py +296 -0
- pyalgoengine-0.12.3.dist-info/METADATA +142 -0
- pyalgoengine-0.12.3.dist-info/RECORD +156 -0
- pyalgoengine-0.12.3.dist-info/WHEEL +5 -0
- pyalgoengine-0.12.3.dist-info/licenses/LICENSE +21 -0
- 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,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
|
+
]
|