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,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