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.
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: fasttq-sdk
3
+ Version: 0.2.0
4
+ Summary: Add your description here
5
+ Author-email: Lingqiao Zhao <forever.3g@hotmail.com>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: websockets>=13.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'
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: fasttq-sdk
3
+ Version: 0.2.0
4
+ Summary: Add your description here
5
+ Author-email: Lingqiao Zhao <forever.3g@hotmail.com>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: websockets>=13.0
@@ -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
+ websockets>=13.0