hyperquant 0.24__tar.gz → 0.26__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 (23) hide show
  1. {hyperquant-0.24 → hyperquant-0.26}/PKG-INFO +2 -2
  2. {hyperquant-0.24 → hyperquant-0.26}/pyproject.toml +2 -2
  3. {hyperquant-0.24 → hyperquant-0.26}/src/hyperquant/core.py +77 -26
  4. {hyperquant-0.24 → hyperquant-0.26}/src/hyperquant/notikit.py +30 -10
  5. {hyperquant-0.24 → hyperquant-0.26}/uv.lock +5 -5
  6. {hyperquant-0.24 → hyperquant-0.26}/.gitignore +0 -0
  7. {hyperquant-0.24 → hyperquant-0.26}/.python-version +0 -0
  8. {hyperquant-0.24 → hyperquant-0.26}/README.md +0 -0
  9. {hyperquant-0.24 → hyperquant-0.26}/pub.sh +0 -0
  10. {hyperquant-0.24 → hyperquant-0.26}/requirements-dev.lock +0 -0
  11. {hyperquant-0.24 → hyperquant-0.26}/requirements.lock +0 -0
  12. {hyperquant-0.24 → hyperquant-0.26}/src/hyperquant/__init__.py +0 -0
  13. {hyperquant-0.24 → hyperquant-0.26}/src/hyperquant/broker/hyperliquid.py +0 -0
  14. {hyperquant-0.24 → hyperquant-0.26}/src/hyperquant/broker/lib/hpstore.py +0 -0
  15. {hyperquant-0.24 → hyperquant-0.26}/src/hyperquant/broker/lib/hyper_types.py +0 -0
  16. {hyperquant-0.24 → hyperquant-0.26}/src/hyperquant/datavison/_util.py +0 -0
  17. {hyperquant-0.24 → hyperquant-0.26}/src/hyperquant/datavison/binance.py +0 -0
  18. {hyperquant-0.24 → hyperquant-0.26}/src/hyperquant/datavison/coinglass.py +0 -0
  19. {hyperquant-0.24 → hyperquant-0.26}/src/hyperquant/datavison/okx.py +0 -0
  20. {hyperquant-0.24 → hyperquant-0.26}/src/hyperquant/db.py +0 -0
  21. {hyperquant-0.24 → hyperquant-0.26}/src/hyperquant/draw.py +0 -0
  22. {hyperquant-0.24 → hyperquant-0.26}/src/hyperquant/logkit.py +0 -0
  23. {hyperquant-0.24 → hyperquant-0.26}/test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 0.24
3
+ Version: 0.26
4
4
  Summary: A minimal yet hyper-efficient backtesting framework for quantitative trading
5
5
  Project-URL: Homepage, https://github.com/yourusername/hyperquant
6
6
  Project-URL: Issues, https://github.com/yourusername/hyperquant/issues
@@ -19,7 +19,7 @@ Requires-Dist: cryptography>=44.0.2
19
19
  Requires-Dist: duckdb>=1.2.2
20
20
  Requires-Dist: numpy>=1.21.0
21
21
  Requires-Dist: pandas>=2.2.3
22
- Requires-Dist: pybotters>=1.9.0
22
+ Requires-Dist: pybotters>=1.9.1
23
23
  Requires-Dist: pyecharts>=2.0.8
24
24
  Description-Content-Type: text/markdown
25
25
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hyperquant"
3
- version = "0.24"
3
+ version = "0.26"
4
4
  description = "A minimal yet hyper-efficient backtesting framework for quantitative trading"
