p123api 2.3.0__tar.gz → 2.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: p123api
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: Portfolio123 API wrapper
5
5
  Home-page: https://github.com/portfolio-123/p123api-py
6
6
  Author: Portfolio123
@@ -0,0 +1 @@
1
+ from .client import Client, ClientException, ClientItemNotFoundException
@@ -3,8 +3,19 @@ import requests
3
3
  import time
4
4
  import pandas
5
5
  from string import Template
6
- from typing import IO, Any, Callable, List, Literal, Optional, Union, overload
7
-
6
+ from typing import IO, Callable, List, Literal, Optional, Union, overload
7
+ from typing_extensions import deprecated
8
+
9
+ from .types import (
10
+ DataSeriesInfoResult,
11
+ DataSeriesResult,
12
+ IdResult,
13
+ RankInfoResult,
14
+ RankingMethod,
15
+ StockFactorInfoResult,
16
+ StockFactorResult,
17
+ StrategyInfoResult,
18
+ )
8
19
 
9
20
  ENDPOINT = "https://api.portfolio123.com"
10
21
  AUTH_PATH = "/auth"
@@ -13,10 +24,11 @@ SCREEN_BACKTEST_PATH = "/screen/backtest"
13
24
  SCREEN_RUN_PATH = "/screen/run"
14
25
  UNIVERSE_PATH = "/universe"
15
26
  RANK_PATH = "/rank"
16
- DATA_PATH = "/data"
17
27
  RANK_RANKS_PATH = "/rank/ranks"
18
28
  RANK_PERF_PATH = "/rank/performance"
19
29
  RANK_TOUCH_PATH = Template("/rank/$id/touch")
30
+ RANK_CREATE = "/rank/create"
31
+ DATA_PATH = "/data"
20
32
  DATA_UNIVERSE_PATH = "/data/universe"
21
33
  DATA_PRICES_PATH = Template("/data/prices/$identifier")
22
34
  STRATEGY_DETAILS_PATH = Template("/strategy/$id")
@@ -25,41 +37,54 @@ STRATEGY_TRADING_SYSTEM_PATH = Template("/strategy/$id/trading-system")
25
37
  BOOK_TRADING_SYSTEM_PATH = Template("/strategy/$id/book-trading-system")
26
38
  SIM_RERUN_PATH = Template("/strategy/$id/rerun")
27
39
  BOOK_SIM_RERUN_PATH = Template("/strategy/$id/book-rerun")
40
+ STRATEGY_INFO_PATH = "/strategy"
28
41
  STRATEGY_REBALANCE_PATH = Template("/strategy/$id/rebalance")
29
42
  STRATEGY_REBALANCE_COMMIT_PATH = Template("/strategy/$id/rebalance/commit")
30
43
  STRATEGY_TRANS_PATH = Template("/strategy/$id/transactions")
44
+ STRATEGY_COPY_PATH = Template("/strategy/$id/copy")
45
+ BOOK_COPY_PATH = Template("/strategy/$id/copy-book")
31
46
  STOCK_FACTOR_UPLOAD_PATH = Template("/stockFactor/upload/$id")
32
47
  STOCK_FACTOR_CREATE_UPDATE_PATH = "/stockFactor"
33
48
  STOCK_FACTOR_DOWNLOAD_PATH = Template("/stockFactor/$id")
34
- STOCK_FACTOR_DELETE_PATH = STOCK_FACTOR_DOWNLOAD_PATH
35
- STOCK_FACTOR_INFO_PATH = STOCK_FACTOR_CREATE_UPDATE_PATH
49
+ STOCK_FACTOR_DELETE_PATH = Template("/stockFactor/$id")
50
+ STOCK_FACTOR_INFO_PATH = "/stockFactor"
36
51
  DATA_SERIES_UPLOAD_PATH = Template("/dataSeries/upload/$id")
37
52
  DATA_SERIES_CREATE_UPDATE_PATH = "/dataSeries"
53
+ DATA_SERIES_INFO_PATH = "/dataSeries"
38
54
  DATA_SERIES_DELETE_PATH = Template("/dataSeries/$id")
39
55
  AIFACTOR_PREDICT_PATH = Template("/aiFactor/predict/$id")
40
56
 
41
57
 
42
58
  class ClientException(Exception):
43
- def __init__(self, message, *, resp=None, exception=None):
59
+ def __init__(self, message, *, resp: Union[requests.Response, None] = None, exception: Union[Exception, None] = None):
44
60
  super().__init__(message)
45
61
  self._resp = resp
46
62
  self._exception = exception
47
63
 
48
- def get_resp(self) -> requests.Response:
64
+ def get_resp(self):
49
65
  return self._resp
50
66
 
51
- def get_cause(self) -> Exception:
67
+ def get_cause(self):
52
68
  return self._exception
53
69
 
70
+ @staticmethod
71
+ def build(message, resp: requests.Response):
72
+ if resp.status_code == 404:
73
+ return ClientItemNotFoundException(resp=resp)
74
+ return ClientException(message=message, resp=resp)
75
+
76
+
77
+ class ClientItemNotFoundException(ClientException):
78
+ def __init__(self, resp: requests.Response):
79
+ super().__init__("Item not found", resp=resp)
80
+
54
81
 
55
82
  class Client:
56
83
  """
57
84
  class for interfacing with P123 API
58
85
  """
59
86
 
60
- def __init__(
61
- self, *, api_id, api_key, auth_extra={}, endpoint=ENDPOINT, verify_requests=True
62
- ):
87
+ def __init__(self, *, api_id, api_key, auth_extra={}, endpoint=ENDPOINT, verify_requests=True):
63
88
  self._endpoint = endpoint
64
89
  self._verify_requests = verify_requests
65
90
  self._max_req_retries = 5
@@ -73,6 +98,7 @@ class Client:
73
98
 
