avanza.avanza
1from datetime import date, datetime 2import math 3import time 4from typing import Dict, List, Optional, Sequence, Union 5 6import requests 7 8from avanza.entities import StopLossOrderEvent, StopLossTrigger 9from avanza.models import * 10 11from .constants import ( 12 HttpMethod, 13 InsightsReportTimePeriod, 14 InstrumentType, 15 ListType, 16 OrderType, 17 Resolution, 18 Route, 19 TimePeriod, 20 TransactionsDetailsType, 21 Condition, 22 CreditType, 23) 24from .credentials import ( 25 backwards_compatible_serialization, 26 BaseCredentials, 27 SecretCredentials, 28) 29 30BASE_URL = "https://www.avanza.se" 31MIN_INACTIVE_MINUTES = 30 32MAX_INACTIVE_MINUTES = 60 * 24 33 34 35class Avanza: 36 def __init__( 37 self, 38 credentials: Union[BaseCredentials, Dict[str, str]], 39 retry_with_next_otp: bool = True, 40 quiet: bool = False, 41 ): 42 """ 43 44 Args: 45 46 credentials: Login credentials. Can be multiple variations 47 Either an instance of TokenCredentials or of SecretCredentials 48 Or a dictionary as the following: 49 Using TOTP secret: 50 { 51 'username': 'MY_USERNAME', 52 'password': 'MY_PASSWORD', 53 'totpSecret': 'MY_TOTP_SECRET' 54 } 55 Using TOTP code: 56 { 57 'username': 'MY_USERNAME', 58 'password': 'MY_PASSWORD', 59 'totpCode': 'MY_TOTP_CODE' 60 } 61 62 retry_with_next_otp: If 63 64 a) the server responded with 401 Unauthorized when trying to 65 log in, and 66 b) the TOTP code used for logging in was generated from a TOTP 67 secret provided in credentials, 68 69 then wait until the next TOTP time-step window and try again 70 with the new OTP. Re-retrying with a new OTP prevents a program 71 using avanza-api from crashing with a 401 HTTP error if, for 72 example, the program is run a second time shortly after it was 73 previously run. 74 75 quiet: Do not print a status message if waiting for the next TOTP 76 time-step window. 77 78 """ 79 if isinstance(credentials, dict): 80 credentials: BaseCredentials = backwards_compatible_serialization( 81 credentials 82 ) 83 self._retry_with_next_otp = retry_with_next_otp 84 self._quiet = quiet 85 86 self._authenticationTimeout = MAX_INACTIVE_MINUTES 87 self._session = requests.Session() 88 89 try: 90 response_body = self.__authenticate(credentials) 91 except requests.exceptions.HTTPError as http_error: 92 if ( 93 http_error.response.status_code == 401 94 and isinstance(credentials, SecretCredentials) 95 and self._retry_with_next_otp 96 ): 97 # Wait for the next TOTP time-step window and try with the new OTP 98 default_otp_interval: int = 30 # We use this pyotp/RFC 6238 default 99 now: float = datetime.now().timestamp() 100 time_to_wait: int = math.ceil( 101 (default_otp_interval - now) % default_otp_interval 102 ) 103 if not self._quiet: 104 print( 105 "Server returned 401 when trying to log in. " 106 f"Will retry with the next OTP in {time_to_wait}s..." 107 ) 108 time.sleep(time_to_wait) 109 response_body = self.__authenticate(credentials) 110 else: 111 raise 112 113 self._authentication_session = response_body["authenticationSession"] 114 self._push_subscription_id = response_body["pushSubscriptionId"] 115 self._customer_id = response_body["customerId"] 116 117 def __authenticate(self, credentials: BaseCredentials): 118 if ( 119 not MIN_INACTIVE_MINUTES 120 <= self._authenticationTimeout 121 <= MAX_INACTIVE_MINUTES 122 ): 123 raise ValueError( 124 f"Session timeout not in range {MIN_INACTIVE_MINUTES} - {MAX_INACTIVE_MINUTES} minutes" 125 ) 126 127 data = { 128 "maxInactiveMinutes": self._authenticationTimeout, 129 "username": credentials.username, 130 "password": credentials.password, 131 } 132 133 response = self._session.post( 134 f"{BASE_URL}{Route.AUTHENTICATION_PATH.value}", json=data 135 ) 136 137 response.raise_for_status() 138 139 response_body = response.json() 140 141 # No second factor required, continue with normal login 142 if response_body.get("twoFactorLogin") is None: 143 self._security_token = response.headers.get("X-SecurityToken") 144 return response_body["successfulLogin"] 145 146 tfa_method = response_body["twoFactorLogin"].get("method") 147 148 if tfa_method != "TOTP": 149 raise ValueError(f"Unsupported two factor method {tfa_method}") 150 151 return self.__validate_2fa(credentials) 152 153 def __validate_2fa(self, credentials: BaseCredentials): 154 response = self._session.post( 155 f"{BASE_URL}{Route.TOTP_PATH.value}", 156 json={"method": "TOTP", "totpCode": credentials.totp_code}, 157 ) 158 159 response.raise_for_status() 160 161 self._security_token = response.headers.get("X-SecurityToken") 162 163 response_body = response.json() 164 165 return response_body 166 167 def __call( 168 self, method: HttpMethod, path: str, options=None, return_content: bool = False 169 ): 170 method_call = { 171 HttpMethod.GET: self._session.get, 172 HttpMethod.POST: self._session.post, 173 HttpMethod.PUT: self._session.put, 174 HttpMethod.DELETE: self._session.delete, 175 }.get(method) 176 177 if method_call is None: 178 raise ValueError(f"Unknown method type {method}") 179 180 data = {} 181 if method == HttpMethod.GET: 182 data["params"] = options 183 else: 184 data["json"] = options 185 186 response = method_call( 187 f"{BASE_URL}{path}", 188 headers={ 189 "X-SecurityToken": self._security_token, 190 }, 191 **data, 192 ) 193 194 response.raise_for_status() 195 196 # Some routes like add/remove instrument from a watch list 197 # only returns 200 OK with no further data about if the operation succeeded 198 if len(response.content) == 0: 199 return None 200 201 if return_content: 202 return response.content 203 204 return response.json() 205 206 def get_overview(self) -> Overview: 207 """Get account and category overviews""" 208 return self.__call(HttpMethod.GET, Route.CATEGORIZED_ACCOUNTS.value) 209 210 def get_accounts_positions(self) -> AccountPositions: 211 """Get investment positions for all account""" 212 213 return self.__call(HttpMethod.GET, Route.ACCOUNTS_POSITIONS_PATH.value) 214 215 def get_account_performance_chart_data( 216 self, url_parameters_ids: list[str], time_period: TimePeriod 217 ) -> AccountPositions: 218 """Get performance chart for accounts. 219 220 Args: 221 222 url_parameters_ids: Scrambled account ids. 223 Can be found in the overview response. 224 225 time_period: Time period to get chart data for 226 227 """ 228 return self.__call( 229 HttpMethod.POST, 230 Route.ACCOUNT_PERFORMANCE_CHART_PATH.value, 231 { 232 "scrambledAccountIds": url_parameters_ids, 233 "timePeriod": time_period.value, 234 }, 235 ) 236 237 def get_credit_info(self, credit_type: CreditType) -> CreditInfo: 238 """Returns creditinfo for accounts 239 CreditType->credited for accounts with credit 240 CreditType->uncredited for accounts with no credit 241 """ 242 return self.__call( 243 HttpMethod.GET, Route.CREDITINFO_PATH.value.format(credit_type) 244 ) 245 246 def get_watchlists(self) -> List[WatchList]: 247 """Get your "Bevakningslistor" """ 248 return self.__call(HttpMethod.GET, Route.WATCHLISTS_PATH.value) 249 250 def add_to_watchlist(self, instrument_id: str, watchlist_id: str) -> None: 251 """Add an instrument to the specified watchlist 252 253 This function returns None if the request was 200 OK, 254 but there is no guarantee that the instrument was added to the list, 255 verify this by calling get_watchlists() 256 """ 257 return self.__call( 258 HttpMethod.POST, 259 Route.WATCHLISTS_ADD_PATH.value.format(watchlist_id, instrument_id), 260 ) 261 262 def remove_from_watchlist(self, instrument_id: str, watchlist_id: str) -> None: 263 """Remove an instrument to the specified watchlist 264 265 This function returns None if the request was 200 OK, 266 but there is no guarantee that the instrument was removed from the list, 267 verify this by calling get_watchlists() 268 """ 269 return self.__call( 270 HttpMethod.POST, 271 Route.WATCHLISTS_REMOVE_PATH.value.format(watchlist_id, instrument_id), 272 ) 273 274 def get_fund_info(self, fund_id: str) -> FundInfo: 275 """Get info about a fund""" 276 277 return self.__call(HttpMethod.GET, Route.FUND_PATH.value.format(fund_id)) 278 279 def get_stock_info(self, stock_id: str) -> StockInfo: 280 """Returns info about a stock""" 281 282 return self.get_instrument(InstrumentType.STOCK, stock_id) 283 284 def get_certificate_info(self, certificate_id: str) -> CertificateInfo: 285 """Returns info about a certificate""" 286 287 return self.get_instrument(InstrumentType.CERTIFICATE, certificate_id) 288 289 def get_certificate_details(self, certificate_id: str) -> CertificateDetails: 290 """Returns additional info about a certificate""" 291 292 return self.get_instrument_details(InstrumentType.CERTIFICATE, certificate_id) 293 294 def get_etf_details(self, etf_id: str) -> EtfDetails: 295 return self.get_instrument_details(InstrumentType.EXCHANGE_TRADED_FUND, etf_id) 296 297 def get_warrant_info(self, warrant_id: str) -> WarrantInfo: 298 """Returns info about a warrant""" 299 300 return self.get_instrument(InstrumentType.WARRANT, warrant_id) 301 302 def get_index_info(self, index_id: str) -> IndexInfo: 303 """Returns info about an index""" 304 305 # Works when sending InstrumentType.STOCK, but not InstrumentType.INDEX 306 return self.get_instrument(InstrumentType.STOCK, index_id) 307 308 def get_analysis(self, instrument_id: str): 309 """Returns analysis data for an instrument""" 310 311 return self.__call( 312 HttpMethod.GET, Route.ANALYSIS_PATH.value.format(instrument_id) 313 ) 314 315 def get_news(self, instrument_id: str) -> News: 316 """Returns latest news data for an instrument""" 317 318 return self.__call(HttpMethod.GET, Route.NEWS_PATH.value.format(instrument_id)) 319 320 def get_forum_posts(self, instrument_id: str) -> ForumPosts: 321 """Returns latest forum posts for an instrument""" 322 323 return self.__call(HttpMethod.GET, Route.FORUM_PATH.value.format(instrument_id)) 324 325 def get_instrument(self, instrument_type: InstrumentType, instrument_id: str): 326 """ 327 Get instrument info 328 For more info on return models for this function see functions 329 [ 330 get_stock_info(), 331 get_fund_info(), 332 get_certificate_info(), 333 get_index_info(), 334 get_warrant_info() 335 ] 336 """ 337 338 return self.__call( 339 HttpMethod.GET, 340 Route.INSTRUMENT_PATH.value.format(instrument_type.value, instrument_id), 341 ) 342 343 def get_instrument_details( 344 self, instrument_type: InstrumentType, instrument_id: str 345 ): 346 """ 347 Get additional instrument info 348 """ 349 if instrument_type is InstrumentType.EXCHANGE_TRADED_FUND: 350 return self.__call( 351 HttpMethod.GET, Route.ETF_DETAILS_PATH.value.format(instrument_id) 352 ) 353 else: 354 return self.__call( 355 HttpMethod.GET, 356 Route.INSTRUMENT_DETAILS_PATH.value.format( 357 instrument_type.value, instrument_id 358 ), 359 ) 360 361 def search_for_stock(self, query: str, limit: int = 10) -> SearchResults: 362 """Search for a stock 363 364 Args: 365 366 query: can be a ISIN ('US0378331005'), 367 name ('Apple'), 368 tickerSymbol ('AAPL') 369 370 limit: maximum number of results to return 371 372 """ 373 return self.search_for_instrument(InstrumentType.STOCK, query, limit) 374 375 def search_for_fund(self, query: str, limit: int = 10) -> SearchResults: 376 """Search for a fund 377 378 Args: 379 380 query: can be a ISIN ('SE0012454338'), 381 name ('Avanza'), 382 tickerSymbol ('Avanza Europa') 383 384 limit: maximum number of results to return 385 386 """ 387 388 return self.search_for_instrument(InstrumentType.FUND, query, limit) 389 390 def search_for_certificate(self, query: str, limit: int = 10) -> SearchResults: 391 """Search for a certificate 392 393 Args: 394 395 query: can be a ISIN, name or tickerSymbol 396 397 limit: maximum number of results to return 398 399 """ 400 401 return self.search_for_instrument(InstrumentType.CERTIFICATE, query, limit) 402 403 def search_for_warrant(self, query: str, limit: int = 10) -> SearchResults: 404 """Search for a warrant 405 406 Args: 407 408 query: can be a ISIN, name or tickerSymbol 409 410 limit: maximum number of results to return 411 412 """ 413 414 return self.search_for_instrument(InstrumentType.WARRANT, query, limit) 415 416 def search_for_instrument( 417 self, instrument_type: InstrumentType, query: str, limit: int = 10 418 ): 419 """Search for a specific instrument 420 421 Args: 422 423 instrument_type: can be STOCK, FUND, BOND etc 424 425 query: can be a ISIN, name or tickerSymbol 426 427 limit: maximum number of results to return 428 429 """ 430 431 options = { 432 "query": query, 433 "searchFilter": {"types": [instrument_type.value.upper()]}, 434 "pagination": {"from": 0, "size": limit}, 435 } 436 result = self.__call( 437 HttpMethod.POST, Route.INSTRUMENT_SEARCH_PATH.value, options=options 438 ) 439 return result["hits"] 440 441 442 def get_order_book(self, order_book_id: str)-> OrderBook: 443 """Get info about an orderbook""" 444 return self.__call( 445 HttpMethod.GET, 446 Route.ORDERBOOK_PATH.value.format(order_book_id) 447 ) 448 449 def get_insights_report( 450 self, account_id: str, time_period: InsightsReportTimePeriod 451 ) -> InsightsReport: 452 """Get report about the development of your owned positions during the specified timeperiod""" 453 return self.__call( 454 HttpMethod.GET, 455 Route.INSIGHTS_PATH.value.format(time_period.value, account_id), 456 ) 457 458 def get_deals(self): 459 """Get currently active deals""" 460 return self.__call(HttpMethod.GET, Route.DEALS_PATH.value) 461 462 def get_orders(self): 463 """Get currently active orders""" 464 return self.__call(HttpMethod.GET, Route.ORDERS_PATH.value) 465 466 def get_inspiration_lists(self) -> List[InspirationListItem]: 467 """Get all available inspiration lists 468 469 This returns lists similar to the ones found on: 470 471 https://www.avanza.se/aktier/aktieinspiration.html 472 473 https://www.avanza.se/fonder/fondinspiration.html 474 475 """ 476 return self.__call(HttpMethod.GET, Route.INSPIRATION_LIST_PATH.value.format("")) 477 478 def get_inspiration_list(self, list_id: Union[ListType, str]) -> InspirationList: 479 """Get inspiration list 480 481 Some lists have an id of an enum value described in ListType, but they can also just have a string id. 482 An example is hhSK8W1o which corresponds to "Most owned stocks", which isn't described in the ListType enum 483 484 """ 485 486 id = list_id.value if isinstance(list_id, ListType) else list_id 487 488 return self.__call(HttpMethod.GET, Route.INSPIRATION_LIST_PATH.value.format(id)) 489 490 def get_chart_data( 491 self, 492 order_book_id: str, 493 period: TimePeriod, 494 resolution: Optional[Resolution] = None, 495 ) -> ChartData: 496 """Return chart data for an order book for the specified time period with given resolution""" 497 options = {"timePeriod": period.value.lower()} 498 499 if resolution is not None: 500 options["resolution"] = resolution.value.lower() 501 502 return self.__call( 503 HttpMethod.GET, Route.CHARTDATA_PATH.value.format(order_book_id), options 504 ) 505 506 def place_order( 507 self, 508 account_id: str, 509 order_book_id: str, 510 order_type: OrderType, 511 price: float, 512 valid_until: date, 513 volume: int, 514 condition: Condition = Condition.NORMAL, 515 ): 516 """Place an order 517 518 Returns: 519 520 If the order was successfully placed: 521 522 { 523 message: str, 524 orderId: str, 525 orderRequestStatus: 'SUCCESS' 526 } 527 528 If the order was not placed: 529 530 { 531 message: str, 532 orderRequestStatus: 'ERROR' 533 } 534 """ 535 536 return self.__call( 537 HttpMethod.POST, 538 Route.ORDER_PLACE_PATH.value, 539 { 540 "accountId": account_id, 541 "orderbookId": order_book_id, 542 "side": order_type.value, 543 "condition": condition.value, 544 "price": price, 545 "validUntil": valid_until.isoformat(), 546 "volume": volume, 547 }, 548 ) 549 550 def place_order_buy_fund(self, account_id: str, order_book_id: str, amount: float): 551 """Place a buy order for a fund 552 553 Returns: 554 555 { 556 message: str, 557 orderId: str, 558 accountId: str, 559 orderRequestStatus: str 560 } 561 """ 562 563 return self.__call( 564 HttpMethod.POST, 565 Route.ORDER_PLACE_PATH_BUY_FUND.value, 566 {"orderbookId": order_book_id, "accountId": account_id, "amount": amount}, 567 ) 568 569 def place_order_sell_fund(self, account_id: str, order_book_id: str, volume: float): 570 """Place a sell order for a fund 571 572 Returns: 573 574 { 575 message: str, 576 orderId: str, 577 accountId: str, 578 orderRequestStatus: str 579 } 580 """ 581 582 return self.__call( 583 HttpMethod.POST, 584 Route.ORDER_PLACE_PATH_SELL_FUND.value, 585 {"orderbookId": order_book_id, "accountId": account_id, "volume": volume}, 586 ) 587 588 def place_stop_loss_order( 589 self, 590 parent_stop_loss_id: str, 591 account_id: str, 592 order_book_id: str, 593 stop_loss_trigger: StopLossTrigger, 594 stop_loss_order_event: StopLossOrderEvent, 595 ): 596 """Place an stop loss order 597 598 Args: 599 600 parent_stop_loss_id: The id of the parent stop loss order. If this is the first stop loss order, this should be "0". 601 602 account_id: A valid account id. 603 604 order_book_id: The order book id of the instrument to place the stop loss order for. 605 606 stop_loss_trigger: The stop loss trigger type. 607 608 stop_loss_order_event: The stop loss order event type. 609 610 Returns: 611 612 If the order was successfully placed: 613 614 { 615 status: 'SUCCESS', 616 stoplossOrderId: str 617 } 618 619 If the order was not placed: 620 621 { 622 status: str, 623 stoplossOrderId: str 624 } 625 """ 626 627 return self.__call( 628 HttpMethod.POST, 629 Route.ORDER_PLACE_STOP_LOSS_PATH.value, 630 { 631 "parentStopLossId": parent_stop_loss_id, 632 "accountId": account_id, 633 "orderBookId": order_book_id, 634 "stopLossTrigger": { 635 "type": stop_loss_trigger.type.value, 636 "value": stop_loss_trigger.value, 637 "validUntil": stop_loss_trigger.valid_until.isoformat(), 638 "valueType": stop_loss_trigger.value_type.value, 639 "triggerOnMarketMakerQuote": stop_loss_trigger.trigger_on_market_maker_quote, 640 }, 641 "stopLossOrderEvent": { 642 "type": stop_loss_order_event.type.value, 643 "price": stop_loss_order_event.price, 644 "volume": stop_loss_order_event.volume, 645 "validDays": stop_loss_order_event.valid_days, 646 "priceType": stop_loss_order_event.price_type.value, 647 "shortSellingAllowed": stop_loss_order_event.short_selling_allowed, 648 }, 649 }, 650 ) 651 652 def edit_order( 653 self, 654 order_id: str, 655 account_id: str, 656 price: float, 657 valid_until: date, 658 volume: int, 659 ): 660 """Update an existing order 661 662 Returns: 663 664 { 665 orderRequestStatus: str, 666 message: str, 667 parameters: List[str], 668 orderId: str 669 } 670 """ 671 672 return self.__call( 673 HttpMethod.POST, 674 Route.ORDER_EDIT_PATH.value, 675 { 676 "accountId": account_id, 677 "metadata": {"orderEntryMode": "STANDARD"}, 678 "openVolume": None, 679 "orderId": order_id, 680 "price": price, 681 "validUntil": valid_until.isoformat(), 682 "volume": volume, 683 }, 684 ) 685 686 def get_order(self, account_id: str, order_id: str): 687 """Get an existing order 688 689 Returns: 690 691 { 692 "orderId": str, 693 "orderbookId": str, 694 "side": str, 695 "state": str, 696 "marketReference": str, 697 "price": float, 698 "message": str, 699 "volume": int, 700 "originalVolume": int, 701 "accountId": str, 702 "condition": str, 703 "validUntil": str, 704 "modifiable": bool, 705 "deletable": bool, 706 } 707 """ 708 709 return self.__call( 710 HttpMethod.GET, 711 Route.ORDER_GET_PATH.value.format( 712 order_id, 713 account_id, 714 ), 715 ) 716 717 def get_all_stop_losses(self): 718 """Get open stop losses 719 720 Returns: 721 722 [{ 723 "id": str, 724 "status": str, 725 "account": { 726 "id": str, 727 "name": str, 728 "type": str, 729 "urlParameterId": str 730 }, 731 "orderbook": { 732 "id": str, 733 "name": str, 734 "countryCode": str, 735 "currency": str, 736 "shortName": str, 737 "type": str 738 }, 739 "hasExcludingChildren": bool, 740 "message": str, 741 "trigger": { 742 "value": int, 743 "type": str, 744 "validUntil": str, 745 "valueType": str 746 }, 747 "order": { 748 "type": str, 749 "price": int, 750 "volume": int, 751 "shortSellingAllowed": bool, 752 "validDays": int, 753 "priceType": str, 754 "priceDecimalPrecision": 0 755 }, 756 "editable": bool, 757 "deletable": bool 758 }] 759 """ 760 return self.__call(HttpMethod.GET, Route.STOP_LOSS_PATH.value) 761 762 def delete_stop_loss_order(self, account_id: str, stop_loss_id: str): 763 """delete a stop loss order 764 765 Args: 766 767 stop_loss_id: The id of the stop loss order to delete. 768 769 account_id: A valid account id. 770 771 Returns: 772 Nothing 773 """ 774 775 return self.__call( 776 HttpMethod.DELETE, 777 Route.ORDER_DELETE_STOP_LOSS_PATH.value.format( 778 account_id, 779 stop_loss_id, 780 ), 781 ) 782 783 def delete_order(self, account_id: str, order_id: str): 784 """Delete an existing order 785 786 Returns: 787 788 { 789 messages: str, 790 orderId: str, 791 parameters: List[str], 792 orderRequestStatus: str 793 } 794 """ 795 return self.__call( 796 HttpMethod.POST, 797 Route.ORDER_DELETE_PATH.value, 798 {"accountId": account_id, "orderId": order_id}, 799 ) 800 801 def get_monthly_savings_by_account_id(self, account_id: str): 802 """Get monthly savings at avanza for specific account 803 804 Returns: 805 806 { 807 'monthlySavings': [{ 808 'account': { 809 'id': str, 810 'name': str, 811 'type': str 812 }, 813 'amount': float, 814 'cash': {'amount': float, 'percent': float}, 815 'externalAccount': { 816 'accountNumber': str, 817 'bankName': str, 818 'clearingNumber': str 819 }, 820 'fundDistributions': [{ 821 'amount': float, 822 'orderbook': { 823 'buyable': bool, 824 'id': str, 825 'name': str 826 }, 827 'percent': float 828 }, 829 'id': str, 830 'name': str, 831 'purchaseDay': int, 832 'status': str, 833 'transferDay': int 834 }], 835 'totalAmount': float 836 } 837 """ 838 839 return self.__call( 840 HttpMethod.GET, Route.MONTHLY_SAVINGS_PATH.value.format(account_id) 841 ) 842 843 def get_all_monthly_savings(self): 844 """Get your monthly savings at Avanza 845 846 Returns: 847 848 { 849 'monthlySavings': [{ 850 'account': { 851 'id': str, 852 'name': str, 853 'type': str 854 }, 855 'amount': float, 856 'cash': {'amount': float, 'percent': float}, 857 'externalAccount': { 858 'accountNumber': str, 859 'bankName': str, 860 'clearingNumber': str 861 }, 862 'fundDistributions': [{ 863 'amount': float, 864 'orderbook': { 865 'buyable': bool, 866 'id': str, 867 'name': str 868 }, 869 'percent': float 870 }, 871 'id': str, 872 'name': str, 873 'purchaseDay': int, 874 'status': str, 875 'transferDay': int 876 }], 877 'totalAmount': float 878 } 879 """ 880 881 return self.__call(HttpMethod.GET, Route.MONTHLY_SAVINGS_PATH.value.format("")) 882 883 def pause_monthly_saving(self, account_id: str, monthly_savings_id: str): 884 """Pause an active monthly saving 885 886 Returns: 887 'OK' 888 889 """ 890 891 return self.__call( 892 HttpMethod.PUT, 893 Route.MONTHLY_SAVINGS_PAUSE_PATH.value.format( 894 account_id, monthly_savings_id 895 ), 896 ) 897 898 def resume_monthly_saving(self, account_id: str, monthly_savings_id: str): 899 """Resume a paused monthly saving 900 901 Returns: 902 'OK' 903 904 """ 905 906 return self.__call( 907 HttpMethod.PUT, 908 Route.MONTHLY_SAVINGS_RESUME_PATH.value.format( 909 account_id, monthly_savings_id 910 ), 911 ) 912 913 def delete_monthly_saving(self, account_id: str, monthly_savings_id: str) -> None: 914 """Deletes a monthly saving 915 916 Returns: 917 None 918 919 """ 920 921 return self.__call( 922 HttpMethod.DELETE, 923 Route.MONTHLY_SAVINGS_REMOVE_PATH.value.format( 924 account_id, monthly_savings_id 925 ), 926 ) 927 928 def create_monthly_saving( 929 self, 930 account_id: str, 931 amount: int, 932 transfer_day_of_month: int, 933 purchase_day_of_month: int, 934 clearing_and_account_number: str, 935 fund_distribution: Dict[str, int], 936 ): 937 """Create a monthly saving at Avanza 938 939 Args: 940 941 account_id: The Avanza account to which the withdrawn money should be transferred to 942 943 amount: minimum amount 100 (SEK) 944 the amount that should be withdrawn from the external account every month 945 946 transfer_day_of_month: valid range (1-31) 947 when the money should be withdrawn from the external account 948 949 purchase_day_of_month: valid range (1-31) 950 when the funds should be purchased, 951 must occur after the transfer_day_of_month 952 953 clearing_and_account_number: The external account from which the money for the monthly savings should be withdrawn from, 954 has to be formatted as follows 'XXXX-XXXXXXXXX' 955 956 fund_distrubution: the percentage distribution of the funds 957 The key is the funds id and the value is the distribution of the amount in a whole percentage 958 The sum of the percentages has to total 100 959 960 Examples: 961 {'41567': 100} 962 {'41567': 50, '878733': 50} 963 {'41567': 25, '878733': 75} 964 965 Returns: 966 967 { 968 'monthlySavingId': str, 969 'status': str 970 } 971 972 monthlySavingId has the following format: 'XX^XXXXXXXXXXXXX^XXXXXX' 973 status should have the value 'ACCEPTED' if the monthly saving was created successfully 974 """ 975 976 if not 1 <= transfer_day_of_month <= 31: 977 raise ValueError( 978 "transfer_day_of_month is outside the valid range of (1-31)" 979 ) 980 981 if not 1 <= purchase_day_of_month <= 31: 982 raise ValueError( 983 "purchase_day_of_month is outside the valid range of (1-31)" 984 ) 985 986 if transfer_day_of_month >= purchase_day_of_month: 987 raise ValueError( 988 "transfer_day_of_month must occur before purchase_day_of_month" 989 ) 990 991 if len(fund_distribution) == 0: 992 raise ValueError("No founds were specified in the fund_distribution") 993 994 if sum(fund_distribution.values()) != 100: 995 raise ValueError("The fund_distribution values must total 100") 996 997 return self.__call( 998 HttpMethod.POST, 999 Route.MONTHLY_SAVINGS_CREATE_PATH.value.format(account_id), 1000 { 1001 "amount": amount, 1002 "autogiro": { 1003 "dayOfMonth": transfer_day_of_month, 1004 "externalClearingAndAccount": clearing_and_account_number, 1005 }, 1006 "fundDistribution": { 1007 "dayOfMonth": purchase_day_of_month, 1008 "fundDistributions": fund_distribution, 1009 }, 1010 }, 1011 ) 1012 1013 def get_transactions_details( 1014 self, 1015 transaction_details_types: Optional[Sequence[TransactionsDetailsType]] = [], 1016 transactions_from: Optional[date] = None, 1017 transactions_to: Optional[date] = None, 1018 isin: Optional[str] = None, 1019 max_elements: Optional[int] = 1000, 1020 ) -> Transactions: 1021 """Get transactions, optionally apply criteria. 1022 1023 Args: 1024 1025 transaction_types: One or more transaction types. 1026 1027 transactions_from: Fetch transactions from this date. 1028 1029 transactions_to: Fetch transactions to this date. 1030 1031 isin: Only fetch transactions for specified isin. 1032 1033 max_elements: Limit result to N transactions. 1034 """ 1035 options = {} 1036 options["maxElements"] = max_elements 1037 1038 if transaction_details_types: 1039 options["transactionTypes"] = ",".join( 1040 [type.value for type in transaction_details_types] 1041 ) 1042 if transactions_from: 1043 options["from"] = transactions_from.isoformat() 1044 if transactions_to: 1045 options["to"] = transactions_to.isoformat() 1046 if isin: 1047 options["isin"] = isin 1048 1049 return self.__call( 1050 HttpMethod.GET, Route.TRANSACTIONS_DETAILS_PATH.value, options 1051 ) 1052 1053 def get_note_as_pdf(self, url_parameter_id: str, note_id: str): 1054 return self.__call( 1055 HttpMethod.GET, 1056 Route.NOTE_PATH.value.format(url_parameter_id, note_id), 1057 return_content=True, 1058 ) 1059 1060 def set_price_alert( 1061 self, 1062 order_book_id: str, 1063 price: float, 1064 valid_until: date, 1065 notification: bool = True, 1066 email: bool = False, 1067 sms: bool = False, 1068 ): 1069 """ 1070 Sets a price alert for the specified orderbook and returns all the existing alerts. 1071 1072 Returns: 1073 1074 [ 1075 { 1076 'alertId': str, 1077 'accountId': str, 1078 'price': float, 1079 'validUntil': str, 1080 'direction': str, 1081 'email': bool, 1082 'notification': bool, 1083 'sms': bool, 1084 } 1085 ] 1086 """ 1087 1088 return self.__call( 1089 HttpMethod.POST, 1090 Route.PRICE_ALERT_PATH.value.format(order_book_id), 1091 { 1092 "price": price, 1093 "validUntil": valid_until.isoformat(), 1094 "notification": notification, 1095 "email": email, 1096 "sms": sms, 1097 }, 1098 ) 1099 1100 def get_price_alert(self, order_book_id: str) -> List[PriceAlert]: 1101 """Gets all the price alerts for the specified orderbook""" 1102 1103 return self.__call( 1104 HttpMethod.GET, 1105 Route.PRICE_ALERT_PATH.value.format(order_book_id), 1106 ) 1107 1108 def delete_price_alert(self, order_book_id: str, alert_id: str): 1109 """ 1110 Deletes a price alert from the specified orderbook and returns the remaining alerts. 1111 1112 Returns: 1113 1114 [ 1115 { 1116 'alertId': str, 1117 'accountId': str, 1118 'price': float, 1119 'validUntil': str, 1120 'direction': str, 1121 'email': bool, 1122 'notification': bool, 1123 'sms': bool, 1124 } 1125 ] 1126 """ 1127 return self.__call( 1128 HttpMethod.DELETE, 1129 Route.PRICE_ALERT_PATH.value.format(order_book_id, alert_id) 1130 + f"/{alert_id}", 1131 ) 1132 1133 def get_offers(self) -> List[Offer]: 1134 """Return current offers""" 1135 1136 return self.__call(HttpMethod.GET, Route.CURRENT_OFFERS_PATH.value)
36class Avanza: 37 def __init__( 38 self, 39 credentials: Union[BaseCredentials, Dict[str, str]], 40 retry_with_next_otp: bool = True, 41 quiet: bool = False, 42 ): 43 """ 44 45 Args: 46 47 credentials: Login credentials. Can be multiple variations 48 Either an instance of TokenCredentials or of SecretCredentials 49 Or a dictionary as the following: 50 Using TOTP secret: 51 { 52 'username': 'MY_USERNAME', 53 'password': 'MY_PASSWORD', 54 'totpSecret': 'MY_TOTP_SECRET' 55 } 56 Using TOTP code: 57 { 58 'username': 'MY_USERNAME', 59 'password': 'MY_PASSWORD', 60 'totpCode': 'MY_TOTP_CODE' 61 } 62 63 retry_with_next_otp: If 64 65 a) the server responded with 401 Unauthorized when trying to 66 log in, and 67 b) the TOTP code used for logging in was generated from a TOTP 68 secret provided in credentials, 69 70 then wait until the next TOTP time-step window and try again 71 with the new OTP. Re-retrying with a new OTP prevents a program 72 using avanza-api from crashing with a 401 HTTP error if, for 73 example, the program is run a second time shortly after it was 74 previously run. 75 76 quiet: Do not print a status message if waiting for the next TOTP 77 time-step window. 78 79 """ 80 if isinstance(credentials, dict): 81 credentials: BaseCredentials = backwards_compatible_serialization( 82 credentials 83 ) 84 self._retry_with_next_otp = retry_with_next_otp 85 self._quiet = quiet 86 87 self._authenticationTimeout = MAX_INACTIVE_MINUTES 88 self._session = requests.Session() 89 90 try: 91 response_body = self.__authenticate(credentials) 92 except requests.exceptions.HTTPError as http_error: 93 if ( 94 http_error.response.status_code == 401 95 and isinstance(credentials, SecretCredentials) 96 and self._retry_with_next_otp 97 ): 98 # Wait for the next TOTP time-step window and try with the new OTP 99 default_otp_interval: int = 30 # We use this pyotp/RFC 6238 default 100 now: float = datetime.now().timestamp() 101 time_to_wait: int = math.ceil( 102 (default_otp_interval - now) % default_otp_interval 103 ) 104 if not self._quiet: 105 print( 106 "Server returned 401 when trying to log in. " 107 f"Will retry with the next OTP in {time_to_wait}s..." 108 ) 109 time.sleep(time_to_wait) 110 response_body = self.__authenticate(credentials) 111 else: 112 raise 113 114 self._authentication_session = response_body["authenticationSession"] 115 self._push_subscription_id = response_body["pushSubscriptionId"] 116 self._customer_id = response_body["customerId"] 117 118 def __authenticate(self, credentials: BaseCredentials): 119 if ( 120 not MIN_INACTIVE_MINUTES 121 <= self._authenticationTimeout 122 <= MAX_INACTIVE_MINUTES 123 ): 124 raise ValueError( 125 f"Session timeout not in range {MIN_INACTIVE_MINUTES} - {MAX_INACTIVE_MINUTES} minutes" 126 ) 127 128 data = { 129 "maxInactiveMinutes": self._authenticationTimeout, 130 "username": credentials.username, 131 "password": credentials.password, 132 } 133 134 response = self._session.post( 135 f"{BASE_URL}{Route.AUTHENTICATION_PATH.value}", json=data 136 ) 137 138 response.raise_for_status() 139 140 response_body = response.json() 141 142 # No second factor required, continue with normal login 143 if response_body.get("twoFactorLogin") is None: 144 self._security_token = response.headers.get("X-SecurityToken") 145 return response_body["successfulLogin"] 146 147 tfa_method = response_body["twoFactorLogin"].get("method") 148 149 if tfa_method != "TOTP": 150 raise ValueError(f"Unsupported two factor method {tfa_method}") 151 152 return self.__validate_2fa(credentials) 153 154 def __validate_2fa(self, credentials: BaseCredentials): 155 response = self._session.post( 156 f"{BASE_URL}{Route.TOTP_PATH.value}", 157 json={"method": "TOTP", "totpCode": credentials.totp_code}, 158 ) 159 160 response.raise_for_status() 161 162 self._security_token = response.headers.get("X-SecurityToken") 163 164 response_body = response.json() 165 166 return response_body 167 168 def __call( 169 self, method: HttpMethod, path: str, options=None, return_content: bool = False 170 ): 171 method_call = { 172 HttpMethod.GET: self._session.get, 173 HttpMethod.POST: self._session.post, 174 HttpMethod.PUT: self._session.put, 175 HttpMethod.DELETE: self._session.delete, 176 }.get(method) 177 178 if method_call is None: 179 raise ValueError(f"Unknown method type {method}") 180 181 data = {} 182 if method == HttpMethod.GET: 183 data["params"] = options 184 else: 185 data["json"] = options 186 187 response = method_call( 188 f"{BASE_URL}{path}", 189 headers={ 190 "X-SecurityToken": self._security_token, 191 }, 192 **data, 193 ) 194 195 response.raise_for_status() 196 197 # Some routes like add/remove instrument from a watch list 198 # only returns 200 OK with no further data about if the operation succeeded 199 if len(response.content) == 0: 200 return None 201 202 if return_content: 203 return response.content 204 205 return response.json() 206 207 def get_overview(self) -> Overview: 208 """Get account and category overviews""" 209 return self.__call(HttpMethod.GET, Route.CATEGORIZED_ACCOUNTS.value) 210 211 def get_accounts_positions(self) -> AccountPositions: 212 """Get investment positions for all account""" 213 214 return self.__call(HttpMethod.GET, Route.ACCOUNTS_POSITIONS_PATH.value) 215 216 def get_account_performance_chart_data( 217 self, url_parameters_ids: list[str], time_period: TimePeriod 218 ) -> AccountPositions: 219 """Get performance chart for accounts. 220 221 Args: 222 223 url_parameters_ids: Scrambled account ids. 224 Can be found in the overview response. 225 226 time_period: Time period to get chart data for 227 228 """ 229 return self.__call( 230 HttpMethod.POST, 231 Route.ACCOUNT_PERFORMANCE_CHART_PATH.value, 232 { 233 "scrambledAccountIds": url_parameters_ids, 234 "timePeriod": time_period.value, 235 }, 236 ) 237 238 def get_credit_info(self, credit_type: CreditType) -> CreditInfo: 239 """Returns creditinfo for accounts 240 CreditType->credited for accounts with credit 241 CreditType->uncredited for accounts with no credit 242 """ 243 return self.__call( 244 HttpMethod.GET, Route.CREDITINFO_PATH.value.format(credit_type) 245 ) 246 247 def get_watchlists(self) -> List[WatchList]: 248 """Get your "Bevakningslistor" """ 249 return self.__call(HttpMethod.GET, Route.WATCHLISTS_PATH.value) 250 251 def add_to_watchlist(self, instrument_id: str, watchlist_id: str) -> None: 252 """Add an instrument to the specified watchlist 253 254 This function returns None if the request was 200 OK, 255 but there is no guarantee that the instrument was added to the list, 256 verify this by calling get_watchlists() 257 """ 258 return self.__call( 259 HttpMethod.POST, 260 Route.WATCHLISTS_ADD_PATH.value.format(watchlist_id, instrument_id), 261 ) 262 263 def remove_from_watchlist(self, instrument_id: str, watchlist_id: str) -> None: 264 """Remove an instrument to the specified watchlist 265 266 This function returns None if the request was 200 OK, 267 but there is no guarantee that the instrument was removed from the list, 268 verify this by calling get_watchlists() 269 """ 270 return self.__call( 271 HttpMethod.POST, 272 Route.WATCHLISTS_REMOVE_PATH.value.format(watchlist_id, instrument_id), 273 ) 274 275 def get_fund_info(self, fund_id: str) -> FundInfo: 276 """Get info about a fund""" 277 278 return self.__call(HttpMethod.GET, Route.FUND_PATH.value.format(fund_id)) 279 280 def get_stock_info(self, stock_id: str) -> StockInfo: 281 """Returns info about a stock""" 282 283 return self.get_instrument(InstrumentType.STOCK, stock_id) 284 285 def get_certificate_info(self, certificate_id: str) -> CertificateInfo: 286 """Returns info about a certificate""" 287 288 return self.get_instrument(InstrumentType.CERTIFICATE, certificate_id) 289 290 def get_certificate_details(self, certificate_id: str) -> CertificateDetails: 291 """Returns additional info about a certificate""" 292 293 return self.get_instrument_details(InstrumentType.CERTIFICATE, certificate_id) 294 295 def get_etf_details(self, etf_id: str) -> EtfDetails: 296 return self.get_instrument_details(InstrumentType.EXCHANGE_TRADED_FUND, etf_id) 297 298 def get_warrant_info(self, warrant_id: str) -> WarrantInfo: 299 """Returns info about a warrant""" 300 301 return self.get_instrument(InstrumentType.WARRANT, warrant_id) 302 303 def get_index_info(self, index_id: str) -> IndexInfo: 304 """Returns info about an index""" 305 306 # Works when sending InstrumentType.STOCK, but not InstrumentType.INDEX 307 return self.get_instrument(InstrumentType.STOCK, index_id) 308 309 def get_analysis(self, instrument_id: str): 310 """Returns analysis data for an instrument""" 311 312 return self.__call( 313 HttpMethod.GET, Route.ANALYSIS_PATH.value.format(instrument_id) 314 ) 315 316 def get_news(self, instrument_id: str) -> News: 317 """Returns latest news data for an instrument""" 318 319 return self.__call(HttpMethod.GET, Route.NEWS_PATH.value.format(instrument_id)) 320 321 def get_forum_posts(self, instrument_id: str) -> ForumPosts: 322 """Returns latest forum posts for an instrument""" 323 324 return self.__call(HttpMethod.GET, Route.FORUM_PATH.value.format(instrument_id)) 325 326 def get_instrument(self, instrument_type: InstrumentType, instrument_id: str): 327 """ 328 Get instrument info 329 For more info on return models for this function see functions 330 [ 331 get_stock_info(), 332 get_fund_info(), 333 get_certificate_info(), 334 get_index_info(), 335 get_warrant_info() 336 ] 337 """ 338 339 return self.__call( 340 HttpMethod.GET, 341 Route.INSTRUMENT_PATH.value.format(instrument_type.value, instrument_id), 342 ) 343 344 def get_instrument_details( 345 self, instrument_type: InstrumentType, instrument_id: str 346 ): 347 """ 348 Get additional instrument info 349 """ 350 if instrument_type is InstrumentType.EXCHANGE_TRADED_FUND: 351 return self.__call( 352 HttpMethod.GET, Route.ETF_DETAILS_PATH.value.format(instrument_id) 353 ) 354 else: 355 return self.__call( 356 HttpMethod.GET, 357 Route.INSTRUMENT_DETAILS_PATH.value.format( 358 instrument_type.value, instrument_id 359 ), 360 ) 361 362 def search_for_stock(self, query: str, limit: int = 10) -> SearchResults: 363 """Search for a stock 364 365 Args: 366 367 query: can be a ISIN ('US0378331005'), 368 name ('Apple'), 369 tickerSymbol ('AAPL') 370 371 limit: maximum number of results to return 372 373 """ 374 return self.search_for_instrument(InstrumentType.STOCK, query, limit) 375 376 def search_for_fund(self, query: str, limit: int = 10) -> SearchResults: 377 """Search for a fund 378 379 Args: 380 381 query: can be a ISIN ('SE0012454338'), 382 name ('Avanza'), 383 tickerSymbol ('Avanza Europa') 384 385 limit: maximum number of results to return 386 387 """ 388 389 return self.search_for_instrument(InstrumentType.FUND, query, limit) 390 391 def search_for_certificate(self, query: str, limit: int = 10) -> SearchResults: 392 """Search for a certificate 393 394 Args: 395 396 query: can be a ISIN, name or tickerSymbol 397 398 limit: maximum number of results to return 399 400 """ 401 402 return self.search_for_instrument(InstrumentType.CERTIFICATE, query, limit) 403 404 def search_for_warrant(self, query: str, limit: int = 10) -> SearchResults: 405 """Search for a warrant 406 407 Args: 408 409 query: can be a ISIN, name or tickerSymbol 410 411 limit: maximum number of results to return 412 413 """ 414 415 return self.search_for_instrument(InstrumentType.WARRANT, query, limit) 416 417 def search_for_instrument( 418 self, instrument_type: InstrumentType, query: str, limit: int = 10 419 ): 420 """Search for a specific instrument 421 422 Args: 423 424 instrument_type: can be STOCK, FUND, BOND etc 425 426 query: can be a ISIN, name or tickerSymbol 427 428 limit: maximum number of results to return 429 430 """ 431 432 options = { 433 "query": query, 434 "searchFilter": {"types": [instrument_type.value.upper()]}, 435 "pagination": {"from": 0, "size": limit}, 436 } 437 result = self.__call( 438 HttpMethod.POST, Route.INSTRUMENT_SEARCH_PATH.value, options=options 439 ) 440 return result["hits"] 441 442 443 def get_order_book(self, order_book_id: str)-> OrderBook: 444 """Get info about an orderbook""" 445 return self.__call( 446 HttpMethod.GET, 447 Route.ORDERBOOK_PATH.value.format(order_book_id) 448 ) 449 450 def get_insights_report( 451 self, account_id: str, time_period: InsightsReportTimePeriod 452 ) -> InsightsReport: 453 """Get report about the development of your owned positions during the specified timeperiod""" 454 return self.__call( 455 HttpMethod.GET, 456 Route.INSIGHTS_PATH.value.format(time_period.value, account_id), 457 ) 458 459 def get_deals(self): 460 """Get currently active deals""" 461 return self.__call(HttpMethod.GET, Route.DEALS_PATH.value) 462 463 def get_orders(self): 464 """Get currently active orders""" 465 return self.__call(HttpMethod.GET, Route.ORDERS_PATH.value) 466 467 def get_inspiration_lists(self) -> List[InspirationListItem]: 468 """Get all available inspiration lists 469 470 This returns lists similar to the ones found on: 471 472 https://www.avanza.se/aktier/aktieinspiration.html 473 474 https://www.avanza.se/fonder/fondinspiration.html 475 476 """ 477 return self.__call(HttpMethod.GET, Route.INSPIRATION_LIST_PATH.value.format("")) 478 479 def get_inspiration_list(self, list_id: Union[ListType, str]) -> InspirationList: 480 """Get inspiration list 481 482 Some lists have an id of an enum value described in ListType, but they can also just have a string id. 483 An example is hhSK8W1o which corresponds to "Most owned stocks", which isn't described in the ListType enum 484 485 """ 486 487 id = list_id.value if isinstance(list_id, ListType) else list_id 488 489 return self.__call(HttpMethod.GET, Route.INSPIRATION_LIST_PATH.value.format(id)) 490 491 def get_chart_data( 492 self, 493 order_book_id: str, 494 period: TimePeriod, 495 resolution: Optional[Resolution] = None, 496 ) -> ChartData: 497 """Return chart data for an order book for the specified time period with given resolution""" 498 options = {"timePeriod": period.value.lower()} 499 500 if resolution is not None: 501 options["resolution"] = resolution.value.lower() 502 503 return self.__call( 504 HttpMethod.GET, Route.CHARTDATA_PATH.value.format(order_book_id), options 505 ) 506 507 def place_order( 508 self, 509 account_id: str, 510 order_book_id: str, 511 order_type: OrderType, 512 price: float, 513 valid_until: date, 514 volume: int, 515 condition: Condition = Condition.NORMAL, 516 ): 517 """Place an order 518 519 Returns: 520 521 If the order was successfully placed: 522 523 { 524 message: str, 525 orderId: str, 526 orderRequestStatus: 'SUCCESS' 527 } 528 529 If the order was not placed: 530 531 { 532 message: str, 533 orderRequestStatus: 'ERROR' 534 } 535 """ 536 537 return self.__call( 538 HttpMethod.POST, 539 Route.ORDER_PLACE_PATH.value, 540 { 541 "accountId": account_id, 542 "orderbookId": order_book_id, 543 "side": order_type.value, 544 "condition": condition.value, 545 "price": price, 546 "validUntil": valid_until.isoformat(), 547 "volume": volume, 548 }, 549 ) 550 551 def place_order_buy_fund(self, account_id: str, order_book_id: str, amount: float): 552 """Place a buy order for a fund 553 554 Returns: 555 556 { 557 message: str, 558 orderId: str, 559 accountId: str, 560 orderRequestStatus: str 561 } 562 """ 563 564 return self.__call( 565 HttpMethod.POST, 566 Route.ORDER_PLACE_PATH_BUY_FUND.value, 567 {"orderbookId": order_book_id, "accountId": account_id, "amount": amount}, 568 ) 569 570 def place_order_sell_fund(self, account_id: str, order_book_id: str, volume: float): 571 """Place a sell order for a fund 572 573 Returns: 574 575 { 576 message: str, 577 orderId: str, 578 accountId: str, 579 orderRequestStatus: str 580 } 581 """ 582 583 return self.__call( 584 HttpMethod.POST, 585 Route.ORDER_PLACE_PATH_SELL_FUND.value, 586 {"orderbookId": order_book_id, "accountId": account_id, "volume": volume}, 587 ) 588 589 def place_stop_loss_order( 590 self, 591 parent_stop_loss_id: str, 592 account_id: str, 593 order_book_id: str, 594 stop_loss_trigger: StopLossTrigger, 595 stop_loss_order_event: StopLossOrderEvent, 596 ): 597 """Place an stop loss order 598 599 Args: 600 601 parent_stop_loss_id: The id of the parent stop loss order. If this is the first stop loss order, this should be "0". 602 603 account_id: A valid account id. 604 605 order_book_id: The order book id of the instrument to place the stop loss order for. 606 607 stop_loss_trigger: The stop loss trigger type. 608 609 stop_loss_order_event: The stop loss order event type. 610 611 Returns: 612 613 If the order was successfully placed: 614 615 { 616 status: 'SUCCESS', 617 stoplossOrderId: str 618 } 619 620 If the order was not placed: 621 622 { 623 status: str, 624 stoplossOrderId: str 625 } 626 """ 627 628 return self.__call( 629 HttpMethod.POST, 630 Route.ORDER_PLACE_STOP_LOSS_PATH.value, 631 { 632 "parentStopLossId": parent_stop_loss_id, 633 "accountId": account_id, 634 "orderBookId": order_book_id, 635 "stopLossTrigger": { 636 "type": stop_loss_trigger.type.value, 637 "value": stop_loss_trigger.value, 638 "validUntil": stop_loss_trigger.valid_until.isoformat(), 639 "valueType": stop_loss_trigger.value_type.value, 640 "triggerOnMarketMakerQuote": stop_loss_trigger.trigger_on_market_maker_quote, 641 }, 642 "stopLossOrderEvent": { 643 "type": stop_loss_order_event.type.value, 644 "price": stop_loss_order_event.price, 645 "volume": stop_loss_order_event.volume, 646 "validDays": stop_loss_order_event.valid_days, 647 "priceType": stop_loss_order_event.price_type.value, 648 "shortSellingAllowed": stop_loss_order_event.short_selling_allowed, 649 }, 650 }, 651 ) 652 653 def edit_order( 654 self, 655 order_id: str, 656 account_id: str, 657 price: float, 658 valid_until: date, 659 volume: int, 660 ): 661 """Update an existing order 662 663 Returns: 664 665 { 666 orderRequestStatus: str, 667 message: str, 668 parameters: List[str], 669 orderId: str 670 } 671 """ 672 673 return self.__call( 674 HttpMethod.POST, 675 Route.ORDER_EDIT_PATH.value, 676 { 677 "accountId": account_id, 678 "metadata": {"orderEntryMode": "STANDARD"}, 679 "openVolume": None, 680 "orderId": order_id, 681 "price": price, 682 "validUntil": valid_until.isoformat(), 683 "volume": volume, 684 }, 685 ) 686 687 def get_order(self, account_id: str, order_id: str): 688 """Get an existing order 689 690 Returns: 691 692 { 693 "orderId": str, 694 "orderbookId": str, 695 "side": str, 696 "state": str, 697 "marketReference": str, 698 "price": float, 699 "message": str, 700 "volume": int, 701 "originalVolume": int, 702 "accountId": str, 703 "condition": str, 704 "validUntil": str, 705 "modifiable": bool, 706 "deletable": bool, 707 } 708 """ 709 710 return self.__call( 711 HttpMethod.GET, 712 Route.ORDER_GET_PATH.value.format( 713 order_id, 714 account_id, 715 ), 716 ) 717 718 def get_all_stop_losses(self): 719 """Get open stop losses 720 721 Returns: 722 723 [{ 724 "id": str, 725 "status": str, 726 "account": { 727 "id": str, 728 "name": str, 729 "type": str, 730 "urlParameterId": str 731 }, 732 "orderbook": { 733 "id": str, 734 "name": str, 735 "countryCode": str, 736 "currency": str, 737 "shortName": str, 738 "type": str 739 }, 740 "hasExcludingChildren": bool, 741 "message": str, 742 "trigger": { 743 "value": int, 744 "type": str, 745 "validUntil": str, 746 "valueType": str 747 }, 748 "order": { 749 "type": str, 750 "price": int, 751 "volume": int, 752 "shortSellingAllowed": bool, 753 "validDays": int, 754 "priceType": str, 755 "priceDecimalPrecision": 0 756 }, 757 "editable": bool, 758 "deletable": bool 759 }] 760 """ 761 return self.__call(HttpMethod.GET, Route.STOP_LOSS_PATH.value) 762 763 def delete_stop_loss_order(self, account_id: str, stop_loss_id: str): 764 """delete a stop loss order 765 766 Args: 767 768 stop_loss_id: The id of the stop loss order to delete. 769 770 account_id: A valid account id. 771 772 Returns: 773 Nothing 774 """ 775 776 return self.__call( 777 HttpMethod.DELETE, 778 Route.ORDER_DELETE_STOP_LOSS_PATH.value.format( 779 account_id, 780 stop_loss_id, 781 ), 782 ) 783 784 def delete_order(self, account_id: str, order_id: str): 785 """Delete an existing order 786 787 Returns: 788 789 { 790 messages: str, 791 orderId: str, 792 parameters: List[str], 793 orderRequestStatus: str 794 } 795 """ 796 return self.__call( 797 HttpMethod.POST, 798 Route.ORDER_DELETE_PATH.value, 799 {"accountId": account_id, "orderId": order_id}, 800 ) 801 802 def get_monthly_savings_by_account_id(self, account_id: str): 803 """Get monthly savings at avanza for specific account 804 805 Returns: 806 807 { 808 'monthlySavings': [{ 809 'account': { 810 'id': str, 811 'name': str, 812 'type': str 813 }, 814 'amount': float, 815 'cash': {'amount': float, 'percent': float}, 816 'externalAccount': { 817 'accountNumber': str, 818 'bankName': str, 819 'clearingNumber': str 820 }, 821 'fundDistributions': [{ 822 'amount': float, 823 'orderbook': { 824 'buyable': bool, 825 'id': str, 826 'name': str 827 }, 828 'percent': float 829 }, 830 'id': str, 831 'name': str, 832 'purchaseDay': int, 833 'status': str, 834 'transferDay': int 835 }], 836 'totalAmount': float 837 } 838 """ 839 840 return self.__call( 841 HttpMethod.GET, Route.MONTHLY_SAVINGS_PATH.value.format(account_id) 842 ) 843 844 def get_all_monthly_savings(self): 845 """Get your monthly savings at Avanza 846 847 Returns: 848 849 { 850 'monthlySavings': [{ 851 'account': { 852 'id': str, 853 'name': str, 854 'type': str 855 }, 856 'amount': float, 857 'cash': {'amount': float, 'percent': float}, 858 'externalAccount': { 859 'accountNumber': str, 860 'bankName': str, 861 'clearingNumber': str 862 }, 863 'fundDistributions': [{ 864 'amount': float, 865 'orderbook': { 866 'buyable': bool, 867 'id': str, 868 'name': str 869 }, 870 'percent': float 871 }, 872 'id': str, 873 'name': str, 874 'purchaseDay': int, 875 'status': str, 876 'transferDay': int 877 }], 878 'totalAmount': float 879 } 880 """ 881 882 return self.__call(HttpMethod.GET, Route.MONTHLY_SAVINGS_PATH.value.format("")) 883 884 def pause_monthly_saving(self, account_id: str, monthly_savings_id: str): 885 """Pause an active monthly saving 886 887 Returns: 888 'OK' 889 890 """ 891 892 return self.__call( 893 HttpMethod.PUT, 894 Route.MONTHLY_SAVINGS_PAUSE_PATH.value.format( 895 account_id, monthly_savings_id 896 ), 897 ) 898 899 def resume_monthly_saving(self, account_id: str, monthly_savings_id: str): 900 """Resume a paused monthly saving 901 902 Returns: 903 'OK' 904 905 """ 906 907 return self.__call( 908 HttpMethod.PUT, 909 Route.MONTHLY_SAVINGS_RESUME_PATH.value.format( 910 account_id, monthly_savings_id 911 ), 912 ) 913 914 def delete_monthly_saving(self, account_id: str, monthly_savings_id: str) -> None: 915 """Deletes a monthly saving 916 917 Returns: 918 None 919 920 """ 921 922 return self.__call( 923 HttpMethod.DELETE, 924 Route.MONTHLY_SAVINGS_REMOVE_PATH.value.format( 925 account_id, monthly_savings_id 926 ), 927 ) 928 929 def create_monthly_saving( 930 self, 931 account_id: str, 932 amount: int, 933 transfer_day_of_month: int, 934 purchase_day_of_month: int, 935 clearing_and_account_number: str, 936 fund_distribution: Dict[str, int], 937 ): 938 """Create a monthly saving at Avanza 939 940 Args: 941 942 account_id: The Avanza account to which the withdrawn money should be transferred to 943 944 amount: minimum amount 100 (SEK) 945 the amount that should be withdrawn from the external account every month 946 947 transfer_day_of_month: valid range (1-31) 948 when the money should be withdrawn from the external account 949 950 purchase_day_of_month: valid range (1-31) 951 when the funds should be purchased, 952 must occur after the transfer_day_of_month 953 954 clearing_and_account_number: The external account from which the money for the monthly savings should be withdrawn from, 955 has to be formatted as follows 'XXXX-XXXXXXXXX' 956 957 fund_distrubution: the percentage distribution of the funds 958 The key is the funds id and the value is the distribution of the amount in a whole percentage 959 The sum of the percentages has to total 100 960 961 Examples: 962 {'41567': 100} 963 {'41567': 50, '878733': 50} 964 {'41567': 25, '878733': 75} 965 966 Returns: 967 968 { 969 'monthlySavingId': str, 970 'status': str 971 } 972 973 monthlySavingId has the following format: 'XX^XXXXXXXXXXXXX^XXXXXX' 974 status should have the value 'ACCEPTED' if the monthly saving was created successfully 975 """ 976 977 if not 1 <= transfer_day_of_month <= 31: 978 raise ValueError( 979 "transfer_day_of_month is outside the valid range of (1-31)" 980 ) 981 982 if not 1 <= purchase_day_of_month <= 31: 983 raise ValueError( 984 "purchase_day_of_month is outside the valid range of (1-31)" 985 ) 986 987 if transfer_day_of_month >= purchase_day_of_month: 988 raise ValueError( 989 "transfer_day_of_month must occur before purchase_day_of_month" 990 ) 991 992 if len(fund_distribution) == 0: 993 raise ValueError("No founds were specified in the fund_distribution") 994 995 if sum(fund_distribution.values()) != 100: 996 raise ValueError("The fund_distribution values must total 100") 997 998 return self.__call( 999 HttpMethod.POST, 1000 Route.MONTHLY_SAVINGS_CREATE_PATH.value.format(account_id), 1001 { 1002 "amount": amount, 1003 "autogiro": { 1004 "dayOfMonth": transfer_day_of_month, 1005 "externalClearingAndAccount": clearing_and_account_number, 1006 }, 1007 "fundDistribution": { 1008 "dayOfMonth": purchase_day_of_month, 1009 "fundDistributions": fund_distribution, 1010 }, 1011 }, 1012 ) 1013 1014 def get_transactions_details( 1015 self, 1016 transaction_details_types: Optional[Sequence[TransactionsDetailsType]] = [], 1017 transactions_from: Optional[date] = None, 1018 transactions_to: Optional[date] = None, 1019 isin: Optional[str] = None, 1020 max_elements: Optional[int] = 1000, 1021 ) -> Transactions: 1022 """Get transactions, optionally apply criteria. 1023 1024 Args: 1025 1026 transaction_types: One or more transaction types. 1027 1028 transactions_from: Fetch transactions from this date. 1029 1030 transactions_to: Fetch transactions to this date. 1031 1032 isin: Only fetch transactions for specified isin. 1033 1034 max_elements: Limit result to N transactions. 1035 """ 1036 options = {} 1037 options["maxElements"] = max_elements 1038 1039 if transaction_details_types: 1040 options["transactionTypes"] = ",".join( 1041 [type.value for type in transaction_details_types] 1042 ) 1043 if transactions_from: 1044 options["from"] = transactions_from.isoformat() 1045 if transactions_to: 1046 options["to"] = transactions_to.isoformat() 1047 if isin: 1048 options["isin"] = isin 1049 1050 return self.__call( 1051 HttpMethod.GET, Route.TRANSACTIONS_DETAILS_PATH.value, options 1052 ) 1053 1054 def get_note_as_pdf(self, url_parameter_id: str, note_id: str): 1055 return self.__call( 1056 HttpMethod.GET, 1057 Route.NOTE_PATH.value.format(url_parameter_id, note_id), 1058 return_content=True, 1059 ) 1060 1061 def set_price_alert( 1062 self, 1063 order_book_id: str, 1064 price: float, 1065 valid_until: date, 1066 notification: bool = True, 1067 email: bool = False, 1068 sms: bool = False, 1069 ): 1070 """ 1071 Sets a price alert for the specified orderbook and returns all the existing alerts. 1072 1073 Returns: 1074 1075 [ 1076 { 1077 'alertId': str, 1078 'accountId': str, 1079 'price': float, 1080 'validUntil': str, 1081 'direction': str, 1082 'email': bool, 1083 'notification': bool, 1084 'sms': bool, 1085 } 1086 ] 1087 """ 1088 1089 return self.__call( 1090 HttpMethod.POST, 1091 Route.PRICE_ALERT_PATH.value.format(order_book_id), 1092 { 1093 "price": price, 1094 "validUntil": valid_until.isoformat(), 1095 "notification": notification, 1096 "email": email, 1097 "sms": sms, 1098 }, 1099 ) 1100 1101 def get_price_alert(self, order_book_id: str) -> List[PriceAlert]: 1102 """Gets all the price alerts for the specified orderbook""" 1103 1104 return self.__call( 1105 HttpMethod.GET, 1106 Route.PRICE_ALERT_PATH.value.format(order_book_id), 1107 ) 1108 1109 def delete_price_alert(self, order_book_id: str, alert_id: str): 1110 """ 1111 Deletes a price alert from the specified orderbook and returns the remaining alerts. 1112 1113 Returns: 1114 1115 [ 1116 { 1117 'alertId': str, 1118 'accountId': str, 1119 'price': float, 1120 'validUntil': str, 1121 'direction': str, 1122 'email': bool, 1123 'notification': bool, 1124 'sms': bool, 1125 } 1126 ] 1127 """ 1128 return self.__call( 1129 HttpMethod.DELETE, 1130 Route.PRICE_ALERT_PATH.value.format(order_book_id, alert_id) 1131 + f"/{alert_id}", 1132 ) 1133 1134 def get_offers(self) -> List[Offer]: 1135 """Return current offers""" 1136 1137 return self.__call(HttpMethod.GET, Route.CURRENT_OFFERS_PATH.value)
37 def __init__( 38 self, 39 credentials: Union[BaseCredentials, Dict[str, str]], 40 retry_with_next_otp: bool = True, 41 quiet: bool = False, 42 ): 43 """ 44 45 Args: 46 47 credentials: Login credentials. Can be multiple variations 48 Either an instance of TokenCredentials or of SecretCredentials 49 Or a dictionary as the following: 50 Using TOTP secret: 51 { 52 'username': 'MY_USERNAME', 53 'password': 'MY_PASSWORD', 54 'totpSecret': 'MY_TOTP_SECRET' 55 } 56 Using TOTP code: 57 { 58 'username': 'MY_USERNAME', 59 'password': 'MY_PASSWORD', 60 'totpCode': 'MY_TOTP_CODE' 61 } 62 63 retry_with_next_otp: If 64 65 a) the server responded with 401 Unauthorized when trying to 66 log in, and 67 b) the TOTP code used for logging in was generated from a TOTP 68 secret provided in credentials, 69 70 then wait until the next TOTP time-step window and try again 71 with the new OTP. Re-retrying with a new OTP prevents a program 72 using avanza-api from crashing with a 401 HTTP error if, for 73 example, the program is run a second time shortly after it was 74 previously run. 75 76 quiet: Do not print a status message if waiting for the next TOTP 77 time-step window. 78 79 """ 80 if isinstance(credentials, dict): 81 credentials: BaseCredentials = backwards_compatible_serialization( 82 credentials 83 ) 84 self._retry_with_next_otp = retry_with_next_otp 85 self._quiet = quiet 86 87 self._authenticationTimeout = MAX_INACTIVE_MINUTES 88 self._session = requests.Session() 89 90 try: 91 response_body = self.__authenticate(credentials) 92 except requests.exceptions.HTTPError as http_error: 93 if ( 94 http_error.response.status_code == 401 95 and isinstance(credentials, SecretCredentials) 96 and self._retry_with_next_otp 97 ): 98 # Wait for the next TOTP time-step window and try with the new OTP 99 default_otp_interval: int = 30 # We use this pyotp/RFC 6238 default 100 now: float = datetime.now().timestamp() 101 time_to_wait: int = math.ceil( 102 (default_otp_interval - now) % default_otp_interval 103 ) 104 if not self._quiet: 105 print( 106 "Server returned 401 when trying to log in. " 107 f"Will retry with the next OTP in {time_to_wait}s..." 108 ) 109 time.sleep(time_to_wait) 110 response_body = self.__authenticate(credentials) 111 else: 112 raise 113 114 self._authentication_session = response_body["authenticationSession"] 115 self._push_subscription_id = response_body["pushSubscriptionId"] 116 self._customer_id = response_body["customerId"]
Args:
credentials: Login credentials. Can be multiple variations
Either an instance of TokenCredentials or of SecretCredentials
Or a dictionary as the following:
Using TOTP secret:
{
'username': 'MY_USERNAME',
'password': 'MY_PASSWORD',
'totpSecret': 'MY_TOTP_SECRET'
}
Using TOTP code:
{
'username': 'MY_USERNAME',
'password': 'MY_PASSWORD',
'totpCode': 'MY_TOTP_CODE'
}
retry_with_next_otp: If
a) the server responded with 401 Unauthorized when trying to
log in, and
b) the TOTP code used for logging in was generated from a TOTP
secret provided in credentials,
then wait until the next TOTP time-step window and try again
with the new OTP. Re-retrying with a new OTP prevents a program
using avanza-api from crashing with a 401 HTTP error if, for
example, the program is run a second time shortly after it was
previously run.
quiet: Do not print a status message if waiting for the next TOTP
time-step window.
207 def get_overview(self) -> Overview: 208 """Get account and category overviews""" 209 return self.__call(HttpMethod.GET, Route.CATEGORIZED_ACCOUNTS.value)
Get account and category overviews
211 def get_accounts_positions(self) -> AccountPositions: 212 """Get investment positions for all account""" 213 214 return self.__call(HttpMethod.GET, Route.ACCOUNTS_POSITIONS_PATH.value)
Get investment positions for all account
216 def get_account_performance_chart_data( 217 self, url_parameters_ids: list[str], time_period: TimePeriod 218 ) -> AccountPositions: 219 """Get performance chart for accounts. 220 221 Args: 222 223 url_parameters_ids: Scrambled account ids. 224 Can be found in the overview response. 225 226 time_period: Time period to get chart data for 227 228 """ 229 return self.__call( 230 HttpMethod.POST, 231 Route.ACCOUNT_PERFORMANCE_CHART_PATH.value, 232 { 233 "scrambledAccountIds": url_parameters_ids, 234 "timePeriod": time_period.value, 235 }, 236 )
Get performance chart for accounts.
Args:
url_parameters_ids: Scrambled account ids.
Can be found in the overview response.
time_period: Time period to get chart data for
238 def get_credit_info(self, credit_type: CreditType) -> CreditInfo: 239 """Returns creditinfo for accounts 240 CreditType->credited for accounts with credit 241 CreditType->uncredited for accounts with no credit 242 """ 243 return self.__call( 244 HttpMethod.GET, Route.CREDITINFO_PATH.value.format(credit_type) 245 )
Returns creditinfo for accounts CreditType->credited for accounts with credit CreditType->uncredited for accounts with no credit
247 def get_watchlists(self) -> List[WatchList]: 248 """Get your "Bevakningslistor" """ 249 return self.__call(HttpMethod.GET, Route.WATCHLISTS_PATH.value)
Get your "Bevakningslistor"
251 def add_to_watchlist(self, instrument_id: str, watchlist_id: str) -> None: 252 """Add an instrument to the specified watchlist 253 254 This function returns None if the request was 200 OK, 255 but there is no guarantee that the instrument was added to the list, 256 verify this by calling get_watchlists() 257 """ 258 return self.__call( 259 HttpMethod.POST, 260 Route.WATCHLISTS_ADD_PATH.value.format(watchlist_id, instrument_id), 261 )
Add an instrument to the specified watchlist
This function returns None if the request was 200 OK, but there is no guarantee that the instrument was added to the list, verify this by calling get_watchlists()
263 def remove_from_watchlist(self, instrument_id: str, watchlist_id: str) -> None: 264 """Remove an instrument to the specified watchlist 265 266 This function returns None if the request was 200 OK, 267 but there is no guarantee that the instrument was removed from the list, 268 verify this by calling get_watchlists() 269 """ 270 return self.__call( 271 HttpMethod.POST, 272 Route.WATCHLISTS_REMOVE_PATH.value.format(watchlist_id, instrument_id), 273 )
Remove an instrument to the specified watchlist
This function returns None if the request was 200 OK, but there is no guarantee that the instrument was removed from the list, verify this by calling get_watchlists()
275 def get_fund_info(self, fund_id: str) -> FundInfo: 276 """Get info about a fund""" 277 278 return self.__call(HttpMethod.GET, Route.FUND_PATH.value.format(fund_id))
Get info about a fund
280 def get_stock_info(self, stock_id: str) -> StockInfo: 281 """Returns info about a stock""" 282 283 return self.get_instrument(InstrumentType.STOCK, stock_id)
Returns info about a stock
285 def get_certificate_info(self, certificate_id: str) -> CertificateInfo: 286 """Returns info about a certificate""" 287 288 return self.get_instrument(InstrumentType.CERTIFICATE, certificate_id)
Returns info about a certificate
290 def get_certificate_details(self, certificate_id: str) -> CertificateDetails: 291 """Returns additional info about a certificate""" 292 293 return self.get_instrument_details(InstrumentType.CERTIFICATE, certificate_id)
Returns additional info about a certificate
298 def get_warrant_info(self, warrant_id: str) -> WarrantInfo: 299 """Returns info about a warrant""" 300 301 return self.get_instrument(InstrumentType.WARRANT, warrant_id)
Returns info about a warrant
303 def get_index_info(self, index_id: str) -> IndexInfo: 304 """Returns info about an index""" 305 306 # Works when sending InstrumentType.STOCK, but not InstrumentType.INDEX 307 return self.get_instrument(InstrumentType.STOCK, index_id)
Returns info about an index
309 def get_analysis(self, instrument_id: str): 310 """Returns analysis data for an instrument""" 311 312 return self.__call( 313 HttpMethod.GET, Route.ANALYSIS_PATH.value.format(instrument_id) 314 )
Returns analysis data for an instrument
316 def get_news(self, instrument_id: str) -> News: 317 """Returns latest news data for an instrument""" 318 319 return self.__call(HttpMethod.GET, Route.NEWS_PATH.value.format(instrument_id))
Returns latest news data for an instrument
321 def get_forum_posts(self, instrument_id: str) -> ForumPosts: 322 """Returns latest forum posts for an instrument""" 323 324 return self.__call(HttpMethod.GET, Route.FORUM_PATH.value.format(instrument_id))
Returns latest forum posts for an instrument
326 def get_instrument(self, instrument_type: InstrumentType, instrument_id: str): 327 """ 328 Get instrument info 329 For more info on return models for this function see functions 330 [ 331 get_stock_info(), 332 get_fund_info(), 333 get_certificate_info(), 334 get_index_info(), 335 get_warrant_info() 336 ] 337 """ 338 339 return self.__call( 340 HttpMethod.GET, 341 Route.INSTRUMENT_PATH.value.format(instrument_type.value, instrument_id), 342 )
Get instrument info For more info on return models for this function see functions [ get_stock_info(), get_fund_info(), get_certificate_info(), get_index_info(), get_warrant_info() ]
344 def get_instrument_details( 345 self, instrument_type: InstrumentType, instrument_id: str 346 ): 347 """ 348 Get additional instrument info 349 """ 350 if instrument_type is InstrumentType.EXCHANGE_TRADED_FUND: 351 return self.__call( 352 HttpMethod.GET, Route.ETF_DETAILS_PATH.value.format(instrument_id) 353 ) 354 else: 355 return self.__call( 356 HttpMethod.GET, 357 Route.INSTRUMENT_DETAILS_PATH.value.format( 358 instrument_type.value, instrument_id 359 ), 360 )
Get additional instrument info
362 def search_for_stock(self, query: str, limit: int = 10) -> SearchResults: 363 """Search for a stock 364 365 Args: 366 367 query: can be a ISIN ('US0378331005'), 368 name ('Apple'), 369 tickerSymbol ('AAPL') 370 371 limit: maximum number of results to return 372 373 """ 374 return self.search_for_instrument(InstrumentType.STOCK, query, limit)
Search for a stock
Args:
query: can be a ISIN ('US0378331005'),
name ('Apple'),
tickerSymbol ('AAPL')
limit: maximum number of results to return
376 def search_for_fund(self, query: str, limit: int = 10) -> SearchResults: 377 """Search for a fund 378 379 Args: 380 381 query: can be a ISIN ('SE0012454338'), 382 name ('Avanza'), 383 tickerSymbol ('Avanza Europa') 384 385 limit: maximum number of results to return 386 387 """ 388 389 return self.search_for_instrument(InstrumentType.FUND, query, limit)
Search for a fund
Args:
query: can be a ISIN ('SE0012454338'),
name ('Avanza'),
tickerSymbol ('Avanza Europa')
limit: maximum number of results to return
391 def search_for_certificate(self, query: str, limit: int = 10) -> SearchResults: 392 """Search for a certificate 393 394 Args: 395 396 query: can be a ISIN, name or tickerSymbol 397 398 limit: maximum number of results to return 399 400 """ 401 402 return self.search_for_instrument(InstrumentType.CERTIFICATE, query, limit)
Search for a certificate
Args:
query: can be a ISIN, name or tickerSymbol
limit: maximum number of results to return
404 def search_for_warrant(self, query: str, limit: int = 10) -> SearchResults: 405 """Search for a warrant 406 407 Args: 408 409 query: can be a ISIN, name or tickerSymbol 410 411 limit: maximum number of results to return 412 413 """ 414 415 return self.search_for_instrument(InstrumentType.WARRANT, query, limit)
Search for a warrant
Args:
query: can be a ISIN, name or tickerSymbol
limit: maximum number of results to return
417 def search_for_instrument( 418 self, instrument_type: InstrumentType, query: str, limit: int = 10 419 ): 420 """Search for a specific instrument 421 422 Args: 423 424 instrument_type: can be STOCK, FUND, BOND etc 425 426 query: can be a ISIN, name or tickerSymbol 427 428 limit: maximum number of results to return 429 430 """ 431 432 options = { 433 "query": query, 434 "searchFilter": {"types": [instrument_type.value.upper()]}, 435 "pagination": {"from": 0, "size": limit}, 436 } 437 result = self.__call( 438 HttpMethod.POST, Route.INSTRUMENT_SEARCH_PATH.value, options=options 439 ) 440 return result["hits"]
Search for a specific instrument
Args:
instrument_type: can be STOCK, FUND, BOND etc
query: can be a ISIN, name or tickerSymbol
limit: maximum number of results to return
443 def get_order_book(self, order_book_id: str)-> OrderBook: 444 """Get info about an orderbook""" 445 return self.__call( 446 HttpMethod.GET, 447 Route.ORDERBOOK_PATH.value.format(order_book_id) 448 )
Get info about an orderbook
450 def get_insights_report( 451 self, account_id: str, time_period: InsightsReportTimePeriod 452 ) -> InsightsReport: 453 """Get report about the development of your owned positions during the specified timeperiod""" 454 return self.__call( 455 HttpMethod.GET, 456 Route.INSIGHTS_PATH.value.format(time_period.value, account_id), 457 )
Get report about the development of your owned positions during the specified timeperiod
459 def get_deals(self): 460 """Get currently active deals""" 461 return self.__call(HttpMethod.GET, Route.DEALS_PATH.value)
Get currently active deals
463 def get_orders(self): 464 """Get currently active orders""" 465 return self.__call(HttpMethod.GET, Route.ORDERS_PATH.value)
Get currently active orders
467 def get_inspiration_lists(self) -> List[InspirationListItem]: 468 """Get all available inspiration lists 469 470 This returns lists similar to the ones found on: 471 472 https://www.avanza.se/aktier/aktieinspiration.html 473 474 https://www.avanza.se/fonder/fondinspiration.html 475 476 """ 477 return self.__call(HttpMethod.GET, Route.INSPIRATION_LIST_PATH.value.format(""))
Get all available inspiration lists
This returns lists similar to the ones found on:
avanza.avanza.se/aktier/aktieinspiration.html">https://wwwavanza.avanza.se/aktier/aktieinspiration.html
avanza.avanza.se/fonder/fondinspiration.html">https://wwwavanza.avanza.se/fonder/fondinspiration.html
479 def get_inspiration_list(self, list_id: Union[ListType, str]) -> InspirationList: 480 """Get inspiration list 481 482 Some lists have an id of an enum value described in ListType, but they can also just have a string id. 483 An example is hhSK8W1o which corresponds to "Most owned stocks", which isn't described in the ListType enum 484 485 """ 486 487 id = list_id.value if isinstance(list_id, ListType) else list_id 488 489 return self.__call(HttpMethod.GET, Route.INSPIRATION_LIST_PATH.value.format(id))
Get inspiration list
Some lists have an id of an enum value described in ListType, but they can also just have a string id. An example is hhSK8W1o which corresponds to "Most owned stocks", which isn't described in the ListType enum
491 def get_chart_data( 492 self, 493 order_book_id: str, 494 period: TimePeriod, 495 resolution: Optional[Resolution] = None, 496 ) -> ChartData: 497 """Return chart data for an order book for the specified time period with given resolution""" 498 options = {"timePeriod": period.value.lower()} 499 500 if resolution is not None: 501 options["resolution"] = resolution.value.lower() 502 503 return self.__call( 504 HttpMethod.GET, Route.CHARTDATA_PATH.value.format(order_book_id), options 505 )
Return chart data for an order book for the specified time period with given resolution
507 def place_order( 508 self, 509 account_id: str, 510 order_book_id: str, 511 order_type: OrderType, 512 price: float, 513 valid_until: date, 514 volume: int, 515 condition: Condition = Condition.NORMAL, 516 ): 517 """Place an order 518 519 Returns: 520 521 If the order was successfully placed: 522 523 { 524 message: str, 525 orderId: str, 526 orderRequestStatus: 'SUCCESS' 527 } 528 529 If the order was not placed: 530 531 { 532 message: str, 533 orderRequestStatus: 'ERROR' 534 } 535 """ 536 537 return self.__call( 538 HttpMethod.POST, 539 Route.ORDER_PLACE_PATH.value, 540 { 541 "accountId": account_id, 542 "orderbookId": order_book_id, 543 "side": order_type.value, 544 "condition": condition.value, 545 "price": price, 546 "validUntil": valid_until.isoformat(), 547 "volume": volume, 548 }, 549 )
Place an order
Returns:
If the order was successfully placed:
{
message: str,
orderId: str,
orderRequestStatus: 'SUCCESS'
}
If the order was not placed:
{
message: str,
orderRequestStatus: 'ERROR'
}
551 def place_order_buy_fund(self, account_id: str, order_book_id: str, amount: float): 552 """Place a buy order for a fund 553 554 Returns: 555 556 { 557 message: str, 558 orderId: str, 559 accountId: str, 560 orderRequestStatus: str 561 } 562 """ 563 564 return self.__call( 565 HttpMethod.POST, 566 Route.ORDER_PLACE_PATH_BUY_FUND.value, 567 {"orderbookId": order_book_id, "accountId": account_id, "amount": amount}, 568 )
Place a buy order for a fund
Returns:
{
message: str,
orderId: str,
accountId: str,
orderRequestStatus: str
}
570 def place_order_sell_fund(self, account_id: str, order_book_id: str, volume: float): 571 """Place a sell order for a fund 572 573 Returns: 574 575 { 576 message: str, 577 orderId: str, 578 accountId: str, 579 orderRequestStatus: str 580 } 581 """ 582 583 return self.__call( 584 HttpMethod.POST, 585 Route.ORDER_PLACE_PATH_SELL_FUND.value, 586 {"orderbookId": order_book_id, "accountId": account_id, "volume": volume}, 587 )
Place a sell order for a fund
Returns:
{
message: str,
orderId: str,
accountId: str,
orderRequestStatus: str
}
589 def place_stop_loss_order( 590 self, 591 parent_stop_loss_id: str, 592 account_id: str, 593 order_book_id: str, 594 stop_loss_trigger: StopLossTrigger, 595 stop_loss_order_event: StopLossOrderEvent, 596 ): 597 """Place an stop loss order 598 599 Args: 600 601 parent_stop_loss_id: The id of the parent stop loss order. If this is the first stop loss order, this should be "0". 602 603 account_id: A valid account id. 604 605 order_book_id: The order book id of the instrument to place the stop loss order for. 606 607 stop_loss_trigger: The stop loss trigger type. 608 609 stop_loss_order_event: The stop loss order event type. 610 611 Returns: 612 613 If the order was successfully placed: 614 615 { 616 status: 'SUCCESS', 617 stoplossOrderId: str 618 } 619 620 If the order was not placed: 621 622 { 623 status: str, 624 stoplossOrderId: str 625 } 626 """ 627 628 return self.__call( 629 HttpMethod.POST, 630 Route.ORDER_PLACE_STOP_LOSS_PATH.value, 631 { 632 "parentStopLossId": parent_stop_loss_id, 633 "accountId": account_id, 634 "orderBookId": order_book_id, 635 "stopLossTrigger": { 636 "type": stop_loss_trigger.type.value, 637 "value": stop_loss_trigger.value, 638 "validUntil": stop_loss_trigger.valid_until.isoformat(), 639 "valueType": stop_loss_trigger.value_type.value, 640 "triggerOnMarketMakerQuote": stop_loss_trigger.trigger_on_market_maker_quote, 641 }, 642 "stopLossOrderEvent": { 643 "type": stop_loss_order_event.type.value, 644 "price": stop_loss_order_event.price, 645 "volume": stop_loss_order_event.volume, 646 "validDays": stop_loss_order_event.valid_days, 647 "priceType": stop_loss_order_event.price_type.value, 648 "shortSellingAllowed": stop_loss_order_event.short_selling_allowed, 649 }, 650 }, 651 )
Place an stop loss order
Args:
parent_stop_loss_id: The id of the parent stop loss order. If this is the first stop loss order, this should be "0".
account_id: A valid account id.
order_book_id: The order book id of the instrument to place the stop loss order for.
stop_loss_trigger: The stop loss trigger type.
stop_loss_order_event: The stop loss order event type.
Returns:
If the order was successfully placed:
{
status: 'SUCCESS',
stoplossOrderId: str
}
If the order was not placed:
{
status: str,
stoplossOrderId: str
}
653 def edit_order( 654 self, 655 order_id: str, 656 account_id: str, 657 price: float, 658 valid_until: date, 659 volume: int, 660 ): 661 """Update an existing order 662 663 Returns: 664 665 { 666 orderRequestStatus: str, 667 message: str, 668 parameters: List[str], 669 orderId: str 670 } 671 """ 672 673 return self.__call( 674 HttpMethod.POST, 675 Route.ORDER_EDIT_PATH.value, 676 { 677 "accountId": account_id, 678 "metadata": {"orderEntryMode": "STANDARD"}, 679 "openVolume": None, 680 "orderId": order_id, 681 "price": price, 682 "validUntil": valid_until.isoformat(), 683 "volume": volume, 684 }, 685 )
Update an existing order
Returns:
{
orderRequestStatus: str,
message: str,
parameters: List[str],
orderId: str
}
687 def get_order(self, account_id: str, order_id: str): 688 """Get an existing order 689 690 Returns: 691 692 { 693 "orderId": str, 694 "orderbookId": str, 695 "side": str, 696 "state": str, 697 "marketReference": str, 698 "price": float, 699 "message": str, 700 "volume": int, 701 "originalVolume": int, 702 "accountId": str, 703 "condition": str, 704 "validUntil": str, 705 "modifiable": bool, 706 "deletable": bool, 707 } 708 """ 709 710 return self.__call( 711 HttpMethod.GET, 712 Route.ORDER_GET_PATH.value.format( 713 order_id, 714 account_id, 715 ), 716 )
Get an existing order
Returns:
{
"orderId": str,
"orderbookId": str,
"side": str,
"state": str,
"marketReference": str,
"price": float,
"message": str,
"volume": int,
"originalVolume": int,
"accountId": str,
"condition": str,
"validUntil": str,
"modifiable": bool,
"deletable": bool,
}
718 def get_all_stop_losses(self): 719 """Get open stop losses 720 721 Returns: 722 723 [{ 724 "id": str, 725 "status": str, 726 "account": { 727 "id": str, 728 "name": str, 729 "type": str, 730 "urlParameterId": str 731 }, 732 "orderbook": { 733 "id": str, 734 "name": str, 735 "countryCode": str, 736 "currency": str, 737 "shortName": str, 738 "type": str 739 }, 740 "hasExcludingChildren": bool, 741 "message": str, 742 "trigger": { 743 "value": int, 744 "type": str, 745 "validUntil": str, 746 "valueType": str 747 }, 748 "order": { 749 "type": str, 750 "price": int, 751 "volume": int, 752 "shortSellingAllowed": bool, 753 "validDays": int, 754 "priceType": str, 755 "priceDecimalPrecision": 0 756 }, 757 "editable": bool, 758 "deletable": bool 759 }] 760 """ 761 return self.__call(HttpMethod.GET, Route.STOP_LOSS_PATH.value)
Get open stop losses
Returns:
[{
"id": str,
"status": str,
"account": {
"id": str,
"name": str,
"type": str,
"urlParameterId": str
},
"orderbook": {
"id": str,
"name": str,
"countryCode": str,
"currency": str,
"shortName": str,
"type": str
},
"hasExcludingChildren": bool,
"message": str,
"trigger": {
"value": int,
"type": str,
"validUntil": str,
"valueType": str
},
"order": {
"type": str,
"price": int,
"volume": int,
"shortSellingAllowed": bool,
"validDays": int,
"priceType": str,
"priceDecimalPrecision": 0
},
"editable": bool,
"deletable": bool
}]
763 def delete_stop_loss_order(self, account_id: str, stop_loss_id: str): 764 """delete a stop loss order 765 766 Args: 767 768 stop_loss_id: The id of the stop loss order to delete. 769 770 account_id: A valid account id. 771 772 Returns: 773 Nothing 774 """ 775 776 return self.__call( 777 HttpMethod.DELETE, 778 Route.ORDER_DELETE_STOP_LOSS_PATH.value.format( 779 account_id, 780 stop_loss_id, 781 ), 782 )
delete a stop loss order
Args:
stop_loss_id: The id of the stop loss order to delete.
account_id: A valid account id.
Returns: Nothing
784 def delete_order(self, account_id: str, order_id: str): 785 """Delete an existing order 786 787 Returns: 788 789 { 790 messages: str, 791 orderId: str, 792 parameters: List[str], 793 orderRequestStatus: str 794 } 795 """ 796 return self.__call( 797 HttpMethod.POST, 798 Route.ORDER_DELETE_PATH.value, 799 {"accountId": account_id, "orderId": order_id}, 800 )
Delete an existing order
Returns:
{
messages: str,
orderId: str,
parameters: List[str],
orderRequestStatus: str
}
802 def get_monthly_savings_by_account_id(self, account_id: str): 803 """Get monthly savings at avanza for specific account 804 805 Returns: 806 807 { 808 'monthlySavings': [{ 809 'account': { 810 'id': str, 811 'name': str, 812 'type': str 813 }, 814 'amount': float, 815 'cash': {'amount': float, 'percent': float}, 816 'externalAccount': { 817 'accountNumber': str, 818 'bankName': str, 819 'clearingNumber': str 820 }, 821 'fundDistributions': [{ 822 'amount': float, 823 'orderbook': { 824 'buyable': bool, 825 'id': str, 826 'name': str 827 }, 828 'percent': float 829 }, 830 'id': str, 831 'name': str, 832 'purchaseDay': int, 833 'status': str, 834 'transferDay': int 835 }], 836 'totalAmount': float 837 } 838 """ 839 840 return self.__call( 841 HttpMethod.GET, Route.MONTHLY_SAVINGS_PATH.value.format(account_id) 842 )
Get monthly savings at avanza for specific account
Returns:
{
'monthlySavings': [{
'account': {
'id': str,
'name': str,
'type': str
},
'amount': float,
'cash': {'amount': float, 'percent': float},
'externalAccount': {
'accountNumber': str,
'bankName': str,
'clearingNumber': str
},
'fundDistributions': [{
'amount': float,
'orderbook': {
'buyable': bool,
'id': str,
'name': str
},
'percent': float
},
'id': str,
'name': str,
'purchaseDay': int,
'status': str,
'transferDay': int
}],
'totalAmount': float
}
844 def get_all_monthly_savings(self): 845 """Get your monthly savings at Avanza 846 847 Returns: 848 849 { 850 'monthlySavings': [{ 851 'account': { 852 'id': str, 853 'name': str, 854 'type': str 855 }, 856 'amount': float, 857 'cash': {'amount': float, 'percent': float}, 858 'externalAccount': { 859 'accountNumber': str, 860 'bankName': str, 861 'clearingNumber': str 862 }, 863 'fundDistributions': [{ 864 'amount': float, 865 'orderbook': { 866 'buyable': bool, 867 'id': str, 868 'name': str 869 }, 870 'percent': float 871 }, 872 'id': str, 873 'name': str, 874 'purchaseDay': int, 875 'status': str, 876 'transferDay': int 877 }], 878 'totalAmount': float 879 } 880 """ 881 882 return self.__call(HttpMethod.GET, Route.MONTHLY_SAVINGS_PATH.value.format(""))
Get your monthly savings at Avanza
Returns:
{
'monthlySavings': [{
'account': {
'id': str,
'name': str,
'type': str
},
'amount': float,
'cash': {'amount': float, 'percent': float},
'externalAccount': {
'accountNumber': str,
'bankName': str,
'clearingNumber': str
},
'fundDistributions': [{
'amount': float,
'orderbook': {
'buyable': bool,
'id': str,
'name': str
},
'percent': float
},
'id': str,
'name': str,
'purchaseDay': int,
'status': str,
'transferDay': int
}],
'totalAmount': float
}
884 def pause_monthly_saving(self, account_id: str, monthly_savings_id: str): 885 """Pause an active monthly saving 886 887 Returns: 888 'OK' 889 890 """ 891 892 return self.__call( 893 HttpMethod.PUT, 894 Route.MONTHLY_SAVINGS_PAUSE_PATH.value.format( 895 account_id, monthly_savings_id 896 ), 897 )
Pause an active monthly saving
Returns: 'OK'
899 def resume_monthly_saving(self, account_id: str, monthly_savings_id: str): 900 """Resume a paused monthly saving 901 902 Returns: 903 'OK' 904 905 """ 906 907 return self.__call( 908 HttpMethod.PUT, 909 Route.MONTHLY_SAVINGS_RESUME_PATH.value.format( 910 account_id, monthly_savings_id 911 ), 912 )
Resume a paused monthly saving
Returns: 'OK'
914 def delete_monthly_saving(self, account_id: str, monthly_savings_id: str) -> None: 915 """Deletes a monthly saving 916 917 Returns: 918 None 919 920 """ 921 922 return self.__call( 923 HttpMethod.DELETE, 924 Route.MONTHLY_SAVINGS_REMOVE_PATH.value.format( 925 account_id, monthly_savings_id 926 ), 927 )
Deletes a monthly saving
Returns: None
929 def create_monthly_saving( 930 self, 931 account_id: str, 932 amount: int, 933 transfer_day_of_month: int, 934 purchase_day_of_month: int, 935 clearing_and_account_number: str, 936 fund_distribution: Dict[str, int], 937 ): 938 """Create a monthly saving at Avanza 939 940 Args: 941 942 account_id: The Avanza account to which the withdrawn money should be transferred to 943 944 amount: minimum amount 100 (SEK) 945 the amount that should be withdrawn from the external account every month 946 947 transfer_day_of_month: valid range (1-31) 948 when the money should be withdrawn from the external account 949 950 purchase_day_of_month: valid range (1-31) 951 when the funds should be purchased, 952 must occur after the transfer_day_of_month 953 954 clearing_and_account_number: The external account from which the money for the monthly savings should be withdrawn from, 955 has to be formatted as follows 'XXXX-XXXXXXXXX' 956 957 fund_distrubution: the percentage distribution of the funds 958 The key is the funds id and the value is the distribution of the amount in a whole percentage 959 The sum of the percentages has to total 100 960 961 Examples: 962 {'41567': 100} 963 {'41567': 50, '878733': 50} 964 {'41567': 25, '878733': 75} 965 966 Returns: 967 968 { 969 'monthlySavingId': str, 970 'status': str 971 } 972 973 monthlySavingId has the following format: 'XX^XXXXXXXXXXXXX^XXXXXX' 974 status should have the value 'ACCEPTED' if the monthly saving was created successfully 975 """ 976 977 if not 1 <= transfer_day_of_month <= 31: 978 raise ValueError( 979 "transfer_day_of_month is outside the valid range of (1-31)" 980 ) 981 982 if not 1 <= purchase_day_of_month <= 31: 983 raise ValueError( 984 "purchase_day_of_month is outside the valid range of (1-31)" 985 ) 986 987 if transfer_day_of_month >= purchase_day_of_month: 988 raise ValueError( 989 "transfer_day_of_month must occur before purchase_day_of_month" 990 ) 991 992 if len(fund_distribution) == 0: 993 raise ValueError("No founds were specified in the fund_distribution") 994 995 if sum(fund_distribution.values()) != 100: 996 raise ValueError("The fund_distribution values must total 100") 997 998 return self.__call( 999 HttpMethod.POST, 1000 Route.MONTHLY_SAVINGS_CREATE_PATH.value.format(account_id), 1001 { 1002 "amount": amount, 1003 "autogiro": { 1004 "dayOfMonth": transfer_day_of_month, 1005 "externalClearingAndAccount": clearing_and_account_number, 1006 }, 1007 "fundDistribution": { 1008 "dayOfMonth": purchase_day_of_month, 1009 "fundDistributions": fund_distribution, 1010 }, 1011 }, 1012 )
Create a monthly saving at Avanza
Args:
account_id: The Avanza account to which the withdrawn money should be transferred to
amount: minimum amount 100 (SEK)
the amount that should be withdrawn from the external account every month
transfer_day_of_month: valid range (1-31)
when the money should be withdrawn from the external account
purchase_day_of_month: valid range (1-31)
when the funds should be purchased,
must occur after the transfer_day_of_month
clearing_and_account_number: The external account from which the money for the monthly savings should be withdrawn from,
has to be formatted as follows 'XXXX-XXXXXXXXX'
fund_distrubution: the percentage distribution of the funds
The key is the funds id and the value is the distribution of the amount in a whole percentage
The sum of the percentages has to total 100
Examples:
{'41567': 100}
{'41567': 50, '878733': 50}
{'41567': 25, '878733': 75}
Returns:
{
'monthlySavingId': str,
'status': str
}
monthlySavingId has the following format: 'XX^XXXXXXXXXXXXX^XXXXXX'
status should have the value 'ACCEPTED' if the monthly saving was created successfully
1014 def get_transactions_details( 1015 self, 1016 transaction_details_types: Optional[Sequence[TransactionsDetailsType]] = [], 1017 transactions_from: Optional[date] = None, 1018 transactions_to: Optional[date] = None, 1019 isin: Optional[str] = None, 1020 max_elements: Optional[int] = 1000, 1021 ) -> Transactions: 1022 """Get transactions, optionally apply criteria. 1023 1024 Args: 1025 1026 transaction_types: One or more transaction types. 1027 1028 transactions_from: Fetch transactions from this date. 1029 1030 transactions_to: Fetch transactions to this date. 1031 1032 isin: Only fetch transactions for specified isin. 1033 1034 max_elements: Limit result to N transactions. 1035 """ 1036 options = {} 1037 options["maxElements"] = max_elements 1038 1039 if transaction_details_types: 1040 options["transactionTypes"] = ",".join( 1041 [type.value for type in transaction_details_types] 1042 ) 1043 if transactions_from: 1044 options["from"] = transactions_from.isoformat() 1045 if transactions_to: 1046 options["to"] = transactions_to.isoformat() 1047 if isin: 1048 options["isin"] = isin 1049 1050 return self.__call( 1051 HttpMethod.GET, Route.TRANSACTIONS_DETAILS_PATH.value, options 1052 )
Get transactions, optionally apply criteria.
Args:
transaction_types: One or more transaction types.
transactions_from: Fetch transactions from this date.
transactions_to: Fetch transactions to this date.
isin: Only fetch transactions for specified isin.
max_elements: Limit result to N transactions.
1061 def set_price_alert( 1062 self, 1063 order_book_id: str, 1064 price: float, 1065 valid_until: date, 1066 notification: bool = True, 1067 email: bool = False, 1068 sms: bool = False, 1069 ): 1070 """ 1071 Sets a price alert for the specified orderbook and returns all the existing alerts. 1072 1073 Returns: 1074 1075 [ 1076 { 1077 'alertId': str, 1078 'accountId': str, 1079 'price': float, 1080 'validUntil': str, 1081 'direction': str, 1082 'email': bool, 1083 'notification': bool, 1084 'sms': bool, 1085 } 1086 ] 1087 """ 1088 1089 return self.__call( 1090 HttpMethod.POST, 1091 Route.PRICE_ALERT_PATH.value.format(order_book_id), 1092 { 1093 "price": price, 1094 "validUntil": valid_until.isoformat(), 1095 "notification": notification, 1096 "email": email, 1097 "sms": sms, 1098 }, 1099 )
Sets a price alert for the specified orderbook and returns all the existing alerts.
Returns:
[
{
'alertId': str,
'accountId': str,
'price': float,
'validUntil': str,
'direction': str,
'email': bool,
'notification': bool,
'sms': bool,
}
]
1101 def get_price_alert(self, order_book_id: str) -> List[PriceAlert]: 1102 """Gets all the price alerts for the specified orderbook""" 1103 1104 return self.__call( 1105 HttpMethod.GET, 1106 Route.PRICE_ALERT_PATH.value.format(order_book_id), 1107 )
Gets all the price alerts for the specified orderbook
1109 def delete_price_alert(self, order_book_id: str, alert_id: str): 1110 """ 1111 Deletes a price alert from the specified orderbook and returns the remaining alerts. 1112 1113 Returns: 1114 1115 [ 1116 { 1117 'alertId': str, 1118 'accountId': str, 1119 'price': float, 1120 'validUntil': str, 1121 'direction': str, 1122 'email': bool, 1123 'notification': bool, 1124 'sms': bool, 1125 } 1126 ] 1127 """ 1128 return self.__call( 1129 HttpMethod.DELETE, 1130 Route.PRICE_ALERT_PATH.value.format(order_book_id, alert_id) 1131 + f"/{alert_id}", 1132 )
Deletes a price alert from the specified orderbook and returns the remaining alerts.
Returns:
[
{
'alertId': str,
'accountId': str,
'price': float,
'validUntil': str,
'direction': str,
'email': bool,
'notification': bool,
'sms': bool,
}
]