5
5
  authors = [
6
6
  { name = "MissinA", email = "1421329142@qq.com" }
@@ -13,7 +13,7 @@ dependencies = [
13
13
  "cryptography>=44.0.2",
14
14
  "numpy>=1.21.0", # Added numpy as a new dependency
15
15
  "duckdb>=1.2.2",
16
- "pybotters>=1.9.0",
16
+ "pybotters>=1.9.1",
17
17
  ]
18
18
  readme = "README.md"
19
19
  requires-python = ">=3.9"
@@ -350,7 +350,7 @@ class Exchange(ExchangeBase):
350
350
  self.record_history(time)
351
351
 
352
352
  # 自动更新账户状态
353
- self.Update({symbol: price}, time=time)
353
+ self.Update({symbol: price}, time=time)
354
354
 
355
355
  return trade
356
356
 
@@ -377,41 +377,92 @@ class Exchange(ExchangeBase):
377
377
  trades.append(trade)
378
378
  return trades
379
379
 
380
- def Update(self, close_price, symbols=None, **kwargs):
380
+ def _recalc_aggregates(self):
381
+ """基于 self.account 中已保存的各 symbol 状态,重算聚合字段。"""
382
+ usdt = self.account['USDT']
383
+ usdt['unrealised_profit'] = 0
384
+ usdt['hold'] = 0
385
+ usdt['long'] = 0
386
+ usdt['short'] = 0
387
+
388
+ for symbol in self.trade_symbols:
389
+ if symbol not in self.account:
390
+ continue
391
+ sym = self.account[symbol]
392
+ px = sym.get('price', 0)
393
+ amt = sym.get('amount', 0)
394
+ hp = sym.get('hold_price', 0)
395
+
396
+ # 仅当价格有效时计入聚合
397
+ if px is not None and not np.isnan(px) and px != 0:
398
+ sym['unrealised_profit'] = (px - hp) * amt
399
+ sym['value'] = amt * px
400
+
401
+ if amt > 0:
402
+ usdt['long'] += sym['value']
403
+ elif amt < 0:
404
+ usdt['short'] += sym['value']
405
+
406
+ usdt['hold'] += abs(sym['value'])
407
+ usdt['unrealised_profit'] += sym['unrealised_profit']
408
+
409
+ usdt['total'] = round(self.account['USDT']['realised_profit'] + self.initial_balance + usdt['unrealised_profit'], 6)
410
+ usdt['leverage'] = round(usdt['hold'] / usdt['total'] if usdt['total'] != 0 else 0.0, 3)
411
+
412
+ def Update(self, close_price=None, symbols=None, partial=True, **kwargs):
413
+ """
414
+ 更新账户状态。
415
+ - partial=True:只更新给定 symbols 的逐符号状态,然后对所有符号做一次聚合重算(推荐)。
416
+ - partial=False:与原逻辑兼容;当提供一部分 symbol 时,也会聚合重算,不会清空未提供符号的信息。
417
+
418
+ 支持三种入参形式:
419
+ 1) close_price 为 dict/Series:symbols 自动取其键/索引
420
+ 2) close_price 为标量 + symbols 为单个字符串
421
+ 3) 显式传 symbols=list[...],close_price 为 dict/Series(从中取价)
422
+ 如果既不传 close_price 也不传 symbols,则只做一次聚合重算(例如你先前已经手动修改了某些 symbol 的 price)。
423
+ """
381
424
  if self.recorded and 'time' not in kwargs:
382
425
  raise ValueError("Time parameter is required in recorded mode.")
383
426
 
384
427
  time = kwargs.get('time', pd.Timestamp.now())
385
- self.account['USDT']['unrealised_profit'] = 0
386
- self.account['USDT']['hold'] = 0
387
- self.account['USDT']['long'] = 0
388
- self.account['USDT']['short'] = 0
428
+
429
+ # 解析 symbols & 价格获取器
389
430
  if symbols is None:
390
- # symbols = self.trade_symbols
391
- # 如果symbols是dict类型, 则取出所有的key, 如果是Series类型, 则取出所有的index
392
431
  if isinstance(close_price, dict):
393
432
  symbols = list(close_price.keys())
394
433
  elif isinstance(close_price, pd.Series):
395
- symbols = close_price.index
434
+ symbols = list(close_price.index)
396
435
  else:
397
- raise ValueError("Symbols should be a list, dict or Series.")
398
-
399
- for symbol in symbols:
400
- if symbol not in self.trade_symbols:
436
+ symbols = []
437
+ elif isinstance(symbols, str):
438
+ symbols = [symbols]
439
+
440
+ def get_px(sym):
441
+ if isinstance(close_price, (int, float, np.floating)) and len(symbols) == 1:
442
+ return float(close_price)
443
+ if isinstance(close_price, dict):
444
+ return close_price.get(sym, np.nan)
445
+ if isinstance(close_price, pd.Series):
446
+ return close_price.get(sym, np.nan)
447
+ return np.nan
448
+
449
+ # 仅更新传入的 symbols(部分更新,不动其它符号已保存信息)
450
+ for sym in symbols:
451
+ if sym not in self.trade_symbols or sym not in self.account:
452
+ # 未登记的交易对直接跳过(或可选择自动登记,但此处保持严格)
453
+ continue
454
+ px = get_px(sym)
455
+ if px is None or np.isnan(px):
456
+ # 价格无效则不覆盖旧价格
401
457
  continue
402
- if not np.isnan(close_price[symbol]):
403
- self.account[symbol]['unrealised_profit'] = (close_price[symbol] - self.account[symbol]['hold_price']) * self.account[symbol]['amount']
404
- self.account[symbol]['price'] = close_price[symbol]
405
- self.account[symbol]['value'] = self.account[symbol]['amount'] * close_price[symbol]
406
- if self.account[symbol]['amount'] > 0:
407
- self.account['USDT']['long'] += self.account[symbol]['value']
408
- if self.account[symbol]['amount'] < 0:
409
- self.account['USDT']['short'] += self.account[symbol]['value']
410
- self.account['USDT']['hold'] += abs(self.account[symbol]['value'])
411
- self.account['USDT']['unrealised_profit'] += self.account[symbol]['unrealised_profit']
412
-
413
- self.account['USDT']['total'] = round(self.account['USDT']['realised_profit'] + self.initial_balance + self.account['USDT']['unrealised_profit'], 6)
414
- self.account['USDT']['leverage'] = round(self.account['USDT']['hold'] / self.account['USDT']['total'], 3)
458
+
459
+ self.account[sym]['price'] = float(px)
460
+ amt = self.account[sym]['amount']
461
+ self.account[sym]['value'] = amt * float(px)
462
+ # 不在这里算 unrealised_profit,聚合阶段统一算
463
+
464
+ # 无论 partial 与否,最后都用“账户中保存的所有 symbol 当前状态”做一次聚合重算
465
+ self._recalc_aggregates()
415
466
 
416
467
  # 记录账户总资产到 history
417
468
  if self.recorded:
@@ -11,9 +11,10 @@ author: 邢不行
11
11
  import base64
12
12
  import hashlib
13
13
  import os.path
14
- import requests
15
14
  import json
16
15
  import traceback
16
+ import aiohttp
17
+ import asyncio
17
18
  from datetime import datetime
18
19
 
19
20
  from hyperquant.logkit import get_logger
@@ -31,13 +32,14 @@ def handle_exception(e: Exception, msg: str = '') -> None:
31
32
 
32
33
 
33
34
  # 企业微信通知
34
- def send_wecom_msg(content, webhook_url):
35
+ async def send_wecom_msg(content: str, webhook_url: str) -> None:
35
36
  if not webhook_url:
36
37
  logger.warning('未配置wecom_webhook_url,不发送信息')
37
38
  return
38
39
  if not content:
39
40
  logger.warning('未配置content,不发送信息')
40
41
  return
42
+
41
43
  try:
42
44
  data = {
43
45
  "msgtype": "text",
@@ -45,9 +47,17 @@ def send_wecom_msg(content, webhook_url):
45
47
  "content": content + '\n' + datetime.now().strftime("%Y-%m-%d %H:%M:%S")
46
48
  }
47
49
  }