74
99
  self._auth_params = {"apiId": api_id, "apiKey": api_key, **auth_extra}
75
100
  self._session = requests.Session()
101
+ self._method_map = {"GET": self._session.get, "POST": self._session.post, "DELETE": self._session.delete}
76
102
 
77
103
  def __enter__(self):
78
104
  return self
@@ -104,18 +130,19 @@ class Client:
104
130
  :return: bool
105
131
  """
106
132
  self._session.headers.clear()
107
- resp = req_with_retry(
133
+ with req_with_retry(
108
134
  self._session.post,
109
135
  self._max_req_retries,
110
136
  url=self._endpoint + AUTH_PATH,
111
137
  json=self._auth_params,
112
138
  verify=self._verify_requests,
113
139
  timeout=30,
114
- )
115
- if resp.status_code == 200:
116
- self._token = resp.text
117
- self._session.headers.update({"Authorization": f"Bearer {resp.text}"})
118
- else:
140
+ ) as resp:
141
+ if resp.status_code == 200:
142
+ self._token = resp.text
143
+ self._session.headers.update({"Authorization": f"Bearer {resp.text}"})
144
+ return
145
+
119
146
  if resp.status_code == 406:
120
147
  message = "user account inactive"
121
148
  elif resp.status_code == 402:
@@ -131,79 +158,49 @@ class Client:
131
158
  raise ClientException(f"API authentication failed{message}", resp=resp)
132
159
 
133
160
  def _req_with_auth_fallback(
134
- self,
135
- *,
136
- name: str,
137
- method: str = "POST",
138
- url: str,
139
- json=None,
140
- params=None,
141
- data=None,
142
- headers=None,
143
- stop: bool = False,
144
- ) -> Optional[requests.Response]:
161
+ self, *, method: Literal["GET", "POST", "DELETE"] = "POST", url: str, json=None, params=None, data=None, headers=None
162
+ ):
145
163
  """
146
164
  Request with authentication fallback, used by all requests (except authentication)
147
- :param name: request action
148
165
  :param method: request method
149
166
  :param url: request url
150
167
  :param json: request json
151
168
  :param params: request params
152
169
  :param data: request data
153
170
  :param headers: request headers
154
- :param stop: flag to stop infinite authentication recursion
155
171
  :return: request response object
156
172
  """
157
- resp = None
158
- if self._session.headers.get("Authorization") is not None:
159
- if method == "POST":
160
- resp = req_with_retry(
161
- self._session.post,
162
- self._max_req_retries,
163
- url=url,
164
- json=json,
165
- params=params,
166
- verify=self._verify_requests,
167
- timeout=self._timeout,
168
- data=data,
169
- headers=headers,
170
- )
171
- else:
172
- req_type = (
173
- self._session.delete if method == "DELETE" else self._session.get
174
- )
175
- resp = req_with_retry(
176
- req_type,
177
- self._max_req_retries,
178
- url=url,
179
- json=json,
180
- params=params,
181
- verify=self._verify_requests,
182
- timeout=self._timeout,
183
- headers=headers,
184
- )
185
- if resp is None or resp.status_code == 401 or resp.status_code == 403:
186
- if not stop:
173
+ reauth = False
174
+ while True:
175
+ if self._session.headers.get("Authorization") is None:
187
176
  self.auth()
188
- return self._req_with_auth_fallback(
189
- name=name,
190
- method=method,
191
- url=url,
192
- json=json,
193
- params=params,
194
- data=data,
195
- headers=headers,
196
- stop=True,
197
- )
198
- elif resp.status_code == 200:
199
- return resp
200
- else:
201
- message = resp.text
202
- if not message and resp.status_code == 402:
203
- message = "request quota exhausted"
204
- if message:
205
- message = ": " + message
206
- raise ClientException(f"API request failed{message}", resp=resp)
177
+ with req_with_retry(
178
+ self._method_map[method],
179
+ self._max_req_retries,
180
+ url=url,
181
+ json=json,
182
+ params=params,
183
+ verify=self._verify_requests,
184
+ timeout=self._timeout,
185
+ data=data,
186
+ headers=headers,
187
+ ) as resp:
188
+
189
+ if resp.status_code == 200:
190
+ return resp.json()
191
+
192
+ if resp.status_code == 401 or resp.status_code == 403:
193
+ del self._session.headers["Authorization"]
194
+ if not reauth:
195
+ reauth = True
196
+ continue
197
+
198
+ message = resp.text
199
+ if not message and resp.status_code == 402:
200
+ message = "request quota exhausted"
201
+ if message:
202
+ message = ": " + message
203
+ raise ClientException.build(f"API request failed{message}", resp=resp)
207
204
 
208
205
  def screen_rolling_backtest(self, params: dict, to_pandas=False):
209
206
  """
@@ -212,11 +209,7 @@ class Client:
212
209
  :param to_pandas:
213
210
  :return:
214
211
  """
215
- ret = self._req_with_auth_fallback(
216
- name="screen rolling backtest",
217
- url=self._endpoint + SCREEN_ROLLING_BACKTEST_PATH,
218
- json=params,
219
- ).json()
212
+ ret = self._req_with_auth_fallback(url=self._endpoint + SCREEN_ROLLING_BACKTEST_PATH, json=params)
220
213
 
221
214
  if to_pandas:
222
215
  rows = ret["rows"]
@@ -237,11 +230,7 @@ class Client:
237
230
  :param to_pandas:
238
231
  :return:
239
232
  """
240
- ret = self._req_with_auth_fallback(
241
- name="screen backtest",
242
- url=self._endpoint + SCREEN_BACKTEST_PATH,
243
- json=params,
244
- ).json()
233
+ ret = self._req_with_auth_fallback(url=self._endpoint + SCREEN_BACKTEST_PATH, json=params)
245
234
 
246
235
  if to_pandas:
