webull-openapi-python-sdk 1.0.6__py3-none-any.whl → 1.0.8__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.
Files changed (33) hide show
  1. samples/__init__.py +1 -1
  2. samples/account/__init__.py +1 -0
  3. samples/account/account_client.py +42 -0
  4. samples/assets/__init__.py +1 -0
  5. samples/assets/assets_client.py +47 -0
  6. samples/data/data_client.py +7 -0
  7. samples/data/data_streaming_client.py +5 -0
  8. samples/data/data_streaming_client_async.py +5 -0
  9. samples/order/__init__.py +1 -0
  10. samples/order/order_option_client.py +92 -0
  11. samples/order/order_stock_client.py +95 -0
  12. samples/trade/trade_client.py +6 -0
  13. samples/trade/trade_client_v2.py +6 -1
  14. samples/trade/trade_client_v3.py +38 -1
  15. webull/__init__.py +1 -1
  16. webull/core/__init__.py +1 -1
  17. webull/core/client.py +7 -0
  18. webull/core/http/initializer/client_initializer.py +1 -1
  19. webull/core/http/initializer/token/token_manager.py +5 -9
  20. webull/core/http/initializer/token/token_storage.py +140 -0
  21. webull/data/__init__.py +1 -1
  22. webull/data/internal/quotes_client.py +6 -0
  23. webull/data/quotes/subscribe/message_pb2.py +12 -12
  24. webull/data/quotes/subscribe/snapshot_result.py +78 -14
  25. webull/trade/__init__.py +1 -1
  26. webull/trade/request/v3/batch_place_order_request.py +52 -0
  27. webull/trade/trade/v3/order_opration_v3.py +13 -0
  28. {webull_openapi_python_sdk-1.0.6.dist-info → webull_openapi_python_sdk-1.0.8.dist-info}/METADATA +1 -1
  29. {webull_openapi_python_sdk-1.0.6.dist-info → webull_openapi_python_sdk-1.0.8.dist-info}/RECORD +33 -24
  30. {webull_openapi_python_sdk-1.0.6.dist-info → webull_openapi_python_sdk-1.0.8.dist-info}/WHEEL +0 -0
  31. {webull_openapi_python_sdk-1.0.6.dist-info → webull_openapi_python_sdk-1.0.8.dist-info}/licenses/LICENSE +0 -0
  32. {webull_openapi_python_sdk-1.0.6.dist-info → webull_openapi_python_sdk-1.0.8.dist-info}/licenses/NOTICE +0 -0
  33. {webull_openapi_python_sdk-1.0.6.dist-info → webull_openapi_python_sdk-1.0.8.dist-info}/top_level.txt +0 -0
samples/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = '1.0.6'
1
+ __version__ = '1.0.8'
@@ -0,0 +1 @@
1
+ # coding=utf-8
@@ -0,0 +1,42 @@
1
+ # Copyright 2022 Webull
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import json
16
+ import uuid
17
+ from time import sleep
18
+
19
+ from webull.core.client import ApiClient
20
+ from webull.trade.trade_client import TradeClient
21
+
22
+ optional_api_endpoint = "<api_endpoint>"
23
+ your_app_key = "<your_app_key>"
24
+ your_app_secret = "<your_app_secret>"
25
+ region_id = "<region_id>" # hk or us
26
+ account_id = "<your_account_id>"
27
+ # The token_dir parameter can be used to specify the directory for storing the 2FA token. Both absolute and relative paths are supported and this option has the highest priority.
28
+ # Alternatively, the storage directory can be configured via an environment variable with the key WEBULL_OPENAPI_TOKEN_DIR, which also supports both absolute and relative paths.
29
+ # If neither is specified, the default configuration will be used, and the token will be stored at conf/token.txt under the current working directory.
30
+ # token_dir = "<your_token_dir>"
31
+ # api_client.set_token_dir(token_dir)
32
+
33
+ api_client = ApiClient(your_app_key, your_app_secret, region_id)
34
+ api_client.add_endpoint(region_id, optional_api_endpoint)
35
+
36
+
37
+ if __name__ == '__main__':
38
+ trade_client = TradeClient(api_client)
39
+
40
+ res = trade_client.account_v2.get_account_list()
41
+ if res.status_code == 200:
42
+ print('get account list:', res.json())
@@ -0,0 +1 @@
1
+ # coding=utf-8
@@ -0,0 +1,47 @@
1
+ # Copyright 2022 Webull
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import json
16
+ import uuid
17
+ from time import sleep
18
+
19
+ from webull.core.client import ApiClient
20
+ from webull.trade.trade_client import TradeClient
21
+
22
+ optional_api_endpoint = "<api_endpoint>"
23
+ your_app_key = "<your_app_key>"
24
+ your_app_secret = "<your_app_secret>"
25
+ region_id = "<region_id>" # hk or us
26
+ account_id = "<your_account_id>"
27
+ # The token_dir parameter can be used to specify the directory for storing the 2FA token. Both absolute and relative paths are supported and this option has the highest priority.
28
+ # Alternatively, the storage directory can be configured via an environment variable with the key WEBULL_OPENAPI_TOKEN_DIR, which also supports both absolute and relative paths.
29
+ # If neither is specified, the default configuration will be used, and the token will be stored at conf/token.txt under the current working directory.
30
+ # token_dir = "<your_token_dir>"
31
+ # api_client.set_token_dir(token_dir)
32
+
33
+ api_client = ApiClient(your_app_key, your_app_secret, region_id)
34
+ api_client.add_endpoint(region_id, optional_api_endpoint)
35
+
36
+
37
+ if __name__ == '__main__':
38
+ trade_client = TradeClient(api_client)
39
+
40
+ res = trade_client.account_v2.get_account_balance(account_id)
41
+ if res.status_code == 200:
42
+ print('get account balance info:', res.json())
43
+
44
+ res = trade_client.account_v2.get_account_position(account_id)
45
+ if res.status_code == 200:
46
+ print('get account positions info:', res.json())
47
+
@@ -24,9 +24,16 @@ optional_api_endpoint = "<api_endpoint>"
24
24
  your_app_key = "<your_app_key>"
25
25
  your_app_secret = "<your_app_secret>"
26
26
  region_id = "<region_id>"
27
+ # The token_dir parameter can be used to specify the directory for storing the 2FA token. Both absolute and relative paths are supported and this option has the highest priority.
28
+ # Alternatively, the storage directory can be configured via an environment variable with the key WEBULL_OPENAPI_TOKEN_DIR, which also supports both absolute and relative paths.
29
+ # If neither is specified, the default configuration will be used, and the token will be stored at conf/token.txt under the current working directory.
30
+ # token_dir = "<your_token_dir>"
31
+ # api_client.set_token_dir(token_dir)
32
+
27
33
  api_client = ApiClient(your_app_key, your_app_secret, region_id)
28
34
  api_client.add_endpoint(region_id, optional_api_endpoint)
29
35
 
36
+
30
37
  if __name__ == '__main__':
31
38
  data_client = DataClient(api_client)
32
39
 
@@ -27,6 +27,11 @@ your_app_secret = "</your_app_secret>"
27
27
  optional_api_endpoint = "</optional_quotes_endpoint>"
28
28
  optional_quotes_endpoint = "</optional_quotes_endpoint>"
29
29
  region_id = '<region_id>'
30
+ # The token_dir parameter can be used to specify the directory for storing the 2FA token. Both absolute and relative paths are supported and this option has the highest priority.
31
+ # Alternatively, the storage directory can be configured via an environment variable with the key WEBULL_OPENAPI_TOKEN_DIR, which also supports both absolute and relative paths.
32
+ # If neither is specified, the default configuration will be used, and the token will be stored at conf/token.txt under the current working directory.
33
+ # token_dir = "<your_token_dir>"
34
+ # data_streaming_client.set_token_dir(token_dir)
30
35
 
31
36
  session_id = uuid.uuid4().hex
