hyperquant 1.48__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.

Potentially problematic release.


This version of hyperquant might be problematic. Click here for more details.

Files changed (42) hide show
  1. hyperquant/__init__.py +8 -0
  2. hyperquant/broker/auth.py +972 -0
  3. hyperquant/broker/bitget.py +311 -0
  4. hyperquant/broker/bitmart.py +720 -0
  5. hyperquant/broker/coinw.py +487 -0
  6. hyperquant/broker/deepcoin.py +651 -0
  7. hyperquant/broker/edgex.py +500 -0
  8. hyperquant/broker/hyperliquid.py +570 -0
  9. hyperquant/broker/lbank.py +661 -0
  10. hyperquant/broker/lib/edgex_sign.py +455 -0
  11. hyperquant/broker/lib/hpstore.py +252 -0
  12. hyperquant/broker/lib/hyper_types.py +48 -0
  13. hyperquant/broker/lib/polymarket/ctfAbi.py +721 -0
  14. hyperquant/broker/lib/polymarket/safeAbi.py +1138 -0
  15. hyperquant/broker/lib/util.py +22 -0
  16. hyperquant/broker/lighter.py +679 -0
  17. hyperquant/broker/models/apexpro.py +150 -0
  18. hyperquant/broker/models/bitget.py +359 -0
  19. hyperquant/broker/models/bitmart.py +635 -0
  20. hyperquant/broker/models/coinw.py +724 -0
  21. hyperquant/broker/models/deepcoin.py +809 -0
  22. hyperquant/broker/models/edgex.py +1053 -0
  23. hyperquant/broker/models/hyperliquid.py +284 -0
  24. hyperquant/broker/models/lbank.py +557 -0
  25. hyperquant/broker/models/lighter.py +868 -0
  26. hyperquant/broker/models/ourbit.py +1155 -0
  27. hyperquant/broker/models/polymarket.py +1071 -0
  28. hyperquant/broker/ourbit.py +550 -0
  29. hyperquant/broker/polymarket.py +2399 -0
  30. hyperquant/broker/ws.py +132 -0
  31. hyperquant/core.py +513 -0
  32. hyperquant/datavison/_util.py +18 -0
  33. hyperquant/datavison/binance.py +111 -0
  34. hyperquant/datavison/coinglass.py +237 -0
  35. hyperquant/datavison/okx.py +177 -0
  36. hyperquant/db.py +191 -0
  37. hyperquant/draw.py +1200 -0
  38. hyperquant/logkit.py +205 -0
  39. hyperquant/notikit.py +124 -0
  40. hyperquant-1.48.dist-info/METADATA +32 -0
  41. hyperquant-1.48.dist-info/RECORD +42 -0
  42. hyperquant-1.48.dist-info/WHEEL +4 -0