247
236
  columns = [
@@ -297,28 +286,14 @@ class Client:
297
286
  rows.append(ret["results"]["upMarkets"])
298
287
  ret["results"]["downMarkets"][0] = "Down Markets"
299
288
  rows.append(ret["results"]["downMarkets"])
300
- panda_results = pandas.DataFrame(
301
- data=rows, columns=ret["results"]["columns"]
302
- )
289
+ panda_results = pandas.DataFrame(data=rows, columns=ret["results"]["columns"])
303
290
 
304
- columns = [
305
- "Date",
306
- "Screen Return",
307
- "Bench Return",
308
- "Turnover %",
309
- "Position Count",
310
- ]
291
+ columns = ["Date", "Screen Return", "Bench Return", "Turnover %", "Position Count"]
311
292
  chart = ret["chart"]
312
293
  rows = []
313
294
  for idx, date in enumerate(chart["dates"]):
314
295
  rows.append(
315
- [
316
- date,
317
- chart["screenReturns"][idx],
318
- chart["benchReturns"][idx],
319
- chart["turnoverPct"][idx],
320
- chart["positionCnt"][idx],
321
- ]
296
+ [date, chart["screenReturns"][idx], chart["benchReturns"][idx], chart["turnoverPct"][idx], chart["positionCnt"][idx]]
322
297
  )
323
298
  panda_chart = pandas.DataFrame(data=rows, columns=columns)
324
299
 
@@ -333,9 +308,7 @@ class Client:
333
308
  :param to_pandas:
334
309
  :return:
335
310
  """
336
- ret = self._req_with_auth_fallback(
337
- name="screen backtest", url=self._endpoint + SCREEN_RUN_PATH, json=params
338
- ).json()
311
+ ret = self._req_with_auth_fallback(url=self._endpoint + SCREEN_RUN_PATH, json=params)
339
312
 
340
313
  if to_pandas:
341
314
  ret = pandas.DataFrame(data=ret["rows"], columns=ret["columns"])
@@ -348,9 +321,7 @@ class Client:
348
321
  :param params:
349
322
  :return:
350
323
  """
351
- return self._req_with_auth_fallback(
352
- name="universe update", url=self._endpoint + UNIVERSE_PATH, json=params
353
- ).json()
324
+ return self._req_with_auth_fallback(url=self._endpoint + UNIVERSE_PATH, json=params)
354
325
 
355
326
  def rank_update(self, params: dict):
356
327
  """
@@ -358,9 +329,7 @@ class Client:
358
329
  :param params:
359
330
  :return:
360
331
  """
361
- return self._req_with_auth_fallback(
362
- name="ranking system update", url=self._endpoint + RANK_PATH, json=params
363
- ).json()
332
+ return self._req_with_auth_fallback(url=self._endpoint + RANK_PATH, json=params)
364
333
 
365
334
  def data(self, params: dict, to_pandas=False):
366
335
  """
@@ -369,9 +338,7 @@ class Client:
369
338
  :param to_pandas:
370
339
  :return:
371
340
  """
372
- ret = self._req_with_auth_fallback(
373
- name="data", url=self._endpoint + DATA_PATH, json=params
374
- ).json()
341
+ ret = self._req_with_auth_fallback(url=self._endpoint + DATA_PATH, json=params)
375
342
 
376
343
  if to_pandas:
377
344
  raw_obj = dict(ret)
@@ -407,9 +374,7 @@ class Client:
407
374
  :param to_pandas:
408
375
  :return:
409
376
  """
410
- ret = self._req_with_auth_fallback(
411
- name="data universe", url=self._endpoint + DATA_UNIVERSE_PATH, json=params
412
- ).json()
377
+ ret = self._req_with_auth_fallback(url=self._endpoint + DATA_UNIVERSE_PATH, json=params)
413
378
 
414
379
  if to_pandas:
415
380
  raw_obj = ret
@@ -417,11 +382,7 @@ class Client:
417
382
  f_indices = range(len(params["formulas"]))
418
383
  if params.get("asOfDt"):
419
384
  for formula_idx in f_indices:
420
- name = (
421
- names[formula_idx]
422
- if names is not None
423
- else f"formula{formula_idx + 1}"
424
- )
385
+ name = names[formula_idx] if names is not None else f"formula{formula_idx + 1}"
425
386
  ret[name] = ret["data"][formula_idx]
426
387
  del ret["dt"], ret["cost"], ret["quotaRemaining"], ret["data"]
427
388
  ret = pandas.DataFrame(ret)
@@ -437,9 +398,7 @@ class Client:
437
398
  includeFigi = True
438
399
  formulas = defaultdict(list)
439
400
  for dtObj in ret["dates"]:
440
- data["dates"].extend(
441
- dtObj["dt"] for _ in range(len(dtObj["p123Uids"]))
442
- )
401
+ data["dates"].extend(dtObj["dt"] for _ in range(len(dtObj["p123Uids"])))
443
402
  data["p123Uids"].extend(dtObj["p123Uids"])
444
403
  data["tickers"].extend(dtObj["tickers"])
445
404
  if includeNames:
@@ -449,11 +408,7 @@ class Client:
449
408
  for formula_idx in f_indices:
450
409
  formulas[formula_idx].extend(dtObj["data"][formula_idx])
451
410
  for formula_idx in f_indices:
452
- name = (
453
- names[formula_idx]
454
- if names is not None
455
- else f"formula{formula_idx + 1}"
456
- )
411
+ name = names[formula_idx] if names is not None else f"formula{formula_idx + 1}"
457
412
  data[name] = formulas[formula_idx]
458
413
  ret = pandas.DataFrame(data)
459
414
  ret.attrs["raw_obj"] = raw_obj
@@ -467,11 +422,7 @@ class Client:
467
422
  :param to_pandas:
468
423
  :return:
469
424
  """
470
- ret = self._req_with_auth_fallback(
471
- name="ranking system ranks",
472
- url=self._endpoint + RANK_RANKS_PATH,
473
- json=params,
474
- ).json()
425
+ ret = self._req_with_auth_fallback(url=self._endpoint + RANK_RANKS_PATH, json=params)
475
426
 
476
427
  if to_pandas:
477
428
  names = dict()
@@ -511,23 +462,63 @@ class Client:
511
462
  :param params:
512
463
  :return:
513
464
  """
514
- return self._req_with_auth_fallback(
515
- name="ranking system performance",
516
- url=self._endpoint + RANK_PERF_PATH,
517
- json=params,
518
- ).json()
465
+ return self._req_with_auth_fallback(url=self._endpoint + RANK_PERF_PATH, json=params)
519
466
 
520
467
  def rank_touch(self, rank_id: int):
521
468
  """
522
469
  Rank touch
523
470
  :param rank_id:
524
471
  """
525
- self._req_with_auth_fallback(
526
- name="rank touch",
472
+ self._req_with_auth_fallback(method="POST", url=self._endpoint + RANK_TOUCH_PATH.substitute(id=rank_id))
473
+
474
+ def rank_create(
475
+ self,
476
+ name: str,
477
+ nodes: str,
478
+ *,
479
+ rankingMethod=RankingMethod.PERCENTILE_NA_NEGATIVE,
480
+ type: Literal["Stock", "ETF"] = "Stock",
481
+ currency="USD",
482
+ ) -> IdResult:
483
+ """
484
+ Creates Ranking System
485
+
486
+ :param name: Rank name
487
+ :param nodes: Rank nodes XML
488
+ :param rankingMethod: Ranking method
489
+ :param type: Ranking method type ["Stock", "ETF"]
490
+ :param currency: Ranking method currency. Example: USD
491
+ :return: rank_id:
492
+ """
493
+ return self._req_with_auth_fallback(
527
494
  method="POST",
528
- url=self._endpoint + RANK_TOUCH_PATH.substitute(id=rank_id),
495
+ url=self._endpoint + RANK_CREATE,
496
+ json={"name": name, "nodes": nodes, "currency": currency, "type": type, "rankingMethod": rankingMethod},
529
497
  )
530
498
 
499
+ @overload
500
+ def rank_get(self, *, id: int) -> RankInfoResult: ...
501
+ @overload
502
+ def rank_get(self, *, name: str) -> RankInfoResult: ...
503
+ def rank_get(self, *, id: Optional[int] = None, name: Optional[str] = None) -> RankInfoResult:
504
+ """
505
+ Gets Rank info
506
+
507
+ :param id: Rank Id
508
+ :param name: Rank name
509
+ :return: RankInfoResult object containing:
510
+ - name (str)
511
+ - id (int)
512
+ - xml (str)
513
+ - currency (str)
514
+ - description (str)
515
+ - rankingMethod (int)
516
+ - type (Literal["Stock", "ETF"])
517
+ - groupUid (int)
518
+ - resolveGroupUid (int)
519
+ """
520
+ return self._req_with_auth_fallback(method="GET", url=self._endpoint + RANK_PATH, params={"id": id, "name": name})
521
+
531
522
  def strategy(self, strategy_id: int):
532
523
  """
533
524
  Strategy details
@@ -535,15 +526,35 @@ class Client:
535
526
  :return:
536
527
  """
537
528
 
529
+ return self._req_with_auth_fallback(method="GET", url=self._endpoint + STRATEGY_DETAILS_PATH.substitute(id=strategy_id))
530
+
531
+ def strategy_copy(self, id: int, name: str, type: Optional[Literal["PTF", "SIM"]] = None) -> IdResult:
532
+ """
533
+ Strategy copy
534
+
535
+ :param id: Strategy Id
536
+ :param name: name of the strategy copy
537
+ :param type: type of the strategy copy ("PTF"|"SIM")
538
+ :return: id
539
+ """
540
+ return self._req_with_auth_fallback(
541
+ method="POST", url=self._endpoint + STRATEGY_COPY_PATH.substitute(id=id), json={"name": name, "type": type}
542
+ )
543
+
544
+ def book_copy(self, id: int, name: str, type: Optional[Literal["BOOK", "BOOKSIM"]] = None) -> IdResult:
545
+ """
546
+ Book copy
547
+
548
+ :param book_id:
549
+ :param name: name of the book copy
550
+ :param type: type of the book copy ("BOOK"|"BOOKSIM")
551
+ :return: id
552
+ """
538
553
  return self._req_with_auth_fallback(
539
- name="strategy details",
540
- method="GET",
541
- url=self._endpoint + STRATEGY_DETAILS_PATH.substitute(id=strategy_id),
542
- ).json()
554
+ method="POST", url=self._endpoint + BOOK_COPY_PATH.substitute(id=id), json={"name": name, "type": type}
555
+ )
543
556
 
544
- def strategy_transactions(
545
- self, strategy_id: int, start: str, end: str, to_pandas=False
546
- ):
557
+ def strategy_transactions(self, strategy_id: int, start: str, end: str, to_pandas=False):
547
558
  """
548
559
  Strategy transactions
549
560
  :param strategy_id:
@@ -553,11 +564,8 @@ class Client:
553
564
  """
554
565
 
555
566
  ret = self._req_with_auth_fallback(
556
- name="strategy transactions",
557
- method="GET",
558
- url=self._endpoint + STRATEGY_TRANS_PATH.substitute(id=strategy_id),
559
- params=[("start", start), ("end", end)],
560
- ).json()
567
+ method="GET", url=self._endpoint + STRATEGY_TRANS_PATH.substitute(id=strategy_id), params=[("start", start), ("end", end)]
568
+ )
561
569
  return pandas.DataFrame(ret["trans"]) if to_pandas else ret
562
570
 
563
571
  def strategy_transaction_import(
@@ -585,18 +593,13 @@ class Client:
585
593
  get_params.append(("makeRebalDtCurr", "1"))
586
594
 
587
595
  return self._req_with_auth_fallback(
588
- name="strategy transaction import",
589
596
  url=self._endpoint + STRATEGY_TRANS_PATH.substitute(id=strategy_id),
590
597
  params=get_params,
591
598
  data=data,
592
599
  headers={"Content-Type": content_type},
593
- ).json()
600
+ )
594
601
 
595
- def strategy_transaction_delete(
596
- self,
597
- strategy_id: int,
598
- params: List[int],
599
- ):
602
+ def strategy_transaction_delete(self, strategy_id: int, params: List[int]):
600
603
  """
601
604
  Strategy transaction delete
602
605
  :param strategy_id:
@@ -604,15 +607,10 @@ class Client:
604
607
  :return:
605
608
  """
606
609
  return self._req_with_auth_fallback(
607
- name="strategy transaction delete",
608
- method="DELETE",
609
- url=self._endpoint + STRATEGY_TRANS_PATH.substitute(id=strategy_id),
610
- json=params,
611
- ).json()
610
+ method="DELETE", url=self._endpoint + STRATEGY_TRANS_PATH.substitute(id=strategy_id), json=params
611
+ )
612
612
 
613
- def strategy_holdings(
614
- self, strategy_id: int, date: Optional[str] = None, to_pandas=False
615
- ):
613
+ def strategy_holdings(self, strategy_id: int, date: Optional[str] = None, to_pandas=False):
616
614
  """
617
615
  Strategy holdings
618
616
  :param strategy_id:
@@ -623,29 +621,20 @@ class Client:
623
621
  get_params = [("date", date)] if date is not None else []
624
622
 
625
623
  ret = self._req_with_auth_fallback(
626
- name="strategy hldings",
627
- method="GET",
628
- url=self._endpoint + STRATEGY_HOLDINGS_PATH.substitute(id=strategy_id),
629
- params=get_params,
630
- ).json()
624
+ method="GET", url=self._endpoint + STRATEGY_HOLDINGS_PATH.substitute(id=strategy_id), params=get_params
625
+ )
631
626
 
632
627
  return pandas.DataFrame(ret["holdings"]) if to_pandas else ret
633
-
634
- def strategy_trading_system(
635
- self, strategy_id: int
636
- ):
628
+
629
+ def strategy_trading_system(self, strategy_id: int):
637
630
  """
638
631
  Strategy trading system
639
632
  :param strategy_id:
640
633
  :return:
641
634
  """
642
635
 
643
- return self._req_with_auth_fallback(
644
- name="strategy trading system",
645
- method="GET",
646
- url=self._endpoint + STRATEGY_TRADING_SYSTEM_PATH.substitute(id=strategy_id)
647
- ).json()
648
-
636
+ return self._req_with_auth_fallback(method="GET", url=self._endpoint + STRATEGY_TRADING_SYSTEM_PATH.substitute(id=strategy_id))
637
+
649
638
  def strategy_trading_system_update(self, strategy_id: int, params: dict):
650
639
  """
651
640
  Live strategy trading system update
@@ -654,12 +643,8 @@ class Client:
654
643
  :return:
655
644
  """
656
645
 
657
- return self._req_with_auth_fallback(
658
- name="live strategy trading system update",
659
- url=self._endpoint + STRATEGY_TRADING_SYSTEM_PATH.substitute(id=strategy_id),
660
- json=params,
661
- ).json()
662
-
646
+ return self._req_with_auth_fallback(url=self._endpoint + STRATEGY_TRADING_SYSTEM_PATH.substitute(id=strategy_id), json=params)
647
+
663
648
  def book_trading_system_update(self, strategy_id: int, params: dict):
