PyAlgoEngine 0.8.0a17__tar.gz → 0.8.0.post3__tar.gz

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 (58) hide show
  1. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/PKG-INFO +1 -1
  2. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/PyAlgoEngine.egg-info/PKG-INFO +1 -1
  3. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/PyAlgoEngine.egg-info/SOURCES.txt +1 -0
  4. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/__init__.py +1 -1
  5. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/apps/backtest/tester.py +11 -9
  6. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/backtest/__init__.py +2 -2
  7. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/backtest/replay.py +127 -18
  8. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/base/__init__.py +5 -0
  9. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/base/market_data.pyi +3 -0
  10. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/base/market_data_buffer.pyi +12 -2
  11. pyalgoengine-0.8.0.post3/algo_engine/base/trade_utils_native.py +693 -0
  12. pyalgoengine-0.8.0.post3/algo_engine/profile/__init__.py +236 -0
  13. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/strategy/strategy_engine.py +6 -5
  14. pyalgoengine-0.8.0a17/algo_engine/profile/__init__.py +0 -121
  15. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/LICENSE +0 -0
  16. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/PyAlgoEngine.egg-info/dependency_links.txt +0 -0
  17. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/PyAlgoEngine.egg-info/requires.txt +0 -0
  18. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/PyAlgoEngine.egg-info/top_level.txt +0 -0
  19. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/README.md +0 -0
  20. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/apps/__init__.py +0 -0
  21. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/apps/backtest/__init__.py +0 -0
  22. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/apps/backtest/doc_server.py +0 -0
  23. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/apps/backtest/web_app.py +0 -0
  24. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/apps/bokeh_server.py +0 -0
  25. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/apps/demo/__init__.py +0 -0
  26. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/apps/demo/test.py +0 -0
  27. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/apps/sim_input/__init__.py +0 -0
  28. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/apps/sim_input/client.py +0 -0
  29. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/apps/sim_input/sim_keyboard.py +0 -0
  30. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/apps/sim_input/sim_mouse.py +0 -0
  31. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/apps/sim_input/window.py +0 -0
  32. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/backtest/__main__.py +0 -0
  33. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/backtest/metrics.py +0 -0
  34. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/backtest/sim_match.py +0 -0
  35. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/base/candlestick.pyi +0 -0
  36. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/base/console_utils.py +0 -0
  37. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/base/finance_decimal.py +0 -0
  38. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/base/market_utils_nt.py +0 -0
  39. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/base/market_utils_posix.py +0 -0
  40. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/base/technical_analysis.py +0 -0
  41. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/base/telemetrics.py +0 -0
  42. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/base/tick.pyi +0 -0
  43. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/base/trade_utils.pyi +0 -0
  44. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/base/transaction.pyi +0 -0
  45. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/engine/__init__.py +0 -0
  46. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/engine/algo_engine.py +0 -0
  47. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/engine/event_engine.py +0 -0
  48. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/engine/market_engine.py +0 -0
  49. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/engine/trade_engine.py +0 -0
  50. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/monitor/__init__.py +0 -0
  51. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/monitor/advanced_data_interface.py +0 -0
  52. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/profile/cn.py +0 -0
  53. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/strategy/__init__.py +0 -0
  54. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/utils/__init__.py +0 -0
  55. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/utils/commit_regularizer.py +0 -0
  56. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/algo_engine/utils/data_utils.py +0 -0
  57. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/setup.cfg +0 -0
  58. {pyalgoengine-0.8.0a17 → pyalgoengine-0.8.0.post3}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyAlgoEngine
3
- Version: 0.8.0a17
3
+ Version: 0.8.0.post3
4
4
  Summary: Basic algo engine
5
5
  Home-page: https://github.com/BolunHan/PyAlgoEngine
6
6
  Author: Bolun.Han
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyAlgoEngine
3
- Version: 0.8.0a17
3
+ Version: 0.8.0.post3
4
4
  Summary: Basic algo engine
5
5
  Home-page: https://github.com/BolunHan/PyAlgoEngine
6
6
  Author: Bolun.Han
