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