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