32
37
  data_streaming_client = DataStreamingClient(your_app_key, your_app_secret, region_id, session_id,
@@ -27,6 +27,11 @@ your_app_secret = "</your_app_secret>"
27
27
  optional_api_endpoint = "</optional_quotes_endpoint>"
28
28
  optional_quotes_endpoint = "</optional_quotes_endpoint>"
29
29
  region_id = '<region_id>'
30
+ # The token_dir parameter can be used to specify the directory for storing the 2FA token. Both absolute and relative paths are supported and this option has the highest priority.
31
+ # Alternatively, the storage directory can be configured via an environment variable with the key WEBULL_OPENAPI_TOKEN_DIR, which also supports both absolute and relative paths.
32
+ # If neither is specified, the default configuration will be used, and the token will be stored at conf/token.txt under the current working directory.
33
+ # token_dir = "<your_token_dir>"
34
+ # data_streaming_client.set_token_dir(token_dir)
30
35
 
31
36
  session_id = uuid.uuid4().hex
32
37
  data_streaming_client = DataStreamingClient(your_app_key, your_app_secret, region_id, session_id,
@@ -0,0 +1 @@
1
+ # coding=utf-8
@@ -0,0 +1,92 @@
1
+ # Copyright 2022 Webull
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import uuid
16
+ from webull.core.client import ApiClient
17
+ from webull.trade.trade_client import TradeClient
18
+
19
+ optional_api_endpoint = "<api_endpoint>"
20
+ your_app_key = "<your_app_key>"
21
+ your_app_secret = "<your_app_secret>"
22
+ region_id = "<region_id>" # hk or us
23
+ account_id = "<your_account_id>"
24
+ # The token_dir parameter can be used to specify the directory for storing the 2FA token. Both absolute and relative paths are supported and this option has the highest priority.
25
+ # Alternatively, the storage directory can be configured via an environment variable with the key WEBULL_OPENAPI_TOKEN_DIR, which also supports both absolute and relative paths.
26
+ # If neither is specified, the default configuration will be used, and the token will be stored at conf/token.txt under the current working directory.
27
+ # token_dir = "<your_token_dir>"
28
+ # api_client.set_token_dir(token_dir)
29
+
30
+ api_client = ApiClient(your_app_key, your_app_secret, region_id)
31
+ api_client.add_endpoint(region_id, optional_api_endpoint)
32
+
33
+ if __name__ == '__main__':
34
+ trade_client = TradeClient(api_client)
35
+
36
+ # Options
37
+ # For option order inquiries, please use the V2 query interface: api.order_v2.get_order_detail(account_id, client_order_id).
38
+ client_order_id = uuid.uuid4().hex
39
+ option_new_orders = [
40
+ {
41
+ "client_order_id": client_order_id,
42
+ "combo_type": "NORMAL",
43
+ "order_type": "LIMIT",
44
+ "quantity": "1",
45
+ "limit_price": "21.25",
46
+ "option_strategy": "SINGLE",
47
+ "side": "BUY",
48
+ "time_in_force": "GTC",
49
+ "entrust_type": "QTY",
50
+ "legs": [
51
+ {
52
+ "side": "BUY",
53
+ "quantity": "1",
54
+ "symbol": "TSLA",
55
+ "strike_price": "400",
56
+ "option_expire_date": "2025-12-26",
57
+ "instrument_type": "OPTION",
58
+ "option_type": "CALL",
59
+ "market": "US"
60
+ }
61
+ ]
62
+ }
63
+ ]
64
+
65
+ # preview
66
+ res = trade_client.order_v2.preview_option(account_id, option_new_orders)
67
+ if res.status_code == 200:
68
+ print("preview option res:", res.json())
69
+
70
+ # place
71
+ res = trade_client.order_v2.place_option(account_id, option_new_orders)
72
+ if res.status_code == 200:
73
+ print("place option res:" , res.json())
74
+
75
+ option_modify_orders = [
76
+ {
77
+ "client_order_id": client_order_id,
78
+ "quantity": "2",
79
+ "limit_price": "21.25"
80
+ }
81
+ ]
82
+ res = trade_client.order_v2.replace_option(account_id, option_modify_orders)
83
+ if res.status_code == 200:
84
+ print("Replace option order res:" , res.json())
85
+
86
+ res = trade_client.order_v2.cancel_option(account_id, client_order_id)
87
+ if res.status_code == 200:
88
+ print("Cancel option order res:" , res.json())
89
+
90
+ res = trade_client.order_v2.get_order_detail(account_id, client_order_id)
91
+ if res.status_code == 200:
92
+ print("Option order detail order res:" , res.json())
@@ -0,0 +1,95 @@
1
+ # Copyright 2022 Webull
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import uuid
16
+ from webull.core.client import ApiClient
17
+ from webull.trade.trade_client import TradeClient
18
+
19
+ optional_api_endpoint = "<api_endpoint>"
20
+ your_app_key = "<your_app_key>"
21
+ your_app_secret = "<your_app_secret>"
22
+ region_id = "<region_id>" # hk or us
23
+ account_id = "<your_account_id>"
24
+ # The token_dir parameter can be used to specify the directory for storing the 2FA token. Both absolute and relative paths are supported and this option has the highest priority.
25
+ # Alternatively, the storage directory can be configured via an environment variable with the key WEBULL_OPENAPI_TOKEN_DIR, which also supports both absolute and relative paths.
26
+ # If neither is specified, the default configuration will be used, and the token will be stored at conf/token.txt under the current working directory.
27
+ # token_dir = "<your_token_dir>"
28
+ # api_client.set_token_dir(token_dir)
29
+
30
+ api_client = ApiClient(your_app_key, your_app_secret, region_id)
31
+ api_client.add_endpoint(region_id, optional_api_endpoint)
32
+
33
+
34
+ if __name__ == '__main__':
35
+ trade_client = TradeClient(api_client)
36
+
37
+
38
+ # simple order
39
+ client_order_id = uuid.uuid4().hex
40
+ print('client order id:', client_order_id)
41
+ new_simple_orders = [
42
+ {
43
+ "client_order_id": client_order_id,
44
+ "symbol": "BULL",
45
+ "instrument_type": "EQUITY",
46
+ "market": "US",
47
+ "order_type": "LIMIT",
48
+ "limit_price": "26",
49
+ "quantity": "1",
50
+ "support_trading_session": "CORE",
51
+ "side": "BUY",
52
+ "time_in_force": "DAY",
53
+ "entrust_type": "QTY"
54
+ }
55
+ ]
56
+ new_hk_stock_simple_orders = [
57
+ {
58
+ "client_order_id": client_order_id,
59
+ "symbol": "00700",
60
+ "instrument_type": "EQUITY",
61
+ "market": "HK",
62
+ "order_type": "ENHANCED_LIMIT",
63
+ "limit_price": "612",
64
+ "quantity": "100",
65
+ "side": "BUY",
66
+ "time_in_force": "DAY",
67
+ "entrust_type": "QTY"
68
+ }
69
+ ]
70
+ res = trade_client.order_v2.preview_order(account_id, new_simple_orders)
71
+ if res.status_code == 200:
72
+ print('preview order res:', res.json())
73
+
74
+ res = trade_client.order_v2.place_order(account_id, new_simple_orders)
75
+ if res.status_code == 200:
76
+ print('place order res:', res.json())
77
+
78
+ modify_simple_orders = [
79
+ {
80
+ "client_order_id": client_order_id,
81
+ "quantity": "2",
82
+ "limit_price": "25"
83
+ }
84
+ ]
85
+ res = trade_client.order_v2.replace_order(account_id, modify_simple_orders)
86
+ if res.status_code == 200:
87
+ print('replace order res:', res.json())
88
+
89
+ res = trade_client.order_v2.cancel_order(account_id, client_order_id)
90
+ if res.status_code == 200:
91
+ print('cancel order res:', res.json())
92
+
93
+ res = trade_client.order_v2.get_order_detail(account_id, client_order_id)
94
+ if res.status_code == 200:
95
+ print('order detail:', res.json())
@@ -25,6 +25,12 @@ your_app_key = "<your_app_key>"
25
25
  your_app_secret = "<your_app_secret>"
26
26
  region_id = "<region_id>"
27
27
  account_id = "<your_account_id>"
28
+ # The token_dir parameter can be used to specify the directory for storing the 2FA token. Both absolute and relative paths are supported and this option has the highest priority.
29
+ # Alternatively, the storage directory can be configured via an environment variable with the key WEBULL_OPENAPI_TOKEN_DIR, which also supports both absolute and relative paths.
30
+ # If neither is specified, the default configuration will be used, and the token will be stored at conf/token.txt under the current working directory.
31
+ # token_dir = "<your_token_dir>"
32
+ # api_client.set_token_dir(token_dir)
33
+
28
34
  api_client = ApiClient(your_app_key, your_app_secret, region_id)
29
35
  api_client.add_endpoint(region_id, optional_api_endpoint)
30
36
 
@@ -24,10 +24,15 @@ your_app_key = "<your_app_key>"
24
24
  your_app_secret = "<your_app_secret>"
25
25
  region_id = "<region_id>"
26
26
  account_id = "<your_account_id>"
27
+ # The token_dir parameter can be used to specify the directory for storing the 2FA token. Both absolute and relative paths are supported and this option has the highest priority.
28
+ # Alternatively, the storage directory can be configured via an environment variable with the key WEBULL_OPENAPI_TOKEN_DIR, which also supports both absolute and relative paths.
29
+ # If neither is specified, the default configuration will be used, and the token will be stored at conf/token.txt under the current working directory.
30
+ # token_dir = "<your_token_dir>"
31
+ # api_client.set_token_dir(token_dir)
32
+
27
33
  api_client = ApiClient(your_app_key, your_app_secret, region_id)
28
34
  api_client.add_endpoint(region_id, optional_api_endpoint)
29
35
 
30
-
31
36
  if __name__ == '__main__':
32
37
  trade_client = TradeClient(api_client)
33
38
 
@@ -24,6 +24,12 @@ your_app_key = "<your_app_key>"
24
24
  your_app_secret = "<your_app_secret>"
25
25
  region_id = "<region_id>"
26
26
  account_id = "<your_account_id>"
27
+ # The token_dir parameter can be used to specify the directory for storing the 2FA token. Both absolute and relative paths are supported and this option has the highest priority.
28
+ # Alternatively, the storage directory can be configured via an environment variable with the key WEBULL_OPENAPI_TOKEN_DIR, which also supports both absolute and relative paths.
29
+ # If neither is specified, the default configuration will be used, and the token will be stored at conf/token.txt under the current working directory.
30
+ # token_dir = "<your_token_dir>"
31
+ # api_client.set_token_dir(token_dir)
32
+
27
33
  api_client = ApiClient(your_app_key, your_app_secret, region_id)
28
34
  api_client.add_endpoint(region_id, optional_api_endpoint)
29
35
 
@@ -202,7 +208,38 @@ if __name__ == '__main__':
202
208
  if res.status_code == 200:
203
209
  print('get master order detail res:', res.json())
204
210
 
205
-
211
+ # batch place order
212
+ batch_place_orders = [
213
+ {
214
+ "combo_type": "NORMAL",
215
+ "client_order_id": uuid.uuid4().hex,
216
+ "instrument_type": "EQUITY",
217
+ "market": "US",
218
+ "symbol": "AAPL",
219
+ "order_type": "MARKET",
220
+ "entrust_type": "QTY",
221
+ "support_trading_session": "CORE",
222
+ "time_in_force": "DAY",
223
+ "side": "BUY",
224
+ "quantity": "1"
225
+ },
226
+ {
227
+ "combo_type": "NORMAL",
228
+ "client_order_id": uuid.uuid4().hex,
229
+ "instrument_type": "EQUITY",
230
+ "market": "US",
231
+ "symbol": "TESL",
232
+ "order_type": "MARKET",
233
+ "entrust_type": "QTY",
234
+ "support_trading_session": "CORE",
235
+ "time_in_force": "DAY",
236
+ "side": "BUY",
237
+ "quantity": "1"
238
+ }
239
+ ]
240
+ res = trade_client.order_v3.batch_place_order(account_id, batch_place_orders)
241
+ if res.status_code == 200:
242
+ print('batch place normal equity order res:', res.json())
206
243
 
207
244
 
208
245
  # ============================================================
webull/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = '1.0.6'
1
+ __version__ = '1.0.8'
webull/core/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = '1.0.6'
1
+ __version__ = '1.0.8'
2
2
 
3
3
  import logging
4
4
 
webull/core/client.py CHANGED
@@ -122,6 +122,7 @@ class ApiClient:
122
122
  self._token_check_duration_seconds = token_check_duration_seconds
123
123
  validation.assert_integer_positive(token_check_interval_seconds, "token_check_interval_seconds")
124
124
  self._token_check_interval_seconds = token_check_interval_seconds
125
+ self._token_dir = None
125
126
 
126
127
  def get_region_id(self):
127
128
  return self._region_id
@@ -164,6 +165,12 @@ class ApiClient:
164
165
  def get_token_check_interval_seconds(self):
165
166
  return self._token_check_interval_seconds
166
167
 
168
+ def set_token_dir(self, token_dir):
169
+ self._token_dir = token_dir
170
+
171
+ def get_token_dir(self):
172
+ return self._token_dir
173
+
167
174
  @staticmethod
168
175
  def user_agent_header():
169
176
  base = '%s (%s %s;%s)' \
@@ -62,7 +62,7 @@ class ClientInitializer:
62
62
  if not ClientInitializer._check_token_enable(api_client):
63
63
  return
64
64
 
65
- token_manager = TokenManager()
65
+ token_manager = TokenManager(api_client.get_token_dir())
66
66
  token_manager.init_token(api_client)
67
67
 
68
68
  @staticmethod
@@ -34,11 +34,10 @@
34
34
  # coding=utf-8
35
35
 
36
36
  import logging
37
- import os
38
37
  import time
39
- from pathlib import Path
40
38
 
41
39
  from webull.core import compat
40
+ from webull.core.http.initializer.token.token_storage import TokenStorage
42
41
  from webull.core.utils import desensitize
43
42
  from webull.core.exception.exceptions import ClientException
44
43
  from webull.core.http.initializer.token.bean.access_token import AccessToken
@@ -47,13 +46,10 @@ from webull.core.http.initializer.token.token_operation import TokenOperation
47
46
  logger = logging.getLogger(__name__)
48
47
 
49
48
  class TokenManager:
50
- TOKEN_FILE_NAME = "conf/token.txt"
51
- CONF_ENV_TOKEN_DIR = os.getenv("WEBULL_OPENAPI_TOKEN_DIR")
52
- DEFAULT_ENV_TOKEN_DIR = os.getcwd()
53
49
 
54
- def __init__(self):
55
- dir_path = Path(self.CONF_ENV_TOKEN_DIR) if self.CONF_ENV_TOKEN_DIR else Path(self.DEFAULT_ENV_TOKEN_DIR)
56
- self.token_file_path = dir_path / self.TOKEN_FILE_NAME
50
+ def __init__(self, custom_token_dir=None):
51
+ token_storage = TokenStorage(custom_token_dir=custom_token_dir)
52
+ self.token_file_path = token_storage.get_token_file_path()
57
53
 
58
54
  def init_token(self, api_client):
59
55
  local_access_token = self.load_token_from_local()
@@ -100,7 +96,7 @@ class TokenManager:
100
96
  def save_token_to_local(self, server_access_token):
101
97
  try:
102
98
  logger.info("save_token_to_local writing token to local file. token:%s expires:%s status:%s",
103
- server_access_token.get("token"), server_access_token.get("expires"), server_access_token.get("status"))
99
+ desensitize.desensitize_token(server_access_token.get("token")), server_access_token.get("expires"), server_access_token.get("status"))
104
100
  self.token_file_path.parent.mkdir(parents=True, exist_ok=True)
105
101
  with open(self.token_file_path, "w", encoding="utf-8") as f:
106
102
  f.write(server_access_token.get("token") + "\n")
@@ -0,0 +1,140 @@
1
+ # Copyright 2022 Webull
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Licensed to the Apache Software Foundation (ASF) under one
16
+ # or more contributor license agreements. See the NOTICE file
17
+ # distributed with this work for additional information
18
+ # regarding copyright ownership. The ASF licenses this file
19
+ # to you under the Apache License, Version 2.0 (the
20
+ # "License"); you may not use this file except in compliance
21
+ # with the License. You may obtain a copy of the License at
22
+ #
23
+ # http://www.apache.org/licenses/LICENSE-2.0
24
+ #
25
+ #
26
+ #
27
+ # Unless required by applicable law or agreed to in writing,
28
+ # software distributed under the License is distributed on an
29
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
30
+ # KIND, either express or implied. See the License for the
31
+ # specific language governing permissions and limitations
32
+ # under the License.
33
+
34
+ # coding=utf-8
35
+
36
+ import logging
37
+ import os
38
+ import platform
39
+ import re
40
+ from pathlib import Path
41
+
42
+ from webull.core.exception.exceptions import ClientException
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+ class TokenStorage:
47
+
48
+ DEFAULT_TOKEN_PATH = "conf"
49
+ DEFAULT_TOKEN_FILE = "token.txt"
50
+ DEFAULT_ENV_TOKEN_DIR = "WEBULL_OPENAPI_TOKEN_DIR"
51
+
52
+ _INVALID_CHARS = {
53
+ "windows": re.compile(r'[<>"|?*]'),
54
+ "posix": re.compile(r'[\0]')
55
+ }
56
+
57
+ def __init__(self, custom_token_dir=None):
58
+ # Resolve the final storage directory (priority: custom direction > environment variable > default).
59
+ self.storage_token_dir = self._resolve_dir(custom_token_dir)
60
+ # Full path validation
61
+ self._validate_path()
62
+ # Ensure the directory exists
63
+ self._ensure_dir_exists()
64
+ # Full path
65
+ self.token_file = self.storage_token_dir / self.DEFAULT_TOKEN_FILE
66
+ logger.info("storage_token initialized path:%s.",self.token_file)
67
+ # Check file exists
68
+ self._check_file_exists()
69
+
70
+ def _resolve_dir(self, custom_token_dir=None) -> Path:
71
+ if custom_token_dir and custom_token_dir.strip():
72
+ # custom direction
73
+ raw_dir = custom_token_dir.strip()
74
+ logger.info("storage_token uses the custom configuration, token_dir:%s.", raw_dir)
75
+ elif env_dir := os.getenv(self.DEFAULT_ENV_TOKEN_DIR):
76
+ # environment variable
77
+ raw_dir = env_dir.strip()
78
+ logger.info("storage_token uses environment variable configuration, %s:%s.", self.DEFAULT_ENV_TOKEN_DIR, raw_dir)
79
+ else:
80
+ # default
81
+ raw_dir = self.DEFAULT_TOKEN_PATH
82
+ logger.info("storage_token uses the default configuration, %s.", raw_dir)
83
+
84
+ # Resolve path
85
+ normalized_token_dir = Path(raw_dir).expanduser().absolute()
86
+ return normalized_token_dir
87
+
88
+ def _validate_path(self):
89
+
90
+ dir_path = self.storage_token_dir
91
+ os_type = platform.system().lower()
92
+
93
+ self._validate_path_syntax(dir_path, os_type)
94
+ self._validate_path_access(dir_path)
95
+
96
+ def _validate_path_syntax(self, path: Path, os_type: str):
97
+ """Syntax validity check"""
98
+ invalid_pattern = self._INVALID_CHARS["windows"] if "windows" in os_type else self._INVALID_CHARS["posix"]
99
+ if invalid_pattern.search(str(path)):
100
+ msg = ("storage_token path contains illegal characters, path:%s." % (str(path)))
101
+ logger.warning(msg)
102
+
103
+ def _validate_path_access(self, path: Path):
104
+ """Accessibility/permission check"""
105
+ if path.exists():
106
+ if not path.is_dir():
107
+ msg = ("storage_token path already exists but is not a directory, path:%s." % (str(path)))
108
+ logger.warning(msg)
109
+ return
110
+ if not os.access(path, os.R_OK | os.W_OK):
111
+ msg = ("storage_token directory has no read/write permission. Please check the configuration, path:%s." % (str(path)))
112
+ logger.warning(msg)
113
+ else:
114
+ parent_dir = path.parent
115
+ if not parent_dir.exists():
116
+ msg = ("storage_token parent directory does not exist, unable to create directory. Please check the configuration, parent path:%s." % (str(parent_dir)))
117
+ logger.warning(msg)
118
+ return
119
+ if not os.access(parent_dir, os.R_OK | os.W_OK | os.X_OK):
120
+ msg = ("storage_token parent directory has no read/write permission. Please check the configuration, parent path:%s." % (str(parent_dir)))
121
+ logger.warning(msg)
122
+
123
+ def _ensure_dir_exists(self):
124
+ """Ensure the directory exists"""
125
+ try:
126
+ self.storage_token_dir.mkdir(parents=True, exist_ok=True)
127
+ except Exception as e:
128
+ msg = ("storage_token failed to create directory, file:%s." % (str(self.storage_token_dir)))
129
+ logger.error(msg)
130
+ raise ClientException("ERROR_STORAGE_TOKEN", str(e)) from e
131
+
132
+ def _check_file_exists(self):
133
+ """Check file exists"""
134
+ full_path = Path(self.token_file)
135
+ if full_path.is_file():
136
+ msg = ("storage_token Note: The token file already exists, the latest token configuration will be overwritten and written to the token file after successful 2FA verification. path:%s." % self.token_file)
137
+ logger.warning(msg)
138
+
139
+ def get_token_file_path(self):
140
+ return self.token_file
webull/data/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  # coding=utf-8
2
2
 
3
- __version__ = '1.0.6'
3
+ __version__ = '1.0.8'
@@ -69,6 +69,7 @@ class QuotesClient(mqttc.Client):
69
69
  self._http_host = http_host
70
70
  self._mqtt_host = mqtt_host
71
71
  self._mqtt_port = mqtt_port
72
+ self._token_dir = None
72
73
  self._quotes_decoder = QuotesDecoder()
73
74
 
74
75
  api_client = ApiClient(app_key, app_secret, region_id)
@@ -158,6 +159,11 @@ class QuotesClient(mqttc.Client):
158
159
  def on_quotes_message(self, func):
159
160
  with self._callback_mutex:
160
161
  self._on_quotes_message = func
162
+
163
+ def set_token_dir(self, token_dir):
164
+ self._token_dir = token_dir
165
+ if token_dir:
166
+ self.api_client.set_token_dir(token_dir)
161
167
 
162
168
  def register_payload_decoder(self, type, decoder):
163
169
  with self._callback_mutex:
@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
13
13
 
14
14
 
15
15
 
16
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rmessage.proto\"Z\n\x05\x42\x61sic\x12\x0e\n\x06symbol\x18\x01 \x01(\t\x12\x15\n\rinstrument_id\x18\x02 \x01(\t\x12\x11\n\ttimestamp\x18\x03 \x01(\t\x12\x17\n\x0ftrading_session\x18\x04 \x01(\t\"\xb6\x01\n\x08Snapshot\x12\x15\n\x05\x62\x61sic\x18\x01 \x01(\x0b\x32\x06.Basic\x12\x12\n\ntrade_time\x18\x02 \x01(\t\x12\r\n\x05price\x18\x03 \x01(\t\x12\x0c\n\x04open\x18\x04 \x01(\t\x12\x0c\n\x04high\x18\x05 \x01(\t\x12\x0b\n\x03low\x18\x06 \x01(\t\x12\x11\n\tpre_close\x18\x07 \x01(\t\x12\x0e\n\x06volume\x18\x08 \x01(\t\x12\x0e\n\x06\x63hange\x18\t \x01(\t\x12\x14\n\x0c\x63hange_ratio\x18\n \x01(\t\"L\n\x05Quote\x12\x15\n\x05\x62\x61sic\x18\x01 \x01(\x0b\x32\x06.Basic\x12\x15\n\x04\x61sks\x18\x02 \x03(\x0b\x32\x07.AskBid\x12\x15\n\x04\x62ids\x18\x03 \x03(\x0b\x32\x07.AskBid\"X\n\x04Tick\x12\x15\n\x05\x62\x61sic\x18\x01 \x01(\x0b\x32\x06.Basic\x12\x0c\n\x04time\x18\x02 \x01(\t\x12\r\n\x05price\x18\x03 \x01(\t\x12\x0e\n\x06volume\x18\x04 \x01(\t\x12\x0c\n\x04side\x18\x05 \x01(\t\"U\n\x06\x41skBid\x12\r\n\x05price\x18\x01 \x01(\t\x12\x0c\n\x04size\x18\x02 \x01(\t\x12\x15\n\x05order\x18\x03 \x03(\x0b\x32\x06.Order\x12\x17\n\x06\x62roker\x18\x04 \x03(\x0b\x32\x07.Broker\"#\n\x05Order\x12\x0c\n\x04mpid\x18\x01 \x01(\t\x12\x0c\n\x04size\x18\x02 \x01(\t\"#\n\x06\x42roker\x12\x0b\n\x03\x62id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\tb\x06proto3')
16
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rmessage.proto\"Z\n\x05\x42\x61sic\x12\x0e\n\x06symbol\x18\x01 \x01(\t\x12\x15\n\rinstrument_id\x18\x02 \x01(\t\x12\x11\n\ttimestamp\x18\x03 \x01(\t\x12\x17\n\x0ftrading_session\x18\x04 \x01(\t\"\xd6\x03\n\x08Snapshot\x12\x15\n\x05\x62\x61sic\x18\x01 \x01(\x0b\x32\x06.Basic\x12\x12\n\ntrade_time\x18\x02 \x01(\t\x12\r\n\x05price\x18\x03 \x01(\t\x12\x0c\n\x04open\x18\x04 \x01(\t\x12\x0c\n\x04high\x18\x05 \x01(\t\x12\x0b\n\x03low\x18\x06 \x01(\t\x12\x11\n\tpre_close\x18\x07 \x01(\t\x12\x0e\n\x06volume\x18\x08 \x01(\t\x12\x0e\n\x06\x63hange\x18\t \x01(\t\x12\x14\n\x0c\x63hange_ratio\x18\n \x01(\t\x12\x16\n\x0e\x65xt_trade_time\x18\x0b \x01(\t\x12\x11\n\text_price\x18\x0c \x01(\t\x12\x10\n\x08\x65xt_high\x18\r \x01(\t\x12\x0f\n\x07\x65xt_low\x18\x0e \x01(\t\x12\x12\n\next_volume\x18\x0f \x01(\t\x12\x12\n\next_change\x18\x10 \x01(\t\x12\x18\n\x10\x65xt_change_ratio\x18\x11 \x01(\t\x12\x16\n\x0eovn_trade_time\x18\x12 \x01(\t\x12\x11\n\tovn_price\x18\x13 \x01(\t\x12\x10\n\x08ovn_high\x18\x14 \x01(\t\x12\x0f\n\x07ovn_low\x18\x15 \x01(\t\x12\x12\n\novn_volume\x18\x16 \x01(\t\x12\x12\n\novn_change\x18\x17 \x01(\t\x12\x18\n\x10ovn_change_ratio\x18\x18 \x01(\t\"L\n\x05Quote\x12\x15\n\x05\x62\x61sic\x18\x01 \x01(\x0b\x32\x06.Basic\x12\x15\n\x04\x61sks\x18\x02 \x03(\x0b\x32\x07.AskBid\x12\x15\n\x04\x62ids\x18\x03 \x03(\x0b\x32\x07.AskBid\"X\n\x04Tick\x12\x15\n\x05\x62\x61sic\x18\x01 \x01(\x0b\x32\x06.Basic\x12\x0c\n\x04time\x18\x02 \x01(\t\x12\r\n\x05price\x18\x03 \x01(\t\x12\x0e\n\x06volume\x18\x04 \x01(\t\x12\x0c\n\x04side\x18\x05 \x01(\t\"U\n\x06\x41skBid\x12\r\n\x05price\x18\x01 \x01(\t\x12\x0c\n\x04size\x18\x02 \x01(\t\x12\x15\n\x05order\x18\x03 \x03(\x0b\x32\x06.Order\x12\x17\n\x06\x62roker\x18\x04 \x03(\x0b\x32\x07.Broker\"#\n\x05Order\x12\x0c\n\x04mpid\x18\x01 \x01(\t\x12\x0c\n\x04size\x18\x02 \x01(\t\"#\n\x06\x42roker\x12\x0b\n\x03\x62id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\tb\x06proto3')
17
17
 
18
18
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
19
19
  _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'message_pb2', globals())