664
649
  """
665
650
  Live book trading system update
@@ -668,12 +653,8 @@ class Client:
668
653
  :return:
669
654
  """
670
655
 
671
- return self._req_with_auth_fallback(
672
- name="live book trading system update",
673
- url=self._endpoint + BOOK_TRADING_SYSTEM_PATH.substitute(id=strategy_id),
674
- json=params,
675
- ).json()
676
-
656
+ return self._req_with_auth_fallback(url=self._endpoint + BOOK_TRADING_SYSTEM_PATH.substitute(id=strategy_id), json=params)
657
+
677
658
  def strategy_rerun(self, strategy_id: int, params: dict):
678
659
  """
679
660
  Simulated strategy rerun
@@ -682,12 +663,8 @@ class Client:
682
663
  :return:
683
664
  """
684
665
 
685
- return self._req_with_auth_fallback(
686
- name="simulated strategy rerun",
687
- url=self._endpoint + SIM_RERUN_PATH.substitute(id=strategy_id),
688
- json=params,
689
- ).json()
690
-
666
+ return self._req_with_auth_fallback(url=self._endpoint + SIM_RERUN_PATH.substitute(id=strategy_id), json=params)
667
+
691
668
  def book_rerun(self, strategy_id: int, params: dict):
692
669
  """
693
670
  Simulated book rerun
@@ -696,11 +673,7 @@ class Client:
696
673
  :return:
697
674
  """
698
675
 
699
- return self._req_with_auth_fallback(
700
- name="simulated book rerun",
701
- url=self._endpoint + BOOK_SIM_RERUN_PATH.substitute(id=strategy_id),
702
- json=params,
703
- ).json()
676
+ return self._req_with_auth_fallback(url=self._endpoint + BOOK_SIM_RERUN_PATH.substitute(id=strategy_id), json=params)
704
677
 
705
678
  def strategy_rebalance(self, strategy_id: int, params: dict):
706
679
  """
@@ -710,11 +683,7 @@ class Client:
710
683
  :return:
711
684
  """