@@ -37,6 +37,7 @@ algo_engine/base/technical_analysis.py
37
37
  algo_engine/base/telemetrics.py
38
38
  algo_engine/base/tick.pyi
39
39
  algo_engine/base/trade_utils.pyi
40
+ algo_engine/base/trade_utils_native.py
40
41
  algo_engine/base/transaction.pyi
41
42
  algo_engine/engine/__init__.py
42
43
  algo_engine/engine/algo_engine.py
@@ -1,4 +1,4 @@
1
- __version__ = "0.8.0.alpha17"
1
+ __version__ = "0.8.0.post3"
2
2
 
3
3
  import logging
4
4
  import os
@@ -8,7 +8,7 @@ import numpy as np
8
8
  from algo_engine.backtest.metrics import TradeMetrics
9
9
  from . import LOGGER
10
10
  from .web_app import WebApp
11
- from ...backtest import SimMatch, ProgressiveReplay
11
+ from ...backtest import SimMatch, ProgressReplay
12
12
  from ...base import MarketData, TradeReport, TradeInstruction
13
13
  from ...profile import Profile, PROFILE
14
14
 
@@ -109,17 +109,18 @@ class Tester(object, metaclass=abc.ABCMeta):
109
109
  pass
110
110
 
111
111
  def run(self, **kwargs):