@@ -23,15 +23,15 @@ if _descriptor._USE_C_DESCRIPTORS == False:
23
23
  _BASIC._serialized_start=17
24
24
  _BASIC._serialized_end=107
25
25
  _SNAPSHOT._serialized_start=110
26
- _SNAPSHOT._serialized_end=292
27
- _QUOTE._serialized_start=294
28
- _QUOTE._serialized_end=370
29
- _TICK._serialized_start=372
30
- _TICK._serialized_end=460
31
- _ASKBID._serialized_start=462
32
- _ASKBID._serialized_end=547
33
- _ORDER._serialized_start=549
34
- _ORDER._serialized_end=584
35
- _BROKER._serialized_start=586
36
- _BROKER._serialized_end=621
26
+ _SNAPSHOT._serialized_end=580
27
+ _QUOTE._serialized_start=582
28
+ _QUOTE._serialized_end=658
29
+ _TICK._serialized_start=660
30
+ _TICK._serialized_end=748
31
+ _ASKBID._serialized_start=750
32
+ _ASKBID._serialized_end=835
33
+ _ORDER._serialized_start=837
34
+ _ORDER._serialized_end=872
35
+ _BROKER._serialized_start=874
36
+ _BROKER._serialized_end=909
37
37
  # @@protoc_insertion_point(module_scope)