48
- r = requests.post(webhook_url, data=json.dumps(data), timeout=10, proxies=proxy)
49
- logger.info(f'调用企业微信接口返回: {r.text}')
50
- logger.ok('成功发送企业微信')
50
+
51
+ async with aiohttp.ClientSession() as session:
52
+ async with session.post(
53
+ webhook_url,
54
+ json=data,
55
+ proxy=proxy.get('http') if proxy else None,
56
+ timeout=aiohttp.ClientTimeout(total=10)
57
+ ) as response:
58
+ result = await response.text()
59
+ logger.info(f'调用企业微信接口返回: {result}')
60
+ logger.ok('成功发送企业微信')
51
61
  except Exception as e:
52
62
  handle_exception(e, '发送企业微信失败')
53
63
 
@@ -66,7 +76,7 @@ class MyEncoder(json.JSONEncoder):
66
76
 
67
77
 
68
78
  # 企业微信发送图片
69
- def send_wecom_img(file_path, webhook_url):
79
+ async def send_wecom_img(file_path: str, webhook_url: str) -> None:
70
80
  """
71
81
  企业微信发送图片
72
82
  :param file_path: 图片地址
@@ -79,13 +89,16 @@ def send_wecom_img(file_path, webhook_url):
79
89
  if not webhook_url:
80
90
  logger.warning('未配置wecom_webhook_url,不发送信息')
81
91
  return
92
+
82
93
  try:
83
94
  with open(file_path, 'rb') as f:
84
95
  image_content = f.read()
96
+
85
97
  image_base64 = base64.b64encode(image_content).decode('utf-8')
86
98
  md5 = hashlib.md5()
87
99
  md5.update(image_content)
88
100
  image_md5 = md5.hexdigest()
101
+
89
102
  data = {
90
103
  'msgtype': 'image',
91
104
  'image': {
@@ -93,10 +106,17 @@ def send_wecom_img(file_path, webhook_url):
93
106
  'md5': image_md5
94
107
  }
95
108
  }
96
- # 服务器上传bytes图片的时候,json.dumps解析会出错,需要自己手动去转一下
97
- r = requests.post(webhook_url, data=json.dumps(data, cls=MyEncoder, indent=4), timeout=10, proxies=proxy)
98
- logger.info(f'调用企业微信接口返回: {r.text}')
99
- logger.ok('成功发送企业微信图片')
109
+
110
+ async with aiohttp.ClientSession() as session:
111
+ async with session.post(
112
+ webhook_url,
113
+ json=data,
114
+ proxy=proxy.get('http') if proxy else None,
115
+ timeout=aiohttp.ClientTimeout(total=10)
116
+ ) as response:
117
+ result = await response.text()
118
+ logger.info(f'调用企业微信接口返回: {result}')
119
+ logger.ok('成功发送企业微信图片')
100
120
  except Exception as e:
101
121
  handle_exception(e, '发送企业微信图片失败')
102
122
  finally:
@@ -530,7 +530,7 @@ wheels = [
530
530
 
531
531
  [[package]]
532
532
  name = "hyperquant"
533
- version = "0.23"
533
+ version = "0.25"
534
534
  source = { editable = "." }
535
535
  dependencies = [
536
536
  { name = "aiohttp" },
@@ -557,7 +557,7 @@ requires-dist = [
557
557
  { name = "duckdb", specifier = ">=1.2.2" },
558
558
  { name = "numpy", specifier = ">=1.21.0" },
559
559
  { name = "pandas", specifier = ">=2.2.3" },
560
- { name = "pybotters", specifier = ">=1.9.0" },
560
+ { name = "pybotters", specifier = ">=1.9.1" },
561
561
  { name = "pyecharts", specifier = ">=2.0.8" },
562
562
  ]
563
563
 
@@ -1221,15 +1221,15 @@ wheels = [
1221
1221
 
1222
1222
  [[package]]
1223
1223
  name = "pybotters"
1224
- version = "1.9.0"
1224
+ version = "1.9.1"
1225
1225
  source = { registry = "https://pypi.org/simple" }
1226
1226
  dependencies = [
1227
1227
  { name = "aiohttp" },
1228
1228
  { name = "typing-extensions", marker = "python_full_version < '3.10'" },
1229
1229
  ]
1230
- sdist = { url = "https://files.pythonhosted.org/packages/1c/32/c90531c4fab11030afba9343188b74bb56b009390e0b873af01a460e2cd2/pybotters-1.9.0.tar.gz", hash = "sha256:91f0d54ae60805ce408494f0ee0cb83c7d4a74f70d49eda8f550bde0aa71a7d1", size = 558643 }
1230
+ sdist = { url = "https://files.pythonhosted.org/packages/0c/c0/182e118d42a818565a5ee807cbce82f435d651ee6e3ab2344a378a285776/pybotters-1.9.1.tar.gz", hash = "sha256:60ebaa3cecd446efd52ed4515038cdacfde298a02137e51aad2185ccf1e5a4d6", size = 559033 }
1231
1231
  wheels = [
1232
- { url = "https://files.pythonhosted.org/packages/a0/6c/c382df909de72ad90bf9b319ccb67e7274a5bd2cb8f178c1322cf4b6da16/pybotters-1.9.0-py3-none-any.whl", hash = "sha256:91a70301e0b0e234351cda270638151a527bbe272d5de12466d8bb130f293ffd", size = 519494 },
1232
+ { url = "https://files.pythonhosted.org/packages/7f/7f/224b3fd8a1e991edc4d5b2d8aeeefb1b75f6c0f1a27eefa8e4ab82bc524d/pybotters-1.9.1-py3-none-any.whl", hash = "sha256:4d75c5b5a103e2fad4b320d8920c1e918c007ce9acb91a7c4e86672986225ebb", size = 519566 },
1233
1233
  ]
1234
1234
 
1235
1235
  [[package]]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes