pyarrow-client 1.0.0__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.
- pyarrow/__version__.py +10 -0
- pyarrow/arrow_utils.py +230 -0
- pyarrow/connect.py +399 -0
- pyarrow/constants.py +128 -0
- pyarrow/exceptions.py +36 -0
- pyarrow/sockets.py +582 -0
- pyarrow_client-1.0.0.dist-info/METADATA +490 -0
- pyarrow_client-1.0.0.dist-info/RECORD +11 -0
- pyarrow_client-1.0.0.dist-info/WHEEL +5 -0
- pyarrow_client-1.0.0.dist-info/licenses/LICENSE +21 -0
- pyarrow_client-1.0.0.dist-info/top_level.txt +1 -0
pyarrow/__version__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
__title__ = 'pyarrow-client' # Match the PyPI package name
|
|
2
|
+
__version__ = '1.0.0'
|
|
3
|
+
__description__ = 'Official Python Client Library for Arrow Trading API'
|
|
4
|
+
__url__ = 'https://arrow.trade'
|
|
5
|
+
__download_url__ = 'https://pypi.org/project/py-arrow/'
|
|
6
|
+
__license__ = 'MIT'
|
|
7
|
+
__author__ = 'Abhishek Jain'
|
|
8
|
+
__author_email__ = 'abhishek.jain@irage.in'
|
|
9
|
+
__maintainer__ = 'Arrow Trading Team'
|
|
10
|
+
__maintainer_email__ = 'support@arrow.trade'
|
pyarrow/arrow_utils.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
expiry_dates = {
|
|
2
|
+
"NIFTY": {
|
|
3
|
+
"2018": [
|
|
4
|
+
"25-Jan-2018",
|
|
5
|
+
"22-Feb-2018",
|
|
6
|
+
"28-Mar-2018",
|
|
7
|
+
"29-Mar-2018",
|
|
8
|
+
"26-Apr-2018",
|
|
9
|
+
"31-May-2018",
|
|
10
|
+
"28-Jun-2018",
|
|
11
|
+
"26-Jul-2018",
|
|
12
|
+
"30-Aug-2018",
|
|
13
|
+
"27-Sep-2018",
|
|
14
|
+
"25-Oct-2018",
|
|
15
|
+
"29-Nov-2018",
|
|
16
|
+
"27-Dec-2018"
|
|
17
|
+
],
|
|
18
|
+
"2019": [
|
|
19
|
+
"31-Jan-2019",
|
|
20
|
+
"14-Feb-2019",
|
|
21
|
+
"21-Feb-2019",
|
|
22
|
+
"28-Feb-2019",
|
|
23
|
+
"07-Mar-2019",
|
|
24
|
+
"14-Mar-2019",
|
|
25
|
+
"20-Mar-2019",
|
|
26
|
+
"28-Mar-2019",
|
|
27
|
+
"04-Apr-2019",
|
|
28
|
+
"11-Apr-2019",
|
|
29
|
+
"18-Apr-2019",
|
|
30
|
+
"25-Apr-2019",
|
|
31
|
+
"02-May-2019",
|
|
32
|
+
"09-May-2019",
|
|
33
|
+
"16-May-2019",
|
|
34
|
+
"23-May-2019",
|
|
35
|
+
"30-May-2019",
|
|
36
|
+
"06-Jun-2019",
|
|
37
|
+
"13-Jun-2019",
|
|
38
|
+
"20-Jun-2019",
|
|
39
|
+
"27-Jun-2019",
|
|
40
|
+
"04-Jul-2019",
|
|
41
|
+
"11-Jul-2019",
|
|
42
|
+
"18-Jul-2019",
|
|
43
|
+
"25-Jul-2019",
|
|
44
|
+
"01-Aug-2019",
|
|
45
|
+
"08-Aug-2019",
|
|
46
|
+
"14-Aug-2019",
|
|
47
|
+
"22-Aug-2019",
|
|
48
|
+
"29-Aug-2019",
|
|
49
|
+
"05-Sep-2019",
|
|
50
|
+
"12-Sep-2019",
|
|
51
|
+
"19-Sep-2019",
|
|
52
|
+
"26-Sep-2019",
|
|
53
|
+
"03-Oct-2019",
|
|
54
|
+
"10-Oct-2019",
|
|
55
|
+
"17-Oct-2019",
|
|
56
|
+
"24-Oct-2019",
|
|
57
|
+
"31-Oct-2019",
|
|
58
|
+
"07-Nov-2019",
|
|
59
|
+
"14-Nov-2019",
|
|
60
|
+
"21-Nov-2019",
|
|
61
|
+
"28-Nov-2019",
|
|
62
|
+
"05-Dec-2019",
|
|
63
|
+
"12-Dec-2019",
|
|
64
|
+
"19-Dec-2019",
|
|
65
|
+
"26-Dec-2019"
|
|
66
|
+
],
|
|
67
|
+
"2020": [
|
|
68
|
+
"02-Jan-2020",
|
|
69
|
+
"09-Jan-2020",
|
|
70
|
+
"16-Jan-2020",
|
|
71
|
+
"23-Jan-2020",
|
|
72
|
+
"30-Jan-2020",
|
|
73
|
+
"06-Feb-2020",
|
|
74
|
+
"13-Feb-2020",
|
|
75
|
+
"20-Feb-2020",
|
|
76
|
+
"27-Feb-2020",
|
|
77
|
+
"05-Mar-2020",
|
|
78
|
+
"12-Mar-2020",
|
|
79
|
+
"19-Mar-2020",
|
|
80
|
+
"26-Mar-2020",
|
|
81
|
+
"01-Apr-2020",
|
|
82
|
+
"09-Apr-2020",
|
|
83
|
+
"16-Apr-2020",
|
|
84
|
+
"23-Apr-2020",
|
|
85
|
+
"30-Apr-2020",
|
|
86
|
+
"07-May-2020",
|
|
87
|
+
"14-May-2020",
|
|
88
|
+
"21-May-2020",
|
|
89
|
+
"28-May-2020",
|
|
90
|
+
"04-Jun-2020",
|
|
91
|
+
"11-Jun-2020",
|
|
92
|
+
"18-Jun-2020",
|
|
93
|
+
"25-Jun-2020",
|
|
94
|
+
"02-Jul-2020",
|
|
95
|
+
"09-Jul-2020",
|
|
96
|
+
"16-Jul-2020",
|
|
97
|
+
"23-Jul-2020",
|
|
98
|
+
"30-Jul-2020",
|
|
99
|
+
"06-Aug-2020",
|
|
100
|
+
"13-Aug-2020",
|
|
101
|
+
"20-Aug-2020",
|
|
102
|
+
"27-Aug-2020",
|
|
103
|
+
"03-Sep-2020",
|
|
104
|
+
"10-Sep-2020",
|
|
105
|
+
"17-Sep-2020",
|
|
106
|
+
"24-Sep-2020",
|
|
107
|
+
"01-Oct-2020",
|
|
108
|
+
"08-Oct-2020",
|
|
109
|
+
"15-Oct-2020",
|
|
110
|
+
"22-Oct-2020",
|
|
111
|
+
"29-Oct-2020",
|
|
112
|
+
"05-Nov-2020",
|
|
113
|
+
"12-Nov-2020",
|
|
114
|
+
"19-Nov-2020",
|
|
115
|
+
"26-Nov-2020",
|
|
116
|
+
"03-Dec-2020",
|
|
117
|
+
"10-Dec-2020",
|
|
118
|
+
"17-Dec-2020",
|
|
119
|
+
"24-Dec-2020",
|
|
120
|
+
"31-Dec-2020"
|
|
121
|
+
],
|
|
122
|
+
"2021": [
|
|
123
|
+
"07-Jan-2021",
|
|
124
|
+
"14-Jan-2021",
|
|
125
|
+
"21-Jan-2021",
|
|
126
|
+
"28-Jan-2021",
|
|
127
|
+
"04-Feb-2021",
|
|
128
|
+
"11-Feb-2021",
|
|
129
|
+
"18-Feb-2021",
|
|
130
|
+
"25-Feb-2021",
|
|
131
|
+
"04-Mar-2021",
|
|
132
|
+
"10-Mar-2021",
|
|
133
|
+
"18-Mar-2021",
|
|
134
|
+
"25-Mar-2021",
|
|
135
|
+
"01-Apr-2021",
|
|
136
|
+
"08-Apr-2021",
|
|
137
|
+
"15-Apr-2021",
|
|
138
|
+
"22-Apr-2021",
|
|
139
|
+
"29-Apr-2021",
|
|
140
|
+
"06-May-2021",
|
|
141
|
+
"12-May-2021",
|
|
142
|
+
"20-May-2021",
|
|
143
|
+
"27-May-2021",
|
|
144
|
+
"03-Jun-2021",
|
|
145
|
+
"10-Jun-2021",
|
|
146
|
+
"17-Jun-2021",
|
|
147
|
+
"24-Jun-2021",
|
|
148
|
+
"01-Jul-2021",
|
|
149
|
+
"08-Jul-2021",
|
|
150
|
+
"15-Jul-2021",
|
|
151
|
+
"22-Jul-2021",
|
|
152
|
+
"29-Jul-2021",
|
|
153
|
+
"05-Aug-2021",
|
|
154
|
+
"12-Aug-2021",
|
|
155
|
+
"18-Aug-2021",
|
|
156
|
+
"26-Aug-2021",
|
|
157
|
+
"02-Sep-2021",
|
|
158
|
+
"09-Sep-2021",
|
|
159
|
+
"16-Sep-2021",
|
|
160
|
+
"23-Sep-2021",
|
|
161
|
+
"30-Sep-2021",
|
|
162
|
+
"07-Oct-2021",
|
|
163
|
+
"14-Oct-2021",
|
|
164
|
+
"21-Oct-2021",
|
|
165
|
+
"28-Oct-2021",
|
|
166
|
+
"03-Nov-2021",
|
|
167
|
+
"11-Nov-2021",
|
|
168
|
+
"18-Nov-2021",
|
|
169
|
+
"25-Nov-2021",
|
|
170
|
+
"02-Dec-2021",
|
|
171
|
+
"09-Dec-2021",
|
|
172
|
+
"16-Dec-2021",
|
|
173
|
+
"23-Dec-2021",
|
|
174
|
+
"30-Dec-2021"
|
|
175
|
+
],
|
|
176
|
+
"2022": ["06-Jan-2022",
|
|
177
|
+
"13-Jan-2022",
|
|
178
|
+
"20-Jan-2022",
|
|
179
|
+
"27-Jan-2022",
|
|
180
|
+
"03-Feb-2022",
|
|
181
|
+
"10-Feb-2022",
|
|
182
|
+
"17-Feb-2022",
|
|
183
|
+
"24-Feb-2022",
|
|
184
|
+
"03-Mar-2022",
|
|
185
|
+
"10-Mar-2022",
|
|
186
|
+
"17-Mar-2022",
|
|
187
|
+
"24-Mar-2022",
|
|
188
|
+
"31-Mar-2022",
|
|
189
|
+
"07-Apr-2022",
|
|
190
|
+
"13-Apr-2022",
|
|
191
|
+
"21-Apr-2022",
|
|
192
|
+
"28-Apr-2022",
|
|
193
|
+
"05-May-2022",
|
|
194
|
+
"12-May-2022",
|
|
195
|
+
"19-May-2022",
|
|
196
|
+
"26-May-2022",
|
|
197
|
+
"02-Jun-2022",
|
|
198
|
+
"09-Jun-2022",
|
|
199
|
+
"16-Jun-2022",
|
|
200
|
+
"23-Jun-2022",
|
|
201
|
+
"30-Jun-2022",
|
|
202
|
+
"07-Jul-2022",
|
|
203
|
+
"14-Jul-2022",
|
|
204
|
+
"21-Jul-2022",
|
|
205
|
+
"28-Jul-2022",
|
|
206
|
+
"04-Aug-2022",
|
|
207
|
+
"11-Aug-2022",
|
|
208
|
+
"18-Aug-2022",
|
|
209
|
+
"25-Aug-2022",
|
|
210
|
+
"01-Sep-2022",
|
|
211
|
+
"08-Sep-2022",
|
|
212
|
+
"15-Sep-2022",
|
|
213
|
+
"22-Sep-2022",
|
|
214
|
+
"29-Sep-2022",
|
|
215
|
+
"06-Oct-2022",
|
|
216
|
+
"13-Oct-2022",
|
|
217
|
+
"20-Oct-2022",
|
|
218
|
+
"27-Oct-2022",
|
|
219
|
+
"03-Nov-2022",
|
|
220
|
+
"10-Nov-2022",
|
|
221
|
+
"17-Nov-2022",
|
|
222
|
+
"24-Nov-2022",
|
|
223
|
+
"01-Dec-2022",
|
|
224
|
+
"08-Dec-2022",
|
|
225
|
+
"15-Dec-2022",
|
|
226
|
+
"22-Dec-2022",
|
|
227
|
+
"29-Dec-2022"],
|
|
228
|
+
|
|
229
|
+
}
|
|
230
|
+
}
|
pyarrow/connect.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
6
|
+
from urllib.parse import urlparse, parse_qs
|
|
7
|
+
import pyarrow.exceptions as ex
|
|
8
|
+
import pyotp
|
|
9
|
+
from pyarrow.arrow_utils import expiry_dates
|
|
10
|
+
|
|
11
|
+
from pyarrow.__version__ import __title__, __version__
|
|
12
|
+
from pyarrow.constants import Exchange, OrderType, ProductType, Retention, TransactionType, Variety
|
|
13
|
+
|
|
14
|
+
import dateutil.parser
|
|
15
|
+
import requests
|
|
16
|
+
import urllib3
|
|
17
|
+
|
|
18
|
+
import pyarrow.constants as constants
|
|
19
|
+
|
|
20
|
+
log = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ArrowClient(object):
|
|
24
|
+
DEFAULT_TIMEOUT = 10
|
|
25
|
+
DEFAULT_ROOT_URL = "https://edge.arrow.trade"
|
|
26
|
+
DEFAULT_LOGIN_URL = "https://api.arrow.trade/auth/app/login"
|
|
27
|
+
VALIDATE_2FA_URL = "https://api.arrow.trade/auth/validate-2fa"
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self, app_id: str,
|
|
31
|
+
timeout: Optional[int] = None,
|
|
32
|
+
debug: bool = False,
|
|
33
|
+
root_url: Optional[str] = None,
|
|
34
|
+
pool_config: Optional[Dict[str, Any]] = None) -> None:
|
|
35
|
+
self.app_id = app_id
|
|
36
|
+
self.req_session = requests.Session()
|
|
37
|
+
self.timeout = timeout or self.DEFAULT_TIMEOUT
|
|
38
|
+
self.debug = debug
|
|
39
|
+
self.root_url = root_url or self.DEFAULT_ROOT_URL
|
|
40
|
+
self.disable_ssl = False
|
|
41
|
+
self.proxies: Dict[str, str] = {}
|
|
42
|
+
|
|
43
|
+
self._token: Optional[str] = None
|
|
44
|
+
self._routes = constants.Routes(root_url=self.root_url)
|
|
45
|
+
|
|
46
|
+
def place_order(self,
|
|
47
|
+
exchange: Exchange,
|
|
48
|
+
symbol: str,
|
|
49
|
+
quantity: int,
|
|
50
|
+
disclosed_quantity: int,
|
|
51
|
+
product: ProductType,
|
|
52
|
+
order_type: OrderType,
|
|
53
|
+
variety: Variety,
|
|
54
|
+
transaction_type: TransactionType,
|
|
55
|
+
price: float,
|
|
56
|
+
validity: Retention,
|
|
57
|
+
tags: Optional[str] = None,
|
|
58
|
+
amo: bool = False,
|
|
59
|
+
trigger_price: Optional[float] = None) -> str:
|
|
60
|
+
params = dict(
|
|
61
|
+
exchange=str(exchange),
|
|
62
|
+
symbol=symbol,
|
|
63
|
+
# token=token,
|
|
64
|
+
quantity="{qty}".format(qty=quantity),
|
|
65
|
+
disclosedQty="{disclosed_qty}".format(disclosed_qty=disclosed_quantity),
|
|
66
|
+
product=str(product),
|
|
67
|
+
order=str(order_type),
|
|
68
|
+
transactionType=str(transaction_type),
|
|
69
|
+
price="{price}".format(price=price),
|
|
70
|
+
validity=str(validity),
|
|
71
|
+
tags=tags or "",
|
|
72
|
+
amo=amo,
|
|
73
|
+
triggerPrice="{trigger_price}".format(trigger_price=trigger_price or "")
|
|
74
|
+
) # type: Dict[str, Any]
|
|
75
|
+
response = self._post(
|
|
76
|
+
self._routes.PLACE_ORDER,
|
|
77
|
+
url_args={"variety": str(variety)},
|
|
78
|
+
params=params
|
|
79
|
+
)
|
|
80
|
+
return response["orderNo"]
|
|
81
|
+
|
|
82
|
+
def set_token(self, token: str) -> None:
|
|
83
|
+
self._token = token
|
|
84
|
+
|
|
85
|
+
def get_token(self) -> str:
|
|
86
|
+
return self._token or ""
|
|
87
|
+
|
|
88
|
+
def invalidate_session(self) -> None:
|
|
89
|
+
self._token = None
|
|
90
|
+
|
|
91
|
+
# TODO
|
|
92
|
+
def get_instruments(self) -> Any:
|
|
93
|
+
# return self._get(self._routes.ALL_INSTRUMENTS)
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
# Order Routes
|
|
97
|
+
def get_order(self, order_id: str) -> List[Dict[str, Any]]:
|
|
98
|
+
data = self._get(self._routes.GET_ORDER_BY_ID, url_args={"order_id": order_id})
|
|
99
|
+
return self._format_order_response(data)
|
|
100
|
+
|
|
101
|
+
def get_orders(self) -> List[Dict[str, Any]]:
|
|
102
|
+
"""Get all user orders."""
|
|
103
|
+
data = self._get(self._routes.GET_USER_ORDERS)
|
|
104
|
+
return self._format_orders_response(data)
|
|
105
|
+
|
|
106
|
+
def get_trades(self) -> List[Dict[str, Any]]:
|
|
107
|
+
"""Get all user trades."""
|
|
108
|
+
data = self._get(self._routes.GET_USER_TRADES)
|
|
109
|
+
return self._format_trades_response(data)
|
|
110
|
+
|
|
111
|
+
def modify_order(
|
|
112
|
+
self,
|
|
113
|
+
order_id: str,
|
|
114
|
+
exchange: Exchange,
|
|
115
|
+
quantity: int,
|
|
116
|
+
symbol: str,
|
|
117
|
+
price: float,
|
|
118
|
+
disclosed_qty: int,
|
|
119
|
+
product: ProductType,
|
|
120
|
+
transaction_type: TransactionType,
|
|
121
|
+
order_type: OrderType,
|
|
122
|
+
validity: Retention,
|
|
123
|
+
amo: bool = False,
|
|
124
|
+
trigger_price: Optional[float] = None,
|
|
125
|
+
tags: Optional[str] = None,
|
|
126
|
+
book_loss_price: Optional[float] = None,
|
|
127
|
+
book_profit_price: Optional[float] = None
|
|
128
|
+
) -> str:
|
|
129
|
+
"""Modify an existing order."""
|
|
130
|
+
params = {
|
|
131
|
+
"exchange": str(exchange),
|
|
132
|
+
"quantity": str(quantity),
|
|
133
|
+
"disclosedQty": str(disclosed_qty),
|
|
134
|
+
"product": str(product),
|
|
135
|
+
"symbol": symbol,
|
|
136
|
+
"transactionType": str(transaction_type),
|
|
137
|
+
"order": str(order_type),
|
|
138
|
+
"price": str(price),
|
|
139
|
+
"validity": str(validity),
|
|
140
|
+
"triggerPrice": str(trigger_price or "0"),
|
|
141
|
+
"bookLossPrice": str(book_loss_price or ""),
|
|
142
|
+
"bookProfitPrice": str(book_profit_price or ""),
|
|
143
|
+
"tags": tags or "",
|
|
144
|
+
"amo": amo
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
response = self._patch(
|
|
148
|
+
self._routes.UPDATE_ORDER_BY_ID,
|
|
149
|
+
url_args={"order_id": order_id},
|
|
150
|
+
params=params
|
|
151
|
+
)
|
|
152
|
+
return response["message"]
|
|
153
|
+
|
|
154
|
+
def cancel_order(self, order_id: str) -> str:
|
|
155
|
+
"""Cancel an existing order."""
|
|
156
|
+
response = self._delete(
|
|
157
|
+
self._routes.DELETE_ORDER,
|
|
158
|
+
url_args={"order_id": order_id}
|
|
159
|
+
)
|
|
160
|
+
return response["message"]
|
|
161
|
+
|
|
162
|
+
# User Details Routes
|
|
163
|
+
def get_user_details(self) -> Dict[str, Any]:
|
|
164
|
+
return self._get(self._routes.USER_DETAILS)
|
|
165
|
+
|
|
166
|
+
# User Holdings
|
|
167
|
+
def get_holdings(self) -> Dict[str, Any]:
|
|
168
|
+
return self._get(self._routes.GET_USER_HOLDINGS)
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def get_expiry_dates(symbol: str, year: str):
|
|
172
|
+
return expiry_dates[symbol][year]
|
|
173
|
+
|
|
174
|
+
# Margin Routes
|
|
175
|
+
def get_order_margin(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
176
|
+
return self._post(self._routes.ORDER_MARGIN, params=params)
|
|
177
|
+
|
|
178
|
+
def get_basket_margin(self, orders: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
179
|
+
return self._post(self._routes.BASKET_MARGIN, params=orders)
|
|
180
|
+
|
|
181
|
+
def _authenticate(self, checksum: str, request_token: str) -> Dict[str, str]:
|
|
182
|
+
params = {
|
|
183
|
+
"appID": self.app_id,
|
|
184
|
+
"token": request_token,
|
|
185
|
+
"checksum": checksum,
|
|
186
|
+
}
|
|
187
|
+
response = self._post("https://api.arrow.trade/auth/app/authenticate-token", params=params)
|
|
188
|
+
if "token" in response:
|
|
189
|
+
self._token = response["token"]
|
|
190
|
+
if self.debug:
|
|
191
|
+
log.debug(f"Token: {self._token}")
|
|
192
|
+
|
|
193
|
+
return response
|
|
194
|
+
|
|
195
|
+
def login_url(self) -> str:
|
|
196
|
+
return f"{self._routes.get_home_url()}?appID={self.app_id}"
|
|
197
|
+
|
|
198
|
+
def login(self, request_token: str, api_secret: str) -> Dict[str, str]:
|
|
199
|
+
checksum = self._generate_checksum(request_token, api_secret)
|
|
200
|
+
response = self._authenticate(checksum, request_token)
|
|
201
|
+
return response
|
|
202
|
+
|
|
203
|
+
def auto_login(self, user_id: str, password: str, api_secret: str, totp_secret: str) -> Dict[str, str]:
|
|
204
|
+
login_url = self.DEFAULT_LOGIN_URL
|
|
205
|
+
login_payload = {
|
|
206
|
+
"userID": user_id,
|
|
207
|
+
"password": password,
|
|
208
|
+
"captchaValue": "",
|
|
209
|
+
"captchaID": None,
|
|
210
|
+
"appID": self.app_id,
|
|
211
|
+
"isAppLogin": True
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# Generate the request ID
|
|
215
|
+
try:
|
|
216
|
+
login_resp = self._post(login_url, params=login_payload)
|
|
217
|
+
request_id = login_resp["requestId"]
|
|
218
|
+
except Exception as e:
|
|
219
|
+
log.error("Failed to generate Request id: {}".format(e))
|
|
220
|
+
raise
|
|
221
|
+
|
|
222
|
+
# Generate the totp
|
|
223
|
+
try:
|
|
224
|
+
passcode = pyotp.TOTP(totp_secret).now()
|
|
225
|
+
except Exception as e:
|
|
226
|
+
log.error("Failed to Generate TOTP: {}".format(e))
|
|
227
|
+
raise
|
|
228
|
+
|
|
229
|
+
# Validate 2FA
|
|
230
|
+
totp_payload = {
|
|
231
|
+
"code": passcode, # Fixed: use passcode instead of request_id
|
|
232
|
+
"requestId": request_id,
|
|
233
|
+
"userID": user_id,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
totp_resp = self._post(self.VALIDATE_2FA_URL, params=totp_payload)
|
|
238
|
+
redirect_url = totp_resp["redirectUrl"]
|
|
239
|
+
except Exception as e:
|
|
240
|
+
log.error("Failed to Generate 2FA URL: {}".format(e))
|
|
241
|
+
raise
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
parsed_url = urlparse(redirect_url)
|
|
245
|
+
request_token = parse_qs(parsed_url.query)["request-token"][0]
|
|
246
|
+
except Exception as e:
|
|
247
|
+
log.error("Failed to generate request token: {}".format(e))
|
|
248
|
+
raise
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
response = self.login(request_token=request_token, api_secret=api_secret)
|
|
252
|
+
self.set_token(response["token"])
|
|
253
|
+
return response
|
|
254
|
+
except Exception as e:
|
|
255
|
+
log.error("Failed to complete login: {}".format(e))
|
|
256
|
+
raise
|
|
257
|
+
|
|
258
|
+
def _generate_checksum(self, request_token: str, api_secret: str) -> str:
|
|
259
|
+
input_str = f"{self.app_id}:{api_secret}:{request_token}"
|
|
260
|
+
return hashlib.sha256(input_str.encode("utf-8")).hexdigest()
|
|
261
|
+
|
|
262
|
+
# HTTP Route Methods
|
|
263
|
+
def _get(self, route: str, url_args: Optional[Dict[str, str]] = None, params: Any = None) -> Any:
|
|
264
|
+
return self._request("GET", route, url_args, params)
|
|
265
|
+
|
|
266
|
+
def _post(self, route: str, url_args: Optional[Dict[str, str]] = None, params: Any = None,
|
|
267
|
+
query_params: Any = None) -> Any:
|
|
268
|
+
return self._request("POST", route, url_args, params, query_params)
|
|
269
|
+
|
|
270
|
+
def _patch(self, route: str, url_args: Optional[Dict[str, str]] = None, params: Any = None,
|
|
271
|
+
query_params: Any = None) -> Any:
|
|
272
|
+
return self._request("PATCH", route, url_args, params, query_params)
|
|
273
|
+
|
|
274
|
+
def _delete(self, route: str, url_args: Optional[Dict[str, str]] = None, params: Any = None) -> Any:
|
|
275
|
+
return self._request("DELETE", route, url_args, params)
|
|
276
|
+
|
|
277
|
+
def _request(self, method: str, route: str,
|
|
278
|
+
url_args: Optional[Dict[str, str]] = None,
|
|
279
|
+
params: Any = None, query_params: Any = None) -> Any:
|
|
280
|
+
# Build URL
|
|
281
|
+
url = route.format(**url_args) if url_args else route
|
|
282
|
+
|
|
283
|
+
# Headers
|
|
284
|
+
headers = {
|
|
285
|
+
"User-Agent": f"pyarrow/{__version__}",
|
|
286
|
+
"appID": self.app_id,
|
|
287
|
+
"Content-Type": "application/json" # Add explicit content type
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# Only add token header if we actually have a token (not empty string)
|
|
291
|
+
if self._token:
|
|
292
|
+
headers["token"] = self._token
|
|
293
|
+
|
|
294
|
+
if self.debug:
|
|
295
|
+
log.debug(f"Request: {method} {url} params={json.dumps(params)} headers={json.dumps(headers)}")
|
|
296
|
+
|
|
297
|
+
# Handle parameters correctly
|
|
298
|
+
json_data = None
|
|
299
|
+
query_params_final = query_params
|
|
300
|
+
|
|
301
|
+
if method in ["POST", "PUT", "PATCH"]:
|
|
302
|
+
json_data = params
|
|
303
|
+
elif method in ["GET", "DELETE"] and params:
|
|
304
|
+
query_params_final = params
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
r = self.req_session.request(
|
|
308
|
+
method=method,
|
|
309
|
+
url=url,
|
|
310
|
+
json=json_data,
|
|
311
|
+
params=query_params_final,
|
|
312
|
+
headers=headers,
|
|
313
|
+
verify=not self.disable_ssl,
|
|
314
|
+
allow_redirects=True,
|
|
315
|
+
timeout=self.timeout,
|
|
316
|
+
proxies=self.proxies
|
|
317
|
+
)
|
|
318
|
+
except Exception as e:
|
|
319
|
+
raise e
|
|
320
|
+
|
|
321
|
+
if self.debug:
|
|
322
|
+
log.debug(f"Response: {method} {url}\n{r.content[:1000]!r}")
|
|
323
|
+
|
|
324
|
+
content_type = r.headers.get("content-type", "")
|
|
325
|
+
|
|
326
|
+
# JSON response
|
|
327
|
+
if "json" in content_type:
|
|
328
|
+
try:
|
|
329
|
+
data = r.json()
|
|
330
|
+
except ValueError:
|
|
331
|
+
raise ex.DataException(f"Invalid JSON response: {r.content!r}")
|
|
332
|
+
|
|
333
|
+
if isinstance(data, dict):
|
|
334
|
+
if data.get(constants.STATUS) == constants.ERROR or data.get(constants.ERROR_CODE):
|
|
335
|
+
if r.status_code == 403:
|
|
336
|
+
# TODO: handle session expiry
|
|
337
|
+
pass
|
|
338
|
+
exp = getattr(ex, str(data.get(constants.ERROR_CODE)), ex.GeneralException)
|
|
339
|
+
raise exp(data.get(constants.ERROR_MESSAGE, "Unknown Error"), code=r.status_code)
|
|
340
|
+
return data.get("data", data)
|
|
341
|
+
return data
|
|
342
|
+
|
|
343
|
+
# Binary response
|
|
344
|
+
if "octet-stream" in content_type:
|
|
345
|
+
return r.content
|
|
346
|
+
|
|
347
|
+
# Unknown response type
|
|
348
|
+
raise ex.DataException(f"Unknown Content-Type ({content_type}) Response: {r.content[:1000]!r}")
|
|
349
|
+
|
|
350
|
+
# Data Parsing Methods
|
|
351
|
+
def _format_order_response(self, data: Union[Dict[str, Any], List[Dict[str, Any]]]) -> List[Dict[str, Any]]:
|
|
352
|
+
"""Format order response data."""
|
|
353
|
+
order_list = [data] if isinstance(data, dict) else data
|
|
354
|
+
return self._format_datetime_fields(order_list, ["timeStamp", "exchangeUpdateTime", "requestTime"])
|
|
355
|
+
|
|
356
|
+
def _format_orders_response(self, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
357
|
+
"""Format orders response data."""
|
|
358
|
+
return self._format_datetime_fields(data, ["timeStamp", "exchangeUpdateTime"])
|
|
359
|
+
|
|
360
|
+
def _format_trades_response(self, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
361
|
+
"""Format trades response data."""
|
|
362
|
+
return self._format_datetime_fields(data, ["fillTime", "timeStamp", "exchangeUpdateTime", "requestTime"])
|
|
363
|
+
|
|
364
|
+
@staticmethod
|
|
365
|
+
def _format_datetime_fields(data: List[Dict[str, Any]], datetime_fields: List[str]) -> List[Dict[str, Any]]:
|
|
366
|
+
"""Convert datetime strings and epoch times in response data."""
|
|
367
|
+
for item in data:
|
|
368
|
+
# Convert datetime strings
|
|
369
|
+
for field in datetime_fields:
|
|
370
|
+
if item.get(field) and len(str(item[field])) == 19:
|
|
371
|
+
item[field] = dateutil.parser.parse(item[field])
|
|
372
|
+
|
|
373
|
+
# Convert epoch time to int
|
|
374
|
+
if item.get("orderTime"):
|
|
375
|
+
dt = datetime.strptime(item["orderTime"], "%Y-%m-%dT%H:%M:%S")
|
|
376
|
+
item["orderTime"] = int(dt.timestamp())
|
|
377
|
+
|
|
378
|
+
return data
|
|
379
|
+
|
|
380
|
+
def get_holidays(self) -> Dict[str, Any]:
|
|
381
|
+
return self._get(self._routes.HOLIDAYS_LIST)
|
|
382
|
+
|
|
383
|
+
def get_index_list(self) -> List[Dict[str, Any]]:
|
|
384
|
+
return self._get(self._routes.INDEX_LIST)
|
|
385
|
+
|
|
386
|
+
def get_option_chain_symbols(self):
|
|
387
|
+
return self._get(self._routes.OPTION_CHAIN_SYMBOLS)
|
|
388
|
+
|
|
389
|
+
def get_option_chain(self, params: Dict[str, str]) -> List[Dict[str, Any]]:
|
|
390
|
+
pass
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# Properties
|
|
394
|
+
@property
|
|
395
|
+
def token(self) -> str:
|
|
396
|
+
return self._token or ""
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
Arrow = ArrowClient
|