@@ -21,22 +21,40 @@ from webull.data.quotes.subscribe.basic_result import BasicResult
21
21
  class SnapshotResult:
22
22
  def __init__(self, pb_snapshot):
23
23
  self.basic = BasicResult(pb_snapshot.basic)
24
+ self.last_trade_time = int(pb_snapshot.trade_time) if pb_snapshot.trade_time else None
25
+ self.price = Decimal(pb_snapshot.price) if pb_snapshot.price else None
24
26
  self.open = Decimal(pb_snapshot.open) if pb_snapshot.open else None
25
27
  self.high = Decimal(pb_snapshot.high) if pb_snapshot.high else None
26
28
  self.low = Decimal(pb_snapshot.low) if pb_snapshot.low else None
27
- self.price = Decimal(pb_snapshot.price) if pb_snapshot.price else None
28
- self.pre_close = Decimal(
29
- pb_snapshot.pre_close) if pb_snapshot.pre_close else None
30
- self.volume = Decimal(
31
- pb_snapshot.volume) if pb_snapshot.volume else None
32
- self.change = Decimal(
33
- pb_snapshot.change) if pb_snapshot.change else None
34
- self.change_ratio = Decimal(
35
- pb_snapshot.change_ratio) if pb_snapshot.change_ratio else None
29
+ self.pre_close = Decimal(pb_snapshot.pre_close) if pb_snapshot.pre_close else None
30
+ self.close = Decimal(pb_snapshot.open) if pb_snapshot.open else None
31
+ self.volume = Decimal(pb_snapshot.volume) if pb_snapshot.volume else None
32
+ self.change = Decimal(pb_snapshot.change) if pb_snapshot.change else None
33
+ self.change_ratio = Decimal(pb_snapshot.change_ratio) if pb_snapshot.change_ratio else None
34
+ self.ext_trade_time = int(pb_snapshot.ext_trade_time) if pb_snapshot.ext_trade_time else None
35
+ self.ext_price = Decimal(pb_snapshot.ext_price) if pb_snapshot.ext_price else None
36
+ self.ext_high = Decimal(pb_snapshot.ext_high) if pb_snapshot.ext_high else None
37
+ self.ext_low = Decimal(pb_snapshot.ext_low) if pb_snapshot.ext_low else None
38
+ self.ext_volume = Decimal(pb_snapshot.ext_volume) if pb_snapshot.ext_volume else None
39
+ self.ext_change = Decimal(pb_snapshot.ext_change) if pb_snapshot.ext_change else None
40
+ self.ext_change_ratio = Decimal(pb_snapshot.ext_change_ratio) if pb_snapshot.ext_change_ratio else None
41
+ self.ovn_trade_time = int(pb_snapshot.ovn_trade_time) if pb_snapshot.ovn_trade_time else None
42
+ self.ovn_price = Decimal(pb_snapshot.ovn_price) if pb_snapshot.ovn_price else None
43
+ self.ovn_high = Decimal(pb_snapshot.ovn_high) if pb_snapshot.ovn_high else None
44
+ self.ovn_low = Decimal(pb_snapshot.ovn_low) if pb_snapshot.ovn_low else None
45
+ self.ovn_volume = Decimal(pb_snapshot.ovn_volume) if pb_snapshot.ovn_volume else None
46
+ self.ovn_change = Decimal(pb_snapshot.ovn_change) if pb_snapshot.ovn_change else None
47
+ self.ovn_change_ratio = Decimal(pb_snapshot.ovn_change_ratio) if pb_snapshot.ovn_change_ratio else None
36
48
 
