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.
- hyperquant/__init__.py +8 -0
- hyperquant/broker/auth.py +972 -0
- hyperquant/broker/bitget.py +311 -0
- hyperquant/broker/bitmart.py +720 -0
- hyperquant/broker/coinw.py +487 -0
- hyperquant/broker/deepcoin.py +651 -0
- hyperquant/broker/edgex.py +500 -0
- hyperquant/broker/hyperliquid.py +570 -0
- hyperquant/broker/lbank.py +661 -0
- hyperquant/broker/lib/edgex_sign.py +455 -0
- hyperquant/broker/lib/hpstore.py +252 -0
- hyperquant/broker/lib/hyper_types.py +48 -0
- hyperquant/broker/lib/polymarket/ctfAbi.py +721 -0
- hyperquant/broker/lib/polymarket/safeAbi.py +1138 -0
- hyperquant/broker/lib/util.py +22 -0
- hyperquant/broker/lighter.py +679 -0
- hyperquant/broker/models/apexpro.py +150 -0
- hyperquant/broker/models/bitget.py +359 -0
- hyperquant/broker/models/bitmart.py +635 -0
- hyperquant/broker/models/coinw.py +724 -0
- hyperquant/broker/models/deepcoin.py +809 -0
- hyperquant/broker/models/edgex.py +1053 -0
- hyperquant/broker/models/hyperliquid.py +284 -0
- hyperquant/broker/models/lbank.py +557 -0
- hyperquant/broker/models/lighter.py +868 -0
- hyperquant/broker/models/ourbit.py +1155 -0
- hyperquant/broker/models/polymarket.py +1071 -0
- hyperquant/broker/ourbit.py +550 -0
- hyperquant/broker/polymarket.py +2399 -0
- hyperquant/broker/ws.py +132 -0
- hyperquant/core.py +513 -0
- hyperquant/datavison/_util.py +18 -0
- hyperquant/datavison/binance.py +111 -0
- hyperquant/datavison/coinglass.py +237 -0
- hyperquant/datavison/okx.py +177 -0
- hyperquant/db.py +191 -0
- hyperquant/draw.py +1200 -0
- hyperquant/logkit.py +205 -0
- hyperquant/notikit.py +124 -0
- hyperquant-1.48.dist-info/METADATA +32 -0
- hyperquant-1.48.dist-info/RECORD +42 -0
- hyperquant-1.48.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import aiohttp
|
|
2
|
+
import asyncio
|
|
3
|
+
from datetime import datetime, date, timedelta
|
|
4
|
+
# from _util import _to_milliseconds
|
|
5
|
+
from ._util import _to_milliseconds
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
class BinanceSwap:
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
self.session = aiohttp.ClientSession()
|
|
11
|
+
|
|
12
|
+
async def get_klines(self, symbol: str, interval: str, start_time, end_time = None, limit: int = 1500):
|
|
13
|
+
"""
|
|
14
|
+
获取U本位合约K线数据,支持获取任意长度(自动分批)
|
|
15
|
+
|
|
16
|
+
:param symbol: 交易对, 如 'BTCUSDT'
|
|
17
|
+
:param interval: K线间隔, 如 '1m', '5m', '1h', '1d'
|
|
18
|
+
:param start_time: 开始时间, 毫秒时间戳或datetime/date类型
|
|
19
|
+
:param end_time: 结束时间, 毫秒时间戳或datetime/date类型, 默认为None表示最新
|
|
20
|
+
:param limit: 每次请求最大K线数量, 最大1500
|
|
21
|
+
:return: K线数据DataFrame
|
|
22
|
+
"""
|
|
23
|
+
url = "https://fapi.binance.com/fapi/v1/klines"
|
|
24
|
+
all_klines = []
|
|
25
|
+
fetch_start = _to_milliseconds(start_time)
|
|
26
|
+
ms_end_time = _to_milliseconds(end_time) if end_time else None
|
|
27
|
+
while True:
|
|
28
|
+
params = {
|
|
29
|
+
"symbol": symbol.upper(),
|
|
30
|
+
"interval": interval,
|
|
31
|
+
"startTime": fetch_start,
|
|
32
|
+
"limit": limit
|
|
33
|
+
}
|
|
34
|
+
if ms_end_time:
|
|
35
|
+
params["endTime"] = ms_end_time
|
|
36
|
+
async with self.session.get(url, params=params) as resp:
|
|
37
|
+
resp.raise_for_status()
|
|
38
|
+
data = await resp.json()
|
|
39
|
+
if not data:
|
|
40
|
+
break
|
|
41
|
+
all_klines.extend(data)
|
|
42
|
+
if len(data) < limit:
|
|
43
|
+
break
|
|
44
|
+
last_open_time = data[-1][0]
|
|
45
|
+
fetch_start = last_open_time + 1
|
|
46
|
+
if ms_end_time and fetch_start >= ms_end_time:
|
|
47
|
+
break
|
|
48
|
+
# 转为DataFrame
|
|
49
|
+
columns = [
|
|
50
|
+
"open_time", "open", "high", "low", "close", "volume",
|
|
51
|
+
"close_time", "quote_asset_volume", "number_of_trades",
|
|
52
|
+
"taker_buy_base_asset_volume", "taker_buy_quote_asset_volume", "ignore"
|
|
53
|
+
]
|
|
54
|
+
df = pd.DataFrame(all_klines, columns=columns)
|
|
55
|
+
# 类型转换
|
|
56
|
+
df["open_time"] = pd.to_datetime(df["open_time"], unit="ms")
|
|
57
|
+
df["close_time"] = pd.to_datetime(df["close_time"], unit="ms")
|
|
58
|
+
for col in ["open", "high", "low", "close", "volume", "quote_asset_volume", "taker_buy_base_asset_volume", "taker_buy_quote_asset_volume"]:
|
|
59
|
+
df[col] = pd.to_numeric(df[col], errors="coerce")
|
|
60
|
+
df["number_of_trades"] = df["number_of_trades"].astype(int)
|
|
61
|
+
return df
|
|
62
|
+
|
|
63
|
+
async def get_index_klines(self, pair: str, interval: str, start_time, end_time=None, limit: int = 1500):
|
|
64
|
+
"""
|
|
65
|
+
获取U本位合约指数K线数据,支持获取任意长度(自动分批)
|
|
66
|
+
|
|
67
|
+
:param pair: 指数对, 如 'BTCUSDT'
|
|
68
|
+
:param interval: K线间隔, 如 '1m', '5m', '1h', '1d'
|
|
69
|
+
:param start_time: 开始时间, 毫秒时间戳或datetime/date类型
|
|
70
|
+
:param end_time: 结束时间, 毫秒时间戳或datetime/date类型, 默认为None表示最新
|
|
71
|
+
:param limit: 每次请求最大K线数量, 最大1500
|
|
72
|
+
:return: 指数K线数据DataFrame
|
|
73
|
+
"""
|
|
74
|
+
url = "https://fapi.binance.com/fapi/v1/indexPriceKlines"
|
|
75
|
+
all_klines = []
|
|
76
|
+
fetch_start = _to_milliseconds(start_time)
|
|
77
|
+
ms_end_time = _to_milliseconds(end_time) if end_time else None
|
|
78
|
+
while True:
|
|
79
|
+
params = {
|
|
80
|
+
"pair": pair.upper(),
|
|
81
|
+
"interval": interval,
|
|
82
|
+
"startTime": fetch_start,
|
|
83
|
+
"limit": limit
|
|
84
|
+
}
|
|
85
|
+
if ms_end_time:
|
|
86
|
+
params["endTime"] = ms_end_time
|
|
87
|
+
async with self.session.get(url, params=params) as resp:
|
|
88
|
+
resp.raise_for_status()
|
|
89
|
+
data = await resp.json()
|
|
90
|
+
if not data:
|
|
91
|
+
break
|
|
92
|
+
all_klines.extend(data)
|
|
93
|
+
if len(data) < limit:
|
|
94
|
+
break
|
|
95
|
+
last_open_time = data[-1][0]
|
|
96
|
+
fetch_start = last_open_time + 1
|
|
97
|
+
if ms_end_time and fetch_start >= ms_end_time:
|
|
98
|
+
break
|
|
99
|
+
columns = [
|
|
100
|
+
"open_time", "open", "high", "low", "close", "volume",
|
|
101
|
+
"close_time", "quote_asset_volume", "number_of_trades",
|
|
102
|
+
"taker_buy_base_asset_volume", "taker_buy_quote_asset_volume", "ignore"
|
|
103
|
+
]
|
|
104
|
+
df = pd.DataFrame(all_klines, columns=columns)
|
|
105
|
+
df["open_time"] = pd.to_datetime(df["open_time"], unit="ms")
|
|
106
|
+
df["close_time"] = pd.to_datetime(df["close_time"], unit="ms")
|
|
107
|
+
for col in ["open", "high", "low", "close", "volume", "quote_asset_volume", "taker_buy_base_asset_volume", "taker_buy_quote_asset_volume"]:
|
|
108
|
+
df[col] = pd.to_numeric(df[col], errors="coerce")
|
|
109
|
+
df["number_of_trades"] = df["number_of_trades"].astype(int)
|
|
110
|
+
return df
|
|
111
|
+
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Coinglass API 数据解密与数据获取模块
|
|
3
|
+
优化:
|
|
4
|
+
- 只保留必要依赖,整理import顺序
|
|
5
|
+
- 增加类型注解和文档
|
|
6
|
+
- 统一异常处理和日志输出
|
|
7
|
+
- 精简冗余代码
|
|
8
|
+
"""
|
|
9
|
+
import asyncio
|
|
10
|
+
import base64
|
|
11
|
+
import json
|
|
12
|
+
import struct
|
|
13
|
+
import time
|
|
14
|
+
import zlib
|
|
15
|
+
import hmac
|
|
16
|
+
import hashlib
|
|
17
|
+
from datetime import datetime, timedelta
|
|
18
|
+
from typing import Any, Dict, List, Optional, Union
|
|
19
|
+
import pandas as pd # type: ignore
|
|
20
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
21
|
+
from cryptography.hazmat.primitives import padding as crypto_padding
|
|
22
|
+
from cryptography.hazmat.backends import default_backend
|
|
23
|
+
import aiohttp
|
|
24
|
+
|
|
25
|
+
# ------------------ 工具函数 ------------------
|
|
26
|
+
class CustomParser:
|
|
27
|
+
@staticmethod
|
|
28
|
+
def parse(data: str) -> Dict[str, Union[List[int], int]]:
|
|
29
|
+
"""
|
|
30
|
+
将字符串转换为数字数组,兼容原始加密逻辑。
|
|
31
|
+
"""
|
|
32
|
+
length = len(data)
|
|
33
|
+
n = [0] * ((length + 3) // 4)
|
|
34
|
+
for r in range(length):
|
|
35
|
+
n[r >> 2] |= (ord(data[r]) & 255) << (24 - (r % 4) * 8)
|
|
36
|
+
return {"n": n, "e": length}
|
|
37
|
+
|
|
38
|
+
def convert_words_to_bytes(words: List[int]) -> bytes:
|
|
39
|
+
"""
|
|
40
|
+
将整数数组转换为字节数组。
|
|
41
|
+
"""
|
|
42
|
+
return b"".join(struct.pack(">I", word) for word in words)
|
|
43
|
+
|
|
44
|
+
def decrypt_and_clean(t: str, e: Dict[str, Any]) -> str:
|
|
45
|
+
"""
|
|
46
|
+
解密、解压缩并清理输入字符串。
|
|
47
|
+
"""
|
|
48
|
+
aes_key = convert_words_to_bytes(e['n'])
|
|
49
|
+
cipher = Cipher(algorithms.AES(aes_key), modes.ECB(), backend=default_backend())
|
|
50
|
+
decryptor = cipher.decryptor()
|
|
51
|
+
encrypted_data = base64.b64decode(t)
|
|
52
|
+
decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize()
|
|
53
|
+
unpadder = crypto_padding.PKCS7(128).unpadder()
|
|
54
|
+
unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
|
|
55
|
+
decompressed_data = zlib.decompress(unpadded_data, wbits=16 + zlib.MAX_WBITS).decode('utf-8')
|
|
56
|
+
return decompressed_data
|
|
57
|
+
|
|
58
|
+
def generate_totp(secret: str, for_time: int, interval: int = 30, digits: int = 6, digest=hashlib.sha1) -> str:
|
|
59
|
+
"""
|
|
60
|
+
基于标准库的TOTP实现。
|
|
61
|
+
"""
|
|
62
|
+
key = base64.b32decode(secret, casefold=True)
|
|
63
|
+
counter = int(for_time // interval)
|
|
64
|
+
msg = counter.to_bytes(8, 'big')
|
|
65
|
+
h = hmac.new(key, msg, digest).digest()
|
|
66
|
+
o = h[-1] & 0x0F
|
|
67
|
+
code = (struct.unpack('>I', h[o:o+4])[0] & 0x7fffffff) % (10 ** digits)
|
|
68
|
+
return str(code).zfill(digits)
|
|
69
|
+
|
|
70
|
+
def generate_encrypted_token() -> str:
|
|
71
|
+
"""
|
|
72
|
+
生成加密token,用于API请求。
|
|
73
|
+
"""
|
|
74
|
+
current_time = int(time.time())
|
|
75
|
+
secret_key = "I65VU7K5ZQL7WB4E"
|
|
76
|
+
otp = generate_totp(secret_key, current_time)
|
|
77
|
+
combined_string = f"{current_time},{otp}"
|
|
78
|
+
aes_key = "1f68efd73f8d4921acc0dead41dd39bc"
|
|
79
|
+
aes_key_bytes = CustomParser.parse(aes_key)
|
|
80
|
+
final_key = convert_words_to_bytes(aes_key_bytes['n'])
|
|
81
|
+
cipher = Cipher(algorithms.AES(final_key), modes.ECB(), backend=default_backend())
|
|
82
|
+
encryptor = cipher.encryptor()
|
|
83
|
+
padder = crypto_padding.PKCS7(128).padder()
|
|
84
|
+
padded_data = padder.update(combined_string.encode('utf-8')) + padder.finalize()
|
|
85
|
+
encrypted_bytes = encryptor.update(padded_data) + encryptor.finalize()
|
|
86
|
+
return base64.b64encode(encrypted_bytes).decode('utf-8')
|
|
87
|
+
|
|
88
|
+
def decrypt_coinglass(data: str, user_header: str, url: str) -> str:
|
|
89
|
+
"""
|
|
90
|
+
解密 Coinglass API 的响应数据。
|
|
91
|
+
"""
|
|
92
|
+
base_key = base64.b64encode(f"coinglass{url}coinglass".encode("utf-8")).decode("utf-8")[:16]
|
|
93
|
+
processed_key = CustomParser.parse(base_key)
|
|
94
|
+
decrypted_key = decrypt_and_clean(user_header, processed_key)
|
|
95
|
+
session_key = decrypt_and_clean(data, CustomParser.parse(decrypted_key))
|
|
96
|
+
return session_key
|
|
97
|
+
|
|
98
|
+
# ------------------ API类 ------------------
|
|
99
|
+
HEADERS = {
|
|
100
|
+
'accept': 'application/json',
|
|
101
|
+
'accept-language': 'en-US,en;q=0.9',
|
|
102
|
+
'cache-ts': str(int(time.time() * 1000)),
|
|
103
|
+
'dnt': '1',
|
|
104
|
+
'encryption': 'true',
|
|
105
|
+
'language': 'en',
|
|
106
|
+
'origin': 'https://www.coinglass.com',
|
|
107
|
+
'priority': 'u=1, i',
|
|
108
|
+
'referer': 'https://www.coinglass.com/',
|
|
109
|
+
'sec-ch-ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
|
|
110
|
+
'sec-ch-ua-mobile': '?0',
|
|
111
|
+
'sec-ch-ua-platform': '"Windows"',
|
|
112
|
+
'sec-fetch-dest': 'empty',
|
|
113
|
+
'sec-fetch-mode': 'cors',
|
|
114
|
+
'sec-fetch-site': 'same-site',
|
|
115
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class CoinglassApi:
|
|
119
|
+
def __init__(self) -> None:
|
|
120
|
+
self.session = aiohttp.ClientSession()
|
|
121
|
+
|
|
122
|
+
async def connect(self):
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
async def dec_data(self, response: aiohttp.ClientResponse) -> Optional[Any]:
|
|
126
|
+
try:
|
|
127
|
+
encrypted_data = (await response.json())['data']
|
|
128
|
+
requests_url = response.url.path
|
|
129
|
+
encrypted_user_header = response.headers.get("user", "HEADERNOTFOUND")
|
|
130
|
+
decrypted = decrypt_coinglass(encrypted_data, encrypted_user_header, requests_url)
|
|
131
|
+
return json.loads(decrypted)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
print(f"解密失败: {e}")
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
async def fetch_base_klines(self, symbol: str, start_time: datetime, end_time: Optional[datetime] = None, ktype: str = '#coin#oi_kline', interval: str = 'm1') -> Any:
|
|
137
|
+
start_ts = int(start_time.timestamp())
|
|
138
|
+
end_ts = int(end_time.timestamp()) if end_time else int(time.time())
|
|
139
|
+
url = 'https://fapi.coinglass.com/api/v2/kline'
|
|
140
|
+
params = {
|
|
141
|
+
'symbol': f'{symbol}{ktype}',
|
|
142
|
+
'interval': interval,
|
|
143
|
+
'endTime': end_ts,
|
|
144
|
+
'startTime': start_ts,
|
|
145
|
+
'minLimit': 'false',
|
|
146
|
+
}
|
|
147
|
+
async with self.session.get(url, params=params, headers=HEADERS) as response:
|
|
148
|
+
if response.status == 200:
|
|
149
|
+
return await self.dec_data(response)
|
|
150
|
+
print(f"请求失败,状态码: {response.status}: {await response.text()}")
|
|
151
|
+
return pd.DataFrame()
|
|
152
|
+
|
|
153
|
+
async def fetch_price_klines(self, symbol: str, start_time: datetime, end_time: Optional[datetime] = None, interval: str = 'm5') -> pd.DataFrame:
|
|
154
|
+
data = await self.fetch_base_klines(symbol, start_time, end_time, '#kline', interval)
|
|
155
|
+
df = pd.DataFrame(data, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
|
|
156
|
+
df[['open', 'high', 'low', 'close', 'volume']] = df[['open', 'high', 'low', 'close', 'volume']].astype(float)
|
|
157
|
+
df['timestamp'] = pd.to_datetime(df['timestamp'], unit="s", utc=True)
|
|
158
|
+
df['symbol'] = symbol
|
|
159
|
+
return df
|
|
160
|
+
|
|
161
|
+
async def fetch_top_account_klines(self, symbol: str, start_time: datetime, end_time: Optional[datetime] = None, interval: str = 'm5') -> Optional[pd.DataFrame]:
|
|
162
|
+
end_ts = int(end_time.timestamp()) if end_time else int(time.time())
|
|
163
|
+
start_ts = int(start_time.timestamp())
|
|
164
|
+
url = 'https://fapi.coinglass.com/api/v2/kline'
|
|
165
|
+
params = {
|
|
166
|
+
'symbol': f'{symbol}#top_account_kline',
|
|
167
|
+
'interval': interval,
|
|
168
|
+
'endTime': end_ts,
|
|
169
|
+
'startTime': start_ts,
|
|
170
|
+
'minLimit': 'false',
|
|
171
|
+
}
|
|
172
|
+
async with self.session.get(url, params=params, headers=HEADERS) as response:
|
|
173
|
+
if response.status == 200:
|
|
174
|
+
data = await self.dec_data(response)
|
|
175
|
+
columns = ['timestamp', 'ratio', 'long_ratio', 'short_ratio']
|
|
176
|
+
df = pd.DataFrame(data, columns=columns)
|
|
177
|
+
df['timestamp'] = pd.to_datetime(df['timestamp'], unit="s", utc=True)
|
|
178
|
+
df[['ratio', 'long_ratio', 'short_ratio']] = df[['ratio', 'long_ratio', 'short_ratio']].astype(float)
|
|
179
|
+
df['symbol'] = symbol
|
|
180
|
+
return df
|
|
181
|
+
print(f"请求失败,状态码: {response.status}: {await response.text()}")
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
async def fetch_oi_klines(self, symbol: str, start_time: datetime, end_time: Optional[datetime] = None, interval: str = 'm5') -> pd.DataFrame:
|
|
185
|
+
data = await self.fetch_base_klines(symbol, start_time, end_time, '#coin#oi_kline', interval)
|
|
186
|
+
df = pd.DataFrame(data, columns=['timestamp', 'open', 'high', 'low', 'close'])
|
|
187
|
+
df[['open', 'high', 'low', 'close']] = df[['open', 'high', 'low', 'close']].astype(float)
|
|
188
|
+
df['timestamp'] = pd.to_datetime(df['timestamp'], unit="s", utc=True)
|
|
189
|
+
df['symbol'] = symbol
|
|
190
|
+
return df
|
|
191
|
+
|
|
192
|
+
async def fetch_liq_klines(self, symbol: str, start_time: datetime, end_time: Optional[datetime] = None, interval: str = 'm5') -> pd.DataFrame:
|
|
193
|
+
data = await self.fetch_base_klines(symbol, start_time, end_time, '#aggregated_liq_kline', interval)
|
|
194
|
+
df = pd.DataFrame(data, columns=['timestamp', 'short_amount', 'long_amount'])
|
|
195
|
+
df[['short_amount', 'long_amount']] = df[['short_amount', 'long_amount']].astype(float)
|
|
196
|
+
df['timestamp'] = pd.to_datetime(df['timestamp'], unit="s", utc=True)
|
|
197
|
+
df['symbol'] = symbol
|
|
198
|
+
return df
|
|
199
|
+
|
|
200
|
+
async def fetch_hyperliquid_top_positions(self) -> Optional[Any]:
|
|
201
|
+
url = 'https://capi.coinglass.com/api/hyperliquid/topPosition'
|
|
202
|
+
async with self.session.get(url, headers=HEADERS) as response:
|
|
203
|
+
if response.status == 200:
|
|
204
|
+
return await self.dec_data(response)
|
|
205
|
+
print(f"请求失败,状态码: {response.status}: {await response.text()}")
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
async def fetch_tickers(self) -> Optional[Any]:
|
|
209
|
+
url = 'https://fapi.coinglass.com/api/select/coins/tickers'
|
|
210
|
+
params = {'exName': 'Binance'}
|
|
211
|
+
async with self.session.get(url, params=params, headers=HEADERS) as response:
|
|
212
|
+
if response.status == 200:
|
|
213
|
+
return await self.dec_data(response)
|
|
214
|
+
print(f"请求失败,状态码: {response.status}: {await response.text()}")
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
async def fetch_symbols(self, only_usdt: bool = True) -> List[str]:
|
|
218
|
+
tickers = await self.fetch_tickers()
|
|
219
|
+
if tickers:
|
|
220
|
+
symbols = [ticker['instrument']['instrumentId'] for ticker in tickers]
|
|
221
|
+
if only_usdt:
|
|
222
|
+
symbols = [symbol for symbol in symbols if symbol.endswith('USDT')]
|
|
223
|
+
return symbols
|
|
224
|
+
return []
|
|
225
|
+
|
|
226
|
+
async def stop(self):
|
|
227
|
+
await self.session.close()
|
|
228
|
+
|
|
229
|
+
# ------------------ 主程序 ------------------
|
|
230
|
+
async def main():
|
|
231
|
+
api = CoinglassApi()
|
|
232
|
+
df = await api.fetch_price_klines('Binance_BTCUSDT', datetime.now() - timedelta(days=1))
|
|
233
|
+
print(df)
|
|
234
|
+
await api.stop()
|
|
235
|
+
|
|
236
|
+
if __name__ == '__main__':
|
|
237
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import aiohttp
|
|
2
|
+
import asyncio
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import time
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from ._util import _to_milliseconds # 确保时间转换函数可用
|
|
7
|
+
|
|
8
|
+
class OKX:
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
self.session = aiohttp.ClientSession()
|
|
11
|
+
self.base_url = "https://www.okx.com/api/v5/market"
|
|
12
|
+
|
|
13
|
+
async def get_klines(self, symbol: str, interval: str, start_time, end_time=None, limit: int = 100):
|
|
14
|
+
"""
|
|
15
|
+
获取 OKX 永续合约 K 线数据,带时间过滤,从 end_time 向 start_time 方向翻页。
|
|
16
|
+
|
|
17
|
+
:param symbol: 交易对, 如 'BTC-USDT'
|
|
18
|
+
:param interval: K 线间隔, 如 '1m', '15m', '1H', '4H', '1D'
|
|
19
|
+
:param start_time: 开始时间(datetime 或 毫秒)
|
|
20
|
+
:param end_time: 结束时间(datetime 或 毫秒), 可选
|
|
21
|
+
:param limit: 每次请求最大数量(OKX 最大 300)
|
|
22
|
+
:return: DataFrame 格式的 K 线数据,按时间升序
|
|
23
|
+
"""
|
|
24
|
+
if 'h' in interval or 'd' in interval:
|
|
25
|
+
interval = interval.upper() # 确保间隔是大写格式
|
|
26
|
+
|
|
27
|
+
url = f"{self.base_url}/history-candles"
|
|
28
|
+
all_rows = []
|
|
29
|
+
# 转换 start_time 和 end_time 到毫秒时间戳
|
|
30
|
+
if isinstance(start_time, (int, float)):
|
|
31
|
+
start_ts = int(start_time)
|
|
32
|
+
else:
|
|
33
|
+
# 处理 datetime 对象
|
|
34
|
+
start_ts = int(start_time.timestamp() * 1000)
|
|
35
|
+
if end_time:
|
|
36
|
+
if isinstance(end_time, (int, float)):
|
|
37
|
+
end_ts = int(end_time)
|
|
38
|
+
else:
|
|
39
|
+
end_ts = int(end_time.timestamp() * 1000)
|
|
40
|
+
else:
|
|
41
|
+
# 如果没有指定结束时间,就用当前时间戳
|
|
42
|
+
end_ts = int(time.time() * 1000)
|
|
43
|
+
|
|
44
|
+
# 每次请求最多返回 limit=300
|
|
45
|
+
batch_limit = min(limit, 300)
|
|
46
|
+
# 初始 after 参数为 end_ts,向过去翻页
|
|
47
|
+
current_after = end_ts
|
|
48
|
+
|
|
49
|
+
while True:
|
|
50
|
+
params = {
|
|
51
|
+
"instId": symbol,
|
|
52
|
+
"bar": interval,
|
|
53
|
+
"limit": str(batch_limit),
|
|
54
|
+
"after": str(current_after)
|
|
55
|
+
}
|
|
56
|
+
# 发送请求
|
|
57
|
+
async with self.session.get(url, params=params) as resp:
|
|
58
|
+
data = await resp.json()
|
|
59
|
+
if not data or data.get("code") != "0" or not data.get("data"):
|
|
60
|
+
# 返回错误或无数据,结束循环
|
|
61
|
+
break
|
|
62
|
+
buf = data["data"] # 每条是 [ts, o, h, l, c, vol, volCcy, volCcyQuote, confirm]
|
|
63
|
+
|
|
64
|
+
# 本批数据按时间从新到旧排列, 最后一条是最旧的
|
|
65
|
+
rows_this_batch = []
|
|
66
|
+
for item in buf:
|
|
67
|
+
ts = int(item[0])
|
|
68
|
+
# 如果已经早于 start_ts,就跳过,并认为后面更旧,也可以结束循环
|
|
69
|
+
if ts < start_ts:
|
|
70
|
+
continue
|
|
71
|
+
# 如果某些条目时间超出 end_ts,也跳过
|
|
72
|
+
if ts > end_ts:
|
|
73
|
+
continue
|
|
74
|
+
# 解析数值字段
|
|
75
|
+
dt = pd.to_datetime(ts, unit='ms', utc=True)
|
|
76
|
+
o = float(item[1]); h = float(item[2]); l = float(item[3]); c = float(item[4]); vol = float(item[5])
|
|
77
|
+
# 按需把每个 K 线封装为字典,后续转换为 DataFrame
|
|
78
|
+
rows_this_batch.append({
|
|
79
|
+
"symbol": symbol,
|
|
80
|
+
"open_time": dt,
|
|
81
|
+
"open": o,
|
|
82
|
+
"high": h,
|
|
83
|
+
"low": l,
|
|
84
|
+
"close": c,
|
|
85
|
+
"volume": vol,
|
|
86
|
+
"interval": interval,
|
|
87
|
+
"confirm": item[8]
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
if not rows_this_batch:
|
|
91
|
+
# 本批没有符合时间范围的数据,直接结束
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
# 累积本批符合条件的行
|
|
95
|
+
all_rows.extend(rows_this_batch)
|
|
96
|
+
|
|
97
|
+
# 检查是否到达 start_ts 之前:buf 最后一项是最旧
|
|
98
|
+
oldest_ts = int(buf[-1][0])
|
|
99
|
+
if oldest_ts < start_ts:
|
|
100
|
+
# 已经翻到 start_time 范围之前,结束循环
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
# 否则,更新 after = oldest_ts,继续向过去翻页
|
|
104
|
+
current_after = oldest_ts
|
|
105
|
+
# 为了不触发速率限制,稍做休眠(根据需要可以调整或删除)
|
|
106
|
+
|
|
107
|
+
# 如果累积到数据,则转换为 DataFrame;否则返回空 DataFrame
|
|
108
|
+
if all_rows:
|
|
109
|
+
df = pd.DataFrame(all_rows)
|
|
110
|
+
# 去重、按时间排序
|
|
111
|
+
df.drop_duplicates(subset=["open_time"], inplace=True)
|
|
112
|
+
df.sort_values("open_time", inplace=True)
|
|
113
|
+
df.reset_index(drop=True, inplace=True)
|
|
114
|
+
return df
|
|
115
|
+
else:
|
|
116
|
+
return pd.DataFrame()
|
|
117
|
+
|
|
118
|
+
async def get_index_klines(self, pair: str, interval: str, start_time, end_time=None, limit: int = 100):
|
|
119
|
+
"""
|
|
120
|
+
获取OKX指数K线数据(自动分批)
|
|
121
|
+
|
|
122
|
+
:param pair: 指数名称, 如 'BTC-USD'
|
|
123
|
+
:param interval: K线间隔, 如 '1m', '1H', '1D'
|
|
124
|
+
:param start_time: 开始时间(毫秒时间戳/datetime/date)
|
|
125
|
+
:param end_time: 结束时间(毫秒时间戳/datetime/date)
|
|
126
|
+
:param limit: 每次请求最大数量(OKX最大300)
|
|
127
|
+
:return: DataFrame格式的指数K线
|
|
128
|
+
"""
|
|
129
|
+
url = f"{self.base_url}/index-candles"
|
|
130
|
+
all_klines = []
|
|
131
|
+
ms_start = _to_milliseconds(start_time)
|
|
132
|
+
ms_end = _to_milliseconds(end_time) if end_time else None
|
|
133
|
+
|
|
134
|
+
params = {
|
|
135
|
+
"instId": pair,
|
|
136
|
+
"bar": interval,
|
|
137
|
+
"limit": min(limit, 300),
|
|
138
|
+
"after": ms_start
|
|
139
|
+
}
|
|
140
|
+
if ms_end:
|
|
141
|
+
params["before"] = ms_end
|
|
142
|
+
|
|
143
|
+
while True:
|
|
144
|
+
async with self.session.get(url, params=params) as resp:
|
|
145
|
+
data = await resp.json()
|
|
146
|
+
if data['code'] != "0":
|
|
147
|
+
raise Exception(f"OKX API Error: {data['msg']} (Code {data['code']})")
|
|
148
|
+
|
|
149
|
+
klines = data['data']
|
|
150
|
+
if not klines:
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
all_klines.extend(klines)
|
|
154
|
+
|
|
155
|
+
if len(klines) < params["limit"]:
|
|
156
|
+
break
|
|
157
|
+
|
|
158
|
+
last_ts = int(klines[-1][0])
|
|
159
|
+
params["after"] = last_ts
|
|
160
|
+
|
|
161
|
+
# 数据转换
|
|
162
|
+
columns = ["open_time", "open", "high", "low", "close", "confirm"]
|
|
163
|
+
df = pd.DataFrame(all_klines, columns=columns)
|
|
164
|
+
|
|
165
|
+
df["open_time"] = pd.to_datetime(df["open_time"].astype(int), unit="ms")
|
|
166
|
+
num_cols = ["open", "high", "low", "close"]
|
|
167
|
+
df[num_cols] = df[num_cols].apply(pd.to_numeric, errors="coerce")
|
|
168
|
+
|
|
169
|
+
return df.sort_values("open_time").reset_index(drop=True)
|
|
170
|
+
|
|
171
|
+
async def close(self):
|
|
172
|
+
"""关闭会话"""
|
|
173
|
+
await self.session.close()
|
|
174
|
+
|
|
175
|
+
# 使用示例
|
|
176
|
+
# async with OKXSwap() as okx:
|
|
177
|
+
# df = await okx.get_klines("BTC-USDT", "1H", datetime(2023,1,1))
|