hyperquant 0.1.0__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.
hyperquant/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .core import *
2
+ from .draw import *
3
+ from .logkit import *
4
+
5
+ __version__ = "0.1.0"
hyperquant/core.py ADDED
@@ -0,0 +1,456 @@
1
+ # %%
2
+ import numpy as np
3
+ import pandas as pd
4
+
5
+ from draw import draw
6
+
7
+
8
+ class ExchangeBase:
9
+ def __init__(self, initial_balance=10000, recorded=False):
10
+ self.initial_balance = initial_balance # 初始的资产
11
+ self.recorded = recorded # 是否记录历史
12
+ self.opt = {
13
+ 'trades': [],
14
+ 'history': [] # 集成 history 到 opt 中
15
+ }
16
+ self.account = {'USDT': {'realised_profit': 0, 'unrealised_profit': 0, 'total': initial_balance,
17
+ 'fee': 0, 'leverage': 0, 'hold': 0, 'long': 0, 'short': 0}}
18
+
19
+ def record_history(self, time):
20
+ """记录当前总资产和时间到 history 中"""
21
+ self.opt['history'].append({
22
+ 'date': time,
23
+ 'total': self.account['USDT']['total']
24
+ })
25
+
26
+ def __getitem__(self, symbol):
27
+ return self.account.get(symbol, None)
28
+
29
+ def __setitem__(self, symbol, value):
30
+ self.account[symbol] = value
31
+
32
+ @property
33
+ def activate_symbols(self):
34
+ return [symbol for symbol in self.trade_symbols if self.account[symbol]['amount'] != 0]
35
+
36
+ @property
37
+ def total(self):
38
+ return self.account['USDT']['total']
39
+
40
+ @property
41
+ def leverage(self):
42
+ return self.account['USDT']['leverage']
43
+
44
+ @property
45
+ def realised_profit(self):
46
+ return self.account['USDT']['realised_profit']
47
+
48
+ @property
49
+ def unrealised_profit(self):
50
+ return self.account['USDT']['unrealised_profit']
51
+
52
+ @property
53
+ def history(self):
54
+ if not self.recorded:
55
+ raise ValueError("History is only available in recorded mode.")
56
+ return self.opt['history']
57
+
58
+ @property
59
+ def available_margin(self):
60
+ return self.account['USDT']['total'] - self.account['USDT']['hold']
61
+
62
+ @property
63
+ def realised_profit(self):
64
+ return self.account['USDT']['realised_profit']
65
+
66
+ @property
67
+ def trades(self):
68
+ return self.opt['trades']
69
+
70
+ @property
71
+ def stats(self):
72
+ if not self.recorded:
73
+ raise ValueError("Stats are only available in recorded mode.")
74
+
75
+ if not self.opt['history']:
76
+ return {
77
+ '初始资产': f'{self.initial_balance:.2f} USDT',
78
+ '当前资产': f'{self.account["USDT"]["total"]:.2f} USDT',
79
+ '已实现利润': f'{self.account["USDT"]["realised_profit"]:.2f} USDT',
80
+ '未实现利润': f'{self.account["USDT"]["unrealised_profit"]:.2f} USDT',
81
+ '总手续费': f'{self.account["USDT"]["fee"]:.2f} USDT',
82
+ '杯杆率': f'{self.account["USDT"]["leverage"]:.2f}',
83
+ '活跃交易对数量': len(self.activate_symbols),
84
+ '持仓价值': f'{self.account["USDT"]["hold"]:.2f} USDT',
85
+ '多头持仓价值': f'{self.account["USDT"]["long"]:.2f} USDT',
86
+ '空头持仓价值': f'{self.account["USDT"]["short"]:.2f} USDT',
87
+ '总交易笔数': 0,
88
+ '胜率': '0.00%',
89
+ '年化收益率': '0.00%',
90
+ '最大回撤时间范围': 'N/A',
91
+ '最大回撤': '0.00%',
92
+ '夏普比率': '0.00'
93
+ }
94
+
95
+ # 创建一个账户历史的DataFrame
96
+ history_df = pd.DataFrame(self.opt['history'])
97
+ history_df = history_df.sort_values(by='date')
98
+ history_df = history_df.drop_duplicates(subset='date')
99
+ history_df = history_df.set_index('date')
100
+
101
+ # 计算累计收益
102
+ history_df['max2here'] = history_df['total'].expanding().max()
103
+ history_df['dd2here'] = history_df['total'] / history_df['max2here'] - 1
104
+ drwa_down_df = history_df.sort_values(by=['dd2here'])
105
+ drwa_down_df = drwa_down_df[drwa_down_df['dd2here'] < 0]
106
+ if drwa_down_df.empty:
107
+ start_date = np.nan
108
+ end_data = np.nan
109
+ max_draw_down = 0
110
+ else:
111
+ max_draw_down = drwa_down_df.iloc[0]['dd2here']
112
+ end_data = drwa_down_df.iloc[0].name
113
+ start_date = history_df[history_df.index <= end_data].sort_values(by='total', ascending=False).iloc[0].name
114
+
115
+ # 计算胜率
116
+ total_trades = len(self.opt['trades'])
117
+ if total_trades == 0:
118
+ win_rate = 0
119
+ else:
120
+ winning_trades = sum(1 for trade in self.opt['trades'] if trade['pos'] > 0)
121
+ losing_trades = sum(1 for trade in self.opt['trades'] if trade['pos'] < 0)
122
+ win_rate = winning_trades / (winning_trades + losing_trades) if (winning_trades + losing_trades) > 0 else 0
123
+
124
+ # 计算年化收益率
125
+ if len(history_df) < 2:
126
+ annual_return = 0
127
+ else:
128
+ start_date_for_return = history_df.index[0]
129
+ end_date_for_return = history_df.index[-1]
130
+ total_days = (end_date_for_return - start_date_for_return).days
131
+ if total_days > 0:
132
+ annual_return = ((history_df['total'].iloc[-1] / self.initial_balance) ** (365 / total_days) - 1)
133
+ else:
134
+ annual_return = 0
135
+
136
+ # 计算夏普比率
137
+ # 计算每日收益率
138
+ daily_history = history_df['total'].resample('D').ffill().dropna()
139
+ daily_returns = daily_history.pct_change().dropna()
140
+ if len(daily_returns) > 1:
141
+ risk_free_rate = 0.03 / 365
142
+ sharpe_ratio = (daily_returns.mean() - risk_free_rate) / daily_returns.std() * np.sqrt(365)
143
+ else:
144
+ sharpe_ratio = 0
145
+
146
+ stats = {
147
+ '初始资产': f'{self.initial_balance:.2f} USDT',
148
+ '当前资产': f'{self.account["USDT"]["total"]:.2f} USDT',
149
+ '已实现利润': f'{self.account["USDT"]["realised_profit"]:.2f} USDT',
150
+ '未实现利润': f'{self.account["USDT"]["unrealised_profit"]:.2f} USDT',
151
+ '总手续费': f'{self.account["USDT"]["fee"]:.2f} USDT',
152
+ '活跃交易对数量': len(self.activate_symbols),
153
+ '持仓价值': f'{self.account["USDT"]["hold"]:.2f} USDT',
154
+ '多头持仓价值': f'{self.account["USDT"]["long"]:.2f} USDT',
155
+ '空头持仓价值': f'{self.account["USDT"]["short"]:.2f} USDT',
156
+ '总交易笔数': total_trades,
157
+ '胜率': f'{win_rate:.2%}',
158
+ '年化收益率': f'{annual_return:.2%}',
159
+ '最大回撤时间范围': (start_date,end_data),
160
+ '最大回撤': f'{max_draw_down:.2%}',
161
+ '夏普比率': f'{sharpe_ratio:.2f}'
162
+ }
163
+ return stats
164
+
165
+ def draw(self, data_df: pd.DataFrame, title: str, indicators: list, show_kline=True, show_total=True, show_base=False):
166
+ """
167
+ :param data_df: 数据 DataFrame
168
+ :param title: 图表标题
169
+ :param indicators: 画图指标 [[('指标名', '指标类型'), ('指标名', '指标类型')], [('指标名', '指标类型')]]
170
+ :param show_kline: 是否显示K线图
171
+ :param show_total: 是否显示总资产曲线
172
+ """
173
+
174
+ # 将 self.history 转换为 DataFrame
175
+ history_df = pd.DataFrame(self.opt['history'])
176
+
177
+ # 按照 'date' 分组,并保留每组的最后一条记录
178
+ history_df = history_df.sort_values('date').groupby('date', as_index=False).last()
179
+
180
+ # 按照 'date' 将 history_df 和 data_df 合并
181
+ data_df = pd.merge(data_df, history_df, on='date', how='left')
182
+
183
+ # 使用前向填充处理 'total' 列的缺失值
184
+ data_df['total'] = data_df['total'].ffill()
185
+
186
+ data_dict = []
187
+ if show_kline:
188
+ # 如果signal列存在,将signal列的值赋值给signal列
189
+ opt = {
190
+ "series_name": "K",
191
+ "draw_type": "Kline",
192
+ "col": ["open", "close", "low", "high"],
193
+ "height": 50,
194
+ }
195
+ if 'signal' in data_df.columns:
196
+ opt['trade_single'] = 'signal'
197
+ data_dict.append(opt)
198
+
199
+
200
+ if indicators:
201
+ for ind in indicators:
202
+ ind_data = {}
203
+ for i, indicator in enumerate(ind):
204
+ if i == 0:
205
+ ind_data = {
206
+ "series_name": indicator[0],
207
+ "draw_type": indicator[1],
208
+ "height": 0,
209
+ }
210
+ else:
211
+ if 'sub_chart' not in ind_data:
212
+ ind_data['sub_chart'] = []
213
+ ind_data['sub_chart'].append(
214
+ {"series_name": indicator[0], "draw_type": indicator[1]}
215
+ )
216
+ data_dict.append(ind_data)
217
+
218
+ if show_total:
219
+ # 绘制基准收益曲线
220
+ total_dict = {
221
+ "series_name": "total",
222
+ "draw_type": "Line",
223
+ "height": 0,
224
+ }
225
+ if show_base:
226
+ data_df.loc[:, "base"] = (data_df["close"] / data_df["close"].iloc[0]) * self.initial_balance
227
+ total_dict['sub_chart'] = [
228
+ {"series_name": "base", "draw_type": "Line"},
229
+ ]
230
+ data_dict.append(total_dict)
231
+
232
+
233
+ sub_width = 40 / (len(data_dict) - 1)
234
+ for d in data_dict:
235
+ if d['draw_type'] != "Kline":
236
+ d['height'] = sub_width
237
+
238
+ draw(
239
+ data_df,
240
+ data_dict=data_dict,
241
+ date_col="date",
242
+ date_formate="%Y-%m-%d %H:%M:%S",
243
+ title=title,
244
+ height_type="%",
245
+ auto_play_space="""
246
+ function auto_play_space(xi){
247
+ return 200;
248
+ }""",
249
+ show=True,
250
+ display_js="""
251
+ // 设置 dataZoom 为最大范围
252
+ window.onload = function() {
253
+ var isSettingZoom = false;
254
+
255
+ // 获取 x 轴的数据
256
+ var xData = chart_option.xAxis[0].data;
257
+ if (xData.length > 0) {
258
+ var startValue = xData[0];
259
+ var endValue = xData[xData.length - 1];
260
+ isSettingZoom = true;
261
+ chart_ins.dispatchAction({
262
+ type: 'dataZoom',
263
+ startValue: startValue,
264
+ endValue: endValue
265
+ });
266
+ isSettingZoom = false;
267
+ }
268
+ }
269
+ """,
270
+ )
271
+
272
+ class Exchange(ExchangeBase):
273
+ def __init__(self, trade_symbols, fee=0.0002, initial_balance=10000, recorded=False):
274
+ super().__init__(initial_balance=initial_balance, recorded=recorded)
275
+ self.fee = fee
276
+ self.trade_symbols = trade_symbols
277
+ self.id_gen = 0
278
+ self.account['USDT'].update({
279
+ 'hold': 0,
280
+ 'long': 0,
281
+ 'short': 0
282
+ })
283
+ for symbol in trade_symbols:
284
+ self.account[symbol] = {'amount': 0, 'hold_price': 0, 'value': 0, 'price': 0,
285
+ 'realised_profit': 0, 'unrealised_profit': 0, 'fee': 0}
286
+
287
+ def Trade(self, symbol, direction, price, amount, **kwargs):
288
+ if self.recorded and 'time' not in kwargs:
289
+ raise ValueError("Time parameter is required in recorded mode.")
290
+
291
+ time = kwargs.get('time', pd.Timestamp.now())
292
+
293
+ self.id_gen += 1
294
+ tid = len(self.trades) if self.recorded else self.id_gen
295
+
296
+ trade = {
297
+ 'symbol': symbol,
298
+ 'exchange': "local",
299
+ 'orderid': tid,
300
+ 'tradeid': tid,
301
+ 'direction': direction,
302
+ 'price': price,
303
+ 'volume': abs(amount),
304
+ 'datetime': time,
305
+ 'gateway_name': "local",
306
+ 'pos': 0 # 初始化盈亏
307
+ }
308
+
309
+ if symbol not in self.trade_symbols:
310
+ self.trade_symbols.append(symbol)
311
+ self.account[symbol] = {'amount': 0, 'hold_price': 0, 'value': 0, 'price': 0,
312
+ 'realised_profit': 0, 'unrealised_profit': 0, 'fee': 0}
313
+
314
+ cover_amount = 0 if direction * self.account[symbol]['amount'] >= 0 else min(abs(self.account[symbol]['amount']), amount)
315
+ open_amount = amount - cover_amount
316
+
317
+ if cover_amount > 0 and np.isnan(price):
318
+ print(f'{symbol} 可能已经下架, 清仓')
319
+ price = self.account[symbol]['price'] if self.account[symbol]['price'] != 0 else self.account[symbol]['hold_price']
320
+ else:
321
+ if np.isnan(price) or np.isnan(amount):
322
+ print(f'{symbol} 价格或者数量为nan, 交易忽略')
323
+ return
324
+
325
+ # 扣除手续费
326
+ self.account['USDT']['realised_profit'] -= price * amount * self.fee
327
+ self.account['USDT']['fee'] += price * amount * self.fee
328
+ self.account[symbol]['fee'] += price * amount * self.fee
329
+
330
+ if cover_amount > 0: # 先平仓
331
+ profit = -direction * (price - self.account[symbol]['hold_price']) * cover_amount
332
+ self.account['USDT']['realised_profit'] += profit # 利润
333
+ self.account[symbol]['realised_profit'] += profit
334
+ self.account[symbol]['amount'] -= -direction * cover_amount
335
+ trade['pos'] = profit # 记录盈亏
336
+ self.account[symbol]['hold_price'] = 0 if self.account[symbol]['amount'] == 0 else self.account[symbol]['hold_price']
337
+
338
+ if open_amount > 0:
339
+ total_cost = self.account[symbol]['hold_price'] * direction * self.account[symbol]['amount'] + price * open_amount
340
+ total_amount = direction * self.account[symbol]['amount'] + open_amount
341
+
342
+ self.account[symbol]['hold_price'] = total_cost / total_amount
343
+ self.account[symbol]['amount'] += direction * open_amount
344
+
345
+ if kwargs:
346
+ self.opt.update(kwargs)
347
+
348
+ # 记录账户总资产到 history
349
+ if self.recorded:
350
+ self.opt['trades'].append(trade)
351
+ self.record_history(time)
352
+
353
+ # 自动更新账户状态
354
+ self.Update({symbol: price}, time=time)
355
+
356
+ return trade
357
+
358
+ def Buy(self, symbol, price, amount, **kwargs):
359
+ return self.Trade(symbol, 1, price, amount, **kwargs)
360
+
361
+ def Sell(self, symbol, price, amount, **kwargs):
362
+ return self.Trade(symbol, -1, price, amount, **kwargs)
363
+
364
+ def CloseAll(self, price, symbols=None, **kwargs):
365
+ if symbols is None:
366
+ symbols = self.trade_symbols
367
+ trades = []
368
+ symbols = [s for s in symbols if s in self.account and self.account[s]['amount'] != 0]
369
+ for symbol in symbols:
370
+ if symbol not in price or np.isnan(price[symbol]):
371
+ print(f'{symbol} 可能已经下架')
372
+ price[symbol] = self.account[symbol]['price'] if self.account[symbol]['price'] != 0 else self.account[symbol]['hold_price']
373
+ if np.isnan(price[symbol]):
374
+ price[symbol] = self.account[symbol]['price'] if self.account[symbol]['price'] != 0 else self.account[symbol]['hold_price']
375
+
376
+ direction = -np.sign(self.account[symbol]['amount'])
377
+ trade = self.Trade(symbol, direction, price[symbol], abs(self.account[symbol]['amount']), **kwargs)
378
+ trades.append(trade)
379
+ return trades
380
+
381
+ def Update(self, close_price, symbols=None, **kwargs):
382
+ if self.recorded and 'time' not in kwargs:
383
+ raise ValueError("Time parameter is required in recorded mode.")
384
+
385
+ time = kwargs.get('time', pd.Timestamp.now())
386
+ self.account['USDT']['unrealised_profit'] = 0
387
+ self.account['USDT']['hold'] = 0
388
+ self.account['USDT']['long'] = 0
389
+ self.account['USDT']['short'] = 0
390
+ if symbols is None:
391
+ # symbols = self.trade_symbols
392
+ # 如果symbols是dict类型, 则取出所有的key, 如果是Series类型, 则取出所有的index
393
+ if isinstance(close_price, dict):
394
+ symbols = list(close_price.keys())
395
+ elif isinstance(close_price, pd.Series):
396
+ symbols = close_price.index
397
+ else:
398
+ raise ValueError("Symbols should be a list, dict or Series.")
399
+
400
+ for symbol in symbols:
401
+ if symbol not in self.trade_symbols:
402
+ continue
403
+ if not np.isnan(close_price[symbol]):
404
+ self.account[symbol]['unrealised_profit'] = (close_price[symbol] - self.account[symbol]['hold_price']) * self.account[symbol]['amount']
405
+ self.account[symbol]['price'] = close_price[symbol]
406
+ self.account[symbol]['value'] = self.account[symbol]['amount'] * close_price[symbol]
407
+ if self.account[symbol]['amount'] > 0:
408
+ self.account['USDT']['long'] += self.account[symbol]['value']
409
+ if self.account[symbol]['amount'] < 0:
410
+ self.account['USDT']['short'] += self.account[symbol]['value']
411
+ self.account['USDT']['hold'] += abs(self.account[symbol]['value'])
412
+ self.account['USDT']['unrealised_profit'] += self.account[symbol]['unrealised_profit']
413
+
414
+ self.account['USDT']['total'] = round(self.account['USDT']['realised_profit'] + self.initial_balance + self.account['USDT']['unrealised_profit'], 6)
415
+ self.account['USDT']['leverage'] = round(self.account['USDT']['hold'] / self.account['USDT']['total'], 3)
416
+
417
+ # 记录账户总资产到 history
418
+ if self.recorded:
419
+ self.record_history(time)
420
+
421
+ # e = Exchange([])
422
+ # e.Sell('DOGEUSDT', 0.3, 3)
423
+ # print(e.account)
424
+
425
+ def gen_back_time(start_date, end_date, train_period_days, test_period_days):
426
+ # 将输入的日期字符串转换为时间戳
427
+ start_date = pd.to_datetime(start_date)
428
+ end_date = pd.to_datetime(end_date)
429
+
430
+ # 定义训练和测试周期
431
+ train_period = pd.Timedelta(days=train_period_days)
432
+ test_period = pd.Timedelta(days=test_period_days)
433
+
434
+ # 存储训练和测试日期区间
435
+ train_date = []
436
+ test_date = []
437
+
438
+ # 确定训练和测试的时间区间
439
+ current_date = start_date
440
+ while current_date + test_period <= end_date:
441
+ tsd_start = current_date
442
+ tsd_end = tsd_start + test_period
443
+ trd_end = tsd_start
444
+ trd_start = trd_end - train_period
445
+
446
+ train_date.append((pd.Timestamp(trd_start.date()), pd.Timestamp(trd_end.date())))
447
+ test_date.append((pd.Timestamp(tsd_start.date()), pd.Timestamp(tsd_end.date())))
448
+
449
+ # 移动到下一个测试周期的开始
450
+ current_date = tsd_end
451
+
452
+ # 将其转换为DataFrame
453
+ train_date = pd.DataFrame(train_date, columns=['x_start', 'x_end'])
454
+ test_date = pd.DataFrame(test_date, columns=['y_start', 'y_end'])
455
+ back_df = pd.concat([train_date, test_date], axis=1)
456
+ return back_df