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,709 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import copy
|
|
3
|
+
import datetime
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Self
|
|
10
|
+
|
|
11
|
+
from . import LOGGER, PROFILE
|
|
12
|
+
from .market_utils import TransactionSide, TransactionData
|
|
13
|
+
|
|
14
|
+
LOGGER = LOGGER.getChild('TradeUtils')
|
|
15
|
+
__all__ = ['OrderState', 'OrderType', 'TradeInstruction', 'TradeReport']
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OrderType(Enum):
|
|
19
|
+
UNKNOWN = -2
|
|
20
|
+
CancelOrder = -1
|
|
21
|
+
Manual = 0
|
|
22
|
+
LimitOrder = 1
|
|
23
|
+
LimitMarketMaking = 1.1
|
|
24
|
+
MarketOrder = 2
|
|
25
|
+
FOK = 2.1
|
|
26
|
+
FAK = 2.2
|
|
27
|
+
IOC = 2.3
|
|
28
|
+
|
|
29
|
+
def __hash__(self):
|
|
30
|
+
return self.value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class OrderState(Enum):
|
|
34
|
+
UNKNOWN = -3
|
|
35
|
+
Rejected = -2 # order rejected
|
|
36
|
+
Invalid = -1 # invalid order
|
|
37
|
+
Pending = 0 # order not sent. CAUTION pending order is not working nor done!
|
|
38
|
+
Sent = 1 # order sent (to exchange)
|
|
39
|
+
Placed = 2 # order placed in exchange
|
|
40
|
+
PartFilled = 3 # order partial filled
|
|
41
|
+
Filled = 4 # order fully filled
|
|
42
|
+
Canceling = 5 # order canceling
|
|
43
|
+
# PartCanceled = 5 # Deprecated
|
|
44
|
+
Canceled = 6 # order stopped and canceled
|
|
45
|
+
|
|
46
|
+
def __hash__(self):
|
|
47
|
+
return self.value
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def is_working(self):
|
|
51
|
+
"""
|
|
52
|
+
order in working status (ready to be filled),
|
|
53
|
+
all non-working status are Pending / Filled / Cancelled / Rejected
|
|
54
|
+
"""
|
|
55
|
+
if self.value == OrderState.Pending.value or \
|
|
56
|
+
self.value == OrderState.Filled.value or \
|
|
57
|
+
self.value == OrderState.Canceled.value or \
|
|
58
|
+
self.value == OrderState.Invalid.value or \
|
|
59
|
+
self.value == OrderState.Rejected.value:
|
|
60
|
+
return False
|
|
61
|
+
else:
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def is_done(self):
|
|
66
|
+
if self.value == OrderState.Filled.value or \
|
|
67
|
+
self.value == OrderState.Canceled.value or \
|
|
68
|
+
self.value == OrderState.Rejected.value or \
|
|
69
|
+
self.value == OrderState.Invalid.value:
|
|
70
|
+
return True
|
|
71
|
+
else:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TradeBaseClass(dict, metaclass=abc.ABCMeta):
|
|
76
|
+
def __init__(self, ticker: str, timestamp: float | None, **kwargs):
|
|
77
|
+
super().__init__(ticker=ticker, timestamp=timestamp)
|
|
78
|
+
|
|
79
|
+
if kwargs:
|
|
80
|
+
self['additional'] = dict(kwargs)
|
|
81
|
+
|
|
82
|
+
def __copy__(self):
|
|
83
|
+
return self.__class__.__init__(**self)
|
|
84
|
+
|
|
85
|
+
def copy(self):
|
|
86
|
+
return self.__copy__()
|
|
87
|
+
|
|
88
|
+
def to_json(self, fmt='str', **kwargs) -> str | dict:
|
|
89
|
+
data_dict = dict(
|
|
90
|
+
dtype=self.__class__.__name__,
|
|
91
|
+
**self
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if 'additional' in data_dict:
|
|
95
|
+
additional = data_dict.pop('additional')
|
|
96
|
+
data_dict.update(additional)
|
|
97
|
+
|
|
98
|
+
if fmt == 'dict':
|
|
99
|
+
return data_dict
|
|
100
|
+
elif fmt == 'str':
|
|
101
|
+
return json.dumps(data_dict, **kwargs)
|
|
102
|
+
else:
|
|
103
|
+
raise ValueError(f'Invalid format {fmt}, except "dict" or "str".')
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def from_json(cls, json_message: str | bytes | bytearray | dict) -> Self:
|
|
107
|
+
if isinstance(json_message, dict):
|
|
108
|
+
json_dict = json_message
|
|
109
|
+
else:
|
|
110
|
+
json_dict = json.loads(json_message)
|
|
111
|
+
|
|
112
|
+
dtype = json_dict.pop('dtype', None)
|
|
113
|
+
if dtype == 'TradeReport':
|
|
114
|
+
return TradeReport.from_json(json_dict)
|
|
115
|
+
elif dtype == 'TickData':
|
|
116
|
+
return TradeInstruction.from_json(json_dict)
|
|
117
|
+
else:
|
|
118
|
+
raise TypeError(f'Invalid dtype {dtype}')
|
|
119
|
+
|
|
120
|
+
@abc.abstractmethod
|
|
121
|
+
def to_list(self) -> list[float | int | str | bool]:
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def from_list(cls, data_list: list[float | int | str | bool]) -> Self:
|
|
126
|
+
dtype = data_list[0]
|
|
127
|
+
|
|
128
|
+
if dtype == 'TradeReport':
|
|
129
|
+
return TradeReport.from_list(data_list)
|
|
130
|
+
elif dtype == 'TradeInstruction':
|
|
131
|
+
return TradeInstruction.from_list(data_list)
|
|
132
|
+
else:
|
|
133
|
+
raise TypeError(f'Invalid dtype {dtype}')
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def ticker(self):
|
|
137
|
+
return self['ticker']
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def timestamp(self):
|
|
141
|
+
return self['timestamp']
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def additional(self):
|
|
145
|
+
if 'additional' not in self:
|
|
146
|
+
self['additional'] = {}
|
|
147
|
+
return self['additional']
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def topic(self) -> str:
|
|
151
|
+
return f'{self.ticker}.{self.__class__.__name__}'
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def market_time(self) -> datetime.datetime | datetime.date:
|
|
155
|
+
return datetime.datetime.fromtimestamp(self.timestamp, tz=PROFILE.time_zone)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class TradeReport(TradeBaseClass):
|
|
159
|
+
|
|
160
|
+
def __init__(
|
|
161
|
+
self, *,
|
|
162
|
+
ticker: str,
|
|
163
|
+
side: int | float | str | TransactionSide,
|
|
164
|
+
price: float,
|
|
165
|
+
volume: float,
|
|
166
|
+
timestamp: float,
|
|
167
|
+
order_id: str,
|
|
168
|
+
trade_id: str = None,
|
|
169
|
+
notional: float = None,
|
|
170
|
+
multiplier: float = None,
|
|
171
|
+
fee: float = None,
|
|
172
|
+
**kwargs
|
|
173
|
+
):
|
|
174
|
+
assert volume >= 0, 'Trade volume must not be negative'
|
|
175
|
+
assert notional >= 0, 'Trade notional must not be negative'
|
|
176
|
+
|
|
177
|
+
super().__init__(ticker=ticker, timestamp=timestamp, **kwargs)
|
|
178
|
+
|
|
179
|
+
self['price'] = price
|
|
180
|
+
self['volume'] = volume
|
|
181
|
+
self['side'] = int(side) if isinstance(side, (int, float)) else TransactionSide(side).value
|
|
182
|
+
|
|
183
|
+
self['order_id'] = order_id
|
|
184
|
+
|
|
185
|
+
if trade_id is not None:
|
|
186
|
+
self['trade_id'] = trade_id
|
|
187
|
+
|
|
188
|
+
if notional is not None and math.isfinite(notional):
|
|
189
|
+
self['notional'] = notional
|
|
190
|
+
|
|
191
|
+
if multiplier is not None and math.isfinite(multiplier):
|
|
192
|
+
self['multiplier'] = multiplier
|
|
193
|
+
|
|
194
|
+
if fee is not None and math.isfinite(fee):
|
|
195
|
+
self['fee'] = fee
|
|
196
|
+
|
|
197
|
+
def __eq__(self, other: Self):
|
|
198
|
+
assert isinstance(other, self.__class__), f'Can only compare with {self.__class__.__name__}'
|
|
199
|
+
|
|
200
|
+
# Fast check: only check the order id and trade id.
|
|
201
|
+
if not self.order_id == other.order_id:
|
|
202
|
+
return False
|
|
203
|
+
elif not self.trade_id == other.trade_id:
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
def __str__(self):
|
|
209
|
+
return f'<TradeReport id={self.trade_id}>([{self.market_time:%Y-%m-%d %H:%M:%S}] {self.ticker} {TransactionSide(self.side).side_name} {self.volume} at {self.price})'
|
|
210
|
+
|
|
211
|
+
def __reduce__(self):
|
|
212
|
+
return self.__class__.from_json, (self.to_json(),)
|
|
213
|
+
|
|
214
|
+
def reset_order_id(self, order_id: int | str = None, _ignore_warning: bool = False) -> Self:
|
|
215
|
+
if not _ignore_warning:
|
|
216
|
+
LOGGER.warning('TradeReport OrderID being reset manually! TradeInstruction.reset_order_id() is the recommended method to do so.')
|
|
217
|
+
|
|
218
|
+
if order_id is not None:
|
|
219
|
+
self['order_id'] = order_id
|
|
220
|
+
else:
|
|
221
|
+
self['order_id'] = uuid.uuid4().int
|
|
222
|
+
|
|
223
|
+
return self
|
|
224
|
+
|
|
225
|
+
def reset_trade_id(self, trade_id: int | str = None) -> Self:
|
|
226
|
+
if trade_id is not None:
|
|
227
|
+
self['trade_id'] = trade_id
|
|
228
|
+
else:
|
|
229
|
+
self['trade_id'] = uuid.uuid4().int
|
|
230
|
+
|
|
231
|
+
return self
|
|
232
|
+
|
|
233
|
+
def to_trade(self) -> TransactionData:
|
|
234
|
+
trade = TransactionData(
|
|
235
|
+
ticker=self.ticker,
|
|
236
|
+
timestamp=self.timestamp,
|
|
237
|
+
price=self.price,
|
|
238
|
+
volume=self.volume,
|
|
239
|
+
side=self.side,
|
|
240
|
+
multiplier=self.multiplier
|
|
241
|
+
)
|
|
242
|
+
return trade
|
|
243
|
+
|
|
244
|
+
def to_list(self) -> list[float | int | str | bool]:
|
|
245
|
+
return [self.__class__.__name__,
|
|
246
|
+
self.ticker,
|
|
247
|
+
self.timestamp,
|
|
248
|
+
self.price,
|
|
249
|
+
self.volume,
|
|
250
|
+
self['side'],
|
|
251
|
+
self.get('multiplier'),
|
|
252
|
+
self.get('notional'),
|
|
253
|
+
self.get('fee'),
|
|
254
|
+
self.get('trade_id'),
|
|
255
|
+
self.order_id]
|
|
256
|
+
|
|
257
|
+
def copy(self, **kwargs):
|
|
258
|
+
new_trade = self.__class__(
|
|
259
|
+
ticker=kwargs.pop('ticker', self.ticker),
|
|
260
|
+
side=kwargs.pop('side', self.side),
|
|
261
|
+
price=kwargs.pop('price', self.price),
|
|
262
|
+
volume=kwargs.pop('volume', self.volume),
|
|
263
|
+
notional=kwargs.pop('notional', self.notional),
|
|
264
|
+
timestamp=kwargs.pop('timestamp', self.timestamp),
|
|
265
|
+
order_id=kwargs.pop('order_id', self.order_id),
|
|
266
|
+
trade_id=kwargs.pop('trade_id', f'{self.trade_id}.copy'),
|
|
267
|
+
multiplier=kwargs.pop('multiplier', self.multiplier),
|
|
268
|
+
fee=kwargs.pop('fee', self.fee)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
return new_trade
|
|
272
|
+
|
|
273
|
+
@classmethod
|
|
274
|
+
def from_json(cls, json_message: str | bytes | bytearray | dict) -> Self:
|
|
275
|
+
if isinstance(json_message, dict):
|
|
276
|
+
json_dict = json_message
|
|
277
|
+
else:
|
|
278
|
+
json_dict = json.loads(json_message)
|
|
279
|
+
|
|
280
|
+
dtype = json_dict.pop('dtype', None)
|
|
281
|
+
if dtype is not None and dtype != cls.__name__:
|
|
282
|
+
raise TypeError(f'dtype mismatch, expect {cls.__name__}, got {dtype}.')
|
|
283
|
+
|
|
284
|
+
self = cls(**json_dict)
|
|
285
|
+
return self
|
|
286
|
+
|
|
287
|
+
@classmethod
|
|
288
|
+
def from_list(cls, data_list: list[float | int | str | bool]) -> Self:
|
|
289
|
+
(dtype, ticker, timestamp, price, volume, side,
|
|
290
|
+
multiplier, notional, fee, trade_id, order_id) = data_list
|
|
291
|
+
|
|
292
|
+
if dtype != cls.__name__:
|
|
293
|
+
raise TypeError(f'dtype mismatch, expect {cls.__name__}, got {dtype}.')
|
|
294
|
+
|
|
295
|
+
kwargs = {}
|
|
296
|
+
|
|
297
|
+
if trade_id is not None:
|
|
298
|
+
kwargs['trade_id'] = trade_id
|
|
299
|
+
|
|
300
|
+
if notional is not None and math.isfinite(notional):
|
|
301
|
+
kwargs['notional'] = notional
|
|
302
|
+
|
|
303
|
+
if multiplier is not None and math.isfinite(multiplier):
|
|
304
|
+
kwargs['multiplier'] = multiplier
|
|
305
|
+
|
|
306
|
+
if fee is not None and math.isfinite(fee):
|
|
307
|
+
kwargs['fee'] = fee
|
|
308
|
+
|
|
309
|
+
return cls(
|
|
310
|
+
ticker=ticker,
|
|
311
|
+
timestamp=timestamp,
|
|
312
|
+
price=price,
|
|
313
|
+
volume=volume,
|
|
314
|
+
side=side,
|
|
315
|
+
order_id=order_id,
|
|
316
|
+
**kwargs
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
@classmethod
|
|
320
|
+
def from_trade(cls, trade_data: TransactionData, order_id: str, trade_id: str = None) -> Self:
|
|
321
|
+
report = cls(
|
|
322
|
+
ticker=trade_data.ticker,
|
|
323
|
+
side=trade_data.side,
|
|
324
|
+
volume=trade_data.volume,
|
|
325
|
+
price=trade_data.price,
|
|
326
|
+
notional=trade_data.notional,
|
|
327
|
+
timestamp=trade_data.timestamp,
|
|
328
|
+
order_id=order_id,
|
|
329
|
+
trade_id=trade_id
|
|
330
|
+
)
|
|
331
|
+
return report
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def price(self) -> float:
|
|
335
|
+
return self['price']
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def volume(self) -> float:
|
|
339
|
+
return self['volume']
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def side(self) -> TransactionSide:
|
|
343
|
+
return TransactionSide(self['side'])
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def multiplier(self) -> float:
|
|
347
|
+
return self.get('multiplier', 1.)
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def fee(self) -> float:
|
|
351
|
+
return self.get('fee', 0.)
|
|
352
|
+
|
|
353
|
+
@property
|
|
354
|
+
def order_id(self) -> int | str:
|
|
355
|
+
return self['order_id']
|
|
356
|
+
|
|
357
|
+
@property
|
|
358
|
+
def trade_id(self) -> int | str:
|
|
359
|
+
if 'trade_id' in self:
|
|
360
|
+
trade_id = self['trade_id']
|
|
361
|
+
else:
|
|
362
|
+
trade_id = self['trade_id'] = uuid.uuid4().int
|
|
363
|
+
|
|
364
|
+
return trade_id
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def notional(self) -> float:
|
|
368
|
+
return self.get('notional', self.price * self.volume * self.multiplier)
|
|
369
|
+
|
|
370
|
+
@property
|
|
371
|
+
def market_price(self) -> float:
|
|
372
|
+
return self.price
|
|
373
|
+
|
|
374
|
+
@property
|
|
375
|
+
def flow(self):
|
|
376
|
+
return self.side.sign * self.volume
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def trade_time(self) -> datetime.datetime:
|
|
380
|
+
return datetime.datetime.fromtimestamp(self.timestamp, tz=PROFILE.time_zone)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class TradeInstruction(TradeBaseClass):
|
|
384
|
+
def __init__(
|
|
385
|
+
self, *,
|
|
386
|
+
ticker: str,
|
|
387
|
+
side: int | float | str | TransactionSide,
|
|
388
|
+
volume: float,
|
|
389
|
+
timestamp: float,
|
|
390
|
+
order_type: int | float | str | OrderType = OrderType.Manual,
|
|
391
|
+
limit_price: float = None,
|
|
392
|
+
order_id: str = None,
|
|
393
|
+
multiplier: float = None,
|
|
394
|
+
**kwargs
|
|
395
|
+
):
|
|
396
|
+
assert volume > 0, f'Invalid trade volume {volume}!'
|
|
397
|
+
super().__init__(ticker=ticker, timestamp=timestamp, **kwargs)
|
|
398
|
+
|
|
399
|
+
self['volume'] = volume
|
|
400
|
+
self['side'] = int(side) if isinstance(side, (int, float)) else TransactionSide(side).value
|
|
401
|
+
self['order_type'] = int(order_type) if isinstance(order_type, (int, float)) else OrderType(order_type).value
|
|
402
|
+
self['order_id'] = order_id if order_id is not None else uuid.uuid4().int
|
|
403
|
+
|
|
404
|
+
if limit_price is not None and math.isfinite(limit_price):
|
|
405
|
+
self['limit_price'] = limit_price
|
|
406
|
+
|
|
407
|
+
if multiplier is not None and math.isfinite(multiplier):
|
|
408
|
+
self['multiplier'] = multiplier
|
|
409
|
+
|
|
410
|
+
self['order_state'] = OrderState.Pending.value
|
|
411
|
+
self['filled_volume'] = 0.
|
|
412
|
+
self['filled_notional'] = 0.
|
|
413
|
+
self['fee'] = 0.
|
|
414
|
+
|
|
415
|
+
# note that 3 additional entries might be added to the TradeInstruction
|
|
416
|
+
# self['ts_placed'] = timestamp
|
|
417
|
+
# self['ts_canceled'] = timestamp
|
|
418
|
+
# self['ts_finished'] = timestamp
|
|
419
|
+
|
|
420
|
+
self.trades: dict[int | str, TradeReport] = {}
|
|
421
|
+
|
|
422
|
+
def __eq__(self, other: Self):
|
|
423
|
+
assert isinstance(other, self.__class__), f'Can only compare with {self.__class__.__name__}'
|
|
424
|
+
|
|
425
|
+
# Fast check: only check the order id and trade id.
|
|
426
|
+
if not self.order_id == other.order_id:
|
|
427
|
+
return False
|
|
428
|
+
|
|
429
|
+
return True
|
|
430
|
+
|
|
431
|
+
def __str__(self):
|
|
432
|
+
if self.limit_price is None or self.order_type == OrderType.MarketOrder:
|
|
433
|
+
return f'<TradeInstruction id={self.order_id}>({self.ticker} {self.order_type.name} {self.side.name} {self.volume}; filled {self.filled_volume:.2f} @ {self.average_price:.2f} now {self.order_state.name})'
|
|
434
|
+
else:
|
|
435
|
+
return f'<TradeInstruction id={self.order_id}>({self.ticker} {self.order_type.name} {self.side.name} {self.volume} limit {self.limit_price:.2f}; filled {self.filled_volume:.2f} @ {self.average_price:.2f} now {self.order_state.name})'
|
|
436
|
+
|
|
437
|
+
def __reduce__(self):
|
|
438
|
+
return self.__class__.from_json, (self.to_json(),)
|
|
439
|
+
|
|
440
|
+
def reset(self):
|
|
441
|
+
self.trades.clear()
|
|
442
|
+
self.order_state = OrderState.Pending
|
|
443
|
+
|
|
444
|
+
self['filled_volume']: float = 0.0
|
|
445
|
+
self['filled_notional']: float = 0.0
|
|
446
|
+
self['fee'] = .0
|
|
447
|
+
|
|
448
|
+
self.pop('ts_placed', None)
|
|
449
|
+
self.pop('ts_canceled', None)
|
|
450
|
+
self.pop('ts_finished', None)
|
|
451
|
+
|
|
452
|
+
def reset_order_id(self, order_id: int | str = None, _ignore_warning: bool = False) -> Self:
|
|
453
|
+
if not _ignore_warning:
|
|
454
|
+
LOGGER.warning(f'{self.__class__.__name__} OrderID being reset manually! Position.reset_order_id() is the recommended method to do so.')
|
|
455
|
+
|
|
456
|
+
if order_id is not None:
|
|
457
|
+
self['order_id'] = order_id
|
|
458
|
+
else:
|
|
459
|
+
self['order_id'] = uuid.uuid4().int
|
|
460
|
+
|
|
461
|
+
for trade_report in self.trades.values():
|
|
462
|
+
trade_report.reset_order_id(order_id=self.order_id, _ignore_warning=True)
|
|
463
|
+
|
|
464
|
+
return self
|
|
465
|
+
|
|
466
|
+
def set_order_state(self, order_state: OrderState, timestamp: float = time.time()) -> Self:
|
|
467
|
+
self.order_state = order_state
|
|
468
|
+
|
|
469
|
+
# assign a start_datetime if order placed
|
|
470
|
+
if order_state == OrderState.Placed:
|
|
471
|
+
self['ts_placed'] = timestamp
|
|
472
|
+
|
|
473
|
+
elif order_state == OrderState.Filled:
|
|
474
|
+
self['ts_finished'] = timestamp
|
|
475
|
+
|
|
476
|
+
if order_state == OrderState.Canceled:
|
|
477
|
+
self['ts_canceled'] = timestamp
|
|
478
|
+
self['ts_finished'] = timestamp
|
|
479
|
+
|
|
480
|
+
return self
|
|
481
|
+
|
|
482
|
+
def fill(self, trade_report: TradeReport) -> Self:
|
|
483
|
+
if trade_report.order_id != self.order_id:
|
|
484
|
+
LOGGER.warning(f'Order ID not match! Instruction ID {self.order_id}; Report ID {trade_report.order_id}')
|
|
485
|
+
return self
|
|
486
|
+
|
|
487
|
+
if trade_report.trade_id in self.trades:
|
|
488
|
+
LOGGER.warning(f'Duplicated trade received!\nInstruction {self}.\nReport {trade_report}.')
|
|
489
|
+
return self
|
|
490
|
+
|
|
491
|
+
if trade_report.volume:
|
|
492
|
+
# update multiplier
|
|
493
|
+
if 'multiplier' in trade_report:
|
|
494
|
+
if 'multiplier' not in self:
|
|
495
|
+
self['multiplier'] = trade_report.multiplier
|
|
496
|
+
elif trade_report.multiplier != self['multiplier']:
|
|
497
|
+
raise ValueError(f'Multiplier not match for order {self} and report {trade_report}.')
|
|
498
|
+
|
|
499
|
+
if trade_report.volume + self.filled_volume > self.volume:
|
|
500
|
+
LOGGER.warning('Fatal error!\nTradeInstruction: \n\t{}\nTradeReport:\n\t{}'.format(str(TradeInstruction), '\n\t'.join([str(x) for x in self.trades.values()]) + f'\n\t<new> {trade_report}'))
|
|
501
|
+
raise ValueError('Fatal error! trade reports filled volume exceed order volume!')
|
|
502
|
+
|
|
503
|
+
self['filled_volume'] += abs(trade_report.volume)
|
|
504
|
+
self['filled_notional'] += abs(trade_report.notional)
|
|
505
|
+
|
|
506
|
+
if self.filled_volume == self.volume:
|
|
507
|
+
self.set_order_state(order_state=OrderState.Filled, timestamp=trade_report.timestamp)
|
|
508
|
+
self['ts_finished'] = trade_report.timestamp
|
|
509
|
+
elif self.filled_volume > 0:
|
|
510
|
+
self.set_order_state(order_state=OrderState.PartFilled)
|
|
511
|
+
|
|
512
|
+
self.trades[trade_report.trade_id] = trade_report
|
|
513
|
+
|
|
514
|
+
return self
|
|
515
|
+
|
|
516
|
+
def cancel_order(self) -> Self:
|
|
517
|
+
self.set_order_state(order_state=OrderState.Canceling)
|
|
518
|
+
|
|
519
|
+
cancel_instruction = copy.copy(self)
|
|
520
|
+
cancel_instruction.set_order_state(order_state=OrderState.Canceled)
|
|
521
|
+
|
|
522
|
+
return cancel_instruction
|
|
523
|
+
|
|
524
|
+
def canceled(self, timestamp: float) -> Self:
|
|
525
|
+
LOGGER.warning(DeprecationWarning('[canceled] depreciated! Use [set_order_state] instead!'), stacklevel=2)
|
|
526
|
+
|
|
527
|
+
self.set_order_state(order_state=OrderState.Canceled, timestamp=timestamp)
|
|
528
|
+
return self
|
|
529
|
+
|
|
530
|
+
def to_json(self, with_trade=True, fmt: str = 'str') -> str | dict:
|
|
531
|
+
json_dict = super().to_json()
|
|
532
|
+
|
|
533
|
+
if self.trades:
|
|
534
|
+
json_dict['trade'] = [report.to_json(fmt='dict') for report in self.trades.values()]
|
|
535
|
+
|
|
536
|
+
if fmt == 'dict':
|
|
537
|
+
return json_dict
|
|
538
|
+
else:
|
|
539
|
+
return json.dumps(json_dict)
|
|
540
|
+
|
|
541
|
+
def to_list(self) -> list[float | int | str | bool]:
|
|
542
|
+
return [self.__class__.__name__,
|
|
543
|
+
self.ticker,
|
|
544
|
+
self.timestamp,
|
|
545
|
+
self.limit_price,
|
|
546
|
+
self.volume,
|
|
547
|
+
self['side'],
|
|
548
|
+
self['order_state'],
|
|
549
|
+
self.get('multiplier'),
|
|
550
|
+
self.get('notional'),
|
|
551
|
+
self.filled_volume,
|
|
552
|
+
self.filled_notional,
|
|
553
|
+
self.fee,
|
|
554
|
+
self.get('order_id')]
|
|
555
|
+
|
|
556
|
+
@classmethod
|
|
557
|
+
def from_json(cls, json_message: str | bytes | bytearray | dict) -> Self:
|
|
558
|
+
if isinstance(json_message, dict):
|
|
559
|
+
json_dict = json_message
|
|
560
|
+
else:
|
|
561
|
+
json_dict = json.loads(json_message)
|
|
562
|
+
|
|
563
|
+
dtype = json_dict.pop('dtype', None)
|
|
564
|
+
trades = json_dict.pop('trades', [])
|
|
565
|
+
|
|
566
|
+
if dtype is not None and dtype != cls.__name__:
|
|
567
|
+
raise TypeError(f'dtype mismatch, expect {cls.__name__}, got {dtype}.')
|
|
568
|
+
|
|
569
|
+
self = cls(**json_dict)
|
|
570
|
+
|
|
571
|
+
for trade_json in json_dict['trades']:
|
|
572
|
+
report = TradeReport.from_json(trade_json)
|
|
573
|
+
self.trades[report.trade_id] = report
|
|
574
|
+
|
|
575
|
+
return self
|
|
576
|
+
|
|
577
|
+
@classmethod
|
|
578
|
+
def from_list(cls, data_list: list[float | int | str | bool]) -> Self:
|
|
579
|
+
(dtype, ticker, timestamp, limit_price, volume, side,
|
|
580
|
+
order_state, multiplier, notional, filled_volume, filled_notional, fee, order_id) = data_list
|
|
581
|
+
|
|
582
|
+
if dtype != cls.__name__:
|
|
583
|
+
raise TypeError(f'dtype mismatch, expect {cls.__name__}, got {dtype}.')
|
|
584
|
+
|
|
585
|
+
kwargs = {}
|
|
586
|
+
|
|
587
|
+
if notional is not None and math.isfinite(notional):
|
|
588
|
+
kwargs['notional'] = notional
|
|
589
|
+
|
|
590
|
+
if multiplier is not None and math.isfinite(multiplier):
|
|
591
|
+
kwargs['multiplier'] = multiplier
|
|
592
|
+
|
|
593
|
+
if fee is not None and math.isfinite(fee):
|
|
594
|
+
kwargs['fee'] = fee
|
|
595
|
+
|
|
596
|
+
self = cls(
|
|
597
|
+
ticker=ticker,
|
|
598
|
+
timestamp=timestamp,
|
|
599
|
+
limit_price=limit_price,
|
|
600
|
+
volume=volume,
|
|
601
|
+
side=side,
|
|
602
|
+
order_id=order_id,
|
|
603
|
+
**kwargs
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
self['filled_volume'] = filled_volume
|
|
607
|
+
self['filled_notional'] = filled_notional
|
|
608
|
+
self['fee'] = fee
|
|
609
|
+
|
|
610
|
+
return self
|
|
611
|
+
|
|
612
|
+
@property
|
|
613
|
+
def is_working(self):
|
|
614
|
+
return self.order_state.is_working
|
|
615
|
+
|
|
616
|
+
@property
|
|
617
|
+
def is_done(self):
|
|
618
|
+
return self.order_state.is_done
|
|
619
|
+
|
|
620
|
+
@property
|
|
621
|
+
def limit_price(self) -> float:
|
|
622
|
+
return self['limit_price']
|
|
623
|
+
|
|
624
|
+
@property
|
|
625
|
+
def volume(self) -> float:
|
|
626
|
+
return self['volume']
|
|
627
|
+
|
|
628
|
+
@property
|
|
629
|
+
def side(self) -> TransactionSide:
|
|
630
|
+
return TransactionSide(self['side'])
|
|
631
|
+
|
|
632
|
+
@property
|
|
633
|
+
def multiplier(self) -> float:
|
|
634
|
+
return self.get('multiplier', 1.)
|
|
635
|
+
|
|
636
|
+
@property
|
|
637
|
+
def fee(self) -> float:
|
|
638
|
+
return self['fee']
|
|
639
|
+
|
|
640
|
+
@fee.setter
|
|
641
|
+
def fee(self, value: float):
|
|
642
|
+
self['fee'] = value
|
|
643
|
+
|
|
644
|
+
@property
|
|
645
|
+
def order_id(self) -> int | str:
|
|
646
|
+
return self['order_id']
|
|
647
|
+
|
|
648
|
+
@property
|
|
649
|
+
def order_type(self) -> OrderType:
|
|
650
|
+
return OrderType(self['order_type'])
|
|
651
|
+
|
|
652
|
+
@property
|
|
653
|
+
def order_state(self) -> OrderState:
|
|
654
|
+
return OrderState(self['order_state'])
|
|
655
|
+
|
|
656
|
+
@order_state.setter
|
|
657
|
+
def order_state(self, value: OrderState):
|
|
658
|
+
if isinstance(value, int):
|
|
659
|
+
self['order_state'] = value
|
|
660
|
+
else:
|
|
661
|
+
self['order_state'] = OrderState(value).value
|
|
662
|
+
|
|
663
|
+
@property
|
|
664
|
+
def filled_volume(self) -> float:
|
|
665
|
+
return self['filled_volume']
|
|
666
|
+
|
|
667
|
+
@property
|
|
668
|
+
def working_volume(self) -> float:
|
|
669
|
+
return self.volume - self.filled_volume
|
|
670
|
+
|
|
671
|
+
@property
|
|
672
|
+
def filled_notional(self) -> float:
|
|
673
|
+
return self['filled_notional']
|
|
674
|
+
|
|
675
|
+
@property
|
|
676
|
+
def average_price(self) -> float:
|
|
677
|
+
if self.filled_volume != 0:
|
|
678
|
+
return self.filled_notional / self.filled_volume / self.multiplier
|
|
679
|
+
else:
|
|
680
|
+
return float('NaN')
|
|
681
|
+
|
|
682
|
+
@property
|
|
683
|
+
def start_time(self) -> datetime.datetime | None:
|
|
684
|
+
return datetime.datetime.fromtimestamp(self.timestamp, tz=PROFILE.time_zone)
|
|
685
|
+
|
|
686
|
+
@property
|
|
687
|
+
def placed_time(self) -> datetime.datetime | None:
|
|
688
|
+
if 'ts_placed' in self:
|
|
689
|
+
return datetime.datetime.fromtimestamp(self['ts_placed'], tz=PROFILE.time_zone)
|
|
690
|
+
|
|
691
|
+
return None
|
|
692
|
+
|
|
693
|
+
@property
|
|
694
|
+
def canceled_time(self) -> datetime.datetime | None:
|
|
695
|
+
if 'ts_canceled' in self:
|
|
696
|
+
return datetime.datetime.fromtimestamp(self['ts_canceled'], tz=PROFILE.time_zone)
|
|
697
|
+
|
|
698
|
+
return None
|
|
699
|
+
|
|
700
|
+
@property
|
|
701
|
+
def finished_time(self) -> datetime.datetime | None:
|
|
702
|
+
if 'ts_finished' in self:
|
|
703
|
+
return datetime.datetime.fromtimestamp(self['ts_finished'], tz=PROFILE.time_zone)
|
|
704
|
+
|
|
705
|
+
return None
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
class TradeOrder(TradeInstruction):
|
|
709
|
+
pass
|