37
49
  def get_basic(self):
38
50
  return self.basic
39
51
 
52
+ def get_last_trade_time(self):
53
+ return self.last_trade_time
54
+
55
+ def get_price(self):
56
+ return self.price
57
+
40
58
  def get_open(self):
41
59
  return self.open
42
60
 
@@ -46,12 +64,12 @@ class SnapshotResult:
46
64
  def get_low(self):
47
65
  return self.low
48
66
 
49
- def get_price(self):
50
- return self.price
51
-
52
67
  def get_pre_close(self):
53
68
  return self.pre_close
54
69
 
70
+ def get_close(self):
71
+ return self.close
72
+
55
73
  def get_volume(self):
56
74
  return self.volume
57
75
 
@@ -61,9 +79,55 @@ class SnapshotResult:
61
79
  def get_change_ratio(self):
62
80
  return self.change_ratio
63
81
 
82
+ def get_ext_trade_time(self):
83
+ return self.ext_trade_time
84
+
85
+ def get_ext_price(self):
86
+ return self.ext_price
87
+
88
+ def get_ext_high(self):
89
+ return self.ext_high
90
+
91
+ def get_ext_low(self):
92
+ return self.ext_low
93
+
94
+ def get_ext_volume(self):
95
+ return self.ext_volume
96
+
97
+ def get_ext_change(self):
98
+ return self.ext_change
99
+
100
+ def get_ext_change_ratio(self):
101
+ return self.ext_change_ratio
102
+
103
+ def get_ovn_trade_time(self):
104
+ return self.ovn_trade_time
105
+
106
+ def get_ovn_price(self):
107
+ return self.ovn_price
108
+
109
+ def get_ovn_high(self):
110
+ return self.ovn_high
111
+
112
+ def get_ovn_low(self):
113
+ return self.ovn_low
114
+
115
+ def get_ovn_volume(self):
116
+ return self.ovn_volume
117
+
118
+ def get_ovn_change(self):
119
+ return self.ovn_change
120
+
121
+ def get_ovn_change_ratio(self):
122
+ return self.ovn_change_ratio
123
+
64
124
  def __repr__(self):
