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,179 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TradeMetrics(object):
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.trades = {}
|
|
10
|
+
self.trade_batch = []
|
|
11
|
+
|
|
12
|
+
self.exposure = 0.
|
|
13
|
+
self.total_pnl = 0.
|
|
14
|
+
self.total_cash_flow = 0.
|
|
15
|
+
|
|
16
|
+
self.current_pnl = 0.
|
|
17
|
+
self.current_cash_flow = 0.
|
|
18
|
+
self.current_trade_batch = {'cash_flow': 0., 'pnl': 0., 'turnover': 0., 'trades': []}
|
|
19
|
+
self.market_price = None
|
|
20
|
+
|
|
21
|
+
def update(self, market_price: float):
|
|
22
|
+
self.market_price = market_price
|
|
23
|
+
self.total_pnl = self.exposure * market_price + self.total_cash_flow
|
|
24
|
+
self.current_pnl = self.exposure * market_price + self.current_cash_flow
|
|
25
|
+
self.current_trade_batch['pnl'] = self.exposure * market_price + self.current_trade_batch['cash_flow']
|
|
26
|
+
|
|
27
|
+
def add_trades(self, side: int, price: float, timestamp: float, volume: float = None, trade_id: int | str = None):
|
|
28
|
+
assert side in {1, -1}, f"trade side must in {1, -1}, got {side}."
|
|
29
|
+
assert volume is None or volume >= 0, "volume must be positive."
|
|
30
|
+
|
|
31
|
+
if volume is None:
|
|
32
|
+
if self.exposure * side < 0:
|
|
33
|
+
volume = abs(self.exposure)
|
|
34
|
+
elif self.exposure * side > 0:
|
|
35
|
+
volume = 0.
|
|
36
|
+
else:
|
|
37
|
+
volume = 1.
|
|
38
|
+
|
|
39
|
+
if trade_id is None:
|
|
40
|
+
trade_id = uuid.uuid4().int
|
|
41
|
+
elif trade_id in self.trades:
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
# split the trades
|
|
45
|
+
if (target_exposure := self.exposure + volume * side) * self.exposure < 0:
|
|
46
|
+
self.add_trades(side=side, volume=abs(self.exposure), price=price, timestamp=timestamp, trade_id=f'{trade_id}.0')
|
|
47
|
+
volume = volume - abs(self.exposure)
|
|
48
|
+
trade_id = f'{trade_id}.1'
|
|
49
|
+
|
|
50
|
+
self.exposure += volume * side
|
|
51
|
+
self.total_cash_flow -= volume * side * price
|
|
52
|
+
self.total_pnl = self.exposure * price + self.total_cash_flow
|
|
53
|
+
self.current_cash_flow -= volume * side * price
|
|
54
|
+
self.current_pnl = self.exposure * price + self.current_cash_flow
|
|
55
|
+
self.market_price = price
|
|
56
|
+
|
|
57
|
+
self.trades[trade_id] = trade_log = dict(
|
|
58
|
+
side=side,
|
|
59
|
+
volume=volume,
|
|
60
|
+
timestamp=timestamp,
|
|
61
|
+
price=price,
|
|
62
|
+
exposure=self.exposure,
|
|
63
|
+
cash_flow=self.current_cash_flow,
|
|
64
|
+
pnl=self.current_pnl
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if 'init_side' not in self.current_trade_batch:
|
|
68
|
+
self.current_trade_batch['init_side'] = side
|
|
69
|
+
|
|
70
|
+
self.current_trade_batch['cash_flow'] -= volume * side * price
|
|
71
|
+
self.current_trade_batch['pnl'] = self.exposure * price + self.current_trade_batch['cash_flow']
|
|
72
|
+
self.current_trade_batch['turnover'] += abs(volume) * price
|
|
73
|
+
self.current_trade_batch['trades'].append(trade_log)
|
|
74
|
+
|
|
75
|
+
if not self.exposure:
|
|
76
|
+
self.trade_batch.append(self.current_trade_batch)
|
|
77
|
+
self.current_trade_batch = {'cash_flow': 0., 'pnl': 0., 'turnover': 0., 'trades': []}
|
|
78
|
+
self.current_pnl = self.current_cash_flow = 0.
|
|
79
|
+
|
|
80
|
+
def add_trades_batch(self, trade_logs: pd.DataFrame):
|
|
81
|
+
for timestamp, row in trade_logs.iterrows(): # type: float, dict
|
|
82
|
+
side = row['side']
|
|
83
|
+
price = row['current_price']
|
|
84
|
+
volume = row['signal']
|
|
85
|
+
self.add_trades(side=side, volume=volume, price=price, timestamp=timestamp)
|
|
86
|
+
|
|
87
|
+
def clear(self):
|
|
88
|
+
self.trades.clear()
|
|
89
|
+
self.trade_batch.clear()
|
|
90
|
+
|
|
91
|
+
self.exposure = 0.
|
|
92
|
+
self.total_pnl = 0.
|
|
93
|
+
self.total_cash_flow = 0.
|
|
94
|
+
|
|
95
|
+
self.current_pnl = 0.
|
|
96
|
+
self.current_cash_flow = 0.
|
|
97
|
+
self.current_trade_batch = {'cash_flow': 0., 'pnl': 0., 'turnover': 0., 'trades': []}
|
|
98
|
+
self.market_price = None
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def summary(self):
|
|
102
|
+
info_dict = dict(
|
|
103
|
+
total_gain=0.,
|
|
104
|
+
total_loss=0.,
|
|
105
|
+
trade_count=0,
|
|
106
|
+
win_count=0,
|
|
107
|
+
lose_count=0,
|
|
108
|
+
turnover=0.,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
for trade_batch in self.trade_batch:
|
|
112
|
+
if trade_batch['pnl'] > 0:
|
|
113
|
+
info_dict['total_gain'] += trade_batch['pnl']
|
|
114
|
+
info_dict['trade_count'] += 1
|
|
115
|
+
info_dict['win_count'] += 1
|
|
116
|
+
info_dict['turnover'] += trade_batch['turnover']
|
|
117
|
+
else:
|
|
118
|
+
info_dict['total_loss'] += trade_batch['pnl']
|
|
119
|
+
info_dict['trade_count'] += 1
|
|
120
|
+
info_dict['lose_count'] += 1
|
|
121
|
+
info_dict['turnover'] += trade_batch['turnover']
|
|
122
|
+
|
|
123
|
+
info_dict['win_rate'] = info_dict['win_count'] / info_dict['trade_count'] if info_dict['trade_count'] else 0.
|
|
124
|
+
info_dict['average_gain'] = info_dict['total_gain'] / info_dict['win_count'] / self.market_price if info_dict['win_count'] else 0.
|
|
125
|
+
info_dict['average_loss'] = info_dict['total_loss'] / info_dict['lose_count'] / self.market_price if info_dict['lose_count'] else 0.
|
|
126
|
+
info_dict['gain_loss_ratio'] = -info_dict['average_gain'] / info_dict['average_loss'] if info_dict['average_loss'] else 1.
|
|
127
|
+
info_dict['long_avg_pnl'] = np.average([_['pnl'] for _ in long_trades]) / self.market_price if (long_trades := [_ for _ in self.trade_batch if _['init_side'] == 1]) else np.nan
|
|
128
|
+
info_dict['short_avg_pnl'] = np.average([_['pnl'] for _ in short_trades]) / self.market_price if (short_trades := [_ for _ in self.trade_batch if _['init_side'] == -1]) else np.nan
|
|
129
|
+
info_dict['ttl_pnl.no_leverage'] = np.sum([trade_batch['pnl'] for trade_batch in self.trade_batch])
|
|
130
|
+
info_dict['net_pnl.optimistic'] = info_dict['ttl_pnl.no_leverage'] - (0.00034 + 0.000023) / 2 * info_dict['turnover']
|
|
131
|
+
|
|
132
|
+
return info_dict
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def info(self):
|
|
136
|
+
trade_info = []
|
|
137
|
+
trade_index = []
|
|
138
|
+
for batch_id, trade_batch in enumerate(self.trade_batch):
|
|
139
|
+
for trade_id, trade_dict in enumerate(trade_batch['trades']):
|
|
140
|
+
trade_info.append(
|
|
141
|
+
dict(
|
|
142
|
+
timestamp=trade_dict['timestamp'],
|
|
143
|
+
side=trade_dict['side'],
|
|
144
|
+
volume=trade_dict['volume'],
|
|
145
|
+
price=trade_dict['price'],
|
|
146
|
+
exposure=trade_dict['exposure'],
|
|
147
|
+
pnl=trade_dict['pnl']
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
trade_index.append((f'batch.{batch_id}', f'trade.{trade_id}'))
|
|
151
|
+
|
|
152
|
+
df = pd.DataFrame(trade_info, index=trade_index)
|
|
153
|
+
return df
|
|
154
|
+
|
|
155
|
+
def to_string(self) -> str:
|
|
156
|
+
metric_info = self.summary
|
|
157
|
+
|
|
158
|
+
fmt_dict = {
|
|
159
|
+
'total_gain': f'{metric_info["total_gain"]:,.3f}',
|
|
160
|
+
'total_loss': f'{metric_info["total_loss"]:,.3f}',
|
|
161
|
+
'trade_count': f'{metric_info["trade_count"]:,}',
|
|
162
|
+
'win_count': f'{metric_info["win_count"]:,}',
|
|
163
|
+
'lose_count': f'{metric_info["lose_count"]:,}',
|
|
164
|
+
'turnover': f'{metric_info["turnover"]:,.3f}',
|
|
165
|
+
'win_rate': f'{metric_info["win_rate"]:.2%}',
|
|
166
|
+
'average_gain': f'{metric_info["average_gain"]:,.4%}',
|
|
167
|
+
'average_loss': f'{metric_info["average_loss"]:,.4%}',
|
|
168
|
+
'long_avg_pnl': f'{metric_info["long_avg_pnl"]:,.4%}',
|
|
169
|
+
'short_avg_pnl': f'{metric_info["short_avg_pnl"]:,.4%}',
|
|
170
|
+
'gain_loss_ratio': f'{metric_info["gain_loss_ratio"]:,.3%}'
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
info_str = (f'Trade Metrics Report:'
|
|
174
|
+
f'\n'
|
|
175
|
+
f'{pd.Series(fmt_dict).to_string()}'
|
|
176
|
+
f'\n'
|
|
177
|
+
f'{self.info.to_string()}')
|
|
178
|
+
|
|
179
|
+
return info_str
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import datetime
|
|
3
|
+
import inspect
|
|
4
|
+
import operator
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
from . import LOGGER
|
|
8
|
+
from ..base import Progress, TickData, TransactionData, TradeData, OrderBook, MarketData
|
|
9
|
+
|
|
10
|
+
LOGGER = LOGGER.getChild('Replay')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Replay(object, metaclass=abc.ABCMeta):
|
|
14
|
+
@abc.abstractmethod
|
|
15
|
+
def __next__(self): ...
|
|
16
|
+
|
|
17
|
+
@abc.abstractmethod
|
|
18
|
+
def __iter__(self): ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SimpleReplay(Replay):
|
|
22
|
+
def __init__(self, **kwargs):
|
|
23
|
+
self.eod = kwargs.pop('eod', None)
|
|
24
|
+
self.bod = kwargs.pop('bod', None)
|
|
25
|
+
|
|
26
|
+
self.replay_task = []
|
|
27
|
+
self.task_progress = 0
|
|
28
|
+
self.task_date = None
|
|
29
|
+
self.progress = Progress(tasks=1, **kwargs)
|
|
30
|
+
|
|
31
|
+
def load(self, data):
|
|
32
|
+
if isinstance(data, dict):
|
|
33
|
+
self.replay_task.extend(list(data.values()))
|
|
34
|
+
else:
|
|
35
|
+
self.replay_task.extend(data)
|
|
36
|
+
|
|
37
|
+
def reset(self):
|
|
38
|
+
self.replay_task.clear()
|
|
39
|
+
self.task_progress = 0
|
|
40
|
+
self.task_date = None
|
|
41
|
+
self.progress.reset()
|
|
42
|
+
|
|
43
|
+
def next_task(self):
|
|
44
|
+
if self.task_progress < len(self.replay_task):
|
|
45
|
+
market_data = self.replay_task[self.task_progress]
|
|
46
|
+
market_time = market_data.market_time
|
|
47
|
+
|
|
48
|
+
if isinstance(market_time, datetime.datetime):
|
|
49
|
+
market_date = market_time.date()
|
|
50
|
+
else:
|
|
51
|
+
market_date = market_time
|
|
52
|
+
|
|
53
|
+
if market_date != self.task_date:
|
|
54
|
+
if callable(self.eod) and self.task_date:
|
|
55
|
+
self.eod(self.task_date)
|
|
56
|
+
|
|
57
|
+
self.task_date = market_date
|
|
58
|
+
self.progress.prompt = f'Replay {market_date:%Y-%m-%d}:'
|
|
59
|
+
|
|
60
|
+
if callable(self.bod):
|
|
61
|
+
self.bod(market_date)
|
|
62
|
+
|
|
63
|
+
self.progress.done_tasks = self.task_progress / len(self.replay_task)
|
|
64
|
+
|
|
65
|
+
if (not self.progress.tick_size) or self.progress.progress >= self.progress.tick_size + self.progress.last_output:
|
|
66
|
+
self.progress.output()
|
|
67
|
+
|
|
68
|
+
self.task_progress += 1
|
|
69
|
+
else:
|
|
70
|
+
raise StopIteration()
|
|
71
|
+
|
|
72
|
+
return market_data
|
|
73
|
+
|
|
74
|
+
def __next__(self):
|
|
75
|
+
try:
|
|
76
|
+
return self.next_task()
|
|
77
|
+
except StopIteration:
|
|
78
|
+
if not self.progress.is_done:
|
|
79
|
+
self.progress.done_tasks = 1
|
|
80
|
+
self.progress.output()
|
|
81
|
+
|
|
82
|
+
self.reset()
|
|
83
|
+
raise StopIteration()
|
|
84
|
+
|
|
85
|
+
def __iter__(self):
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ProgressiveReplay(Replay):
|
|
90
|
+
"""
|
|
91
|
+
progressively loading and replaying market data
|
|
92
|
+
|
|
93
|
+
requires arguments
|
|
94
|
+
loader: a data loading function. Expect loader = Callable(market_date: datetime.date, ticker: str, dtype: str| type) -> dict[any, MarketData]
|
|
95
|
+
start_date & end_date: the given replay period
|
|
96
|
+
or calendar: the given replay calendar.
|
|
97
|
+
|
|
98
|
+
accepts kwargs:
|
|
99
|
+
ticker / tickers: the given symbols to replay, expect a str| list[str]
|
|
100
|
+
dtype / dtypes: the given dtype(s) of symbol to replay, expect a str | type, list[str | type]. default = all, which is (TradeData, TickData, OrderBook)
|
|
101
|
+
subscription / subscribe: the given ticker-dtype pair to replay, expect a list[dict[str, str | type]]
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
loader,
|
|
107
|
+
**kwargs
|
|
108
|
+
):
|
|
109
|
+
self.loader = loader
|
|
110
|
+
self.start_date: datetime.date | None = kwargs.pop('start_date', None)
|
|
111
|
+
self.end_date: datetime.date | None = kwargs.pop('end_date', None)
|
|
112
|
+
self.calendar: list[datetime.date] | None = kwargs.pop('calendar', None)
|
|
113
|
+
|
|
114
|
+
self.eod = kwargs.pop('eod', None)
|
|
115
|
+
self.bod = kwargs.pop('bod', None)
|
|
116
|
+
|
|
117
|
+
self.replay_subscription = {}
|
|
118
|
+
self.replay_calendar = []
|
|
119
|
+
self.replay_task = []
|
|
120
|
+
|
|
121
|
+
self.date_progress = 0
|
|
122
|
+
self.task_progress = 0
|
|
123
|
+
self.progress = Progress(tasks=1, **kwargs)
|
|
124
|
+
|
|
125
|
+
tickers: list[str] = kwargs.pop('ticker', kwargs.pop('tickers', []))
|
|
126
|
+
dtypes: list[str | type] = kwargs.pop('dtype', kwargs.pop('dtypes', [TradeData, TransactionData, OrderBook, TickData]))
|
|
127
|
+
|
|
128
|
+
if not all([arg_name in inspect.getfullargspec(loader).args for arg_name in ['market_date', 'ticker', 'dtype']]):
|
|
129
|
+
raise TypeError('loader function has 3 requires args, market_date, ticker and dtype.')
|
|
130
|
+
|
|
131
|
+
if isinstance(tickers, str):
|
|
132
|
+
tickers = [tickers]
|
|
133
|
+
elif isinstance(tickers, Iterable):
|
|
134
|
+
tickers = list(tickers)
|
|
135
|
+
else:
|
|
136
|
+
raise TypeError(f'Invalid ticker {tickers}, expect str or list[str]')
|
|
137
|
+
|
|
138
|
+
if isinstance(dtypes, str) or inspect.isclass(dtypes):
|
|
139
|
+
dtypes = [dtypes]
|
|
140
|
+
elif isinstance(dtypes, Iterable):
|
|
141
|
+
dtypes = list(dtypes)
|
|
142
|
+
else:
|
|
143
|
+
raise TypeError(f'Invalid dtype {dtypes}, expect str or list[str]')
|
|
144
|
+
|
|
145
|
+
for ticker in tickers:
|
|
146
|
+
for dtype in dtypes:
|
|
147
|
+
self.add_subscription(ticker=ticker, dtype=dtype)
|
|
148
|
+
|
|
149
|
+
subscription = kwargs.pop('subscription', kwargs.pop('subscribe', []))
|
|
150
|
+
|
|
151
|
+
if isinstance(subscription, dict):
|
|
152
|
+
subscription = [subscription]
|
|
153
|
+
|
|
154
|
+
for sub in subscription:
|
|
155
|
+
self.add_subscription(**sub)
|
|
156
|
+
|
|
157
|
+
self.reset()
|
|
158
|
+
|
|
159
|
+
def add_subscription(self, ticker: str, dtype: type | str):
|
|
160
|
+
if isinstance(dtype, str):
|
|
161
|
+
pass
|
|
162
|
+
elif inspect.isclass(dtype):
|
|
163
|
+
dtype = dtype.__name__
|
|
164
|
+
else:
|
|
165
|
+
raise ValueError(f'Invalid dtype {dtype}, expect str or class.')
|
|
166
|
+
|
|
167
|
+
topic = f'{ticker}.{dtype}'
|
|
168
|
+
self.replay_subscription[topic] = (ticker, dtype)
|
|
169
|
+
|
|
170
|
+
def remove_subscription(self, ticker: str, dtype: type | str):
|
|
171
|
+
if isinstance(dtype, str):
|
|
172
|
+
pass
|
|
173
|
+
else:
|
|
174
|
+
dtype = dtype.__name__
|
|
175
|
+
|
|
176
|
+
topic = f'{ticker}.{dtype}'
|
|
177
|
+
self.replay_subscription.pop(topic, None)
|
|
178
|
+
|
|
179
|
+
def reset(self):
|
|
180
|
+
if self.calendar is None:
|
|
181
|
+
self.replay_calendar = [self.start_date + datetime.timedelta(days=i) for i in range((self.end_date - self.start_date).days + 1)]
|
|
182
|
+
else:
|
|
183
|
+
self.replay_calendar = self.calendar
|
|
184
|
+
|
|
185
|
+
self.task_progress = 0
|
|
186
|
+
self.date_progress = sum([1 for _ in self.replay_calendar if _ < self.start_date])
|
|
187
|
+
self.progress.reset()
|
|
188
|
+
|
|
189
|
+
if self.date_progress:
|
|
190
|
+
self.progress.done_tasks = self.date_progress / len(self.replay_calendar)
|
|
191
|
+
|
|
192
|
+
def next_trade_day(self):
|
|
193
|
+
if self.date_progress < len(self.replay_calendar):
|
|
194
|
+
market_date = self.replay_calendar[self.date_progress]
|
|
195
|
+
self.progress.prompt = f'Replay {market_date:%Y-%m-%d} ({self.date_progress + 1} / {len(self.replay_calendar)}):'
|
|
196
|
+
for topic in self.replay_subscription:
|
|
197
|
+
ticker, dtype = self.replay_subscription[topic]
|
|
198
|
+
LOGGER.info(f'{self} loading {market_date} {ticker} {dtype}')
|
|
199
|
+
data = self.loader(market_date=market_date, ticker=ticker, dtype=dtype)
|
|
200
|
+
if isinstance(data, dict):
|
|
201
|
+
self.replay_task.extend(list(data.values()))
|
|
202
|
+
elif isinstance(data, (list, tuple)):
|
|
203
|
+
self.replay_task.extend(data)
|
|
204
|
+
|
|
205
|
+
LOGGER.info(f'{market_date} data loaded! {len(self.replay_task):,} entries.')
|
|
206
|
+
self.date_progress += 1
|
|
207
|
+
else:
|
|
208
|
+
raise StopIteration()
|
|
209
|
+
|
|
210
|
+
self.replay_task.sort(key=operator.attrgetter('timestamp', 'ticker', '__class__.__name__'))
|
|
211
|
+
|
|
212
|
+
def next_task(self):
|
|
213
|
+
if self.task_progress < len(self.replay_task):
|
|
214
|
+
data = self.replay_task[self.task_progress]
|
|
215
|
+
self.task_progress += 1
|
|
216
|
+
else:
|
|
217
|
+
if self.eod is not None and self.date_progress and (self.date_progress >= len(self.replay_calendar) or self.replay_calendar[self.date_progress] > self.start_date):
|
|
218
|
+
self.eod(market_date=self.replay_calendar[self.date_progress - 1], replay=self)
|
|
219
|
+
|
|
220
|
+
self.replay_task.clear()
|
|
221
|
+
self.task_progress = 0
|
|
222
|
+
|
|
223
|
+
if self.bod is not None and self.date_progress < len(self.replay_calendar):
|
|
224
|
+
self.bod(market_date=self.replay_calendar[self.date_progress], replay=self)
|
|
225
|
+
|
|
226
|
+
self.next_trade_day()
|
|
227
|
+
|
|
228
|
+
# the bod process should be moved here!
|
|
229
|
+
|
|
230
|
+
data = self.next_task()
|
|
231
|
+
|
|
232
|
+
if self.replay_task and self.replay_calendar:
|
|
233
|
+
current_progress = (self.date_progress - 1 + (self.task_progress / len(self.replay_task))) / len(self.replay_calendar)
|
|
234
|
+
self.progress.done_tasks = current_progress
|
|
235
|
+
else:
|
|
236
|
+
self.progress.done_tasks = 1
|
|
237
|
+
|
|
238
|
+
if (not self.progress.tick_size) \
|
|
239
|
+
or self.progress.progress >= self.progress.tick_size + self.progress.last_output \
|
|
240
|
+
or self.progress.is_done:
|
|
241
|
+
self.progress.output()
|
|
242
|
+
|
|
243
|
+
return data
|
|
244
|
+
|
|
245
|
+
def __next__(self) -> MarketData:
|
|
246
|
+
try:
|
|
247
|
+
return self.next_task()
|
|
248
|
+
except StopIteration:
|
|
249
|
+
if not self.progress.is_done:
|
|
250
|
+
self.progress.done_tasks = 1
|
|
251
|
+
self.progress.output()
|
|
252
|
+
|
|
253
|
+
self.reset()
|
|
254
|
+
raise StopIteration()
|
|
255
|
+
|
|
256
|
+
def __iter__(self):
|
|
257
|
+
self.reset()
|
|
258
|
+
return self
|
|
259
|
+
|
|
260
|
+
def __repr__(self):
|
|
261
|
+
return f'{self.__class__.__name__}{{id={id(self)}, from={self.start_date}, to={self.end_date}}}'
|