712
685
 
713
- ret = self._req_with_auth_fallback(
714
- name="strategy rebalance",
715
- url=self._endpoint + STRATEGY_REBALANCE_PATH.substitute(id=strategy_id),
716
- json=params,
717
- ).json()
686
+ ret = self._req_with_auth_fallback(url=self._endpoint + STRATEGY_REBALANCE_PATH.substitute(id=strategy_id), json=params)
718
687
 
719
688
  return ret
720
689
 
@@ -726,12 +695,7 @@ class Client:
726
695
  :return:
727
696
  """
728
697
 
729
- ret = self._req_with_auth_fallback(
730
- name="strategy rebalance commit",
731
- url=self._endpoint
732
- + STRATEGY_REBALANCE_COMMIT_PATH.substitute(id=strategy_id),
733
- json=params,
734
- ).json()
698
+ ret = self._req_with_auth_fallback(url=self._endpoint + STRATEGY_REBALANCE_COMMIT_PATH.substitute(id=strategy_id), json=params)
735
699
 
736
700
  return ret
737
701
 
@@ -739,12 +703,12 @@ class Client:
739
703
  self,
740
704
  factor_id: int,
741
705
  data: Union[str, IO[str]],
742
- column_separator: str = None,
743
- existing_data: str = None,
744
- date_format: str = None,
745
- decimal_separator: str = None,
746
- ignore_errors: bool = None,
747
- ignore_duplicates: bool = None,
706
+ column_separator: Union[str, None] = None,
707
+ existing_data: Union[str, None] = None,
708
+ date_format: Union[str, None] = None,
709
+ decimal_separator: Union[str, None] = None,
710
+ ignore_errors: Union[bool, None] = None,
711
+ ignore_duplicates: Union[bool, None] = None,
748
712
  ):
749
713
  """
750
714
  Stock factor data upload
@@ -770,27 +734,18 @@ class Client:
770
734
  if ignore_errors is not None:
771
735
  get_params.append(("onError", "continue" if ignore_errors else "stop"))
772
736
  if ignore_duplicates is not None:
773
- get_params.append(
774
- ("onDuplicates", "continue" if ignore_duplicates else "stop")
775
- )
737
+ get_params.append(("onDuplicates", "continue" if ignore_duplicates else "stop"))
776
738
  return self._req_with_auth_fallback(
777
- name="stock factor data upload",
778
- url=self._endpoint + STOCK_FACTOR_UPLOAD_PATH.substitute(id=factor_id),
779
- params=get_params,
780
- data=data,
781
- ).json()
739
+ url=self._endpoint + STOCK_FACTOR_UPLOAD_PATH.substitute(id=factor_id), params=get_params, data=data
740
+ )
782
741
 
783
- def stock_factor_create_update(self, params: dict):
742
+ def stock_factor_create_update(self, params: dict) -> StockFactorResult:
784
743
  """
785
744
  Stock factor create/update
786
745
  :param params:
787
746
  :return:
788
747
  """
789
- return self._req_with_auth_fallback(
790
- name="stock factor create/update",
791
- url=self._endpoint + STOCK_FACTOR_CREATE_UPDATE_PATH,
792
- json=params,
793
- ).json()
748
+ return self._req_with_auth_fallback(url=self._endpoint + STOCK_FACTOR_CREATE_UPDATE_PATH, json=params)
794
749
 
795
750
  def stock_factor_delete(self, factor_id: int):
796
751
  """
@@ -798,27 +753,23 @@ class Client:
798
753
  :param factor_id: id of the data stock factor to delete
799
754
  :return:
800
755
  """
801
- return self._req_with_auth_fallback(
802
- name="stock factor delete",
803
- method="DELETE",
804
- url=self._endpoint + STOCK_FACTOR_DELETE_PATH.substitute(id=factor_id),
805
- ).json()
756
+ return self._req_with_auth_fallback(url=self._endpoint + STOCK_FACTOR_DELETE_PATH.substitute(id=factor_id), method="DELETE")
806
757
 
807
758
  def data_series_upload(
808
759
  self,
809
760
  series_id: int,
810
761
  data: Union[str, IO[str]],
811
- existing_data: str = None,
812
- date_format: str = None,
813
- decimal_separator: str = None,
814
- ignore_errors: bool = None,
815
- ignore_duplicates: bool = None,
816
- contains_header_row: bool = None,
762
+ existing_data: Union[str, None] = None,
763
+ date_format: Union[str, None] = None,
764
+ decimal_separator: Union[str, None] = None,
765
+ ignore_errors: Union[bool, None] = None,
766
+ ignore_duplicates: Union[bool, None] = None,
767
+ contains_header_row: Union[bool, None] = None,
817
768
  ):
818
769
  """
819
770
  Data series upload
820
771
  :param series_id:
821
- :param file:
772
+ :param data:
822
773
  :param existing_data: overwrite, skip or delete
823
774
  :param date_format: dd for day, mm for month and yyyy for year, any separator allowed (defaults to yyyy-mm-dd)
824
775
  :param decimal_separator: . or ,
@@ -837,29 +788,20 @@ class Client:
837
788
  if ignore_errors is not None:
838
789
  get_params.append(("onError", "continue" if ignore_errors else "stop"))
839
790
  if ignore_duplicates is not None:
840
- get_params.append(
841
- ("onDuplicates", "continue" if ignore_duplicates else "stop")
842
- )
791
+ get_params.append(("onDuplicates", "continue" if ignore_duplicates else "stop"))
843
792
  if contains_header_row is not None:
844
793
  get_params.append(("headerRow", contains_header_row))
845
794
  return self._req_with_auth_fallback(
846
- name="data series upload",
847
- url=self._endpoint + DATA_SERIES_UPLOAD_PATH.substitute(id=series_id),
848
- params=get_params,
849
- data=data,
850
- ).json()
795
+ url=self._endpoint + DATA_SERIES_UPLOAD_PATH.substitute(id=series_id), params=get_params, data=data
796
+ )
851
797
 
852
- def data_series_create_update(self, params: dict):
798
+ def data_series_create_update(self, params: dict) -> DataSeriesResult:
853
799
  """
