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