65
- return "%s, open:%s, high:%s, low:%s, price:%s, pre_close:%s, volume:%s, change:%s, change_ratio:%s" \
66
- % (self.basic, self.open, self.high, self.low, self.price, self.pre_close, self.volume, self.change, self.change_ratio)
125
+ attrs = ['last_trade_time', 'price', 'open', 'high', 'low', 'pre_close', 'close', 'volume', 'change', 'change_ratio']
126
+ ext_attrs = [f"ext_{name}" for name in ['trade_time', 'price', 'high', 'low', 'volume', 'change', 'change_ratio']]
127
+ ovn_attrs = [f"ovn_{name}" for name in ['trade_time', 'price', 'high', 'low', 'volume', 'change', 'change_ratio']]
128
+ all_attrs = attrs + ext_attrs + ovn_attrs
129
+ attr_str = ', '.join(f"{name}:{getattr(self, name)}" for name in all_attrs)
130
+ return f"{self.basic}, {attr_str}"
67
131
 
68
132
  def __str__(self):
69
133
  return self.__repr__()
webull/trade/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = '1.0.6'
1
+ __version__ = '1.0.8'
@@ -0,0 +1,52 @@
1
+ # Copyright 2022 Webull
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import json
15
+ # coding=utf-8
16
+
17
+ from webull.core.request import ApiRequest
18
+
19
+
20
+ class BatchPlaceOrderRequest(ApiRequest):
21
+ def __init__(self):
22
+ super().__init__("/openapi/trade/order/batch-place", version='v2', method="POST", body_params={})
23
+
24
+ def set_account_id(self, account_id):
25
+ self.add_body_params("account_id", account_id)
26
+
27
+ def set_batch_orders(self, batch_orders):
28
+ self.add_body_params("batch_orders", batch_orders)
29
+
30
+ def add_custom_headers_from_order(self, batch_orders):
31
+ if not batch_orders:
32
+ return
33
+
34
+ if isinstance(batch_orders, list) and batch_orders[0]:
35
+ first_order = batch_orders[0]
36
+ leg_list = first_order.get("legs")
37
+ if leg_list is not None and isinstance(leg_list, list):
38
+ for sub_leg in leg_list:
39
+ if (sub_leg and isinstance(sub_leg, dict)
40
+ and sub_leg.get("instrument_type") == "OPTION"):
41
+ instrument_type = sub_leg.get("instrument_type")
42
+ market = sub_leg.get("market")
43
+ category = market + "_" + instrument_type
44
+ if category is not None:
45
+ self.add_header("category", category)
46
+ return
47
+
48
+ market = first_order.get("market")
49
+ instrument_type = first_order.get("instrument_type")
50
+ category = market + "_" + instrument_type
51
+ if category is not None:
52
+ self.add_header("category", category)
@@ -14,6 +14,7 @@
14
14
  # coding=utf-8
15
15
  from webull.trade.request.v3.preview_order_request import PreviewOrderRequest
16
16
  from webull.trade.request.v3.place_order_request import PlaceOrderRequest
17
+ from webull.trade.request.v3.batch_place_order_request import BatchPlaceOrderRequest
17
18
  from webull.trade.request.v3.replace_order_request import ReplaceOrderRequest
18
19
  from webull.trade.request.v3.cancel_order_request import CancelOrderRequest
19
20
  from webull.trade.request.v3.get_order_detail_request import OrderDetailRequest
@@ -50,6 +51,18 @@ class OrderOperationV3:
50
51
  response = self.client.get_response(place_order_request)
51
52
  return response
52
53
 
54
+ def batch_place_order(self, account_id, batch_orders):
55
+ """
56
+ This interface is currently supported only for Webull US.
57
+ Support for other regions will be available in future updates.
58
+ """
59
+ batch_place_order_request = BatchPlaceOrderRequest()
60
+ batch_place_order_request.set_account_id(account_id=account_id)
61
+ batch_place_order_request.set_batch_orders(batch_orders=batch_orders)
62
+ batch_place_order_request.add_custom_headers_from_order(batch_orders)
63
+ response = self.client.get_response(batch_place_order_request)
64
+ return response
65
+
53
66
  def replace_order(self, account_id, modify_orders, client_combo_order_id=None):