854
800
  Data series create/update
855
801
  :param params:
856
802
  :return:
857
803
  """
858
- return self._req_with_auth_fallback(
859
- name="data series create/update",
860
- url=self._endpoint + DATA_SERIES_CREATE_UPDATE_PATH,
861
- json=params,
862
- ).json()
804
+ return self._req_with_auth_fallback(url=self._endpoint + DATA_SERIES_CREATE_UPDATE_PATH, json=params)
863
805
 
864
806
  def data_series_delete(self, series_id: int):
865
807
  """
@@ -867,11 +809,7 @@ class Client:
867
809
  :param series_id: id of the data series to delete
868
810
  :return:
869
811
  """
870
- return self._req_with_auth_fallback(
871
- name="data series delete",
872
- method="DELETE",
873
- url=self._endpoint + DATA_SERIES_DELETE_PATH.substitute(id=series_id),
874
- ).json()
812
+ return self._req_with_auth_fallback(method="DELETE", url=self._endpoint + DATA_SERIES_DELETE_PATH.substitute(id=series_id))
875
813
 
876
814
  def get_api_id(self):
877
815
  return self._auth_params["apiId"]
@@ -883,11 +821,7 @@ class Client:
883
821
  :param params:
884
822
  :return:
885
823
  """
886
- ret = self._req_with_auth_fallback(
887
- name="AI Factor predict",
888
- url=self._endpoint + AIFACTOR_PREDICT_PATH.substitute(id=predictor_id),
889
- json=params,
890
- ).json()
824
+ ret = self._req_with_auth_fallback(url=self._endpoint + AIFACTOR_PREDICT_PATH.substitute(id=predictor_id), json=params)
891
825
 
892
826
  if to_pandas:
893
827
  data = {"p123Uid": ret["p123Uids"], "ticker": ret["tickers"]}
@@ -902,16 +836,7 @@ class Client:
902
836
  [
903
837
  df,
904
838
  pandas.DataFrame(ret["data"], columns=ret["features"]),
905
- *(
906
- (
907
- pandas.DataFrame(
908
- ret["rawData"],
909
- columns=["raw " + x for x in ret["features"]],
910
- ),
911
- )
912
- if "rawData" in ret
913
- else ()
914
- ),
839
+ *((pandas.DataFrame(ret["rawData"], columns=["raw " + x for x in ret["features"]]),) if "rawData" in ret else ()),
915
840
  ],
916
841
  axis="columns",
917
842
  )
@@ -921,63 +846,84 @@ class Client:
921
846
 
922
847
  def stock_factor_download(self, factor_id: int):
923
848
  """
924
- Strategy details
925
- :param strategy_id:
849
+ Stock factor download
850
+ :param factor_id:
926
851
  :return:
927
852
  """
928
- return self._req_with_auth_fallback(
929
- name="stock factor download",
930
- method="GET",
931
- url=self._endpoint + STOCK_FACTOR_DOWNLOAD_PATH.substitute(id=factor_id),
932
- ).json()
933
-
853
+ return self._req_with_auth_fallback(method="GET", url=self._endpoint + STOCK_FACTOR_DOWNLOAD_PATH.substitute(id=factor_id))
854
+
934
855
  def data_prices(self, identifier: Union[int, str], start: str, end: Optional[str], to_pandas=False):
935
- """
936
- """
856
+ """ """
937
857
  get_params = [("start", start)]
938
858
  if end is not None:
939
859
  get_params.append(("end", end))
940
860
  ret = self._req_with_auth_fallback(
941
- name="download security prices",
942
- method="GET",
943
- url=self._endpoint + DATA_PRICES_PATH.substitute(identifier=identifier),
944
- params=get_params
945
- ).json()
861
+ method="GET", url=self._endpoint + DATA_PRICES_PATH.substitute(identifier=identifier), params=get_params
862
+ )
946
863
  return pandas.DataFrame(ret["prices"]) if to_pandas else ret
947
-
948
864
 
949
- @overload
950
- def stock_factor_info(self, *, factor_id: int) -> Any:
951
- ...
952
- @overload
953
- def stock_factor_info(self, *, name: str) -> Any:
954
- ...
955
- def stock_factor_info(self, *, factor_id: Optional[int] = None, name: Optional[str] = None):
865
+ @overload
866
+ def stock_factor_info(self, *, id: int) -> StockFactorInfoResult: ...
867
+ @overload
868
+ @deprecated("use overload accepting `id` parameter instead")
869
+ def stock_factor_info(self, *, factor_id: int) -> StockFactorInfoResult: ...
870
+ @overload
871
+ def stock_factor_info(self, *, name: str) -> StockFactorInfoResult: ...
872
+ def stock_factor_info(
873
+ self, *, id: Optional[int] = None, factor_id: Optional[int] = None, name: Optional[str] = None
874
+ ) -> StockFactorInfoResult:
956
875
  """
957
876
  Stock factor info, only specify factor_id or name
958
877
  """