@@ -0,0 +1,132 @@
1
+ import asyncio
2
+ import time
3
+ from typing import Any
4
+
5
+ import pybotters
6
+ from aiohttp import WSMsgType
7
+ from pybotters.ws import ClientWebSocketResponse, logger
8
+
9
+
10
+ class Heartbeat:
11
+ @staticmethod
12
+ async def ourbit(ws: pybotters.ws.ClientWebSocketResponse):
13
+ while not ws.closed:
14
+ await ws.send_str('{"method":"ping"}')
15
+ await asyncio.sleep(10.0)
16
+
17
+ async def ourbit_spot(ws: pybotters.ws.ClientWebSocketResponse):
18
+ while not ws.closed:
19
+ await ws.send_str('{"method":"ping"}')
20
+ await asyncio.sleep(10.0)
21
+
22
+ @staticmethod
23
+ async def edgex(ws: pybotters.ws.ClientWebSocketResponse):
24
+ while not ws.closed:
25
+ now = str(int(time.time() * 1000))
26
+ await ws.send_json({"type": "ping", "time": now})
27
+ await asyncio.sleep(20.0)
28
+
29
+ @staticmethod
30
+ async def lbank(ws: ClientWebSocketResponse):
31
+ while not ws.closed:
32
+ await ws.send_str('ping')
33
+ await asyncio.sleep(6)
34
+
35
+ @staticmethod
36
+ async def coinw(ws: ClientWebSocketResponse):
37
+ while not ws.closed:
38
+ await ws.send_json({"event": "ping"})
39
+ await asyncio.sleep(3.0)
40
+
41
+ @staticmethod
42
+ async def deepcoin(ws: ClientWebSocketResponse):
43
+ while not ws.closed:
44
+ await ws.send_str("ping")
45
+ await asyncio.sleep(30)
46
+
47
+ @staticmethod
48
+ async def lighter(ws: ClientWebSocketResponse):
49
+ while not ws.closed:
50
+ await ws.send_json({"type":"ping"})
51
+ await asyncio.sleep(3)
52
+
53
+
54
+
55
+ pybotters.ws.HeartbeatHosts.items['futures.ourbit.com'] = Heartbeat.ourbit
56
+ pybotters.ws.HeartbeatHosts.items['www.ourbit.com'] = Heartbeat.ourbit_spot
57
+ pybotters.ws.HeartbeatHosts.items['quote.edgex.exchange'] = Heartbeat.edgex
58
+ pybotters.ws.HeartbeatHosts.items['uuws.rerrkvifj.com'] = Heartbeat.lbank
59
+ pybotters.ws.HeartbeatHosts.items['ws.futurescw.com'] = Heartbeat.coinw
60
+ pybotters.ws.HeartbeatHosts.items['stream.deepcoin.com'] = Heartbeat.deepcoin
61
+ pybotters.ws.HeartbeatHosts.items['mainnet.zklighter.elliot.ai'] = Heartbeat.lighter
62
+ # pybotters.ws.HeartbeatHosts.items['ws-subscriptions-clob.polymarket.com'] = Heartbeat.polymarket
63
+
64
+ class WssAuth:
65
+ @staticmethod
66
+ async def ourbit(ws: ClientWebSocketResponse):
67
+ key: str = ws._response._session.__dict__["_apis"][
68
+ pybotters.ws.AuthHosts.items[ws._response.url.host].name
69
+ ][0]
70
+ await ws.send_json(
71
+ {
72
+ "method": "login",
73
+ "param": {
74
+ "token": key
75
+ }
76
+ }
77
+ )
78
+ async for msg in ws:
79
+ # {"channel":"rs.login","data":"success","ts":1756470267848}
80
+ data = msg.json()
81
+ if data.get("channel") == "rs.login":
82
+ if data.get("data") == "success":
83
+ break
84
+ else:
85
+ logger.warning(f"WebSocket login failed: {data}")
86
+
87
+ @staticmethod
88
+ async def coinw(ws: ClientWebSocketResponse):
89
+ creds = ws._response._session.__dict__["_apis"].get(
90
+ pybotters.ws.AuthHosts.items[ws._response.url.host].name
91
+ )
92
+ if not creds:
93
+ raise RuntimeError("CoinW credentials are required for websocket login.")
94
+ if isinstance(creds, dict):
95
+ raise RuntimeError("CoinW credentials must be a sequence, not a dict.")
96
+ if len(creds) < 1:
97
+ raise RuntimeError("CoinW credentials are incomplete.")
98
+
99
+ api_key = creds[0]
100
+ secret = creds[1] if len(creds) > 1 else ""
101
+
102
+ await ws.send_json(
103
+ {
104
+ "event": "login",
105
+ "params": {
106
+ "api_key": api_key,
107
+ "passphrase": secret.decode(),
108
+ },
109
+ }
110
+ )
111
+
112
+ async for msg in ws:
113
+ if msg.type != WSMsgType.TEXT:
114
+ continue
115
+ try:
116
+ data:dict = msg.json()
117
+ except Exception: # pragma: no cover - defensive
118
+ continue
119
+
120
+ channel = data.get("channel")
121
+ event_type = data.get("type")
122
+ if channel == "login" or event_type == "login":
123
+ result = data.get("data", {}).get("result")
124
+ if result is not True:
125
+ raise RuntimeError(f"CoinW WebSocket login failed: {data}")
126
+ break
127
+ if data.get("event") == "pong":
128
+ # ignore heartbeat responses while waiting
129
+ continue
130
+
131
+ pybotters.ws.AuthHosts.items['futures.ourbit.com'] = pybotters.auth.Item("ourbit", WssAuth.ourbit)
132
+ pybotters.ws.AuthHosts.items['ws.futurescw.com'] = pybotters.auth.Item("coinw", WssAuth.coinw)
hyperquant/core.py ADDED
@@ -0,0 +1,513 @@
1
+ # %%
2
+ import numpy as np
3
+ import pandas as pd
4
+ from .draw import draw
5
+
6
+
7
+ class ExchangeBase:
8
+ def __init__(self, initial_balance=10000, recorded=False):
9
+ self.initial_balance = initial_balance # 初始的资产
10
+ self.recorded = recorded # 是否记录历史
11
+ self.opt = {
12
+ 'trades': [],
13
+ 'history': [] # 集成 history 到 opt 中
14
+ }
15
+ self.account = {'USDT': {'realised_profit': 0, 'unrealised_profit': 0, 'total': initial_balance,
16
+ 'fee': 0, 'leverage': 0, 'hold': 0, 'long': 0, 'short': 0}}
17
+
18
+ def record_history(self, time):
19
+ """记录当前总资产和时间到 history 中"""
20
+ self.opt['history'].append({
21
+ 'date': time,
22
+ 'total': self.account['USDT']['total']
23
+ })
24
+
25
+ def __getitem__(self, symbol):
26
+ return self.account.get(symbol, None)
27
+
28
+ def __setitem__(self, symbol, value):
29
+ self.account[symbol] = value
30
+
31
+ @property
32
+ def activate_symbols(self):
33
+ return [symbol for symbol in self.trade_symbols if self.account[symbol]['amount'] != 0]
34
+
35
+ @property
36
+ def total(self):
37
+ return self.account['USDT']['total']
38
+
39
+ @property
40
+ def leverage(self):
41
+ return self.account['USDT']['leverage']
42
+
43
+ @property
44
+ def realised_profit(self):
45
+ return self.account['USDT']['realised_profit']
46
+
47
+ @property
48
+ def unrealised_profit(self):
49
+ return self.account['USDT']['unrealised_profit']
50
+
51
+ @property
52
+ def history(self):
53
+ if not self.recorded:
54
+ raise ValueError("History is only available in recorded mode.")
55
+ return self.opt['history']
56
+
57
+ @property
58
+ def available_margin(self):
59
+ return self.account['USDT']['total'] - self.account['USDT']['hold']
60
+
61
+ @property
62
+ def realised_profit(self):
63
+ return self.account['USDT']['realised_profit']
64
+
65
+ @property
66
+ def trades(self):
67
+ return self.opt['trades']
68
+
69
+ @property
70
+ def stats(self):
71
+ if not self.recorded:
72
+ raise ValueError("Stats are only available in recorded mode.")
73
+
74
+ if not self.opt['history']:
75
+ return {
76
+ '初始资产': f'{self.initial_balance:.2f} USDT',
77
+ '当前资产': f'{self.account["USDT"]["total"]:.2f} USDT',
78
+ '已实现利润': f'{self.account["USDT"]["realised_profit"]:.2f} USDT',
79
+ '未实现利润': f'{self.account["USDT"]["unrealised_profit"]:.2f} USDT',
80
+ '总手续费': f'{self.account["USDT"]["fee"]:.2f} USDT',
81
+ '杯杆率': f'{self.account["USDT"]["leverage"]:.2f}',
82
+ '活跃交易对数量': len(self.activate_symbols),
83
+ '持仓价值': f'{self.account["USDT"]["hold"]:.2f} USDT',
84
+ '多头持仓价值': f'{self.account["USDT"]["long"]:.2f} USDT',
85
+ '空头持仓价值': f'{self.account["USDT"]["short"]:.2f} USDT',
86
+ '总交易笔数': 0,
87
+ '胜率': '0.00%',
88
+ '年化收益率': '0.00%',
89
+ '最大回撤时间范围': 'N/A',
90
+ '最大回撤': '0.00%',
91
+ '夏普比率': '0.00'
92
+ }
93
+
94
+ # 创建一个账户历史的DataFrame
95
+ history_df = pd.DataFrame(self.opt['history'])
96
+ history_df = history_df.sort_values(by='date')
97
+ history_df = history_df.drop_duplicates(subset='date')
98
+ history_df = history_df.set_index('date')
99
+
100
+ # 计算累计收益
101
+ history_df['max2here'] = history_df['total'].expanding().max()
102
+ history_df['dd2here'] = history_df['total'] / history_df['max2here'] - 1
103
+ drwa_down_df = history_df.sort_values(by=['dd2here'])
104
+ drwa_down_df = drwa_down_df[drwa_down_df['dd2here'] < 0]
105
+ if drwa_down_df.empty:
106
+ start_date = np.nan
107
+ end_data = np.nan
108
+ max_draw_down = 0
109
+ else:
110
+ max_draw_down = drwa_down_df.iloc[0]['dd2here']
111
+ end_data = drwa_down_df.iloc[0].name
112
+ start_date = history_df[history_df.index <= end_data].sort_values(by='total', ascending=False).iloc[0].name
113
+
114
+ # 计算胜率
115
+ total_trades = len(self.opt['trades'])
116
+ if total_trades == 0:
117
+ win_rate = 0
118
+ else:
119
+ winning_trades = sum(1 for trade in self.opt['trades'] if trade['pos'] > 0)
120
+ losing_trades = sum(1 for trade in self.opt['trades'] if trade['pos'] < 0)
121
+ win_rate = winning_trades / (winning_trades + losing_trades) if (winning_trades + losing_trades) > 0 else 0
122
+
123
+ # 计算年化收益率
124
+ if len(history_df) < 2:
125
+ annual_return = 0
126
+ else:
127
+ start_date_for_return = history_df.index[0]
128
+ end_date_for_return = history_df.index[-1]
129
+ total_days = (end_date_for_return - start_date_for_return).days
130
+ if total_days > 0:
131
+ annual_return = ((history_df['total'].iloc[-1] / self.initial_balance) ** (365 / total_days) - 1)
132
+ else:
133
+ annual_return = 0
134
+
135
+ # 计算夏普比率
136
+ # 计算每日收益率
137
+ daily_history = history_df['total'].resample('D').ffill().dropna()
138
+ daily_returns = daily_history.pct_change().dropna()
139
+ if len(daily_returns) > 1:
140
+ risk_free_rate = 0.03 / 365
141
+ sharpe_ratio = (daily_returns.mean() - risk_free_rate) / daily_returns.std() * np.sqrt(365)
142
+ else:
143
+ sharpe_ratio = 0
144
+
145
+ stats = {
146
+ '初始资产': f'{self.initial_balance:.2f} USDT',
147
+ '当前资产': f'{self.account["USDT"]["total"]:.2f} USDT',
148
+ '已实现利润': f'{self.account["USDT"]["realised_profit"]:.2f} USDT',
149
+ '未实现利润': f'{self.account["USDT"]["unrealised_profit"]:.2f} USDT',
150
+ '总手续费': f'{self.account["USDT"]["fee"]:.2f} USDT',
151
+ '活跃交易对数量': len(self.activate_symbols),
152
+ '持仓价值': f'{self.account["USDT"]["hold"]:.2f} USDT',
153
+ '多头持仓价值': f'{self.account["USDT"]["long"]:.2f} USDT',
154
+ '空头持仓价值': f'{self.account["USDT"]["short"]:.2f} USDT',
155
+ '总交易笔数': total_trades,
156
+ '胜率': f'{win_rate:.2%}',
157
+ '年化收益率': f'{annual_return:.2%}',
158
+ '最大回撤时间范围': (start_date,end_data),
159
+ '最大回撤': f'{max_draw_down:.2%}',
160
+ '夏普比率': f'{sharpe_ratio:.2f}'
161
+ }
162
+ return stats
163
+
164
+ def draw(self, data_df: pd.DataFrame, title: str, indicators: list, show_kline=True, show_total=True, show_base=False):
165
+ """
166
+ :param data_df: 数据 DataFrame
167
+ :param title: 图表标题
168
+ :param indicators: 画图指标 [[('指标名', '指标类型'), ('指标名', '指标类型')], [('指标名', '指标类型')]]
169
+ :param show_kline: 是否显示K线图
170
+ :param show_total: 是否显示总资产曲线
171
+ """
172
+
173
+ # 将 self.history 转换为 DataFrame
174
+ history_df = pd.DataFrame(self.opt['history'])
175
+
176
+ # 按照 'date' 分组,并保留每组的最后一条记录
177
+ history_df = history_df.sort_values('date').groupby('date', as_index=False).last()
178
+
179
+ # 按照 'date' 将 history_df 和 data_df 合并
180
+ data_df = pd.merge(data_df, history_df, on='date', how='left')
181
+
182
+ # 使用前向填充处理 'total' 列的缺失值
183
+ data_df['total'] = data_df['total'].ffill()
184
+
185
+ data_dict = []
186
+ if show_kline:
187
+ # 如果signal列存在,将signal列的值赋值给signal列
188
+ opt = {
189
+ "series_name": "K",
190
+ "draw_type": "Kline",
191
+ "col": ["open", "close", "low", "high"],
192
+ "height": 50,
193
+ }
194
+ if 'signal' in data_df.columns:
195
+ opt['trade_single'] = 'signal'
196
+ data_dict.append(opt)
197
+
198
+
199
+ if indicators:
200
+ for ind in indicators:
201
+ ind_data = {}
202
+ for i, indicator in enumerate(ind):
203
+ if i == 0:
204
+ ind_data = {
205
+ "series_name": indicator[0],
206
+ "draw_type": indicator[1],
207
+ "height": 0,
208
+ }
209
+ else:
210
+ if 'sub_chart' not in ind_data:
211
+ ind_data['sub_chart'] = []
212
+ ind_data['sub_chart'].append(
213
+ {"series_name": indicator[0], "draw_type": indicator[1]}
214
+ )
215
+ data_dict.append(ind_data)
216
+
217
+ if show_total:
218
+ # 绘制基准收益曲线
219
+ total_dict = {
220
+ "series_name": "total",
221
+ "draw_type": "Line",
222
+ "height": 0,
223
+ }
224
+ if show_base:
225
+ data_df.loc[:, "base"] = (data_df["close"] / data_df["close"].iloc[0]) * self.initial_balance
226
+ total_dict['sub_chart'] = [
227
+ {"series_name": "base", "draw_type": "Line"},
228
+ ]
229
+ data_dict.append(total_dict)
230
+
231
+
232
+ sub_width = 40 / (len(data_dict) - 1)
233
+ for d in data_dict:
234
+ if d['draw_type'] != "Kline":
235
+ d['height'] = sub_width
236
+
237
+ draw(
238
+ data_df,
239
+ data_dict=data_dict,
240
+ date_col="date",
241
+ date_formate="%Y-%m-%d %H:%M:%S",
242
+ title=title,
243
+ height_type="%",
244
+ auto_play_space="""
245
+ function auto_play_space(xi){
246
+ return 200;
247
+ }""",
248
+ show=True,
249
+ display_js="""
250
+ // 设置 dataZoom 为最大范围
251
+ window.onload = function() {
252
+ var isSettingZoom = false;
253
+
254
+ // 获取 x 轴的数据
255
+ var xData = chart_option.xAxis[0].data;
256
+ if (xData.length > 0) {
257
+ var startValue = xData[0];
258
+ var endValue = xData[xData.length - 1];
259
+ isSettingZoom = true;
260
+ chart_ins.dispatchAction({
261
+ type: 'dataZoom',
262
+ startValue: startValue,
263
+ endValue: endValue
264
+ });
265
+ isSettingZoom = false;
266
+ }
267
+ }
268
+ """,
269
+ )
270
+
271
+ class Exchange(ExchangeBase):
272
+ def __init__(self, trade_symbols:list=[], fee=0.0002, initial_balance=10000, recorded=False):
273
+ super().__init__(initial_balance=initial_balance, recorded=recorded)
274
+ self.fee = fee
275
+ self.trade_symbols:list = trade_symbols
276
+ self.id_gen = 0
277
+ self.account['USDT'].update({
278
+ 'hold': 0,
279
+ 'long': 0,
280
+ 'short': 0
281
+ })
282
+ for symbol in trade_symbols:
283
+ self.account[symbol] = self._act_template
284
+
285
+ @property
286
+ def _act_template(self):
287
+ return {'amount': 0, 'hold_price': 0, 'value': 0, 'price': 0,
288
+ 'realised_profit': 0, 'unrealised_profit': 0, 'fee': 0}.copy()
289
+
290
+ def Trade(self, symbol, direction, price, amount, **kwargs):
291
+ if self.recorded and 'time' not in kwargs:
292
+ raise ValueError("Time parameter is required in recorded mode.")
293
+
294
+ time = kwargs.get('time', pd.Timestamp.now())
295
+
296
+ self.id_gen += 1
297
+ tid = len(self.trades) if self.recorded else self.id_gen
298
+
299
+ trade = {
300
+ 'symbol': symbol,
301
+ 'exchange': "local",
302
+ 'orderid': tid,
303
+ 'tradeid': tid,
304
+ 'direction': direction,
305
+ 'price': price,
306
+ 'volume': abs(amount),
307
+ 'datetime': time,
308
+ 'gateway_name': "local",
309
+ 'pos': 0 # 初始化盈亏
310
+ }
311
+
312
+ if symbol not in self.trade_symbols:
313
+ self.trade_symbols.append(symbol)
314
+ self.account[symbol] = self._act_template
315
+
316
+ cover_amount = 0 if direction * self.account[symbol]['amount'] >= 0 else min(abs(self.account[symbol]['amount']), amount)
317
+ open_amount = amount - cover_amount
318
+
319
+ if cover_amount > 0 and np.isnan(price):
320
+ print(f'{symbol} 可能已经下架, 清仓')
321
+ price = self.account[symbol]['price'] if self.account[symbol]['price'] != 0 else self.account[symbol]['hold_price']
322
+ else:
323
+ if np.isnan(price) or np.isnan(amount):
324
+ print(f'{symbol} 价格或者数量为nan, 交易忽略')
325
+ return
326
+
327
+ # 扣除手续费
328
+ self.account['USDT']['realised_profit'] -= price * amount * self.fee
329
+ self.account['USDT']['fee'] += price * amount * self.fee
330
+ self.account[symbol]['fee'] += price * amount * self.fee
331
+
332
+ if cover_amount > 0: # 先平仓
333
+ profit = -direction * (price - self.account[symbol]['hold_price']) * cover_amount
334
+ self.account['USDT']['realised_profit'] += profit # 利润
335
+ self.account[symbol]['realised_profit'] += profit
336
+ self.account[symbol]['amount'] -= -direction * cover_amount
337
+ trade['pos'] = profit # 记录盈亏
338
+
339
+ trade['pos_rate'] = -direction * (price / self.account[symbol]['hold_price'] - 1) if self.account[symbol]['hold_price'] != 0 else 0
340
+
341
+ self.account[symbol]['hold_price'] = 0 if self.account[symbol]['amount'] == 0 else self.account[symbol]['hold_price']
342
+
343
+ if open_amount > 0:
344
+ total_cost = self.account[symbol]['hold_price'] * direction * self.account[symbol]['amount'] + price * open_amount
345
+ total_amount = direction * self.account[symbol]['amount'] + open_amount
346
+
347
+ self.account[symbol]['hold_price'] = total_cost / total_amount
348
+ self.account[symbol]['amount'] += direction * open_amount
349
+
350
+ if kwargs:
351
+ self.opt.update(kwargs)
352
+ self.account[symbol].update(kwargs)
353
+
354
+ # 记录账户总资产到 history
355
+ if self.recorded:
356
+ self.opt['trades'].append(trade)
357
+ self.record_history(time)
358
+
359
+ # 自动更新账户状态
360
+ self.Update({symbol: price}, time=time)
361
+
362
+ return trade
363
+
364
+ def Buy(self, symbol, price, amount, **kwargs):
365
+ return self.Trade(symbol, 1, price, amount, **kwargs)
366
+
367
+ def Sell(self, symbol, price, amount, **kwargs):
368
+ return self.Trade(symbol, -1, price, amount, **kwargs)
369
+
370
+ def CloseAll(self, price, symbols=None, **kwargs):
371
+ if symbols is None:
372
+ symbols = self.trade_symbols
373
+ trades = []
374
+ symbols = [s for s in symbols if s in self.account and self.account[s]['amount'] != 0]
375
+ for symbol in symbols:
376
+ if symbol not in price or np.isnan(price[symbol]):
377
+ print(f'{symbol} 可能已经下架')
378
+ price[symbol] = self.account[symbol]['price'] if self.account[symbol]['price'] != 0 else self.account[symbol]['hold_price']
379
+ if np.isnan(price[symbol]):
380
+ price[symbol] = self.account[symbol]['price'] if self.account[symbol]['price'] != 0 else self.account[symbol]['hold_price']
381
+
382
+ direction = -np.sign(self.account[symbol]['amount'])
383
+ trade = self.Trade(symbol, direction, price[symbol], abs(self.account[symbol]['amount']), **kwargs)
384
+ trades.append(trade)
385
+ return trades
386
+
387
+ def _recalc_aggregates(self):
388
+ """基于 self.account 中已保存的各 symbol 状态,重算聚合字段。"""
389
+ usdt = self.account['USDT']
390
+ usdt['unrealised_profit'] = 0
391
+ usdt['hold'] = 0
392
+ usdt['long'] = 0
393
+ usdt['short'] = 0
394
+
395
+ for symbol in self.trade_symbols:
396
+ if symbol not in self.account:
397
+ continue
398
+ sym = self.account[symbol]
399
+ px = sym.get('price', 0)
400
+ amt = sym.get('amount', 0)
401
+ hp = sym.get('hold_price', 0)
402
+
403
+ # 仅当价格有效时计入聚合
404
+ if px is not None and not np.isnan(px) and px != 0:
405
+ sym['unrealised_profit'] = (px - hp) * amt
406
+ sym['value'] = amt * px
407
+
408
+ if amt > 0:
409
+ usdt['long'] += sym['value']
410
+ elif amt < 0:
411
+ usdt['short'] += sym['value']
412
+
413
+ usdt['hold'] += abs(sym['value'])
414
+ usdt['unrealised_profit'] += sym['unrealised_profit']
415
+
416
+ usdt['total'] = round(self.account['USDT']['realised_profit'] + self.initial_balance + usdt['unrealised_profit'], 6)
417
+ usdt['leverage'] = round(usdt['hold'] / usdt['total'] if usdt['total'] != 0 else 0.0, 3)
418
+
419
+ def Update(self, close_price=None, symbols=None, partial=True, **kwargs):
420
+ """
421
+ 更新账户状态。
422
+ - partial=True:只更新给定 symbols 的逐符号状态,然后对所有符号做一次聚合重算(推荐)。
423
+ - partial=False:与原逻辑兼容;当提供一部分 symbol 时,也会聚合重算,不会清空未提供符号的信息。
424
+
425
+ 支持三种入参形式:
426
+ 1) close_price 为 dict/Series:symbols 自动取其键/索引
427
+ 2) close_price 为标量 + symbols 为单个字符串
428
+ 3) 显式传 symbols=list[...],close_price 为 dict/Series(从中取价)
429
+ 如果既不传 close_price 也不传 symbols,则只做一次聚合重算(例如你先前已经手动修改了某些 symbol 的 price)。
430
+ """
431
+ if self.recorded and 'time' not in kwargs:
432
+ raise ValueError("Time parameter is required in recorded mode.")
433
+
434
+ time = kwargs.get('time', pd.Timestamp.now())
435
+
436
+ # 解析 symbols & 价格获取器
437
+ if symbols is None:
438
+ if isinstance(close_price, dict):
439
+ symbols = list(close_price.keys())
440
+ elif isinstance(close_price, pd.Series):
441
+ symbols = list(close_price.index)
442
+ else:
443
+ symbols = []
444
+ elif isinstance(symbols, str):
445
+ symbols = [symbols]
446
+
447
+ def get_px(sym):
448
+ if isinstance(close_price, (int, float, np.floating)) and len(symbols) == 1:
449
+ return float(close_price)
450
+ if isinstance(close_price, dict):
451
+ return close_price.get(sym, np.nan)
452
+ if isinstance(close_price, pd.Series):
453
+ return close_price.get(sym, np.nan)
454
+ return np.nan
455
+
456
+ # 仅更新传入的 symbols(部分更新,不动其它符号已保存信息)
457
+ for sym in symbols:
458
+ if sym not in self.trade_symbols or sym not in self.account:
459
+ # 未登记的交易对直接跳过(或可选择自动登记,但此处保持严格)
460
+ continue
461
+ px = get_px(sym)
462
+ if px is None or np.isnan(px):
463
+ # 价格无效则不覆盖旧价格
464
+ continue
465
+
466
+ self.account[sym]['price'] = float(px)
467
+ amt = self.account[sym]['amount']
468
+ self.account[sym]['value'] = amt * float(px)
469
+ # 不在这里算 unrealised_profit,聚合阶段统一算
470
+
471
+ # 无论 partial 与否,最后都用“账户中保存的所有 symbol 当前状态”做一次聚合重算
472
+ self._recalc_aggregates()
473
+
474
+ # 记录账户总资产到 history
475
+ if self.recorded:
476
+ self.record_history(time)
477
+
478
+ # e = Exchange([])
479
+ # e.Sell('DOGEUSDT', 0.3, 3)
480
+ # print(e.account)
481
+
482
+ def gen_back_time(start_date, end_date, train_period_days, test_period_days):
483
+ # 将输入的日期字符串转换为时间戳
484
+ start_date = pd.to_datetime(start_date)
485
+ end_date = pd.to_datetime(end_date)
486
+
487
+ # 定义训练和测试周期
488
+ train_period = pd.Timedelta(days=train_period_days)
489
+ test_period = pd.Timedelta(days=test_period_days)
490
+
491
+ # 存储训练和测试日期区间
492
+ train_date = []
493
+ test_date = []
494
+
495
+ # 确定训练和测试的时间区间
496
+ current_date = start_date
497
+ while current_date + test_period <= end_date:
498
+ tsd_start = current_date
499
+ tsd_end = tsd_start + test_period
500
+ trd_end = tsd_start
501
+ trd_start = trd_end - train_period
502
+
503
+ train_date.append((pd.Timestamp(trd_start.date()), pd.Timestamp(trd_end.date())))
504
+ test_date.append((pd.Timestamp(tsd_start.date()), pd.Timestamp(tsd_end.date())))
505
+
506
+ # 移动到下一个测试周期的开始
507
+ current_date = tsd_end
508
+
509
+ # 将其转换为DataFrame
510
+ train_date = pd.DataFrame(train_date, columns=['x_start', 'x_end'])
511
+ test_date = pd.DataFrame(test_date, columns=['y_start', 'y_end'])
512
+ back_df = pd.concat([train_date, test_date], axis=1)
513
+ return back_df
@@ -0,0 +1,18 @@
1
+ from datetime import date, datetime
2
+
3
+
4
+ def _to_milliseconds( t):
5
+ """
6
+ 支持毫秒时间戳或datetime/date类型,返回毫秒时间戳
7
+ """
8
+ if t is None:
9
+ return None
10
+ if isinstance(t, int):
11
+ return t
12
+ if isinstance(t, float):
13
+ return int(t * 1000)
14
+ if isinstance(t, datetime):
15
+ return int(t.timestamp() * 1000)
16
+ if isinstance(t, date):
17
+ return int(datetime.combine(t, datetime.min.time()).timestamp() * 1000)
18
+ raise ValueError(f"不支持的时间类型: {type(t)}")