54
67
  """
55
68
  This interface is currently supported only for Webull US.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webull-openapi-python-sdk
3
- Version: 1.0.6
3
+ Version: 1.0.8
4
4
  Summary: Webull Python SDK.
5
5
  Home-page:
6
6
  Author: Webull
@@ -1,16 +1,23 @@
1
- samples/__init__.py,sha256=iNeZ6LZ9iCV_ugsqAh5K1fFqbKkfU8o3NbLYBWI3m1I,22
1
+ samples/__init__.py,sha256=mFFUUCx5TqyW1TTFRrWDhXXVMJDMRxXWrkHanVtp9oY,22
2
+ samples/account/__init__.py,sha256=eoZ6GfifbqhMLNzjlqRDVil-yyBkOmVN9ujSgJWNBlY,15
3
+ samples/account/account_client.py,sha256=vwh-nI_JnjwcIeOr1sQPMzG8pAqZwliUusE49ZmYE3s,1796
4
+ samples/assets/__init__.py,sha256=eoZ6GfifbqhMLNzjlqRDVil-yyBkOmVN9ujSgJWNBlY,15
5
+ samples/assets/assets_client.py,sha256=p9LS2Td3gbUMkHn4LD_9RMCFuaQRUbsKvG4PhZBGJDc,1974
2
6
  samples/data/__init__.py,sha256=eoZ6GfifbqhMLNzjlqRDVil-yyBkOmVN9ujSgJWNBlY,15
3
- samples/data/data_client.py,sha256=a2LBi1KqakA01KR-y0nsB9HqUsH0Hj3nAsk0tP5KhlI,4287
4
- samples/data/data_streaming_client.py,sha256=EILzTCTQ0WkPymdq7V3OW_-JQdfRSZ8aBBYkbpujwqg,3498
5
- samples/data/data_streaming_client_async.py,sha256=7KyODfZLBFLFRJcux6BtacS05cPKDn78G4lZa978Qgo,3853
7
+ samples/data/data_client.py,sha256=rH91Yj7oIF6dBOOzF99cUn_12cgDLkw02SNIQM2nB1o,4868
8
+ samples/data/data_streaming_client.py,sha256=IdCUNdfo2_dR9OLgWZuIc9U9s-0-uNjwqkf9aLoG-44,4088
9
+ samples/data/data_streaming_client_async.py,sha256=o_u1ch8JdbcYnLCvDCCUzO-Z1IG7BUKncu_GB_kCbOs,4443
10
+ samples/order/__init__.py,sha256=eoZ6GfifbqhMLNzjlqRDVil-yyBkOmVN9ujSgJWNBlY,15
11
+ samples/order/order_option_client.py,sha256=njxr1QZc69_VbHsp9D9cOv1_-AekO2uNYKiY7o0-1Eg,3602
12
+ samples/order/order_stock_client.py,sha256=hYwVwVFVERk-YS7gRcW-LuGfAvcF8VJHZs1qwLcFhYU,3529
6
13
  samples/trade/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- samples/trade/trade_client.py,sha256=bXX3onGK6aRFpopeE3yDQAx4Xo9dDc-k9gGbwB9El0A,5678
8
- samples/trade/trade_client_v2.py,sha256=qKlAhalnB770PyVRvPxnLOTW7PvXXfr0WIoJphY4jG4,10192
9
- samples/trade/trade_client_v3.py,sha256=4lR6AAFmfpp440XQFPG30oIqUz_PpicUGvrpWoMQ8lU,14884
14
+ samples/trade/trade_client.py,sha256=LyWSvZJH4ByCkRORqP9WdkXfx65A2b6fhc9_GzL8DOM,6258
15
+ samples/trade/trade_client_v2.py,sha256=IO-zmRiUnCmIetTdiYunPh8jVK2VaPrOR77Dwvz1auY,10771
16
+ samples/trade/trade_client_v3.py,sha256=70f4VJYWcSJP3ppA8mRAoftJqjRTn5GTeGZck8eLCTg,16526
10
17
  samples/trade/trade_event_client.py,sha256=uOx9EwFaves2yaTNm13BHpYC0IHXSoBL8BWPbDLeFw4,2469
11
- webull/__init__.py,sha256=iNeZ6LZ9iCV_ugsqAh5K1fFqbKkfU8o3NbLYBWI3m1I,22
12
- webull/core/__init__.py,sha256=x2CgEC54GYD2NVdrqypNOZ9qo3nqJK4EfI78rCpttAU,225
13
- webull/core/client.py,sha256=1xH9XS92AKdUiVUPJYtzeaIl88bwa_ChDALJAxrXWQ0,16229
18
+ webull/__init__.py,sha256=mFFUUCx5TqyW1TTFRrWDhXXVMJDMRxXWrkHanVtp9oY,22
19
+ webull/core/__init__.py,sha256=l5SgEaJNDBxUASBJsNGBJYu3AocnG4Mr89RIOokPiTA,225
20
+ webull/core/client.py,sha256=OE_ghPA558eE85MmaU_I3Tg0j-osCQFChTk-6dy4OW0,16398
14
21
  webull/core/compat.py,sha256=HDis0D271oQ6OCpA7ViX10NJdhfXdoBfzx6nuV3vrnI,3114
15
22
  webull/core/headers.py,sha256=7aMt3_YtaL9Yhqj2T1g7ESQgkI78SAXIk7hMX5U5FgE,2019
16
23
  webull/core/request.py,sha256=BMR9kv3KDpaX2hh9BX7qPn5hhTB7XqJkMj2OSXPvoUk,8340
@@ -50,14 +57,15 @@ webull/core/http/protocol_type.py,sha256=DIPZ3i_r8nxaW5hVRVTyjwpKtWWTSHoGN_nA1SB
50
57
  webull/core/http/request.py,sha256=GUrxb5n2ozVtb45-yBXe9iT0E61guez_udluMKF1k1w,3769
51
58
  webull/core/http/response.py,sha256=Q4W2azXn9hqYuR1NVt8NlHmIaFAi0lmD3ejXRygk2KA,5868
52
59
  webull/core/http/initializer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
- webull/core/http/initializer/client_initializer.py,sha256=olKBWcloSpGOSoMkHez0wQ9arwsqxGTQN5-YQAetcQk,4171
60
+ webull/core/http/initializer/client_initializer.py,sha256=2Uya5T2i9qqwW0JTzjSk7HWsoHW1AOiEduAnx4Zu8k8,4197
54
61
  webull/core/http/initializer/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
62
  webull/core/http/initializer/config/config_operation.py,sha256=_1MILnzVu61XSV74yLsEzqLAt4GQNiJ9QpLN3kws9Q0,1766
56
63
  webull/core/http/initializer/config/bean/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
64
  webull/core/http/initializer/config/bean/query_config_request.py,sha256=WOLYv06KD6l47LH0VKs7_Npm3AmyPKj3b6nY9OrA7n0,1569
58
65
  webull/core/http/initializer/token/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
- webull/core/http/initializer/token/token_manager.py,sha256=nc9F14uJ5iwqi2MK9R7QvhU9SqBIlWr8wr2jYrmtxRM,11197
66
+ webull/core/http/initializer/token/token_manager.py,sha256=_5AFmiAtBBVgDf0VUaplZMyf74qbQ5o0N88XKEmGOis,11110
60
67
  webull/core/http/initializer/token/token_operation.py,sha256=iI2H1cKfAe3gpVptHCniZso77kuPnf3jyg5b4QYQD5E,2577
68
+ webull/core/http/initializer/token/token_storage.py,sha256=ncGOBxR-Mtt-DFk29DTCv9T92PRRC6Ud1bVMTa46NE8,5834
61
69
  webull/core/http/initializer/token/bean/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
62
70
  webull/core/http/initializer/token/bean/access_token.py,sha256=wK5UrTnAMacO_vkhym-BcmbJdkHJcYHIpFcXJrQp1r8,1529
63
71
  webull/core/http/initializer/token/bean/check_token_request.py,sha256=mYuwJPiwlnNGHxlgBMqkzGji9ZOOJ9c-o_hRwfFx0vA,1658
@@ -178,7 +186,7 @@ webull/core/vendored/requests/packages/urllib3/util/ssl_.py,sha256=aRUKc1WIyS-sU
178
186
  webull/core/vendored/requests/packages/urllib3/util/timeout.py,sha256=sAyiBBds7eOk1oM3ulvVMWZiqx1B743puHqK92XwBcY,10325
179
187
  webull/core/vendored/requests/packages/urllib3/util/url.py,sha256=_CgqbyNrQWubrv_y5aWhuutz3mnbj1cvTUe4VYbGYWA,7367
180
188
  webull/core/vendored/requests/packages/urllib3/util/wait.py,sha256=0FHS8R3OrMU-97XWt8AxuUStkSGXTct9CfOwY_fWn7U,5971
181
- webull/data/__init__.py,sha256=LXH5atFzj-GoZQ4-tPUfhhOIAT61DA8dpVqg6ZwaGmk,38
189
+ webull/data/__init__.py,sha256=s8jHH-qZ5jY8Rsk-YS41NY0gFUU5YjdRBlDw0egN2-4,38
182
190
  webull/data/data_client.py,sha256=JGouoFd37-sxpP6GyOJ0rRw7UuBmXqoIcis0jiRGJPQ,1871
183
191
  webull/data/data_streaming_client.py,sha256=hYqrdKTeIB3Xmp7LMjMwN1XE6Y7bKXX0y-lGX1ty6W4,3870
184
192
  webull/data/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -196,7 +204,7 @@ webull/data/common/timespan.py,sha256=Mg-dnvIWclVZRhIkjDiGzRlnOK457PWZ8sY5I8NXRB
196
204
  webull/data/internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
197
205
  webull/data/internal/default_retry_policy.py,sha256=g94o5LKtbB5XoDiZWHp7yKGOWTTTOT89bz1ZSlT79mI,3307
198
206
  webull/data/internal/exceptions.py,sha256=qtInXjns6ce6KQTtJOxz8M4bywuOJe2sNSAwGJqTWDs,1800
199
- webull/data/internal/quotes_client.py,sha256=9cg6FS_vUgaQTuC5Eb0ASau6A0yyCrBDj5Q1FXden1M,12822
207
+ webull/data/internal/quotes_client.py,sha256=PCOIteRz1QzwAva9D1DenrGOHOjm_XLrB250Qeqq1Fk,13009
200
208
  webull/data/internal/quotes_decoder.py,sha256=Y849H41h-cnKPjFe9xDsvG3bWD3l7FnMcCM16yu5RuM,1420
201
209
  webull/data/internal/quotes_payload_decoder.py,sha256=x2XGYDu4wSdc1LLrcIo3BXeb5jgqylFK7_8pfWv_xFU,998
202
210
  webull/data/internal/quotes_topic.py,sha256=kG4RZTjj01U0e54qoOrJHlLrPeBzXekySasm2uoyXVo,1138
@@ -210,13 +218,13 @@ webull/data/quotes/subscribe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
210
218
  webull/data/quotes/subscribe/ask_bid_result.py,sha256=EWu5dFMb_BwFGYPLe-f9uCD8_Ftl1wguhpU1qXi2zaY,1552
211
219
  webull/data/quotes/subscribe/basic_result.py,sha256=l9pxGlt8HLHwE3plq_6J0OPZ_X4ZjjRfa1f9nExu2dE,1450
212
220
  webull/data/quotes/subscribe/broker_result.py,sha256=E6pM0Ydr6tU_bPSOeyu1KJf7oYgbO-lIXLIdJQANH4I,1013
213
- webull/data/quotes/subscribe/message_pb2.py,sha256=I6scR0qacKJWWhBdZq9tvCzSYUXZ9-TuPwpAmnp-FgY,2570
221
+ webull/data/quotes/subscribe/message_pb2.py,sha256=JimxZN9hCEvt_NF0eYRNZJttEsCAc3bWAelE5Tva7yM,3136
214
222
  webull/data/quotes/subscribe/order_result.py,sha256=bqLjRzCDK0jpQBMm00QxfgV-SPg04lOLYxc8SgpkFDw,912
215
223
  webull/data/quotes/subscribe/payload_type.py,sha256=nacQubAzKjutfddNdilckpsbamCUt6nrBnzrxgMOrWQ,674
216
224
  webull/data/quotes/subscribe/quote_decoder.py,sha256=vTDKi3TaQ79ODTydVcEEiJR2g0fQLPf5hUOqo7Mi1gw,1016
217
225
  webull/data/quotes/subscribe/quote_result.py,sha256=2SoRzSGANphAut1jleNnfZtUhpqqbELRcs1biu440p8,1442
218
226
  webull/data/quotes/subscribe/snapshot_decoder.py,sha256=dukBHXa971LHp9oPkgQ4mqL_8yIAU5XSlUIUjpMz2NY,1045
219
- webull/data/quotes/subscribe/snapshot_result.py,sha256=T46uw-hxmWDCJdhXfFsdSn3UtE50Bsc3oYJ9rYS1f6o,2311
227
+ webull/data/quotes/subscribe/snapshot_result.py,sha256=U-YPGAVklcCsXZsp-9TPUUIfvqEgbPsou4Mhkkbxzp8,5077
220
228
  webull/data/quotes/subscribe/tick_decoder.py,sha256=Qb__nJth2vsDMk2bWfNxY-mUGTUPzSZPO-ukHVxq80g,1008
221
229
  webull/data/quotes/subscribe/tick_result.py,sha256=i2-4ZD1GK5EI3NhxkhSmUXfKnkBMjrwVBzLoDUY95OU,1467
222
230
  webull/data/request/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -240,7 +248,7 @@ webull/data/request/get_snapshot_request.py,sha256=OD2PiaxmBCboFDaU8DFTwvfNd2zOb
240
248
  webull/data/request/get_tick_request.py,sha256=rUHhQgC8Z_6ensLyCkg3DDK55SlEi0Bc_dhpCIIspDs,1366
241
249
  webull/data/request/subscribe_request.py,sha256=dm93Q5Q4gigAMqAobolaYYSs3wM6QddPUZOjEtJsX-8,1471
242
250
  webull/data/request/unsubscribe_request.py,sha256=hQA4mYM64PgmQEA2otl47-3nCXqjsSY_weFA3_wNbMM,1446
243
- webull/trade/__init__.py,sha256=iNeZ6LZ9iCV_ugsqAh5K1fFqbKkfU8o3NbLYBWI3m1I,22
251
+ webull/trade/__init__.py,sha256=iCEPnhz-knfGRAO4Ep2uQaYf4xwhPIjjcgAcNjga8kc,21
244
252
  webull/trade/trade_client.py,sha256=_6lH4KO-jKgKWU28fWNsc9-KEHto-WvaP0gT96BJ_dw,2184
245
253
  webull/trade/trade_events_client.py,sha256=4a7qupeT3dKJRKBQqXNhCai6ENzX6PA4yjTDHHxPR0w,8892
246
254
  webull/trade/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -302,6 +310,7 @@ webull/trade/request/v2/preview_order_request.py,sha256=gRld4FcpBFQk7X24hzgs5lmM
302
310
  webull/trade/request/v2/replace_option_request.py,sha256=0S86ToPdfZlZmf85jBQ39a1RkQ8wY7FJZA4hdxmyjN0,1189
303
311
  webull/trade/request/v2/replace_order_request.py,sha256=rptKekCa6uSj1rzg8TT7jnuMVCDquhev6naPESqpihU,1193
304
312
  webull/trade/request/v3/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
313
+ webull/trade/request/v3/batch_place_order_request.py,sha256=QW6EZ-mZVTdLVbzJsr0VxA5Bca4hRgAq9_x6N9xVjnk,2115
305
314
  webull/trade/request/v3/cancel_order_request.py,sha256=x3_5w-0_qwN6CjW2yuM4oLd51QuuTtXNVKZQ23MYscs,1024
306
315
  webull/trade/request/v3/get_order_detail_request.py,sha256=jYuU8vHdE0fkXZdHqyuZ-rnhHUlQ9Qo5r7FvzmCfEKE,1000
307
316
  webull/trade/request/v3/get_order_history_request.py,sha256=iCZEUs6JHOlRuA-u1zL1QvfD8h7gaEg1TR0X4mWxIVo,1304
@@ -318,10 +327,10 @@ webull/trade/trade/v2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
318
327
  webull/trade/trade/v2/account_info_v2.py,sha256=IGY_BGTrZ0h7yQ_nDodNtmKen9gXW6heUFb7VLRQ9bY,2142
319
328
  webull/trade/trade/v2/order_operation_v2.py,sha256=m54RH2j45CBBWEnqe4KxrsltAF44XKtPMT4kv8t7djQ,12745
320
329
  webull/trade/trade/v3/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
321
- webull/trade/trade/v3/order_opration_v3.py,sha256=4OTodBrcfyRuZOMXodNNHjCtsQxCV-5JQXEFBB1S_cE,6738
322
- webull_openapi_python_sdk-1.0.6.dist-info/licenses/LICENSE,sha256=ALOnsLtb1aHxmDJg3-oMi0BO-i-cjfyZaOBfnnavKMc,11359
323
- webull_openapi_python_sdk-1.0.6.dist-info/licenses/NOTICE,sha256=X5TApte6CPV10b96Cb70IRLusXmiRmK_R-dB-1tQM_I,2018
324
- webull_openapi_python_sdk-1.0.6.dist-info/METADATA,sha256=sGda_Dr-h3X5GSFfTa1RIpbcZrVk8I7M4EPEr1DW6V8,702
325
- webull_openapi_python_sdk-1.0.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
326
- webull_openapi_python_sdk-1.0.6.dist-info/top_level.txt,sha256=h8pEjNDGWS2ZUZ2vYFpUShoMQT0ZRIQaD57QJWD8_aI,15
327
- webull_openapi_python_sdk-1.0.6.dist-info/RECORD,,
330
+ webull/trade/trade/v3/order_opration_v3.py,sha256=_L10--m_XhoMe8YeYjQXVvRoTAWoRRzLU59UeneyMrU,7428
331
+ webull_openapi_python_sdk-1.0.8.dist-info/licenses/LICENSE,sha256=ALOnsLtb1aHxmDJg3-oMi0BO-i-cjfyZaOBfnnavKMc,11359
332
+ webull_openapi_python_sdk-1.0.8.dist-info/licenses/NOTICE,sha256=X5TApte6CPV10b96Cb70IRLusXmiRmK_R-dB-1tQM_I,2018
333
+ webull_openapi_python_sdk-1.0.8.dist-info/METADATA,sha256=jcAKegwV60urTKWSWSD1BhcsnEPVUU49Cz22cpSAKU8,702
334
+ webull_openapi_python_sdk-1.0.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
335
+ webull_openapi_python_sdk-1.0.8.dist-info/top_level.txt,sha256=h8pEjNDGWS2ZUZ2vYFpUShoMQT0ZRIQaD57QJWD8_aI,15
336
+ webull_openapi_python_sdk-1.0.8.dist-info/RECORD,,