112
- replay = ProgressiveReplay(
112
+ replay = ProgressReplay(
113
113
  loader=self.load_data,
114
- tickers=list(self.subscription),
115
- dtype=['TickData', 'TradeData'],
116
114
  start_date=self.start_date,
117
115
  end_date=self.end_date,
118
116
  bod=self.bod,
119
117
  eod=self.eod,
120
- tick_size=kwargs.get('progress_tick_size', 0.001),
121
118
  )
122
119
 
120
+ for ticker in self.subscription:
121
+ replay.add_subscription(ticker, dtype='TickData')
122
+ replay.add_subscription(ticker, dtype='TradeData')
123
+
123
124
  _start_ts = time.time()
124
125
 
125
126
  for market_data in replay:
@@ -222,17 +223,18 @@ class StrategyTester(Tester):
222
223
  if not self.event_engine.active:
223
224
  self.event_engine.start()
224
225
 
225
- replay = ProgressiveReplay(
226
+ replay = ProgressReplay(
226
227
  loader=self.load_data,
227
- tickers=list(self.subscription),
228
- dtype=['TickData', 'TradeData'],
229
228
  start_date=self.start_date,
230
229
  end_date=self.end_date,
231
230
  bod=self.bod,
232
231
  eod=self.eod,
233
- tick_size=kwargs.get('progress_tick_size', 0.001),
234
232
  )
235
233
 
234
+ for ticker in self.subscription:
235
+ replay.add_subscription(ticker, dtype='TickData')
236
+ replay.add_subscription(ticker, dtype='TradeData')
237
+
236
238
  _start_ts = time.time()
237
239
 
238
240
  for market_data in replay:
@@ -13,7 +13,7 @@ def set_logger(logger: logging.Logger):
13
13
  sim_match.LOGGER = LOGGER.getChild('SimMatch')
14
14
 
15
15
 
16
- from .replay import PyDataScope, MarketDateCallable, MarketDataLoader, MarketDataBulkLoader, Replay, SimpleReplay, ProgressReplay, ProgressiveReplay
16
+ from .replay import PyDataScope, MarketDateCallable, MarketDataLoader, MarketDataBulkLoader, Replay, SimpleReplay, ProgressReplay
17
17
  from .sim_match import SimMatch
18
18
 
19
- __all__ = ['PyDataScope', 'MarketDateCallable', 'MarketDataLoader', 'MarketDataBulkLoader', 'Replay', 'SimpleReplay', 'ProgressReplay', 'ProgressiveReplay', 'SimMatch']
19
+ __all__ = ['PyDataScope', 'MarketDateCallable', 'MarketDataLoader', 'MarketDataBulkLoader', 'Replay', 'SimpleReplay', 'ProgressReplay', 'SimMatch']
@@ -2,6 +2,7 @@ import abc
2
2
  import datetime
3
3
  import enum
4
4
  import inspect
5
+ import logging
5
6
  import operator
6
7
  import warnings
7
8
  from collections.abc import Sequence, Mapping, Iterable, Callable
@@ -244,7 +245,7 @@ class SimpleReplay(Replay):
244
245
 
245
246
  def __iter__(self):
246
247
  self._calendar = self.calendar or [self.start_date + datetime.timedelta(days=i) for i in range((self.end_date - self.start_date).days + 1)]
247
- self._market_date = self.market_date or sorted(_ for _ in self._calendar if _ >= self.market_date)[0]
248
+ self._market_date = sorted(_ for _ in self._calendar if _ >= self.market_date)[0]
248
249
  self._status = {market_date: 'skipped' if market_date < self.market_date else 'idle' for market_date in self._calendar}
249
250
  self._idx_buffer = 0
250
251
  self._idx_date = sum([1 for _ in self._calendar if _ < self.market_date])
@@ -353,7 +354,7 @@ class SimpleReplay(Replay):
353
354
  if not hasattr(self, '_buffer'):
354
355
  raise RuntimeError(f'{self.__class__.__name__} not started yet.')
355
356
 
356
- return (self._idx_date + (self._idx_buffer / self._buffer_size - 1)) / len(self._calendar)
357
+ return (self._idx_date + self._idx_buffer / self._buffer_size) / len(self._calendar)
357
358
 
358
359
  @property
359
360
  def tickers(self) -> list[str]:
@@ -387,7 +388,7 @@ class ProgressReplay(SimpleReplay):
387
388
  calendar: Sequence[datetime.date] = None,
388
389
  bod: MarketDateCallable = None,
389
390
  eod: MarketDateCallable = None,
390
- **tqdm_kwargs
391
+ **pbar_config
391
392
  ):
392
393
  super().__init__(
393
394
  loader=loader,
@@ -399,40 +400,148 @@ class ProgressReplay(SimpleReplay):
399
400
  eod=eod
400
401
  )
401
402
 
402
- self._tqdm_kwargs = {
403
+ self.pbar_config = {
404
+ 'backend': pbar_config.pop('backend', 'tqdm'), # tqdm or native
405
+ 'config': pbar_config,
406
+ }
407
+ self._pbar = None
408
+
409
+ def _init_pbar_tqdm(self):
410
+ from tqdm.auto import tqdm
411
+ from tqdm.std import tqdm as tqdm_std
412
+ from tqdm.contrib.logging import _TqdmLoggingHandler, _get_first_found_console_logging_handler, _is_console_logging_handler
413
+
414
+ tqdm_config = {
403
415
  'total': 1,
404
416
  'unit_scale': True,
405
417
  'unit': 'percent',
406
418
  'mininterval': 0.1,
407
419
  'miniters': 0.001,
408
- **tqdm_kwargs
420
+ **self.pbar_config['config'],
409
421
  }
422
+ self._pbar = tqdm(**tqdm_config)
423
+
424
+ self.pbar_config['loggers'] = loggers = [LOGGER.root] + [_ for _ in LOGGER.root.manager.loggerDict.values() if isinstance(_, logging.Logger) and _.handlers]
425
+ self.pbar_config['original_handlers_list'] = [logger.handlers for logger in loggers]
426
+ for logger in loggers:
427
+ tqdm_handler = _TqdmLoggingHandler(tqdm_std)
428
+ orig_handler = _get_first_found_console_logging_handler(logger.handlers)
429
+ if orig_handler is not None:
430
+ tqdm_handler.setFormatter(orig_handler.formatter)
431
+ tqdm_handler.stream = orig_handler.stream
432
+ logger.handlers = [handler for handler in logger.handlers if not _is_console_logging_handler(handler)] + [tqdm_handler]
433
+
434
+ self.add_bod(self._init_pbar_tqdm_secondary, priority=0)
435
+ self.add_eod(self._close_pbar_tqdm_secondary, priority=0)
436
+ self.add_bod(self._update_tqdm_prefix, priority=0)
437
+ self._update_pbar_progress = self._update_tqdm_progress
438
+
439
+ def _init_pbar_tqdm_secondary(self, market_date):
440
+ from tqdm.auto import tqdm
441
+
442
+ tqdm_secondary_config = {
443
+ 'total': 1,
444
+ 'unit_scale': True,
445
+ 'unit': 'percent',
446
+ 'mininterval': 0.1,
447
+ 'miniters': 0.001,
448
+ **self.pbar_config['config'],
449
+ }
450
+ self._pbar_secondary = tqdm(**tqdm_secondary_config)
451
+ prompt = f'Progress Total ({self._idx_date + 1} / {len(self._calendar)})'
452
+ prompt_secondary = f'Progress [{market_date:%Y-%m-%d}]'
453
+ prompt_length = max(len(prompt), len(prompt_secondary))
454
+ self._pbar_secondary.n = 0
455
+ self._pbar_secondary.set_description(prompt_secondary.ljust(prompt_length))
456
+ self._pbar_secondary.refresh()
457
+
458
+ def _close_pbar_tqdm_secondary(self, market_date: datetime.date):
459
+ self._pbar_secondary.n = 1
460
+ # self._pbar_secondary.refresh()
461
+ self._pbar_secondary.close()
462
+ self._pbar_secondary = None
463
+
464
+ def _init_pbar_native(self):
465
+ from ..base import Progress
466
+
467
+ progress_config = dict(
468
+ tasks=1,
469
+ tick_size=0.001,
470
+ **self.pbar_config['config'],
471
+ )
472
+
473
+ self.add_bod(self._update_native_prefix, priority=0)
474
+ self._pbar = Progress(**progress_config)
475
+ self._update_pbar_progress = self._update_native_progress
476
+
477
+ def _update_tqdm_prefix(self, market_date: datetime.date):
478
+ prompt = f'Progress Total ({self._idx_date + 1} / {len(self._calendar)})'
479
+ self._pbar.set_description(prompt)
480
+ self._pbar.refresh()
481
+
482
+ def _update_native_prefix(self, market_date: datetime.date):
483
+ self._pbar.prompt = f'Replay {market_date:%Y-%m-%d} ({self._idx_date + 1} / {len(self._calendar)}):'
484
+ self._pbar.output()
485
+
486
+ def _close_pbar_tqdm(self):
487
+ for logger, original_handlers in zip(self.pbar_config['loggers'], self.pbar_config['original_handlers_list']):
488
+ logger.handlers = original_handlers
489
+
490
+ self._pbar.n = 1
491
+ # self._pbar.refresh()
492
+ self._pbar.close()
410
493
  self._pbar = None
411
- self.add_bod(self._update_progress_bar, priority=0)
494
+
495
+ def _close_pbar_native(self):
496
+ self._pbar.done_tasks = 1
497
+ self._pbar.output()
498
+
499
+ def _update_tqdm_progress(self):
500
+ self._pbar.n = self.progress
501
+ self._pbar.update(0)
502
+
503
+ self._pbar_secondary.n = self._idx_buffer / self._buffer_size
504
+ self._pbar_secondary.update(0)
505
+
506
+ def _update_native_progress(self):
507
+ self._pbar.done_tasks = self.progress
508
+
509
+ if (not self._pbar.tick_size) \
510
+ or self._pbar.progress >= self._pbar.tick_size + self._pbar.last_output \
511
+ or self._pbar.is_done:
512
+ self._pbar.output()
412
513
 
413
514
  def __iter__(self):
414
- from tqdm.auto import tqdm
415
- self._pbar = tqdm(**self._tqdm_kwargs)
515
+ pbar_backend = self.pbar_config['backend']
516
+
517
+ match pbar_backend:
518
+ case 'tqdm':
519
+ self._init_pbar_tqdm()
520
+ case 'native':
521
+ self._init_pbar_native()
522
+ case _:
523
+ raise NotImplementedError(f'Invalid pbar backend {pbar_backend}')
524
+
416
525
  return super().__iter__()
417
526
 
418
527
  def __next__(self) -> MarketData:
419
528
  try:
420
529
  result = super().__next__()
421
- if self._pbar:
422
- self._pbar.update(self.progress)
423
- self._pbar.refresh()
530
+ if self._pbar is not None:
531
+ self._update_pbar_progress()
424
532
  return result
425
533
  except StopIteration:
426
534
  if self._pbar is not None:
427
- self._pbar.close()
428
- self._pbar = None
535
+ pbar_backend = self.pbar_config['backend']
536
+ match pbar_backend:
537
+ case 'tqdm':
538
+ self._close_pbar_tqdm()
539
+ case 'native':
540
+ self._close_pbar_native()
541
+ case _:
542
+ raise NotImplementedError(f'Invalid pbar backend {pbar_backend}')
429
543
  raise
430
544
 
431
- def _update_progress_bar(self, market_date: datetime.date):
432
- if self._pbar:
433
- self._pbar.set_description(f'Replay {market_date:%Y-%m-%d} ({self._idx_date + 1} / {len(self._calendar)})')
434
- self._pbar.refresh()
435
-
436
545
 
437
546
  class ProgressiveReplay(SimpleReplay):
438
547
  """
@@ -5,6 +5,8 @@ import pathlib
5
5
  from .telemetrics import LOGGER
6
6
  from ..profile import PROFILE
7
7
 
8
+ USE_CYTHON = True
9
+
8
10
 
9
11
  def set_logger(logger: logging.Logger):
10
12
  global LOGGER
@@ -15,6 +17,9 @@ def set_logger(logger: logging.Logger):
15
17
 
16
18
 
17
19
  def check_cython_module(cython_module) -> bool:
20
+ if not USE_CYTHON:
21
+ return False
22
+
18
23
  for name in cython_module:
19
24
  cython_ext = '.pyd' if os.name == 'nt' else '.so'
20
25
  for file in pathlib.Path(__file__).parent.glob(f'*{cython_ext}'):
@@ -19,6 +19,9 @@ class MarketData:
19
19
 
20
20
  def __init__(self, ticker: str, timestamp: float, **kwargs: Any) -> None: ...
21
21
 
22
+ def update(self, name: str, value: Any) -> None:
23
+ ...
24
+
22
25
  @classmethod
23
26
  def buffer_size(cls) -> int: ...
24
27
 
@@ -31,7 +31,7 @@ class MarketDataBuffer:
31
31
 
32
32
  def to_bytes(self) -> bytes: ...
33
33
 
34
- def update(self, dtype: int, **kwargs: dict[str, Any]) -> None: ...
34
+ def update(self, dtype: int, **kwargs: Any) -> None: ...
35
35
 
36
36
  def __getitem__(self, idx: int) -> MarketData | TransactionData | OrderData | TickDataLite | TickData | BarData: ...
37
37
 
@@ -79,6 +79,10 @@ class MarketDataConcurrentBuffer:
79
79
  capacity: int = ...
80
80
  ) -> None: ...
81
81
 
82
+ def get_head(self, worker_id: int) -> int: ...
83
+
84
+ def min_head(self) -> int: ...
85
+
82
86
  def is_empty(self, worker_id: int) -> bool: ...
83
87
 
84
88
  def is_empty_all(self) -> bool: ...
@@ -89,4 +93,10 @@ class MarketDataConcurrentBuffer:
89
93
 
90
94
  def get(self, idx: int) -> MarketData | TransactionData | OrderData | TickDataLite | TickData | BarData: ...
91
95
 
92
- def listen(self, worker_id: int, timeout: float = ...) -> MarketData | TransactionData | OrderData | TickDataLite | TickData | BarData: ...
96
+ def listen(self, worker_id: int, block: bool = True, timeout: float = ...) -> MarketData | TransactionData | OrderData | TickDataLite | TickData | BarData: ...
97
+
98
+ @property
99
+ def head(self) -> list[int]: ...
100
+
101
+ @property
102
+ def tail(self) -> int: ...