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_market_data(self, order_book_id: str)-> MarketData: 450 """Marketdata includes current order depth and quote.""" 451 return self.__call( 452 HttpMethod.GET, 453 Route.MARKETDATA_PATH.value.format(order_book_id) 454 ) 455 456 def get_insights_report( 457 self, account_ids: list[str], time_period: InsightsReportTimePeriod 458 ) -> InsightsReport: 459 """Get report about the development of your owned positions during the specified timeperiod""" 460 return self.__call( 461 HttpMethod.POST, 462 Route.INSIGHTS_PATH.value, 463 options={"timePeriod": time_period.value, "accountIds": account_ids}, 464 ) 465 466 def get_deals(self): 467 """Get currently active deals""" 468 return self.__call(HttpMethod.GET, Route.DEALS_PATH.value) 469 470 def get_orders(self): 471 """Get currently active orders""" 472 return self.__call(HttpMethod.GET, Route.ORDERS_PATH.value) 473 474 def get_inspiration_lists(self) -> List[InspirationListItem]: 475 """Get all available inspiration lists 476 477 This returns lists similar to the ones found on: 478 479 https://www.avanza.se/aktier/aktieinspiration.html 480 481 https://www.avanza.se/fonder/fondinspiration.html 482 483 """ 484 return self.__call(HttpMethod.GET, Route.INSPIRATION_LIST_PATH.value.format("")) 485 486 def get_inspiration_list(self, list_id: Union[ListType, str]) -> InspirationList: 487 """Get inspiration list 488 489 Some lists have an id of an enum value described in ListType, but they can also just have a string id. 490 An example is hhSK8W1o which corresponds to "Most owned stocks", which isn't described in the ListType enum 491 492 """ 493 494 id = list_id.value if isinstance(list_id, ListType) else list_id 495 496 return self.__call(HttpMethod.GET, Route.INSPIRATION_LIST_PATH.value.format(id)) 497 498 def get_chart_data( 499 self, 500 order_book_id: str, 501 period: TimePeriod, 502 resolution: Optional[Resolution] = None, 503 ) -> ChartData: 504 """Return chart data for an order book for the specified time period with given resolution""" 505 options = {"timePeriod": period.value.lower()} 506 507 if resolution is not None: 508 options["resolution"] = resolution.value.lower() 509 510 return self.__call( 511 HttpMethod.GET, Route.CHARTDATA_PATH.value.format(order_book_id), options 512 ) 513 514 def place_order( 515 self, 516 account_id: str, 517 order_book_id: str, 518 order_type: OrderType, 519 price: float, 520 valid_until: date, 521 volume: int, 522 condition: Condition = Condition.NORMAL, 523 ): 524 """Place an order 525 526 Returns: 527 528 If the order was successfully placed: 529 530 { 531 message: str, 532 orderId: str, 533 orderRequestStatus: 'SUCCESS' 534 } 535 536 If the order was not placed: 537 538 { 539 message: str, 540 orderRequestStatus: 'ERROR' 541 } 542 """ 543 544 return self.__call( 545 HttpMethod.POST, 546 Route.ORDER_PLACE_PATH.value, 547 { 548 "accountId": account_id, 549 "orderbookId": order_book_id, 550 "side": order_type.value, 551 "condition": condition.value, 552 "price": price, 553 "validUntil": valid_until.isoformat(), 554 "volume": volume, 555 }, 556 ) 557 558 def place_order_buy_fund(self, account_id: str, order_book_id: str, amount: float): 559 """Place a buy order for a fund 560 561 Returns: 562 563 { 564 message: str, 565 orderId: str, 566 accountId: str, 567 orderRequestStatus: str 568 } 569 """ 570 571 return self.__call( 572 HttpMethod.POST, 573 Route.ORDER_PLACE_PATH_BUY_FUND.value, 574 {"orderbookId": order_book_id, "accountId": account_id, "amount": amount}, 575 ) 576 577 def place_order_sell_fund(self, account_id: str, order_book_id: str, volume: float): 578 """Place a sell order for a fund 579 580 Returns: 581 582 { 583 message: str, 584 orderId: str, 585 accountId: str, 586 orderRequestStatus: str 587 } 588 """ 589 590 return self.__call( 591 HttpMethod.POST, 592 Route.ORDER_PLACE_PATH_SELL_FUND.value, 593 {"orderbookId": order_book_id, "accountId": account_id, "volume": volume}, 594 ) 595 596 def place_stop_loss_order( 597 self, 598 parent_stop_loss_id: str, 599 account_id: str, 600 order_book_id: str, 601 stop_loss_trigger: StopLossTrigger, 602 stop_loss_order_event: StopLossOrderEvent, 603 ): 604 """Place an stop loss order 605 606 Args: 607 608 parent_stop_loss_id: The id of the parent stop loss order. If this is the first stop loss order, this should be "0". 609 610 account_id: A valid account id. 611 612 order_book_id: The order book id of the instrument to place the stop loss order for. 613 614 stop_loss_trigger: The stop loss trigger type. 615 616 stop_loss_order_event: The stop loss order event type. 617 618 Returns: 619 620 If the order was successfully placed: 621 622 { 623 status: 'SUCCESS', 624 stoplossOrderId: str 625 } 626 627 If the order was not placed: 628 629 { 630 status: str, 631 stoplossOrderId: str 632 } 633 """ 634 635 return self.__call( 636 HttpMethod.POST, 637 Route.ORDER_PLACE_STOP_LOSS_PATH.value, 638 { 639 "parentStopLossId": parent_stop_loss_id, 640 "accountId": account_id, 641 "orderBookId": order_book_id, 642 "stopLossTrigger": { 643 "type": stop_loss_trigger.type.value, 644 "value": stop_loss_trigger.value, 645 "validUntil": stop_loss_trigger.valid_until.isoformat(), 646 "valueType": stop_loss_trigger.value_type.value, 647 "triggerOnMarketMakerQuote": stop_loss_trigger.trigger_on_market_maker_quote, 648 }, 649 "stopLossOrderEvent": { 650 "type": stop_loss_order_event.type.value, 651 "price": stop_loss_order_event.price, 652 "volume": stop_loss_order_event.volume, 653 "validDays": stop_loss_order_event.valid_days, 654 "priceType": stop_loss_order_event.price_type.value, 655 "shortSellingAllowed": stop_loss_order_event.short_selling_allowed, 656 }, 657 }, 658 ) 659 660 def edit_order( 661 self, 662 order_id: str, 663 account_id: str, 664 price: float, 665 valid_until: date, 666 volume: int, 667 ): 668 """Update an existing order 669 670 Returns: 671 672 { 673 orderRequestStatus: str, 674 message: str, 675 parameters: List[str], 676 orderId: str 677 } 678 """ 679 680 return self.__call( 681 HttpMethod.POST, 682 Route.ORDER_EDIT_PATH.value, 683 { 684 "accountId": account_id, 685 "metadata": {"orderEntryMode": "STANDARD"}, 686 "openVolume": None, 687 "orderId": order_id, 688 "price": price, 689 "validUntil": valid_until.isoformat(), 690 "volume": volume, 691 }, 692 ) 693 694 def get_order(self, account_id: str, order_id: str): 695 """Get an existing order 696 697 Returns: 698 699 { 700 "orderId": str, 701 "orderbookId": str, 702 "side": str, 703 "state": str, 704 "marketReference": str, 705 "price": float, 706 "message": str, 707 "volume": int, 708 "originalVolume": int, 709 "accountId": str, 710 "condition": str, 711 "validUntil": str, 712 "modifiable": bool, 713 "deletable": bool, 714 } 715 """ 716 717 return self.__call( 718 HttpMethod.GET, 719 Route.ORDER_GET_PATH.value.format( 720 order_id, 721 account_id, 722 ), 723 ) 724 725 def get_all_stop_losses(self): 726 """Get open stop losses 727 728 Returns: 729 730 [{ 731 "id": str, 732 "status": str, 733 "account": { 734 "id": str, 735 "name": str, 736 "type": str, 737 "urlParameterId": str 738 }, 739 "orderbook": { 740 "id": str, 741 "name": str, 742 "countryCode": str, 743 "currency": str, 744 "shortName": str, 745 "type": str 746 }, 747 "hasExcludingChildren": bool, 748 "message": str, 749 "trigger": { 750 "value": int, 751 "type": str, 752 "validUntil": str, 753 "valueType": str 754 }, 755 "order": { 756 "type": str, 757 "price": int, 758 "volume": int, 759 "shortSellingAllowed": bool, 760 "validDays": int, 761 "priceType": str, 762 "priceDecimalPrecision": 0 763 }, 764 "editable": bool, 765 "deletable": bool 766 }] 767 """ 768 return self.__call(HttpMethod.GET, Route.STOP_LOSS_PATH.value) 769 770 def delete_stop_loss_order(self, account_id: str, stop_loss_id: str): 771 """delete a stop loss order 772 773 Args: 774 775 stop_loss_id: The id of the stop loss order to delete. 776 777 account_id: A valid account id. 778 779 Returns: 780 Nothing 781 """ 782 783 return self.__call( 784 HttpMethod.DELETE, 785 Route.ORDER_DELETE_STOP_LOSS_PATH.value.format( 786 account_id, 787 stop_loss_id, 788 ), 789 ) 790 791 def delete_order(self, account_id: str, order_id: str): 792 """Delete an existing order 793 794 Returns: 795 796 { 797 messages: str, 798 orderId: str, 799 parameters: List[str], 800 orderRequestStatus: str 801 } 802 """ 803 return self.__call( 804 HttpMethod.POST, 805 Route.ORDER_DELETE_PATH.value, 806 {"accountId": account_id, "orderId": order_id}, 807 ) 808 809 def get_monthly_savings_by_account_id(self, account_id: str): 810 """Get monthly savings at avanza for specific account 811 812 Returns: 813 814 { 815 'monthlySavings': [{ 816 'account': { 817 'id': str, 818 'name': str, 819 'type': str 820 }, 821 'amount': float, 822 'cash': {'amount': float, 'percent': float}, 823 'externalAccount': { 824 'accountNumber': str, 825 'bankName': str, 826 'clearingNumber': str 827 }, 828 'fundDistributions': [{ 829 'amount': float, 830 'orderbook': { 831 'buyable': bool, 832 'id': str, 833 'name': str 834 }, 835 'percent': float 836 }, 837 'id': str, 838 'name': str, 839 'purchaseDay': int, 840 'status': str, 841 'transferDay': int 842 }], 843 'totalAmount': float 844 } 845 """ 846 847 return self.__call( 848 HttpMethod.GET, Route.MONTHLY_SAVINGS_PATH.value.format(account_id) 849 ) 850 851 def get_all_monthly_savings(self): 852 """Get your monthly savings at Avanza 853 854 Returns: 855 856 { 857 'monthlySavings': [{ 858 'account': { 859 'id': str, 860 'name': str, 861 'type': str 862 }, 863 'amount': float, 864 'cash': {'amount': float, 'percent': float}, 865 'externalAccount': { 866 'accountNumber': str, 867 'bankName': str, 868 'clearingNumber': str 869 }, 870 'fundDistributions': [{ 871 'amount': float, 872 'orderbook': { 873 'buyable': bool, 874 'id': str, 875 'name': str 876 }, 877 'percent': float 878 }, 879 'id': str, 880 'name': str, 881 'purchaseDay': int, 882 'status': str, 883 'transferDay': int 884 }], 885 'totalAmount': float 886 } 887 """ 888 889 return self.__call(HttpMethod.GET, Route.MONTHLY_SAVINGS_PATH.value.format("")) 890 891 def pause_monthly_saving(self, account_id: str, monthly_savings_id: str): 892 """Pause an active monthly saving 893 894 Returns: 895 'OK' 896 897 """ 898 899 return self.__call( 900 HttpMethod.PUT, 901 Route.MONTHLY_SAVINGS_PAUSE_PATH.value.format( 902 account_id, monthly_savings_id 903 ), 904 ) 905 906 def resume_monthly_saving(self, account_id: str, monthly_savings_id: str): 907 """Resume a paused monthly saving 908 909 Returns: 910 'OK' 911 912 """ 913 914 return self.__call( 915 HttpMethod.PUT, 916 Route.MONTHLY_SAVINGS_RESUME_PATH.value.format( 917 account_id, monthly_savings_id 918 ), 919 ) 920 921 def delete_monthly_saving(self, account_id: str, monthly_savings_id: str) -> None: 922 """Deletes a monthly saving 923 924 Returns: 925 None 926 927 """ 928 929 return self.__call( 930 HttpMethod.DELETE, 931 Route.MONTHLY_SAVINGS_REMOVE_PATH.value.format( 932 account_id, monthly_savings_id 933 ), 934 ) 935 936 def create_monthly_saving( 937 self, 938 account_id: str, 939 amount: int, 940 transfer_day_of_month: int, 941 purchase_day_of_month: int, 942 clearing_and_account_number: str, 943 fund_distribution: Dict[str, int], 944 ): 945 """Create a monthly saving at Avanza 946 947 Args: 948 949 account_id: The Avanza account to which the withdrawn money should be transferred to 950 951 amount: minimum amount 100 (SEK) 952 the amount that should be withdrawn from the external account every month 953 954 transfer_day_of_month: valid range (1-31) 955 when the money should be withdrawn from the external account 956 957 purchase_day_of_month: valid range (1-31) 958 when the funds should be purchased, 959 must occur after the transfer_day_of_month 960 961 clearing_and_account_number: The external account from which the money for the monthly savings should be withdrawn from, 962 has to be formatted as follows 'XXXX-XXXXXXXXX' 963 964 fund_distrubution: the percentage distribution of the funds 965 The key is the funds id and the value is the distribution of the amount in a whole percentage 966 The sum of the percentages has to total 100 967 968 Examples: 969 {'41567': 100} 970 {'41567': 50, '878733': 50} 971 {'41567': 25, '878733': 75} 972 973 Returns: 974 975 { 976 'monthlySavingId': str, 977 'status': str 978 } 979 980 monthlySavingId has the following format: 'XX^XXXXXXXXXXXXX^XXXXXX' 981 status should have the value 'ACCEPTED' if the monthly saving was created successfully 982 """ 983 984 if not 1 <= transfer_day_of_month <= 31: 985 raise ValueError( 986 "transfer_day_of_month is outside the valid range of (1-31)" 987 ) 988 989 if not 1 <= purchase_day_of_month <= 31: 990 raise ValueError( 991 "purchase_day_of_month is outside the valid range of (1-31)" 992 ) 993 994 if transfer_day_of_month >= purchase_day_of_month: 995 raise ValueError( 996 "transfer_day_of_month must occur before purchase_day_of_month" 997 ) 998 999 if len(fund_distribution) == 0: 1000 raise ValueError("No founds were specified in the fund_distribution") 1001 1002 if sum(fund_distribution.values()) != 100: 1003 raise ValueError("The fund_distribution values must total 100") 1004 1005 return self.__call( 1006 HttpMethod.POST, 1007 Route.MONTHLY_SAVINGS_CREATE_PATH.value.format(account_id), 1008 { 1009 "amount": amount, 1010 "autogiro": { 1011 "dayOfMonth": transfer_day_of_month, 1012 "externalClearingAndAccount": clearing_and_account_number, 1013 }, 1014 "fundDistribution": { 1015 "dayOfMonth": purchase_day_of_month, 1016 "fundDistributions": fund_distribution, 1017 }, 1018 }, 1019 ) 1020 1021 def get_transactions_details( 1022 self, 1023 transaction_details_types: Optional[Sequence[TransactionsDetailsType]] = [], 1024 transactions_from: Optional[date] = None, 1025 transactions_to: Optional[date] = None, 1026 isin: Optional[str] = None, 1027 max_elements: Optional[int] = 1000, 1028 ) -> Transactions: 1029 """Get transactions, optionally apply criteria. 1030 1031 Args: 1032 1033 transaction_types: One or more transaction types. 1034 1035 transactions_from: Fetch transactions from this date. 1036 1037 transactions_to: Fetch transactions to this date. 1038 1039 isin: Only fetch transactions for specified isin. 1040 1041 max_elements: Limit result to N transactions. 1042 """ 1043 options = {} 1044 options["maxElements"] = max_elements 1045 1046 if transaction_details_types: 1047 options["transactionTypes"] = ",".join( 1048 [type.value for type in transaction_details_types] 1049 ) 1050 if transactions_from: 1051 options["from"] = transactions_from.isoformat() 1052 if transactions_to: 1053 options["to"] = transactions_to.isoformat() 1054 if isin: 1055 options["isin"] = isin 1056 1057 return self.__call( 1058 HttpMethod.GET, Route.TRANSACTIONS_DETAILS_PATH.value, options 1059 ) 1060 1061 def get_note_as_pdf(self, url_parameter_id: str, note_id: str): 1062 return self.__call( 1063 HttpMethod.GET, 1064 Route.NOTE_PATH.value.format(url_parameter_id, note_id), 1065 return_content=True, 1066 ) 1067 1068 def set_price_alert( 1069 self, 1070 order_book_id: str, 1071 price: float, 1072 valid_until: date, 1073 notification: bool = True, 1074 email: bool = False, 1075 sms: bool = False, 1076 ): 1077 """ 1078 Sets a price alert for the specified orderbook and returns all the existing alerts. 1079 1080 Returns: 1081 1082 [ 1083 { 1084 'alertId': str, 1085 'accountId': str, 1086 'price': float, 1087 'validUntil': str, 1088 'direction': str, 1089 'email': bool, 1090 'notification': bool, 1091 'sms': bool, 1092 } 1093 ] 1094 """ 1095 1096 return self.__call( 1097 HttpMethod.POST, 1098 Route.PRICE_ALERT_PATH.value.format(order_book_id), 1099 { 1100 "price": price, 1101 "validUntil": valid_until.isoformat(), 1102 "notification": notification, 1103 "email": email, 1104 "sms": sms, 1105 }, 1106 ) 1107 1108 def get_price_alert(self, order_book_id: str) -> List[PriceAlert]: 1109 """Gets all the price alerts for the specified orderbook""" 1110 1111 return self.__call( 1112 HttpMethod.GET, 1113 Route.PRICE_ALERT_PATH.value.format(order_book_id), 1114 ) 1115 1116 def delete_price_alert(self, order_book_id: str, alert_id: str): 1117 """ 1118 Deletes a price alert from the specified orderbook and returns the remaining alerts. 1119 1120 Returns: 1121 1122 [ 1123 { 1124 'alertId': str, 1125 'accountId': str, 1126 'price': float, 1127 'validUntil': str, 1128 'direction': str, 1129 'email': bool, 1130 'notification': bool, 1131 'sms': bool, 1132 } 1133 ] 1134 """ 1135 return self.__call( 1136 HttpMethod.DELETE, 1137 Route.PRICE_ALERT_PATH.value.format(order_book_id, alert_id) 1138 + f"/{alert_id}", 1139 ) 1140 1141 def get_offers(self) -> List[Offer]: 1142 """Return current offers""" 1143 1144 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_market_data(self, order_book_id: str)-> MarketData: 451 """Marketdata includes current order depth and quote.""" 452 return self.__call( 453 HttpMethod.GET, 454 Route.MARKETDATA_PATH.value.format(order_book_id) 455 ) 456 457 def get_insights_report( 458 self, account_ids: list[str], time_period: InsightsReportTimePeriod 459 ) -> InsightsReport: 460 """Get report about the development of your owned positions during the specified timeperiod""" 461 return self.__call( 462 HttpMethod.POST, 463 Route.INSIGHTS_PATH.value, 464 options={"timePeriod": time_period.value, "accountIds": account_ids}, 465 ) 466 467 def get_deals(self): 468 """Get currently active deals""" 469 return self.__call(HttpMethod.GET, Route.DEALS_PATH.value) 470 471 def get_orders(self): 472 """Get currently active orders""" 473 return self.__call(HttpMethod.GET, Route.ORDERS_PATH.value) 474 475 def get_inspiration_lists(self) -> List[InspirationListItem]: 476 """Get all available inspiration lists 477 478 This returns lists similar to the ones found on: 479 480 https://www.avanza.se/aktier/aktieinspiration.html 481 482 https://www.avanza.se/fonder/fondinspiration.html 483 484 """ 485 return self.__call(HttpMethod.GET, Route.INSPIRATION_LIST_PATH.value.format("")) 486 487 def get_inspiration_list(self, list_id: Union[ListType, str]) -> InspirationList: 488 """Get inspiration list 489 490 Some lists have an id of an enum value described in ListType, but they can also just have a string id. 491 An example is hhSK8W1o which corresponds to "Most owned stocks", which isn't described in the ListType enum 492 493 """ 494 495 id = list_id.value if isinstance(list_id, ListType) else list_id 496 497 return self.__call(HttpMethod.GET, Route.INSPIRATION_LIST_PATH.value.format(id)) 498 499 def get_chart_data( 500 self, 501 order_book_id: str, 502 period: TimePeriod, 503 resolution: Optional[Resolution] = None, 504 ) -> ChartData: 505 """Return chart data for an order book for the specified time period with given resolution""" 506 options = {"timePeriod": period.value.lower()} 507 508 if resolution is not None: 509 options["resolution"] = resolution.value.lower() 510 511 return self.__call( 512 HttpMethod.GET, Route.CHARTDATA_PATH.value.format(order_book_id), options 513 ) 514 515 def place_order( 516 self, 517 account_id: str, 518 order_book_id: str, 519 order_type: OrderType, 520 price: float, 521 valid_until: date, 522 volume: int, 523 condition: Condition = Condition.NORMAL, 524 ): 525 """Place an order 526 527 Returns: 528 529 If the order was successfully placed: 530 531 { 532 message: str, 533 orderId: str, 534 orderRequestStatus: 'SUCCESS' 535 } 536 537 If the order was not placed: 538 539 { 540 message: str, 541 orderRequestStatus: 'ERROR' 542 } 543 """ 544 545 return self.__call( 546 HttpMethod.POST, 547 Route.ORDER_PLACE_PATH.value, 548 { 549 "accountId": account_id, 550 "orderbookId": order_book_id, 551 "side": order_type.value, 552 "condition": condition.value, 553 "price": price, 554 "validUntil": valid_until.isoformat(), 555 "volume": volume, 556 }, 557 ) 558 559 def place_order_buy_fund(self, account_id: str, order_book_id: str, amount: float): 560 """Place a buy order for a fund 561 562 Returns: 563 564 { 565 message: str, 566 orderId: str, 567 accountId: str, 568 orderRequestStatus: str 569 } 570 """ 571 572 return self.__call( 573 HttpMethod.POST, 574 Route.ORDER_PLACE_PATH_BUY_FUND.value, 575 {"orderbookId": order_book_id, "accountId": account_id, "amount": amount}, 576 ) 577 578 def place_order_sell_fund(self, account_id: str, order_book_id: str, volume: float): 579 """Place a sell order for a fund 580 581 Returns: 582 583 { 584 message: str, 585 orderId: str, 586 accountId: str, 587 orderRequestStatus: str 588 } 589 """ 590 591 return self.__call( 592 HttpMethod.POST, 593 Route.ORDER_PLACE_PATH_SELL_FUND.value, 594 {"orderbookId": order_book_id, "accountId": account_id, "volume": volume}, 595 ) 596 597 def place_stop_loss_order( 598 self, 599 parent_stop_loss_id: str, 600 account_id: str, 601 order_book_id: str, 602 stop_loss_trigger: StopLossTrigger, 603 stop_loss_order_event: StopLossOrderEvent, 604 ): 605 """Place an stop loss order 606 607 Args: 608 609 parent_stop_loss_id: The id of the parent stop loss order. If this is the first stop loss order, this should be "0". 610 611 account_id: A valid account id. 612 613 order_book_id: The order book id of the instrument to place the stop loss order for. 614 615 stop_loss_trigger: The stop loss trigger type. 616 617 stop_loss_order_event: The stop loss order event type. 618 619 Returns: 620 621 If the order was successfully placed: 622 623 { 624 status: 'SUCCESS', 625 stoplossOrderId: str 626 } 627 628 If the order was not placed: 629 630 { 631 status: str, 632 stoplossOrderId: str 633 } 634 """ 635 636 return self.__call( 637 HttpMethod.POST, 638 Route.ORDER_PLACE_STOP_LOSS_PATH.value, 639 { 640 "parentStopLossId": parent_stop_loss_id, 641 "accountId": account_id, 642 "orderBookId": order_book_id, 643 "stopLossTrigger": { 644 "type": stop_loss_trigger.type.value, 645 "value": stop_loss_trigger.value, 646 "validUntil": stop_loss_trigger.valid_until.isoformat(), 647 "valueType": stop_loss_trigger.value_type.value, 648 "triggerOnMarketMakerQuote": stop_loss_trigger.trigger_on_market_maker_quote, 649 }, 650 "stopLossOrderEvent": { 651 "type": stop_loss_order_event.type.value, 652 "price": stop_loss_order_event.price, 653 "volume": stop_loss_order_event.volume, 654 "validDays": stop_loss_order_event.valid_days, 655 "priceType": stop_loss_order_event.price_type.value, 656 "shortSellingAllowed": stop_loss_order_event.short_selling_allowed, 657 }, 658 }, 659 ) 660 661 def edit_order( 662 self, 663 order_id: str, 664 account_id: str, 665 price: float, 666 valid_until: date, 667 volume: int, 668 ): 669 """Update an existing order 670 671 Returns: 672 673 { 674 orderRequestStatus: str, 675 message: str, 676 parameters: List[str], 677 orderId: str 678 } 679 """ 680 681 return self.__call( 682 HttpMethod.POST, 683 Route.ORDER_EDIT_PATH.value, 684 { 685 "accountId": account_id, 686 "metadata": {"orderEntryMode": "STANDARD"}, 687 "openVolume": None, 688 "orderId": order_id, 689 "price": price, 690 "validUntil": valid_until.isoformat(), 691 "volume": volume, 692 }, 693 ) 694 695 def get_order(self, account_id: str, order_id: str): 696 """Get an existing order 697 698 Returns: 699 700 { 701 "orderId": str, 702 "orderbookId": str, 703 "side": str, 704 "state": str, 705 "marketReference": str, 706 "price": float, 707 "message": str, 708 "volume": int, 709 "originalVolume": int, 710 "accountId": str, 711 "condition": str, 712 "validUntil": str, 713 "modifiable": bool, 714 "deletable": bool, 715 } 716 """ 717 718 return self.__call( 719 HttpMethod.GET, 720 Route.ORDER_GET_PATH.value.format( 721 order_id, 722 account_id, 723 ), 724 ) 725 726 def get_all_stop_losses(self): 727 """Get open stop losses 728 729 Returns: 730 731 [{ 732 "id": str, 733 "status": str, 734 "account": { 735 "id": str, 736 "name": str, 737 "type": str, 738 "urlParameterId": str 739 }, 740 "orderbook": { 741 "id": str, 742 "name": str, 743 "countryCode": str, 744 "currency": str, 745 "shortName": str, 746 "type": str 747 }, 748 "hasExcludingChildren": bool, 749 "message": str, 750 "trigger": { 751 "value": int, 752 "type": str, 753 "validUntil": str, 754 "valueType": str 755 }, 756 "order": { 757 "type": str, 758 "price": int, 759 "volume": int, 760 "shortSellingAllowed": bool, 761 "validDays": int, 762 "priceType": str, 763 "priceDecimalPrecision": 0 764 }, 765 "editable": bool, 766 "deletable": bool 767 }] 768 """ 769 return self.__call(HttpMethod.GET, Route.STOP_LOSS_PATH.value) 770 771 def delete_stop_loss_order(self, account_id: str, stop_loss_id: str): 772 """delete a stop loss order 773 774 Args: 775 776 stop_loss_id: The id of the stop loss order to delete. 777 778 account_id: A valid account id. 779 780 Returns: 781 Nothing 782 """ 783 784 return self.__call( 785 HttpMethod.DELETE, 786 Route.ORDER_DELETE_STOP_LOSS_PATH.value.format( 787 account_id, 788 stop_loss_id, 789 ), 790 ) 791 792 def delete_order(self, account_id: str, order_id: str): 793 """Delete an existing order 794 795 Returns: 796 797 { 798 messages: str, 799 orderId: str, 800 parameters: List[str], 801 orderRequestStatus: str 802 } 803 """ 804 return self.__call( 805 HttpMethod.POST, 806 Route.ORDER_DELETE_PATH.value, 807 {"accountId": account_id, "orderId": order_id}, 808 ) 809 810 def get_monthly_savings_by_account_id(self, account_id: str): 811 """Get monthly savings at avanza for specific account 812 813 Returns: 814 815 { 816 'monthlySavings': [{ 817 'account': { 818 'id': str, 819 'name': str, 820 'type': str 821 }, 822 'amount': float, 823 'cash': {'amount': float, 'percent': float}, 824 'externalAccount': { 825 'accountNumber': str, 826 'bankName': str, 827 'clearingNumber': str 828 }, 829 'fundDistributions': [{ 830 'amount': float, 831 'orderbook': { 832 'buyable': bool, 833 'id': str, 834 'name': str 835 }, 836 'percent': float 837 }, 838 'id': str, 839 'name': str, 840 'purchaseDay': int, 841 'status': str, 842 'transferDay': int 843 }], 844 'totalAmount': float 845 } 846 """ 847 848 return self.__call( 849 HttpMethod.GET, Route.MONTHLY_SAVINGS_PATH.value.format(account_id) 850 ) 851 852 def get_all_monthly_savings(self): 853 """Get your monthly savings at Avanza 854 855 Returns: 856 857 { 858 'monthlySavings': [{ 859 'account': { 860 'id': str, 861 'name': str, 862 'type': str 863 }, 864 'amount': float, 865 'cash': {'amount': float, 'percent': float}, 866 'externalAccount': { 867 'accountNumber': str, 868 'bankName': str, 869 'clearingNumber': str 870 }, 871 'fundDistributions': [{ 872 'amount': float, 873 'orderbook': { 874 'buyable': bool, 875 'id': str, 876 'name': str 877 }, 878 'percent': float 879 }, 880 'id': str, 881 'name': str, 882 'purchaseDay': int, 883 'status': str, 884 'transferDay': int 885 }], 886 'totalAmount': float 887 } 888 """ 889 890 return self.__call(HttpMethod.GET, Route.MONTHLY_SAVINGS_PATH.value.format("")) 891 892 def pause_monthly_saving(self, account_id: str, monthly_savings_id: str): 893 """Pause an active monthly saving 894 895 Returns: 896 'OK' 897 898 """ 899 900 return self.__call( 901 HttpMethod.PUT, 902 Route.MONTHLY_SAVINGS_PAUSE_PATH.value.format( 903 account_id, monthly_savings_id 904 ), 905 ) 906 907 def resume_monthly_saving(self, account_id: str, monthly_savings_id: str): 908 """Resume a paused monthly saving 909 910 Returns: 911 'OK' 912 913 """ 914 915 return self.__call( 916 HttpMethod.PUT, 917 Route.MONTHLY_SAVINGS_RESUME_PATH.value.format( 918 account_id, monthly_savings_id 919 ), 920 ) 921 922 def delete_monthly_saving(self, account_id: str, monthly_savings_id: str) -> None: 923 """Deletes a monthly saving 924 925 Returns: 926 None 927 928 """ 929 930 return self.__call( 931 HttpMethod.DELETE, 932 Route.MONTHLY_SAVINGS_REMOVE_PATH.value.format( 933 account_id, monthly_savings_id 934 ), 935 ) 936 937 def create_monthly_saving( 938 self, 939 account_id: str, 940 amount: int, 941 transfer_day_of_month: int, 942 purchase_day_of_month: int, 943 clearing_and_account_number: str, 944 fund_distribution: Dict[str, int], 945 ): 946 """Create a monthly saving at Avanza 947 948 Args: 949 950 account_id: The Avanza account to which the withdrawn money should be transferred to 951 952 amount: minimum amount 100 (SEK) 953 the amount that should be withdrawn from the external account every month 954 955 transfer_day_of_month: valid range (1-31) 956 when the money should be withdrawn from the external account 957 958 purchase_day_of_month: valid range (1-31) 959 when the funds should be purchased, 960 must occur after the transfer_day_of_month 961 962 clearing_and_account_number: The external account from which the money for the monthly savings should be withdrawn from, 963 has to be formatted as follows 'XXXX-XXXXXXXXX' 964 965 fund_distrubution: the percentage distribution of the funds 966 The key is the funds id and the value is the distribution of the amount in a whole percentage 967 The sum of the percentages has to total 100 968 969 Examples: 970 {'41567': 100} 971 {'41567': 50, '878733': 50} 972 {'41567': 25, '878733': 75} 973 974 Returns: 975 976 { 977 'monthlySavingId': str, 978 'status': str 979 } 980 981 monthlySavingId has the following format: 'XX^XXXXXXXXXXXXX^XXXXXX' 982 status should have the value 'ACCEPTED' if the monthly saving was created successfully 983 """ 984 985 if not 1 <= transfer_day_of_month <= 31: 986 raise ValueError( 987 "transfer_day_of_month is outside the valid range of (1-31)" 988 ) 989 990 if not 1 <= purchase_day_of_month <= 31: 991 raise ValueError( 992 "purchase_day_of_month is outside the valid range of (1-31)" 993 ) 994 995 if transfer_day_of_month >= purchase_day_of_month: 996 raise ValueError( 997 "transfer_day_of_month must occur before purchase_day_of_month" 998 ) 999 1000 if len(fund_distribution) == 0: 1001 raise ValueError("No founds were specified in the fund_distribution") 1002 1003 if sum(fund_distribution.values()) != 100: 1004 raise ValueError("The fund_distribution values must total 100") 1005 1006 return self.__call( 1007 HttpMethod.POST, 1008 Route.MONTHLY_SAVINGS_CREATE_PATH.value.format(account_id), 1009 { 1010 "amount": amount, 1011 "autogiro": { 1012 "dayOfMonth": transfer_day_of_month, 1013 "externalClearingAndAccount": clearing_and_account_number, 1014 }, 1015 "fundDistribution": { 1016 "dayOfMonth": purchase_day_of_month, 1017 "fundDistributions": fund_distribution, 1018 }, 1019 }, 1020 ) 1021 1022 def get_transactions_details( 1023 self, 1024 transaction_details_types: Optional[Sequence[TransactionsDetailsType]] = [], 1025 transactions_from: Optional[date] = None, 1026 transactions_to: Optional[date] = None, 1027 isin: Optional[str] = None, 1028 max_elements: Optional[int] = 1000, 1029 ) -> Transactions: 1030 """Get transactions, optionally apply criteria. 1031 1032 Args: 1033 1034 transaction_types: One or more transaction types. 1035 1036 transactions_from: Fetch transactions from this date. 1037 1038 transactions_to: Fetch transactions to this date. 1039 1040 isin: Only fetch transactions for specified isin. 1041 1042 max_elements: Limit result to N transactions. 1043 """ 1044 options = {} 1045 options["maxElements"] = max_elements 1046 1047 if transaction_details_types: 1048 options["transactionTypes"] = ",".join( 1049 [type.value for type in transaction_details_types] 1050 ) 1051 if transactions_from: 1052 options["from"] = transactions_from.isoformat() 1053 if transactions_to: 1054 options["to"] = transactions_to.isoformat() 1055 if isin: 1056 options["isin"] = isin 1057 1058 return self.__call( 1059 HttpMethod.GET, Route.TRANSACTIONS_DETAILS_PATH.value, options 1060 ) 1061 1062 def get_note_as_pdf(self, url_parameter_id: str, note_id: str): 1063 return self.__call( 1064 HttpMethod.GET, 1065 Route.NOTE_PATH.value.format(url_parameter_id, note_id), 1066 return_content=True, 1067 ) 1068 1069 def set_price_alert( 1070 self, 1071 order_book_id: str, 1072 price: float, 1073 valid_until: date, 1074 notification: bool = True, 1075 email: bool = False, 1076 sms: bool = False, 1077 ): 1078 """ 1079 Sets a price alert for the specified orderbook and returns all the existing alerts. 1080 1081 Returns: 1082 1083 [ 1084 { 1085 'alertId': str, 1086 'accountId': str, 1087 'price': float, 1088 'validUntil': str, 1089 'direction': str, 1090 'email': bool, 1091 'notification': bool, 1092 'sms': bool, 1093 } 1094 ] 1095 """ 1096 1097 return self.__call( 1098 HttpMethod.POST, 1099 Route.PRICE_ALERT_PATH.value.format(order_book_id), 1100 { 1101 "price": price, 1102 "validUntil": valid_until.isoformat(), 1103 "notification": notification, 1104 "email": email, 1105 "sms": sms, 1106 }, 1107 ) 1108 1109 def get_price_alert(self, order_book_id: str) -> List[PriceAlert]: 1110 """Gets all the price alerts for the specified orderbook""" 1111 1112 return self.__call( 1113 HttpMethod.GET, 1114 Route.PRICE_ALERT_PATH.value.format(order_book_id), 1115 ) 1116 1117 def delete_price_alert(self, order_book_id: str, alert_id: str): 1118 """ 1119 Deletes a price alert from the specified orderbook and returns the remaining alerts. 1120 1121 Returns: 1122 1123 [ 1124 { 1125 'alertId': str, 1126 'accountId': str, 1127 'price': float, 1128 'validUntil': str, 1129 'direction': str, 1130 'email': bool, 1131 'notification': bool, 1132 'sms': bool, 1133 } 1134 ] 1135 """ 1136 return self.__call( 1137 HttpMethod.DELETE, 1138 Route.PRICE_ALERT_PATH.value.format(order_book_id, alert_id) 1139 + f"/{alert_id}", 1140 ) 1141 1142 def get_offers(self) -> List[Offer]: 1143 """Return current offers""" 1144 1145 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_market_data(self, order_book_id: str)-> MarketData: 451 """Marketdata includes current order depth and quote.""" 452 return self.__call( 453 HttpMethod.GET, 454 Route.MARKETDATA_PATH.value.format(order_book_id) 455 )
Marketdata includes current order depth and quote.
457 def get_insights_report( 458 self, account_ids: list[str], time_period: InsightsReportTimePeriod 459 ) -> InsightsReport: 460 """Get report about the development of your owned positions during the specified timeperiod""" 461 return self.__call( 462 HttpMethod.POST, 463 Route.INSIGHTS_PATH.value, 464 options={"timePeriod": time_period.value, "accountIds": account_ids}, 465 )
Get report about the development of your owned positions during the specified timeperiod
467 def get_deals(self): 468 """Get currently active deals""" 469 return self.__call(HttpMethod.GET, Route.DEALS_PATH.value)
Get currently active deals
471 def get_orders(self): 472 """Get currently active orders""" 473 return self.__call(HttpMethod.GET, Route.ORDERS_PATH.value)
Get currently active orders
475 def get_inspiration_lists(self) -> List[InspirationListItem]: 476 """Get all available inspiration lists 477 478 This returns lists similar to the ones found on: 479 480 https://www.avanza.se/aktier/aktieinspiration.html 481 482 https://www.avanza.se/fonder/fondinspiration.html 483 484 """ 485 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
487 def get_inspiration_list(self, list_id: Union[ListType, str]) -> InspirationList: 488 """Get inspiration list 489 490 Some lists have an id of an enum value described in ListType, but they can also just have a string id. 491 An example is hhSK8W1o which corresponds to "Most owned stocks", which isn't described in the ListType enum 492 493 """ 494 495 id = list_id.value if isinstance(list_id, ListType) else list_id 496 497 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
499 def get_chart_data( 500 self, 501 order_book_id: str, 502 period: TimePeriod, 503 resolution: Optional[Resolution] = None, 504 ) -> ChartData: 505 """Return chart data for an order book for the specified time period with given resolution""" 506 options = {"timePeriod": period.value.lower()} 507 508 if resolution is not None: 509 options["resolution"] = resolution.value.lower() 510 511 return self.__call( 512 HttpMethod.GET, Route.CHARTDATA_PATH.value.format(order_book_id), options 513 )
Return chart data for an order book for the specified time period with given resolution
515 def place_order( 516 self, 517 account_id: str, 518 order_book_id: str, 519 order_type: OrderType, 520 price: float, 521 valid_until: date, 522 volume: int, 523 condition: Condition = Condition.NORMAL, 524 ): 525 """Place an order 526 527 Returns: 528 529 If the order was successfully placed: 530 531 { 532 message: str, 533 orderId: str, 534 orderRequestStatus: 'SUCCESS' 535 } 536 537 If the order was not placed: 538 539 { 540 message: str, 541 orderRequestStatus: 'ERROR' 542 } 543 """ 544 545 return self.__call( 546 HttpMethod.POST, 547 Route.ORDER_PLACE_PATH.value, 548 { 549 "accountId": account_id, 550 "orderbookId": order_book_id, 551 "side": order_type.value, 552 "condition": condition.value, 553 "price": price, 554 "validUntil": valid_until.isoformat(), 555 "volume": volume, 556 }, 557 )
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'
}
559 def place_order_buy_fund(self, account_id: str, order_book_id: str, amount: float): 560 """Place a buy order for a fund 561 562 Returns: 563 564 { 565 message: str, 566 orderId: str, 567 accountId: str, 568 orderRequestStatus: str 569 } 570 """ 571 572 return self.__call( 573 HttpMethod.POST, 574 Route.ORDER_PLACE_PATH_BUY_FUND.value, 575 {"orderbookId": order_book_id, "accountId": account_id, "amount": amount}, 576 )
Place a buy order for a fund
Returns:
{
message: str,
orderId: str,
accountId: str,
orderRequestStatus: str
}
578 def place_order_sell_fund(self, account_id: str, order_book_id: str, volume: float): 579 """Place a sell order for a fund 580 581 Returns: 582 583 { 584 message: str, 585 orderId: str, 586 accountId: str, 587 orderRequestStatus: str 588 } 589 """ 590 591 return self.__call( 592 HttpMethod.POST, 593 Route.ORDER_PLACE_PATH_SELL_FUND.value, 594 {"orderbookId": order_book_id, "accountId": account_id, "volume": volume}, 595 )
Place a sell order for a fund
Returns:
{
message: str,
orderId: str,
accountId: str,
orderRequestStatus: str
}
597 def place_stop_loss_order( 598 self, 599 parent_stop_loss_id: str, 600 account_id: str, 601 order_book_id: str, 602 stop_loss_trigger: StopLossTrigger, 603 stop_loss_order_event: StopLossOrderEvent, 604 ): 605 """Place an stop loss order 606 607 Args: 608 609 parent_stop_loss_id: The id of the parent stop loss order. If this is the first stop loss order, this should be "0". 610 611 account_id: A valid account id. 612 613 order_book_id: The order book id of the instrument to place the stop loss order for. 614 615 stop_loss_trigger: The stop loss trigger type. 616 617 stop_loss_order_event: The stop loss order event type. 618 619 Returns: 620 621 If the order was successfully placed: 622 623 { 624 status: 'SUCCESS', 625 stoplossOrderId: str 626 } 627 628 If the order was not placed: 629 630 { 631 status: str, 632 stoplossOrderId: str 633 } 634 """ 635 636 return self.__call( 637 HttpMethod.POST, 638 Route.ORDER_PLACE_STOP_LOSS_PATH.value, 639 { 640 "parentStopLossId": parent_stop_loss_id, 641 "accountId": account_id, 642 "orderBookId": order_book_id, 643 "stopLossTrigger": { 644 "type": stop_loss_trigger.type.value, 645 "value": stop_loss_trigger.value, 646 "validUntil": stop_loss_trigger.valid_until.isoformat(), 647 "valueType": stop_loss_trigger.value_type.value, 648 "triggerOnMarketMakerQuote": stop_loss_trigger.trigger_on_market_maker_quote, 649 }, 650 "stopLossOrderEvent": { 651 "type": stop_loss_order_event.type.value, 652 "price": stop_loss_order_event.price, 653 "volume": stop_loss_order_event.volume, 654 "validDays": stop_loss_order_event.valid_days, 655 "priceType": stop_loss_order_event.price_type.value, 656 "shortSellingAllowed": stop_loss_order_event.short_selling_allowed, 657 }, 658 }, 659 )
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
}
661 def edit_order( 662 self, 663 order_id: str, 664 account_id: str, 665 price: float, 666 valid_until: date, 667 volume: int, 668 ): 669 """Update an existing order 670 671 Returns: 672 673 { 674 orderRequestStatus: str, 675 message: str, 676 parameters: List[str], 677 orderId: str 678 } 679 """ 680 681 return self.__call( 682 HttpMethod.POST, 683 Route.ORDER_EDIT_PATH.value, 684 { 685 "accountId": account_id, 686 "metadata": {"orderEntryMode": "STANDARD"}, 687 "openVolume": None, 688 "orderId": order_id, 689 "price": price, 690 "validUntil": valid_until.isoformat(), 691 "volume": volume, 692 }, 693 )
Update an existing order
Returns:
{
orderRequestStatus: str,
message: str,
parameters: List[str],
orderId: str
}
695 def get_order(self, account_id: str, order_id: str): 696 """Get an existing order 697 698 Returns: 699 700 { 701 "orderId": str, 702 "orderbookId": str, 703 "side": str, 704 "state": str, 705 "marketReference": str, 706 "price": float, 707 "message": str, 708 "volume": int, 709 "originalVolume": int, 710 "accountId": str, 711 "condition": str, 712 "validUntil": str, 713 "modifiable": bool, 714 "deletable": bool, 715 } 716 """ 717 718 return self.__call( 719 HttpMethod.GET, 720 Route.ORDER_GET_PATH.value.format( 721 order_id, 722 account_id, 723 ), 724 )
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,
}
726 def get_all_stop_losses(self): 727 """Get open stop losses 728 729 Returns: 730 731 [{ 732 "id": str, 733 "status": str, 734 "account": { 735 "id": str, 736 "name": str, 737 "type": str, 738 "urlParameterId": str 739 }, 740 "orderbook": { 741 "id": str, 742 "name": str, 743 "countryCode": str, 744 "currency": str, 745 "shortName": str, 746 "type": str 747 }, 748 "hasExcludingChildren": bool, 749 "message": str, 750 "trigger": { 751 "value": int, 752 "type": str, 753 "validUntil": str, 754 "valueType": str 755 }, 756 "order": { 757 "type": str, 758 "price": int, 759 "volume": int, 760 "shortSellingAllowed": bool, 761 "validDays": int, 762 "priceType": str, 763 "priceDecimalPrecision": 0 764 }, 765 "editable": bool, 766 "deletable": bool 767 }] 768 """ 769 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
}]
771 def delete_stop_loss_order(self, account_id: str, stop_loss_id: str): 772 """delete a stop loss order 773 774 Args: 775 776 stop_loss_id: The id of the stop loss order to delete. 777 778 account_id: A valid account id. 779 780 Returns: 781 Nothing 782 """ 783 784 return self.__call( 785 HttpMethod.DELETE, 786 Route.ORDER_DELETE_STOP_LOSS_PATH.value.format( 787 account_id, 788 stop_loss_id, 789 ), 790 )
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
792 def delete_order(self, account_id: str, order_id: str): 793 """Delete an existing order 794 795 Returns: 796 797 { 798 messages: str, 799 orderId: str, 800 parameters: List[str], 801 orderRequestStatus: str 802 } 803 """ 804 return self.__call( 805 HttpMethod.POST, 806 Route.ORDER_DELETE_PATH.value, 807 {"accountId": account_id, "orderId": order_id}, 808 )
Delete an existing order
Returns:
{
messages: str,
orderId: str,
parameters: List[str],
orderRequestStatus: str
}
810 def get_monthly_savings_by_account_id(self, account_id: str): 811 """Get monthly savings at avanza for specific account 812 813 Returns: 814 815 { 816 'monthlySavings': [{ 817 'account': { 818 'id': str, 819 'name': str, 820 'type': str 821 }, 822 'amount': float, 823 'cash': {'amount': float, 'percent': float}, 824 'externalAccount': { 825 'accountNumber': str, 826 'bankName': str, 827 'clearingNumber': str 828 }, 829 'fundDistributions': [{ 830 'amount': float, 831 'orderbook': { 832 'buyable': bool, 833 'id': str, 834 'name': str 835 }, 836 'percent': float 837 }, 838 'id': str, 839 'name': str, 840 'purchaseDay': int, 841 'status': str, 842 'transferDay': int 843 }], 844 'totalAmount': float 845 } 846 """ 847 848 return self.__call( 849 HttpMethod.GET, Route.MONTHLY_SAVINGS_PATH.value.format(account_id) 850 )
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
}
852 def get_all_monthly_savings(self): 853 """Get your monthly savings at Avanza 854 855 Returns: 856 857 { 858 'monthlySavings': [{ 859 'account': { 860 'id': str, 861 'name': str, 862 'type': str 863 }, 864 'amount': float, 865 'cash': {'amount': float, 'percent': float}, 866 'externalAccount': { 867 'accountNumber': str, 868 'bankName': str, 869 'clearingNumber': str 870 }, 871 'fundDistributions': [{ 872 'amount': float, 873 'orderbook': { 874 'buyable': bool, 875 'id': str, 876 'name': str 877 }, 878 'percent': float 879 }, 880 'id': str, 881 'name': str, 882 'purchaseDay': int, 883 'status': str, 884 'transferDay': int 885 }], 886 'totalAmount': float 887 } 888 """ 889 890 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
}
892 def pause_monthly_saving(self, account_id: str, monthly_savings_id: str): 893 """Pause an active monthly saving 894 895 Returns: 896 'OK' 897 898 """ 899 900 return self.__call( 901 HttpMethod.PUT, 902 Route.MONTHLY_SAVINGS_PAUSE_PATH.value.format( 903 account_id, monthly_savings_id 904 ), 905 )
Pause an active monthly saving
Returns: 'OK'
907 def resume_monthly_saving(self, account_id: str, monthly_savings_id: str): 908 """Resume a paused monthly saving 909 910 Returns: 911 'OK' 912 913 """ 914 915 return self.__call( 916 HttpMethod.PUT, 917 Route.MONTHLY_SAVINGS_RESUME_PATH.value.format( 918 account_id, monthly_savings_id 919 ), 920 )
Resume a paused monthly saving
Returns: 'OK'
922 def delete_monthly_saving(self, account_id: str, monthly_savings_id: str) -> None: 923 """Deletes a monthly saving 924 925 Returns: 926 None 927 928 """ 929 930 return self.__call( 931 HttpMethod.DELETE, 932 Route.MONTHLY_SAVINGS_REMOVE_PATH.value.format( 933 account_id, monthly_savings_id 934 ), 935 )
Deletes a monthly saving
Returns: None
937 def create_monthly_saving( 938 self, 939 account_id: str, 940 amount: int, 941 transfer_day_of_month: int, 942 purchase_day_of_month: int, 943 clearing_and_account_number: str, 944 fund_distribution: Dict[str, int], 945 ): 946 """Create a monthly saving at Avanza 947 948 Args: 949 950 account_id: The Avanza account to which the withdrawn money should be transferred to 951 952 amount: minimum amount 100 (SEK) 953 the amount that should be withdrawn from the external account every month 954 955 transfer_day_of_month: valid range (1-31) 956 when the money should be withdrawn from the external account 957 958 purchase_day_of_month: valid range (1-31) 959 when the funds should be purchased, 960 must occur after the transfer_day_of_month 961 962 clearing_and_account_number: The external account from which the money for the monthly savings should be withdrawn from, 963 has to be formatted as follows 'XXXX-XXXXXXXXX' 964 965 fund_distrubution: the percentage distribution of the funds 966 The key is the funds id and the value is the distribution of the amount in a whole percentage 967 The sum of the percentages has to total 100 968 969 Examples: 970 {'41567': 100} 971 {'41567': 50, '878733': 50} 972 {'41567': 25, '878733': 75} 973 974 Returns: 975 976 { 977 'monthlySavingId': str, 978 'status': str 979 } 980 981 monthlySavingId has the following format: 'XX^XXXXXXXXXXXXX^XXXXXX' 982 status should have the value 'ACCEPTED' if the monthly saving was created successfully 983 """ 984 985 if not 1 <= transfer_day_of_month <= 31: 986 raise ValueError( 987 "transfer_day_of_month is outside the valid range of (1-31)" 988 ) 989 990 if not 1 <= purchase_day_of_month <= 31: 991 raise ValueError( 992 "purchase_day_of_month is outside the valid range of (1-31)" 993 ) 994 995 if transfer_day_of_month >= purchase_day_of_month: 996 raise ValueError( 997 "transfer_day_of_month must occur before purchase_day_of_month" 998 ) 999 1000 if len(fund_distribution) == 0: 1001 raise ValueError("No founds were specified in the fund_distribution") 1002 1003 if sum(fund_distribution.values()) != 100: 1004 raise ValueError("The fund_distribution values must total 100") 1005 1006 return self.__call( 1007 HttpMethod.POST, 1008 Route.MONTHLY_SAVINGS_CREATE_PATH.value.format(account_id), 1009 { 1010 "amount": amount, 1011 "autogiro": { 1012 "dayOfMonth": transfer_day_of_month, 1013 "externalClearingAndAccount": clearing_and_account_number, 1014 }, 1015 "fundDistribution": { 1016 "dayOfMonth": purchase_day_of_month, 1017 "fundDistributions": fund_distribution, 1018 }, 1019 }, 1020 )
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
1022 def get_transactions_details( 1023 self, 1024 transaction_details_types: Optional[Sequence[TransactionsDetailsType]] = [], 1025 transactions_from: Optional[date] = None, 1026 transactions_to: Optional[date] = None, 1027 isin: Optional[str] = None, 1028 max_elements: Optional[int] = 1000, 1029 ) -> Transactions: 1030 """Get transactions, optionally apply criteria. 1031 1032 Args: 1033 1034 transaction_types: One or more transaction types. 1035 1036 transactions_from: Fetch transactions from this date. 1037 1038 transactions_to: Fetch transactions to this date. 1039 1040 isin: Only fetch transactions for specified isin. 1041 1042 max_elements: Limit result to N transactions. 1043 """ 1044 options = {} 1045 options["maxElements"] = max_elements 1046 1047 if transaction_details_types: 1048 options["transactionTypes"] = ",".join( 1049 [type.value for type in transaction_details_types] 1050 ) 1051 if transactions_from: 1052 options["from"] = transactions_from.isoformat() 1053 if transactions_to: 1054 options["to"] = transactions_to.isoformat() 1055 if isin: 1056 options["isin"] = isin 1057 1058 return self.__call( 1059 HttpMethod.GET, Route.TRANSACTIONS_DETAILS_PATH.value, options 1060 )
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.
1069 def set_price_alert( 1070 self, 1071 order_book_id: str, 1072 price: float, 1073 valid_until: date, 1074 notification: bool = True, 1075 email: bool = False, 1076 sms: bool = False, 1077 ): 1078 """ 1079 Sets a price alert for the specified orderbook and returns all the existing alerts. 1080 1081 Returns: 1082 1083 [ 1084 { 1085 'alertId': str, 1086 'accountId': str, 1087 'price': float, 1088 'validUntil': str, 1089 'direction': str, 1090 'email': bool, 1091 'notification': bool, 1092 'sms': bool, 1093 } 1094 ] 1095 """ 1096 1097 return self.__call( 1098 HttpMethod.POST, 1099 Route.PRICE_ALERT_PATH.value.format(order_book_id), 1100 { 1101 "price": price, 1102 "validUntil": valid_until.isoformat(), 1103 "notification": notification, 1104 "email": email, 1105 "sms": sms, 1106 }, 1107 )
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,
}
]
1109 def get_price_alert(self, order_book_id: str) -> List[PriceAlert]: 1110 """Gets all the price alerts for the specified orderbook""" 1111 1112 return self.__call( 1113 HttpMethod.GET, 1114 Route.PRICE_ALERT_PATH.value.format(order_book_id), 1115 )
Gets all the price alerts for the specified orderbook
1117 def delete_price_alert(self, order_book_id: str, alert_id: str): 1118 """ 1119 Deletes a price alert from the specified orderbook and returns the remaining alerts. 1120 1121 Returns: 1122 1123 [ 1124 { 1125 'alertId': str, 1126 'accountId': str, 1127 'price': float, 1128 'validUntil': str, 1129 'direction': str, 1130 'email': bool, 1131 'notification': bool, 1132 'sms': bool, 1133 } 1134 ] 1135 """ 1136 return self.__call( 1137 HttpMethod.DELETE, 1138 Route.PRICE_ALERT_PATH.value.format(order_book_id, alert_id) 1139 + f"/{alert_id}", 1140 )
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,
}
]