fasttq-sdk 0.2.0__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.
- fasttq_sdk-0.2.0/PKG-INFO +8 -0
- fasttq_sdk-0.2.0/README.md +0 -0
- fasttq_sdk-0.2.0/pyproject.toml +22 -0
- fasttq_sdk-0.2.0/setup.cfg +4 -0
- fasttq_sdk-0.2.0/src/fasttq/__init__.py +0 -0
- fasttq_sdk-0.2.0/src/fasttq/common.py +17 -0
- fasttq_sdk-0.2.0/src/fasttq/download.py +193 -0
- fasttq_sdk-0.2.0/src/fasttq/instrument.py +119 -0
- fasttq_sdk-0.2.0/src/fasttq/util.py +13 -0
- fasttq_sdk-0.2.0/src/fasttq_sdk.egg-info/PKG-INFO +8 -0
- fasttq_sdk-0.2.0/src/fasttq_sdk.egg-info/SOURCES.txt +12 -0
- fasttq_sdk-0.2.0/src/fasttq_sdk.egg-info/dependency_links.txt +1 -0
- fasttq_sdk-0.2.0/src/fasttq_sdk.egg-info/requires.txt +1 -0
- fasttq_sdk-0.2.0/src/fasttq_sdk.egg-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=80.0.0", "wheel>=0.36.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fasttq-sdk"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Add your description here"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"websockets>=13.0",
|
|
13
|
+
]
|
|
14
|
+
authors = [
|
|
15
|
+
{name = "Lingqiao Zhao", email = "forever.3g@hotmail.com"}
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.packages.find]
|
|
19
|
+
where = ["src/"]
|
|
20
|
+
|
|
21
|
+
[tool.uv]
|
|
22
|
+
index-url = 'https://pypi.tuna.tsinghua.edu.cn/simple'
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import secrets
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
_RD = random.Random(secrets.randbits(128))
|
|
6
|
+
_BATCH_SIZE = 2000
|
|
7
|
+
_DEFAULT_TIMEOUT = 2
|
|
8
|
+
_FREE_URL = 'wss://free-api.shinnytech.com/t/nfmd/front/mobile'
|
|
9
|
+
_PRO_URL = 'wss://api.shinnytech.com/t/nfmd/front/mobile'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def gen_uuid(prefix: str):
|
|
13
|
+
return f"{prefix}_{_RD.getrandbits(128):032x}"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def peek(client):
|
|
17
|
+
await client.send('{"aid": "peek_message"}')
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import datetime
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import websockets
|
|
6
|
+
from fasttq.common import gen_uuid, peek, _BATCH_SIZE, _DEFAULT_TIMEOUT, _PRO_URL
|
|
7
|
+
from fasttq.util import get_td_start, get_td_end
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_TICK_FIELDS = [
|
|
11
|
+
'datetime',
|
|
12
|
+
'last_price',
|
|
13
|
+
'open_interest',
|
|
14
|
+
'volume',
|
|
15
|
+
'amount',
|
|
16
|
+
'highest',
|
|
17
|
+
'lowest',
|
|
18
|
+
'average',
|
|
19
|
+
'ask_price1',
|
|
20
|
+
'ask_volume1',
|
|
21
|
+
'ask_price2',
|
|
22
|
+
'ask_volume2',
|
|
23
|
+
'ask_price3',
|
|
24
|
+
'ask_volume3',
|
|
25
|
+
'ask_price4',
|
|
26
|
+
'ask_volume4',
|
|
27
|
+
'ask_price5',
|
|
28
|
+
'ask_volume5',
|
|
29
|
+
'bid_price1',
|
|
30
|
+
'bid_volume1',
|
|
31
|
+
'bid_price2',
|
|
32
|
+
'bid_volume2',
|
|
33
|
+
'bid_price3',
|
|
34
|
+
'bid_volume3',
|
|
35
|
+
'bid_price4',
|
|
36
|
+
'bid_volume4',
|
|
37
|
+
'bid_price5',
|
|
38
|
+
'bid_volume5'
|
|
39
|
+
]
|
|
40
|
+
_BAR_FIELDS = [
|
|
41
|
+
'datetime',
|
|
42
|
+
'open',
|
|
43
|
+
'high',
|
|
44
|
+
'low',
|
|
45
|
+
'close',
|
|
46
|
+
'volume',
|
|
47
|
+
'open_oi',
|
|
48
|
+
'close_oi'
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _gen_uuid():
|
|
53
|
+
return gen_uuid("PYSDK_downloader")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def _release_chart(client, uuid: str):
|
|
57
|
+
o = {
|
|
58
|
+
"aid": "set_chart",
|
|
59
|
+
"chart_id": uuid,
|
|
60
|
+
"ins_list": "",
|
|
61
|
+
"duration": 0,
|
|
62
|
+
"view_width": _BATCH_SIZE
|
|
63
|
+
}
|
|
64
|
+
await client.send(json.dumps(o))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def _request_start(client, uuid: str, ins: str, duration: int, start_nano: int):
|
|
68
|
+
o = {
|
|
69
|
+
"aid": "set_chart",
|
|
70
|
+
"chart_id": uuid,
|
|
71
|
+
"ins_list": ins,
|
|
72
|
+
"duration": duration,
|
|
73
|
+
"view_width": _BATCH_SIZE,
|
|
74
|
+
"focus_datetime": start_nano,
|
|
75
|
+
"focus_position": 0
|
|
76
|
+
}
|
|
77
|
+
await client.send(json.dumps(o))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def _request_next(client, uuid: str, ins: str, duration: int, left: int):
|
|
81
|
+
o = {
|
|
82
|
+
"aid": "set_chart",
|
|
83
|
+
"chart_id": uuid,
|
|
84
|
+
"ins_list": ins,
|
|
85
|
+
"duration": duration,
|
|
86
|
+
"view_width": _BATCH_SIZE,
|
|
87
|
+
"left_kline_id": left
|
|
88
|
+
}
|
|
89
|
+
await client.send(json.dumps(o))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def _recv_data_chunk(client, uuid: str, ins: str, duration: int, end_nano: int):
|
|
93
|
+
res = []
|
|
94
|
+
right_id = 0
|
|
95
|
+
|
|
96
|
+
while True:
|
|
97
|
+
await peek(client)
|
|
98
|
+
try:
|
|
99
|
+
msg = await asyncio.wait_for(client.recv(), _DEFAULT_TIMEOUT)
|
|
100
|
+
except asyncio.TimeoutError:
|
|
101
|
+
# print('Timeout') # a timeout means no data available
|
|
102
|
+
return 0, res
|
|
103
|
+
|
|
104
|
+
rtn_records = json.loads(msg)['data']
|
|
105
|
+
|
|
106
|
+
data_dict = {}
|
|
107
|
+
meta_data = None
|
|
108
|
+
has_data = False
|
|
109
|
+
|
|
110
|
+
for record in rtn_records:
|
|
111
|
+
if 'charts' in record and uuid in record['charts']:
|
|
112
|
+
meta_data = record['charts'][uuid]
|
|
113
|
+
|
|
114
|
+
if 'ticks' in record and duration == 0:
|
|
115
|
+
data = record['ticks']
|
|
116
|
+
if ins not in data or 'data' not in data[ins]:
|
|
117
|
+
continue
|
|
118
|
+
has_data = True
|
|
119
|
+
data_dict.update(data[ins]['data'])
|
|
120
|
+
elif 'klines' in record and duration != 0:
|
|
121
|
+
data = record['klines']
|
|
122
|
+
if ins not in data or str(duration) not in data[ins] or 'data' not in data[ins][str(duration)]:
|
|
123
|
+
continue
|
|
124
|
+
has_data = True
|
|
125
|
+
data_dict.update(data[ins][str(duration)]['data'])
|
|
126
|
+
|
|
127
|
+
if not has_data:
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
if not right_id: # first valid meta data always returns left/right ids
|
|
131
|
+
right_id = meta_data['right_id']
|
|
132
|
+
|
|
133
|
+
for line_id, data in data_dict.items():
|
|
134
|
+
line_id = int(line_id)
|
|
135
|
+
if line_id > right_id or data is None or data['datetime'] > end_nano:
|
|
136
|
+
continue
|
|
137
|
+
res.append(data)
|
|
138
|
+
|
|
139
|
+
if not meta_data['more_data']:
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
return right_id + 1, res
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def _download_single(client, ins: str, start_nano: int, end_nano: int, path: str, duration: int):
|
|
146
|
+
uuid = _gen_uuid()
|
|
147
|
+
fields = _BAR_FIELDS if duration else _TICK_FIELDS
|
|
148
|
+
|
|
149
|
+
with open(path, 'w') as fp:
|
|
150
|
+
fp.write(','.join(fields))
|
|
151
|
+
fp.write('\n')
|
|
152
|
+
|
|
153
|
+
await _request_start(client, uuid, ins, duration, start_nano)
|
|
154
|
+
|
|
155
|
+
nxt_left, data = await _recv_data_chunk(client, uuid, ins, duration, end_nano)
|
|
156
|
+
while len(data) >= _BATCH_SIZE:
|
|
157
|
+
data.sort(key=lambda x: x['datetime'])
|
|
158
|
+
for line in data:
|
|
159
|
+
fp.write(','.join([str(line[f]) if f in line else '' for f in fields]))
|
|
160
|
+
fp.write('\n')
|
|
161
|
+
await _request_next(client, uuid, ins, duration, nxt_left)
|
|
162
|
+
nxt_left, data = await _recv_data_chunk(client, uuid, ins, duration, end_nano)
|
|
163
|
+
|
|
164
|
+
data.sort(key=lambda x: x['datetime'])
|
|
165
|
+
|
|
166
|
+
# write remaining
|
|
167
|
+
for line in data:
|
|
168
|
+
fp.write(','.join([str(line[f]) if f in line else '' for f in fields]))
|
|
169
|
+
fp.write('\n')
|
|
170
|
+
|
|
171
|
+
await _release_chart(client, uuid)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
async def _download(ins_list: list, start: int, end: int, target_folder: str, period: int = 0):
|
|
175
|
+
start_nano = start * int(1e6)
|
|
176
|
+
end_nano = end * int(1e6)
|
|
177
|
+
duration = period * int(1e9)
|
|
178
|
+
|
|
179
|
+
async with websockets.connect(_PRO_URL, max_size=None) as client:
|
|
180
|
+
await client.recv()
|
|
181
|
+
for ins in ins_list:
|
|
182
|
+
await _download_single(client, ins, start_nano, end_nano, os.path.join(target_folder, ins), duration)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def download(ins_list: list, start: int, end: int, target_folder: str, period: int = 0):
|
|
186
|
+
asyncio.get_event_loop().run_until_complete(_download(ins_list, start, end, target_folder, period))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def download_td(ins_list: list, td: int, target_folder: str, period: int = 0):
|
|
190
|
+
epoch = int(datetime.datetime(td // 10000, td // 100 % 100, td % 100).timestamp() * 1e3)
|
|
191
|
+
start = get_td_start(epoch)
|
|
192
|
+
end = get_td_end(epoch)
|
|
193
|
+
return download(ins_list, start, end, target_folder, period)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import websockets
|
|
4
|
+
from fasttq.common import gen_uuid, peek, _FREE_URL, _DEFAULT_TIMEOUT
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_StrListArg = list | str | None
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_short_query = """
|
|
11
|
+
... on basic { instrument_id }
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_full_query = """
|
|
16
|
+
... on basic { class trading_time {day night} instrument_id instrument_name price_tick }
|
|
17
|
+
... on stock { stock_dividend_ratio cash_dividend_ratio }
|
|
18
|
+
... on fund { cash_dividend_ratio }
|
|
19
|
+
... on tradeable { volume_multiple quote_multiple upper_limit lower_limit }
|
|
20
|
+
... on bond { maturity_datetime }
|
|
21
|
+
... on index { index_multiple}
|
|
22
|
+
... on securities { currency face_value first_trading_datetime buy_volume_unit sell_volume_unit status }
|
|
23
|
+
... on future { product_id delivery_year delivery_month expire_datetime }
|
|
24
|
+
... on option { product_short_name expire_datetime last_exercise_datetime strike_price call_or_put exercise_type}
|
|
25
|
+
... on derivative {
|
|
26
|
+
underlying {
|
|
27
|
+
count edges { underlying_multiple node { ... on basic { instrument_id instrument_name } } }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _gen_uuid():
|
|
34
|
+
return gen_uuid("PYSDK_api")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def _send_query(client, uuid: str, query: str, variables: dict):
|
|
38
|
+
o = {
|
|
39
|
+
'aid': 'ins_query',
|
|
40
|
+
'query_id': uuid,
|
|
41
|
+
'query': query,
|
|
42
|
+
'variables': variables
|
|
43
|
+
}
|
|
44
|
+
await client.send(json.dumps(o))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def _release_query(client, uuid: str):
|
|
48
|
+
o = {
|
|
49
|
+
'aid': 'ins_query',
|
|
50
|
+
'query_id': uuid,
|
|
51
|
+
'query': '',
|
|
52
|
+
'variables': {}
|
|
53
|
+
}
|
|
54
|
+
await client.send(json.dumps(o))
|
|
55
|
+
await peek(client)
|
|
56
|
+
await client.recv()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def _query_symbol_info(query: str, variables: dict, timeout: float) -> list:
|
|
60
|
+
async with websockets.connect(_FREE_URL, max_size=None) as client:
|
|
61
|
+
await client.recv()
|
|
62
|
+
|
|
63
|
+
uuid = _gen_uuid()
|
|
64
|
+
await _send_query(client, uuid, query, variables)
|
|
65
|
+
res = None
|
|
66
|
+
|
|
67
|
+
while res is None:
|
|
68
|
+
await peek(client)
|
|
69
|
+
msg = await asyncio.wait_for(client.recv(), max(timeout, _DEFAULT_TIMEOUT))
|
|
70
|
+
|
|
71
|
+
rtn_records = json.loads(msg)['data']
|
|
72
|
+
for record in rtn_records:
|
|
73
|
+
if 'symbols' in record and record['symbols'][uuid]:
|
|
74
|
+
res = record['symbols'][uuid]['result']['multi_symbol_info']
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
await _release_query(client, uuid)
|
|
78
|
+
return res
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _get_graphql_vars(
|
|
82
|
+
instruments: _StrListArg = None,
|
|
83
|
+
ins_class: _StrListArg = None,
|
|
84
|
+
exchange_id: _StrListArg = None
|
|
85
|
+
):
|
|
86
|
+
if instruments and isinstance(instruments, str):
|
|
87
|
+
instruments = [instruments]
|
|
88
|
+
if ins_class and isinstance(ins_class, str):
|
|
89
|
+
ins_class = [ins_class]
|
|
90
|
+
if exchange_id and isinstance(exchange_id, str):
|
|
91
|
+
exchange_id = [exchange_id]
|
|
92
|
+
|
|
93
|
+
query_keys = {}
|
|
94
|
+
query_vars = {}
|
|
95
|
+
|
|
96
|
+
if instruments:
|
|
97
|
+
query_keys['instrument_id'] = 'String'
|
|
98
|
+
query_vars['instrument_id'] = instruments
|
|
99
|
+
if ins_class:
|
|
100
|
+
query_keys['class'] = 'Class'
|
|
101
|
+
query_vars['class'] = ins_class
|
|
102
|
+
if exchange_id:
|
|
103
|
+
query_keys['exchange_id'] = 'String'
|
|
104
|
+
query_vars['exchange_id'] = exchange_id
|
|
105
|
+
|
|
106
|
+
return query_keys, query_vars
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def query_symbol_info(
|
|
110
|
+
instruments: _StrListArg = None,
|
|
111
|
+
ins_class: _StrListArg = None,
|
|
112
|
+
exchange_id: _StrListArg = None,
|
|
113
|
+
timeout: float = 0.0
|
|
114
|
+
):
|
|
115
|
+
query_keys, query_vars = _get_graphql_vars(instruments=instruments, ins_class=ins_class, exchange_id=exchange_id)
|
|
116
|
+
query = 'query(' + ','.join([f'${k}:[{query_keys[k]}]' for k in query_keys]) \
|
|
117
|
+
+ '){multi_symbol_info(' + ','.join([f'{k}:${k}' for k in query_keys]) \
|
|
118
|
+
+ '){' + _full_query + '}}'
|
|
119
|
+
return asyncio.run(_query_symbol_info(query, query_vars, timeout))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
def get_td_start(td_epoch: int) -> int:
|
|
4
|
+
begin_mark = 631123200000 # 1990-01-01
|
|
5
|
+
start_time = td_epoch - 21600000
|
|
6
|
+
week_day = (start_time - begin_mark) // 86400000 % 7
|
|
7
|
+
if week_day >= 5:
|
|
8
|
+
start_time -= 86400000 * (week_day - 4)
|
|
9
|
+
return start_time
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_td_end(td_epoch: int) -> int:
|
|
13
|
+
return td_epoch + 64799999 # 17:59:59
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/fasttq/__init__.py
|
|
4
|
+
src/fasttq/common.py
|
|
5
|
+
src/fasttq/download.py
|
|
6
|
+
src/fasttq/instrument.py
|
|
7
|
+
src/fasttq/util.py
|
|
8
|
+
src/fasttq_sdk.egg-info/PKG-INFO
|
|
9
|
+
src/fasttq_sdk.egg-info/SOURCES.txt
|
|
10
|
+
src/fasttq_sdk.egg-info/dependency_links.txt
|
|
11
|
+
src/fasttq_sdk.egg-info/requires.txt
|
|
12
|
+
src/fasttq_sdk.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
websockets>=13.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fasttq
|