878
+ if id is not None:
879
+ params = {"id": id}
880
+ elif factor_id is not None:
881
+ params = {"id": factor_id}
882
+ else:
883
+ params = {"name": name}
884
+ return self._req_with_auth_fallback(method="GET", url=self._endpoint + STOCK_FACTOR_INFO_PATH, params=params)
885
+
886
+ @overload
887
+ def data_series_info(self, *, id: int) -> DataSeriesInfoResult: ...
888
+ @overload
889
+ def data_series_info(self, *, name: str) -> DataSeriesInfoResult: ...
890
+ def data_series_info(self, *, id: Optional[int] = None, name: Optional[str] = None) -> DataSeriesInfoResult:
891
+ """
892
+ Data series info, only specify factor_id or name
893
+ """
894
+ return self._req_with_auth_fallback(
895
+ method="GET", url=self._endpoint + DATA_SERIES_INFO_PATH, params={"name": name} if id is None else {"id": id}
896
+ )
897
+
898
+ @overload
899
+ def strategy_info(self, *, id: int) -> StrategyInfoResult: ...
900
+ @overload
901
+ def strategy_info(self, *, name: str) -> StrategyInfoResult: ...
902
+ def strategy_info(self, *, id: Optional[int] = None, name: Optional[str] = None) -> StrategyInfoResult:
903
+ """
904
+ Strategy info, only specify factor_id or name
905
+ """
959
906
  return self._req_with_auth_fallback(
960
- name="stock factor info",
961
- method="GET",
962
- url=self._endpoint + STOCK_FACTOR_INFO_PATH,
963
- params={"name": name} if factor_id is None else {"id": factor_id}
964
- ).json()
907
+ method="GET", url=self._endpoint + STRATEGY_INFO_PATH, params={"name": name} if id is None else {"id": id}
908
+ )
965
909
 
966
910
 
967
- def req_with_retry(req: Callable[..., requests.Response], max_tries=None, **kwargs):
911
+ def req_with_retry(req: Callable[..., requests.Response], max_tries=5, **kwargs):
968
912
  tries = 0
969
- if max_tries is None:
970
- max_tries = 5
971
- resp = None
972
- while tries < max_tries:
913
+ while True:
973
914
  if tries > 0:
974
915
  time.sleep(2 * tries)
975
916
  try:
976
917
  resp = req(**kwargs)
977
- if resp.status_code < 500:
978
- break
918
+ exception = None
979
919
  except requests.ConnectionError as e:
980
- if tries + 1 == max_tries:
981
- raise ClientException("Cannot connect to API", exception=e)
920
+ resp = None
921
+ exception = e
922
+ if resp is not None:
923
+ if resp.status_code < 500:
924
+ return resp
925
+ resp.close()
982
926
  tries += 1
983
- return resp
927
+ if tries >= max_tries:
928
+ break
929
+ raise ClientException("Cannot connect to API", exception=exception)
@@ -0,0 +1,53 @@
1
+ from enum import IntEnum
2
+ from typing import Literal, Optional, TypedDict
3
+
4
+
5
+ class SharedResult(TypedDict):
6
+ cost: int
7
+ quotaRemaining: str
8
+
9
+
10
+ class IdResult(SharedResult):
11
+ id: int
12
+
13
+
14
+ class DataSeriesResult(SharedResult):
15
+ dataSeriesId: int
16
+
17
+
18
+ class DataSeriesInfoResult(DataSeriesResult):
19
+ name: str
20
+ description: str
21
+
22
+
23
+ class StockFactorResult(SharedResult):
24
+ factorId: int
25
+
26
+
27
+ class StockFactorInfoResult(StockFactorResult):
28
+ name: str
29
+ description: str
30
+
31
+
32
+ class RankInfoResult(SharedResult):
33
+ name: str
34
+ id: int
35
+ xml: str
36
+ currency: str
37
+ rankingMethod: int
38
+ type: Literal["Stock", "ETF"]
39
+ description: Optional[str]
40
+ groupUid: int
41
+ resolveGroupUid: int
42
+
43
+
44
+ class RankingMethod(IntEnum):
45
+ PERCENTILE_NA_NEGATIVE = 2
46
+ PERCENTILE_NA_NEUTRAL = 4
47
+ NORMAL_DISTRIBUTION = 1
48
+
49
+
50
+ class StrategyInfoResult(TypedDict):
51
+ strategyId: int
52
+ name: str
53
+ description: str
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: p123api
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: Portfolio123 API wrapper
5
5
  Home-page: https://github.com/portfolio-123/p123api-py
6
6
  Author: Portfolio123
@@ -1,8 +1,10 @@
1
1
  LICENSE
2
2
  README.md
3
+ pyproject.toml
3
4
  setup.py
4
5
  p123api/__init__.py
5
6
  p123api/client.py
7
+ p123api/types.py
6
8
  p123api.egg-info/PKG-INFO
7
9
  p123api.egg-info/SOURCES.txt
8
10
  p123api.egg-info/dependency_links.txt
@@ -0,0 +1,12 @@
1
+ [tool.black]
2
+ line-length = 140
3
+ skip-magic-trailing-comma = true
4
+
5
+ [tool.pyright]
6
+ pythonVersion = "3.6"
7
+ typeCheckingMode = "basic"
8
+ deprecateTypingAliases = true
9
+ disableBytesTypePromotions = true
10
+ strictDictionaryInference = true
11
+ strictListInference = true
12
+ strictSetInference = true
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="p123api",
8
- version="2.3.0",
8
+ version="2.4.0",
9
9
  author="Portfolio123",
10
10
  author_email="info@portfolio123.com",
11
11
  description="Portfolio123 API wrapper",
@@ -13,11 +13,7 @@ setuptools.setup(
13
13
  long_description_content_type="text/markdown",
14
14
  url="https://github.com/portfolio-123/p123api-py",
15
15
  packages=setuptools.find_packages(),
16
- classifiers=[
17
- "Programming Language :: Python :: 3",
18
- "License :: OSI Approved :: MIT License",
19
- "Operating System :: OS Independent",
20
- ],
16
+ classifiers=["Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent"],
21
17
  python_requires=">=3.6",
22
18
  install_requires=["pandas", "requests"],
23
19
  )
@@ -1 +0,0 @@
1
- from .client import Client, ClientException
File without changes
File without changes
File without changes