avanza.avanza

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

Args:

credentials: Login credentials. Can be multiple variations
    Either an instance of TokenCredentials or of SecretCredentials
    Or a dictionary as the following:
    Using TOTP secret:
        {
            'username': 'MY_USERNAME',
            'password': 'MY_PASSWORD',
            'totpSecret': 'MY_TOTP_SECRET'
        }
    Using TOTP code:
        {
            'username': 'MY_USERNAME',
            'password': 'MY_PASSWORD',
            'totpCode': 'MY_TOTP_CODE'
        }

retry_with_next_otp: If

    a) the server responded with 401 Unauthorized when trying to
       log in, and
    b) the TOTP code used for logging in was generated from a TOTP
       secret provided in credentials,

    then wait until the next TOTP time-step window and try again
    with the new OTP. Re-retrying with a new OTP prevents a program
    using avanza-api from crashing with a 401 HTTP error if, for
    example, the program is run a second time shortly after it was
    previously run.

quiet: Do not print a status message if waiting for the next TOTP
    time-step window.
def get_overview(self) -> avanza.models.overview.Overview:
207    def get_overview(self) -> Overview:
208        """Get account and category overviews"""
209        return self.__call(HttpMethod.GET, Route.CATEGORIZED_ACCOUNTS.value)

Get account and category overviews

def get_accounts_positions(self) -> avanza.models.account_posititions.AccountPositions:
211    def get_accounts_positions(self) -> AccountPositions:
212        """Get investment positions for all account"""
213
214        return self.__call(HttpMethod.GET, Route.ACCOUNTS_POSITIONS_PATH.value)

Get investment positions for all account

def get_account_performance_chart_data( self, url_parameters_ids: list[str], time_period: avanza.constants.TimePeriod) -> avanza.models.account_posititions.AccountPositions:
216    def get_account_performance_chart_data(
217        self, url_parameters_ids: list[str], time_period: TimePeriod
218    ) -> AccountPositions:
219        """Get performance chart for accounts.
220
221        Args:
222
223            url_parameters_ids: Scrambled account ids.
224                Can be found in the overview response.
225
226            time_period: Time period to get chart data for
227
228        """
229        return self.__call(
230            HttpMethod.POST,
231            Route.ACCOUNT_PERFORMANCE_CHART_PATH.value,
232            {
233                "scrambledAccountIds": url_parameters_ids,
234                "timePeriod": time_period.value,
235            },
236        )

Get performance chart for accounts.

Args:

url_parameters_ids: Scrambled account ids.
    Can be found in the overview response.

time_period: Time period to get chart data for
def get_credit_info( self, credit_type: avanza.constants.CreditType) -> avanza.models.credit_info.CreditInfo:
238    def get_credit_info(self, credit_type: CreditType) -> CreditInfo:
239        """Returns creditinfo for accounts
240        CreditType->credited for accounts with credit
241        CreditType->uncredited for accounts with no credit
242        """
243        return self.__call(
244            HttpMethod.GET, Route.CREDITINFO_PATH.value.format(credit_type)
245        )

Returns creditinfo for accounts CreditType->credited for accounts with credit CreditType->uncredited for accounts with no credit

def get_watchlists(self) -> List[avanza.models.watch_list.WatchList]:
247    def get_watchlists(self) -> List[WatchList]:
248        """Get your "Bevakningslistor" """
249        return self.__call(HttpMethod.GET, Route.WATCHLISTS_PATH.value)

Get your "Bevakningslistor"

def add_to_watchlist(self, instrument_id: str, watchlist_id: str) -> None:
251    def add_to_watchlist(self, instrument_id: str, watchlist_id: str) -> None:
252        """Add an instrument to the specified watchlist
253
254        This function returns None if the request was 200 OK,
255        but there is no guarantee that the instrument was added to the list,
256        verify this by calling get_watchlists()
257        """
258        return self.__call(
259            HttpMethod.POST,
260            Route.WATCHLISTS_ADD_PATH.value.format(watchlist_id, instrument_id),
261        )

Add an instrument to the specified watchlist

This function returns None if the request was 200 OK, but there is no guarantee that the instrument was added to the list, verify this by calling get_watchlists()

def remove_from_watchlist(self, instrument_id: str, watchlist_id: str) -> None:
263    def remove_from_watchlist(self, instrument_id: str, watchlist_id: str) -> None:
264        """Remove an instrument to the specified watchlist
265
266        This function returns None if the request was 200 OK,
267        but there is no guarantee that the instrument was removed from the list,
268        verify this by calling get_watchlists()
269        """
270        return self.__call(
271            HttpMethod.POST,
272            Route.WATCHLISTS_REMOVE_PATH.value.format(watchlist_id, instrument_id),
273        )

Remove an instrument to the specified watchlist

This function returns None if the request was 200 OK, but there is no guarantee that the instrument was removed from the list, verify this by calling get_watchlists()

def get_fund_info(self, fund_id: str) -> avanza.models.fund_info.FundInfo:
275    def get_fund_info(self, fund_id: str) -> FundInfo:
276        """Get info about a fund"""
277
278        return self.__call(HttpMethod.GET, Route.FUND_PATH.value.format(fund_id))

Get info about a fund

def get_stock_info(self, stock_id: str) -> avanza.models.stock_info.StockInfo:
280    def get_stock_info(self, stock_id: str) -> StockInfo:
281        """Returns info about a stock"""
282
283        return self.get_instrument(InstrumentType.STOCK, stock_id)

Returns info about a stock

def get_certificate_info( self, certificate_id: str) -> avanza.models.certificate_info.CertificateInfo:
285    def get_certificate_info(self, certificate_id: str) -> CertificateInfo:
286        """Returns info about a certificate"""
287
288        return self.get_instrument(InstrumentType.CERTIFICATE, certificate_id)

Returns info about a certificate

def get_certificate_details( self, certificate_id: str) -> avanza.models.certificate_details.CertificateDetails:
290    def get_certificate_details(self, certificate_id: str) -> CertificateDetails:
291        """Returns additional info about a certificate"""
292
293        return self.get_instrument_details(InstrumentType.CERTIFICATE, certificate_id)

Returns additional info about a certificate

def get_etf_details(self, etf_id: str) -> avanza.models.etf_details.EtfDetails:
295    def get_etf_details(self, etf_id: str) -> EtfDetails:
296        return self.get_instrument_details(InstrumentType.EXCHANGE_TRADED_FUND, etf_id)
def get_warrant_info(self, warrant_id: str) -> avanza.models.warrant_info.WarrantInfo:
298    def get_warrant_info(self, warrant_id: str) -> WarrantInfo:
299        """Returns info about a warrant"""
300
301        return self.get_instrument(InstrumentType.WARRANT, warrant_id)

Returns info about a warrant

def get_index_info(self, index_id: str) -> avanza.models.index_info.IndexInfo:
303    def get_index_info(self, index_id: str) -> IndexInfo:
304        """Returns info about an index"""
305
306        # Works when sending InstrumentType.STOCK, but not InstrumentType.INDEX
307        return self.get_instrument(InstrumentType.STOCK, index_id)

Returns info about an index

def get_analysis(self, instrument_id: str):
309    def get_analysis(self, instrument_id: str):
310        """Returns analysis data for an instrument"""
311
312        return self.__call(
313            HttpMethod.GET, Route.ANALYSIS_PATH.value.format(instrument_id)
314        )

Returns analysis data for an instrument

def get_news(self, instrument_id: str) -> avanza.models.news.News:
316    def get_news(self, instrument_id: str) -> News:
317        """Returns latest news data for an instrument"""
318
319        return self.__call(HttpMethod.GET, Route.NEWS_PATH.value.format(instrument_id))

Returns latest news data for an instrument

def get_forum_posts(self, instrument_id: str) -> avanza.models.forum_posts.ForumPosts:
321    def get_forum_posts(self, instrument_id: str) -> ForumPosts:
322        """Returns latest forum posts for an instrument"""
323
324        return self.__call(HttpMethod.GET, Route.FORUM_PATH.value.format(instrument_id))

Returns latest forum posts for an instrument

def get_instrument( self, instrument_type: avanza.constants.InstrumentType, instrument_id: str):
326    def get_instrument(self, instrument_type: InstrumentType, instrument_id: str):
327        """
328        Get instrument info
329        For more info on return models for this function see functions
330        [
331            get_stock_info(),
332            get_fund_info(),
333            get_certificate_info(),
334            get_index_info(),
335            get_warrant_info()
336        ]
337        """
338
339        return self.__call(
340            HttpMethod.GET,
341            Route.INSTRUMENT_PATH.value.format(instrument_type.value, instrument_id),
342        )

Get instrument info For more info on return models for this function see functions [ get_stock_info(), get_fund_info(), get_certificate_info(), get_index_info(), get_warrant_info() ]

def get_instrument_details( self, instrument_type: avanza.constants.InstrumentType, instrument_id: str):
344    def get_instrument_details(
345        self, instrument_type: InstrumentType, instrument_id: str
346    ):
347        """
348        Get additional instrument info
349        """
350        if instrument_type is InstrumentType.EXCHANGE_TRADED_FUND:
351            return self.__call(
352                HttpMethod.GET, Route.ETF_DETAILS_PATH.value.format(instrument_id)
353            )
354        else:
355            return self.__call(
356                HttpMethod.GET,
357                Route.INSTRUMENT_DETAILS_PATH.value.format(
358                    instrument_type.value, instrument_id
359                ),
360            )

Get additional instrument info

def search_for_stock(self, query: str, limit: int = 10) -> TypeAdapter(List[SearchResult]):
362    def search_for_stock(self, query: str, limit: int = 10) -> SearchResults:
363        """Search for a stock
364
365        Args:
366
367            query: can be a ISIN ('US0378331005'),
368                name ('Apple'),
369                tickerSymbol ('AAPL')
370
371            limit: maximum number of results to return
372
373        """
374        return self.search_for_instrument(InstrumentType.STOCK, query, limit)

Search for a stock

Args:

query: can be a ISIN ('US0378331005'),
    name ('Apple'),
    tickerSymbol ('AAPL')

limit: maximum number of results to return
def search_for_fund(self, query: str, limit: int = 10) -> TypeAdapter(List[SearchResult]):
376    def search_for_fund(self, query: str, limit: int = 10) -> SearchResults:
377        """Search for a fund
378
379        Args:
380
381            query: can be a ISIN ('SE0012454338'),
382                name ('Avanza'),
383                tickerSymbol ('Avanza Europa')
384
385            limit: maximum number of results to return
386
387        """
388
389        return self.search_for_instrument(InstrumentType.FUND, query, limit)

Search for a fund

Args:

query: can be a ISIN ('SE0012454338'),
    name ('Avanza'),
    tickerSymbol ('Avanza Europa')

limit: maximum number of results to return
def search_for_certificate(self, query: str, limit: int = 10) -> TypeAdapter(List[SearchResult]):
391    def search_for_certificate(self, query: str, limit: int = 10) -> SearchResults:
392        """Search for a certificate
393
394        Args:
395
396            query: can be a ISIN, name or tickerSymbol
397
398            limit: maximum number of results to return
399
400        """
401
402        return self.search_for_instrument(InstrumentType.CERTIFICATE, query, limit)

Search for a certificate

Args:

query: can be a ISIN, name or tickerSymbol

limit: maximum number of results to return
def search_for_warrant(self, query: str, limit: int = 10) -> TypeAdapter(List[SearchResult]):
404    def search_for_warrant(self, query: str, limit: int = 10) -> SearchResults:
405        """Search for a warrant
406
407        Args:
408
409            query: can be a ISIN, name or tickerSymbol
410
411            limit: maximum number of results to return
412
413        """
414
415        return self.search_for_instrument(InstrumentType.WARRANT, query, limit)

Search for a warrant

Args:

query: can be a ISIN, name or tickerSymbol

limit: maximum number of results to return
def search_for_instrument( self, instrument_type: avanza.constants.InstrumentType, query: str, limit: int = 10):
417    def search_for_instrument(
418        self, instrument_type: InstrumentType, query: str, limit: int = 10
419    ):
420        """Search for a specific instrument
421
422        Args:
423
424            instrument_type: can be STOCK, FUND, BOND etc
425
426            query: can be a ISIN, name or tickerSymbol
427
428            limit: maximum number of results to return
429
430        """
431
432        options = {
433            "query": query,
434            "searchFilter": {"types": [instrument_type.value.upper()]},
435            "pagination": {"from": 0, "size": limit},
436        }
437        result = self.__call(
438            HttpMethod.POST, Route.INSTRUMENT_SEARCH_PATH.value, options=options
439        )
440        return result["hits"]

Search for a specific instrument

Args:

instrument_type: can be STOCK, FUND, BOND etc

query: can be a ISIN, name or tickerSymbol

limit: maximum number of results to return
def get_order_book(self, order_book_id: str) -> avanza.models.order_book.OrderBook:
443    def get_order_book(self, order_book_id: str)-> OrderBook:
444        """Get info about an orderbook"""
445        return self.__call(
446            HttpMethod.GET,
447            Route.ORDERBOOK_PATH.value.format(order_book_id)
448        )

Get info about an orderbook

def get_insights_report( self, account_id: str, time_period: avanza.constants.InsightsReportTimePeriod) -> avanza.models.insights_report.InsightsReport:
450    def get_insights_report(
451        self, account_id: str, time_period: InsightsReportTimePeriod
452    ) -> InsightsReport:
453        """Get report about the development of your owned positions during the specified timeperiod"""
454        return self.__call(
455            HttpMethod.GET,
456            Route.INSIGHTS_PATH.value.format(time_period.value, account_id),
457        )

Get report about the development of your owned positions during the specified timeperiod

def get_deals(self):
459    def get_deals(self):
460        """Get currently active deals"""
461        return self.__call(HttpMethod.GET, Route.DEALS_PATH.value)

Get currently active deals

def get_orders(self):
463    def get_orders(self):
464        """Get currently active orders"""
465        return self.__call(HttpMethod.GET, Route.ORDERS_PATH.value)

Get currently active orders

def get_inspiration_lists(self) -> List[avanza.models.list_inspiration_lists.InspirationListItem]:
467    def get_inspiration_lists(self) -> List[InspirationListItem]:
468        """Get all available inspiration lists
469
470        This returns lists similar to the ones found on:
471
472        https://www.avanza.se/aktier/aktieinspiration.html
473
474        https://www.avanza.se/fonder/fondinspiration.html
475
476        """
477        return self.__call(HttpMethod.GET, Route.INSPIRATION_LIST_PATH.value.format(""))

Get all available inspiration lists

This returns lists similar to the ones found on:

avanza.avanza.se/aktier/aktieinspiration.html">https://wwwavanza.avanza.se/aktier/aktieinspiration.html

avanza.avanza.se/fonder/fondinspiration.html">https://wwwavanza.avanza.se/fonder/fondinspiration.html

def get_inspiration_list( self, list_id: Union[avanza.constants.ListType, str]) -> avanza.models.inspiration_list.InspirationList:
479    def get_inspiration_list(self, list_id: Union[ListType, str]) -> InspirationList:
480        """Get inspiration list
481
482        Some lists have an id of an enum value described in ListType, but they can also just have a string id.
483        An example is hhSK8W1o which corresponds to "Most owned stocks", which isn't described in the ListType enum
484
485        """
486
487        id = list_id.value if isinstance(list_id, ListType) else list_id
488
489        return self.__call(HttpMethod.GET, Route.INSPIRATION_LIST_PATH.value.format(id))

Get inspiration list

Some lists have an id of an enum value described in ListType, but they can also just have a string id. An example is hhSK8W1o which corresponds to "Most owned stocks", which isn't described in the ListType enum

def get_chart_data( self, order_book_id: str, period: avanza.constants.TimePeriod, resolution: Optional[avanza.constants.Resolution] = None) -> avanza.models.chart_data.ChartData:
491    def get_chart_data(
492        self,
493        order_book_id: str,
494        period: TimePeriod,
495        resolution: Optional[Resolution] = None,
496    ) -> ChartData:
497        """Return chart data for an order book for the specified time period with given resolution"""
498        options = {"timePeriod": period.value.lower()}
499
500        if resolution is not None:
501            options["resolution"] = resolution.value.lower()
502
503        return self.__call(
504            HttpMethod.GET, Route.CHARTDATA_PATH.value.format(order_book_id), options
505        )

Return chart data for an order book for the specified time period with given resolution

def place_order( self, account_id: str, order_book_id: str, order_type: avanza.constants.OrderType, price: float, valid_until: datetime.date, volume: int, condition: avanza.constants.Condition = <Condition.NORMAL: 'NORMAL'>):
507    def place_order(
508        self,
509        account_id: str,
510        order_book_id: str,
511        order_type: OrderType,
512        price: float,
513        valid_until: date,
514        volume: int,
515        condition: Condition = Condition.NORMAL,
516    ):
517        """Place an order
518
519        Returns:
520
521            If the order was successfully placed:
522
523            {
524                message: str,
525                orderId: str,
526                orderRequestStatus: 'SUCCESS'
527            }
528
529            If the order was not placed:
530
531            {
532                message: str,
533                orderRequestStatus: 'ERROR'
534            }
535        """
536
537        return self.__call(
538            HttpMethod.POST,
539            Route.ORDER_PLACE_PATH.value,
540            {
541                "accountId": account_id,
542                "orderbookId": order_book_id,
543                "side": order_type.value,
544                "condition": condition.value,
545                "price": price,
546                "validUntil": valid_until.isoformat(),
547                "volume": volume,
548            },
549        )

Place an order

Returns:

If the order was successfully placed:

{
    message: str,
    orderId: str,
    orderRequestStatus: 'SUCCESS'
}

If the order was not placed:

{
    message: str,
    orderRequestStatus: 'ERROR'
}
def place_order_buy_fund(self, account_id: str, order_book_id: str, amount: float):
551    def place_order_buy_fund(self, account_id: str, order_book_id: str, amount: float):
552        """Place a buy order for a fund
553
554        Returns:
555
556            {
557                message: str,
558                orderId: str,
559                accountId: str,
560                orderRequestStatus: str
561            }
562        """
563
564        return self.__call(
565            HttpMethod.POST,
566            Route.ORDER_PLACE_PATH_BUY_FUND.value,
567            {"orderbookId": order_book_id, "accountId": account_id, "amount": amount},
568        )

Place a buy order for a fund

Returns:

{
    message: str,
    orderId: str,
    accountId: str,
    orderRequestStatus: str
}
def place_order_sell_fund(self, account_id: str, order_book_id: str, volume: float):
570    def place_order_sell_fund(self, account_id: str, order_book_id: str, volume: float):
571        """Place a sell order for a fund
572
573        Returns:
574
575            {
576                message: str,
577                orderId: str,
578                accountId: str,
579                orderRequestStatus: str
580            }
581        """
582
583        return self.__call(
584            HttpMethod.POST,
585            Route.ORDER_PLACE_PATH_SELL_FUND.value,
586            {"orderbookId": order_book_id, "accountId": account_id, "volume": volume},
587        )

Place a sell order for a fund

Returns:

{
    message: str,
    orderId: str,
    accountId: str,
    orderRequestStatus: str
}
def place_stop_loss_order( self, parent_stop_loss_id: str, account_id: str, order_book_id: str, stop_loss_trigger: avanza.entities.StopLossTrigger, stop_loss_order_event: avanza.entities.StopLossOrderEvent):
589    def place_stop_loss_order(
590        self,
591        parent_stop_loss_id: str,
592        account_id: str,
593        order_book_id: str,
594        stop_loss_trigger: StopLossTrigger,
595        stop_loss_order_event: StopLossOrderEvent,
596    ):
597        """Place an stop loss order
598
599        Args:
600
601            parent_stop_loss_id: The id of the parent stop loss order. If this is the first stop loss order, this should be "0".
602
603            account_id: A valid account id.
604
605            order_book_id: The order book id of the instrument to place the stop loss order for.
606
607            stop_loss_trigger: The stop loss trigger type.
608
609            stop_loss_order_event: The stop loss order event type.
610
611        Returns:
612
613            If the order was successfully placed:
614
615            {
616                status: 'SUCCESS',
617                stoplossOrderId: str
618            }
619
620            If the order was not placed:
621
622            {
623                status: str,
624                stoplossOrderId: str
625            }
626        """
627
628        return self.__call(
629            HttpMethod.POST,
630            Route.ORDER_PLACE_STOP_LOSS_PATH.value,
631            {
632                "parentStopLossId": parent_stop_loss_id,
633                "accountId": account_id,
634                "orderBookId": order_book_id,
635                "stopLossTrigger": {
636                    "type": stop_loss_trigger.type.value,
637                    "value": stop_loss_trigger.value,
638                    "validUntil": stop_loss_trigger.valid_until.isoformat(),
639                    "valueType": stop_loss_trigger.value_type.value,
640                    "triggerOnMarketMakerQuote": stop_loss_trigger.trigger_on_market_maker_quote,
641                },
642                "stopLossOrderEvent": {
643                    "type": stop_loss_order_event.type.value,
644                    "price": stop_loss_order_event.price,
645                    "volume": stop_loss_order_event.volume,
646                    "validDays": stop_loss_order_event.valid_days,
647                    "priceType": stop_loss_order_event.price_type.value,
648                    "shortSellingAllowed": stop_loss_order_event.short_selling_allowed,
649                },
650            },
651        )

Place an stop loss order

Args:

parent_stop_loss_id: The id of the parent stop loss order. If this is the first stop loss order, this should be "0".

account_id: A valid account id.

order_book_id: The order book id of the instrument to place the stop loss order for.

stop_loss_trigger: The stop loss trigger type.

stop_loss_order_event: The stop loss order event type.

Returns:

If the order was successfully placed:

{
    status: 'SUCCESS',
    stoplossOrderId: str
}

If the order was not placed:

{
    status: str,
    stoplossOrderId: str
}
def edit_order( self, order_id: str, account_id: str, price: float, valid_until: datetime.date, volume: int):
653    def edit_order(
654        self,
655        order_id: str,
656        account_id: str,
657        price: float,
658        valid_until: date,
659        volume: int,
660    ):
661        """Update an existing order
662
663        Returns:
664
665            {
666                orderRequestStatus: str,
667                message: str,
668                parameters: List[str],
669                orderId: str
670            }
671        """
672
673        return self.__call(
674            HttpMethod.POST,
675            Route.ORDER_EDIT_PATH.value,
676            {
677                "accountId": account_id,
678                "metadata": {"orderEntryMode": "STANDARD"},
679                "openVolume": None,
680                "orderId": order_id,
681                "price": price,
682                "validUntil": valid_until.isoformat(),
683                "volume": volume,
684            },
685        )

Update an existing order

Returns:

{
    orderRequestStatus: str,
    message: str,
    parameters: List[str],
    orderId: str
}
def get_order(self, account_id: str, order_id: str):
687    def get_order(self, account_id: str, order_id: str):
688        """Get an existing order
689
690        Returns:
691
692            {
693                "orderId": str,
694                "orderbookId": str,
695                "side": str,
696                "state": str,
697                "marketReference": str,
698                "price": float,
699                "message": str,
700                "volume": int,
701                "originalVolume": int,
702                "accountId": str,
703                "condition": str,
704                "validUntil": str,
705                "modifiable": bool,
706                "deletable": bool,
707            }
708        """
709
710        return self.__call(
711            HttpMethod.GET,
712            Route.ORDER_GET_PATH.value.format(
713                order_id,
714                account_id,
715            ),
716        )

Get an existing order

Returns:

{
    "orderId": str,
    "orderbookId": str,
    "side": str,
    "state": str,
    "marketReference": str,
    "price": float,
    "message": str,
    "volume": int,
    "originalVolume": int,
    "accountId": str,
    "condition": str,
    "validUntil": str,
    "modifiable": bool,
    "deletable": bool,
}
def get_all_stop_losses(self):
718    def get_all_stop_losses(self):
719        """Get open stop losses
720
721        Returns:
722
723            [{
724                "id": str,
725                "status": str,
726                "account": {
727                    "id": str,
728                    "name": str,
729                    "type": str,
730                    "urlParameterId": str
731                },
732                "orderbook": {
733                    "id": str,
734                    "name": str,
735                    "countryCode": str,
736                    "currency": str,
737                    "shortName": str,
738                    "type": str
739                },
740                "hasExcludingChildren": bool,
741                "message": str,
742                "trigger": {
743                    "value": int,
744                    "type": str,
745                    "validUntil": str,
746                    "valueType": str
747                },
748                "order": {
749                    "type": str,
750                    "price": int,
751                    "volume": int,
752                    "shortSellingAllowed": bool,
753                    "validDays": int,
754                    "priceType": str,
755                    "priceDecimalPrecision": 0
756                },
757                "editable": bool,
758                "deletable": bool
759            }]
760        """
761        return self.__call(HttpMethod.GET, Route.STOP_LOSS_PATH.value)

Get open stop losses

Returns:

[{
    "id": str,
    "status": str,
    "account": {
        "id": str,
        "name": str,
        "type": str,
        "urlParameterId": str
    },
    "orderbook": {
        "id": str,
        "name": str,
        "countryCode": str,
        "currency": str,
        "shortName": str,
        "type": str
    },
    "hasExcludingChildren": bool,
    "message": str,
    "trigger": {
        "value": int,
        "type": str,
        "validUntil": str,
        "valueType": str
    },
    "order": {
        "type": str,
        "price": int,
        "volume": int,
        "shortSellingAllowed": bool,
        "validDays": int,
        "priceType": str,
        "priceDecimalPrecision": 0
    },
    "editable": bool,
    "deletable": bool
}]
def delete_stop_loss_order(self, account_id: str, stop_loss_id: str):
763    def delete_stop_loss_order(self, account_id: str, stop_loss_id: str):
764        """delete a stop loss order
765
766        Args:
767
768            stop_loss_id: The id of the stop loss order to delete.
769
770            account_id: A valid account id.
771
772        Returns:
773            Nothing
774        """
775
776        return self.__call(
777            HttpMethod.DELETE,
778            Route.ORDER_DELETE_STOP_LOSS_PATH.value.format(
779                account_id,
780                stop_loss_id,
781            ),
782        )

delete a stop loss order

Args:

stop_loss_id: The id of the stop loss order to delete.

account_id: A valid account id.

Returns: Nothing

def delete_order(self, account_id: str, order_id: str):
784    def delete_order(self, account_id: str, order_id: str):
785        """Delete an existing order
786
787        Returns:
788
789            {
790                messages: str,
791                orderId: str,
792                parameters: List[str],
793                orderRequestStatus: str
794            }
795        """
796        return self.__call(
797            HttpMethod.POST,
798            Route.ORDER_DELETE_PATH.value,
799            {"accountId": account_id, "orderId": order_id},
800        )

Delete an existing order

Returns:

{
    messages: str,
    orderId: str,
    parameters: List[str],
    orderRequestStatus: str
}
def get_monthly_savings_by_account_id(self, account_id: str):
802    def get_monthly_savings_by_account_id(self, account_id: str):
803        """Get monthly savings at avanza for specific account
804
805        Returns:
806
807            {
808                'monthlySavings': [{
809                    'account': {
810                        'id': str,
811                        'name': str,
812                        'type': str
813                    },
814                    'amount': float,
815                    'cash': {'amount': float, 'percent': float},
816                    'externalAccount': {
817                        'accountNumber': str,
818                        'bankName': str,
819                        'clearingNumber': str
820                    },
821                    'fundDistributions': [{
822                        'amount': float,
823                        'orderbook': {
824                            'buyable': bool,
825                            'id': str,
826                            'name': str
827                        },
828                        'percent': float
829                    },
830                    'id': str,
831                    'name': str,
832                    'purchaseDay': int,
833                    'status': str,
834                    'transferDay': int
835                }],
836                'totalAmount': float
837            }
838        """
839
840        return self.__call(
841            HttpMethod.GET, Route.MONTHLY_SAVINGS_PATH.value.format(account_id)
842        )

Get monthly savings at avanza for specific account

Returns:

{
    'monthlySavings': [{
        'account': {
            'id': str,
            'name': str,
            'type': str
        },
        'amount': float,
        'cash': {'amount': float, 'percent': float},
        'externalAccount': {
            'accountNumber': str,
            'bankName': str,
            'clearingNumber': str
        },
        'fundDistributions': [{
            'amount': float,
            'orderbook': {
                'buyable': bool,
                'id': str,
                'name': str
            },
            'percent': float
        },
        'id': str,
        'name': str,
        'purchaseDay': int,
        'status': str,
        'transferDay': int
    }],
    'totalAmount': float
}
def get_all_monthly_savings(self):
844    def get_all_monthly_savings(self):
845        """Get your monthly savings at Avanza
846
847        Returns:
848
849            {
850                'monthlySavings': [{
851                    'account': {
852                        'id': str,
853                        'name': str,
854                        'type': str
855                    },
856                    'amount': float,
857                    'cash': {'amount': float, 'percent': float},
858                    'externalAccount': {
859                        'accountNumber': str,
860                        'bankName': str,
861                        'clearingNumber': str
862                    },
863                    'fundDistributions': [{
864                        'amount': float,
865                        'orderbook': {
866                            'buyable': bool,
867                            'id': str,
868                            'name': str
869                        },
870                        'percent': float
871                    },
872                    'id': str,
873                    'name': str,
874                    'purchaseDay': int,
875                    'status': str,
876                    'transferDay': int
877                }],
878                'totalAmount': float
879            }
880        """
881
882        return self.__call(HttpMethod.GET, Route.MONTHLY_SAVINGS_PATH.value.format(""))

Get your monthly savings at Avanza

Returns:

{
    'monthlySavings': [{
        'account': {
            'id': str,
            'name': str,
            'type': str
        },
        'amount': float,
        'cash': {'amount': float, 'percent': float},
        'externalAccount': {
            'accountNumber': str,
            'bankName': str,
            'clearingNumber': str
        },
        'fundDistributions': [{
            'amount': float,
            'orderbook': {
                'buyable': bool,
                'id': str,
                'name': str
            },
            'percent': float
        },
        'id': str,
        'name': str,
        'purchaseDay': int,
        'status': str,
        'transferDay': int
    }],
    'totalAmount': float
}
def pause_monthly_saving(self, account_id: str, monthly_savings_id: str):
884    def pause_monthly_saving(self, account_id: str, monthly_savings_id: str):
885        """Pause an active monthly saving
886
887        Returns:
888            'OK'
889
890        """
891
892        return self.__call(
893            HttpMethod.PUT,
894            Route.MONTHLY_SAVINGS_PAUSE_PATH.value.format(
895                account_id, monthly_savings_id
896            ),
897        )

Pause an active monthly saving

Returns: 'OK'

def resume_monthly_saving(self, account_id: str, monthly_savings_id: str):
899    def resume_monthly_saving(self, account_id: str, monthly_savings_id: str):
900        """Resume a paused monthly saving
901
902        Returns:
903            'OK'
904
905        """
906
907        return self.__call(
908            HttpMethod.PUT,
909            Route.MONTHLY_SAVINGS_RESUME_PATH.value.format(
910                account_id, monthly_savings_id
911            ),
912        )

Resume a paused monthly saving

Returns: 'OK'

def delete_monthly_saving(self, account_id: str, monthly_savings_id: str) -> None:
914    def delete_monthly_saving(self, account_id: str, monthly_savings_id: str) -> None:
915        """Deletes a monthly saving
916
917        Returns:
918            None
919
920        """
921
922        return self.__call(
923            HttpMethod.DELETE,
924            Route.MONTHLY_SAVINGS_REMOVE_PATH.value.format(
925                account_id, monthly_savings_id
926            ),
927        )

Deletes a monthly saving

Returns: None

def create_monthly_saving( self, account_id: str, amount: int, transfer_day_of_month: int, purchase_day_of_month: int, clearing_and_account_number: str, fund_distribution: Dict[str, int]):
 929    def create_monthly_saving(
 930        self,
 931        account_id: str,
 932        amount: int,
 933        transfer_day_of_month: int,
 934        purchase_day_of_month: int,
 935        clearing_and_account_number: str,
 936        fund_distribution: Dict[str, int],
 937    ):
 938        """Create a monthly saving at Avanza
 939
 940        Args:
 941
 942            account_id: The Avanza account to which the withdrawn money should be transferred to
 943
 944            amount: minimum amount 100 (SEK)
 945                the amount that should be withdrawn from the external account every month
 946
 947            transfer_day_of_month: valid range (1-31)
 948                when the money should be withdrawn from the external account
 949
 950            purchase_day_of_month: valid range (1-31)
 951                when the funds should be purchased,
 952                must occur after the transfer_day_of_month
 953
 954            clearing_and_account_number: The external account from which the money for the monthly savings should be withdrawn from,
 955                has to be formatted as follows 'XXXX-XXXXXXXXX'
 956
 957            fund_distrubution: the percentage distribution of the funds
 958                The key is the funds id and the value is the distribution of the amount in a whole percentage
 959                The sum of the percentages has to total 100
 960
 961                Examples:
 962                    {'41567': 100}
 963                    {'41567': 50, '878733': 50}
 964                    {'41567': 25, '878733': 75}
 965
 966        Returns:
 967
 968            {
 969                'monthlySavingId': str,
 970                'status': str
 971            }
 972
 973            monthlySavingId has the following format: 'XX^XXXXXXXXXXXXX^XXXXXX'
 974            status should have the value 'ACCEPTED' if the monthly saving was created successfully
 975        """
 976
 977        if not 1 <= transfer_day_of_month <= 31:
 978            raise ValueError(
 979                "transfer_day_of_month is outside the valid range of (1-31)"
 980            )
 981
 982        if not 1 <= purchase_day_of_month <= 31:
 983            raise ValueError(
 984                "purchase_day_of_month is outside the valid range of (1-31)"
 985            )
 986
 987        if transfer_day_of_month >= purchase_day_of_month:
 988            raise ValueError(
 989                "transfer_day_of_month must occur before purchase_day_of_month"
 990            )
 991
 992        if len(fund_distribution) == 0:
 993            raise ValueError("No founds were specified in the fund_distribution")
 994
 995        if sum(fund_distribution.values()) != 100:
 996            raise ValueError("The fund_distribution values must total 100")
 997
 998        return self.__call(
 999            HttpMethod.POST,
1000            Route.MONTHLY_SAVINGS_CREATE_PATH.value.format(account_id),
1001            {
1002                "amount": amount,
1003                "autogiro": {
1004                    "dayOfMonth": transfer_day_of_month,
1005                    "externalClearingAndAccount": clearing_and_account_number,
1006                },
1007                "fundDistribution": {
1008                    "dayOfMonth": purchase_day_of_month,
1009                    "fundDistributions": fund_distribution,
1010                },
1011            },
1012        )

Create a monthly saving at Avanza

Args:

account_id: The Avanza account to which the withdrawn money should be transferred to

amount: minimum amount 100 (SEK)
    the amount that should be withdrawn from the external account every month

transfer_day_of_month: valid range (1-31)
    when the money should be withdrawn from the external account

purchase_day_of_month: valid range (1-31)
    when the funds should be purchased,
    must occur after the transfer_day_of_month

clearing_and_account_number: The external account from which the money for the monthly savings should be withdrawn from,
    has to be formatted as follows 'XXXX-XXXXXXXXX'

fund_distrubution: the percentage distribution of the funds
    The key is the funds id and the value is the distribution of the amount in a whole percentage
    The sum of the percentages has to total 100

    Examples:
        {'41567': 100}
        {'41567': 50, '878733': 50}
        {'41567': 25, '878733': 75}

Returns:

{
    'monthlySavingId': str,
    'status': str
}

monthlySavingId has the following format: 'XX^XXXXXXXXXXXXX^XXXXXX'
status should have the value 'ACCEPTED' if the monthly saving was created successfully
def get_transactions_details( self, transaction_details_types: Optional[Sequence[avanza.constants.TransactionsDetailsType]] = [], transactions_from: Optional[datetime.date] = None, transactions_to: Optional[datetime.date] = None, isin: Optional[str] = None, max_elements: Optional[int] = 1000) -> avanza.models.transaction.Transactions:
1014    def get_transactions_details(
1015        self,
1016        transaction_details_types: Optional[Sequence[TransactionsDetailsType]] = [],
1017        transactions_from: Optional[date] = None,
1018        transactions_to: Optional[date] = None,
1019        isin: Optional[str] = None,
1020        max_elements: Optional[int] = 1000,
1021    ) -> Transactions:
1022        """Get transactions, optionally apply criteria.
1023
1024        Args:
1025
1026            transaction_types: One or more transaction types.
1027
1028            transactions_from: Fetch transactions from this date.
1029
1030            transactions_to: Fetch transactions to this date.
1031
1032            isin: Only fetch transactions for specified isin.
1033
1034            max_elements: Limit result to N transactions.
1035        """
1036        options = {}
1037        options["maxElements"] = max_elements
1038
1039        if transaction_details_types:
1040            options["transactionTypes"] = ",".join(
1041                [type.value for type in transaction_details_types]
1042            )
1043        if transactions_from:
1044            options["from"] = transactions_from.isoformat()
1045        if transactions_to:
1046            options["to"] = transactions_to.isoformat()
1047        if isin:
1048            options["isin"] = isin
1049
1050        return self.__call(
1051            HttpMethod.GET, Route.TRANSACTIONS_DETAILS_PATH.value, options
1052        )

Get transactions, optionally apply criteria.

Args:

transaction_types: One or more transaction types.

transactions_from: Fetch transactions from this date.

transactions_to: Fetch transactions to this date.

isin: Only fetch transactions for specified isin.

max_elements: Limit result to N transactions.
def get_note_as_pdf(self, url_parameter_id: str, note_id: str):
1054    def get_note_as_pdf(self, url_parameter_id: str, note_id: str):
1055        return self.__call(
1056            HttpMethod.GET,
1057            Route.NOTE_PATH.value.format(url_parameter_id, note_id),
1058            return_content=True,
1059        )
def set_price_alert( self, order_book_id: str, price: float, valid_until: datetime.date, notification: bool = True, email: bool = False, sms: bool = False):
1061    def set_price_alert(
1062        self,
1063        order_book_id: str,
1064        price: float,
1065        valid_until: date,
1066        notification: bool = True,
1067        email: bool = False,
1068        sms: bool = False,
1069    ):
1070        """
1071        Sets a price alert for the specified orderbook and returns all the existing alerts.
1072
1073        Returns:
1074
1075            [
1076                {
1077                    'alertId': str,
1078                    'accountId': str,
1079                    'price': float,
1080                    'validUntil': str,
1081                    'direction': str,
1082                    'email': bool,
1083                    'notification': bool,
1084                    'sms': bool,
1085                }
1086            ]
1087        """
1088
1089        return self.__call(
1090            HttpMethod.POST,
1091            Route.PRICE_ALERT_PATH.value.format(order_book_id),
1092            {
1093                "price": price,
1094                "validUntil": valid_until.isoformat(),
1095                "notification": notification,
1096                "email": email,
1097                "sms": sms,
1098            },
1099        )

Sets a price alert for the specified orderbook and returns all the existing alerts.

Returns:

[
    {
        'alertId': str,
        'accountId': str,
        'price': float,
        'validUntil': str,
        'direction': str,
        'email': bool,
        'notification': bool,
        'sms': bool,
    }
]
def get_price_alert(self, order_book_id: str) -> List[avanza.models.price_alert.PriceAlert]:
1101    def get_price_alert(self, order_book_id: str) -> List[PriceAlert]:
1102        """Gets all the price alerts for the specified orderbook"""
1103
1104        return self.__call(
1105            HttpMethod.GET,
1106            Route.PRICE_ALERT_PATH.value.format(order_book_id),
1107        )

Gets all the price alerts for the specified orderbook

def delete_price_alert(self, order_book_id: str, alert_id: str):
1109    def delete_price_alert(self, order_book_id: str, alert_id: str):
1110        """
1111        Deletes a price alert from the specified orderbook and returns the remaining alerts.
1112
1113        Returns:
1114
1115            [
1116                {
1117                    'alertId': str,
1118                    'accountId': str,
1119                    'price': float,
1120                    'validUntil': str,
1121                    'direction': str,
1122                    'email': bool,
1123                    'notification': bool,
1124                    'sms': bool,
1125                }
1126            ]
1127        """
1128        return self.__call(
1129            HttpMethod.DELETE,
1130            Route.PRICE_ALERT_PATH.value.format(order_book_id, alert_id)
1131            + f"/{alert_id}",
1132        )

Deletes a price alert from the specified orderbook and returns the remaining alerts.

Returns:

[
    {
        'alertId': str,
        'accountId': str,
        'price': float,
        'validUntil': str,
        'direction': str,
        'email': bool,
        'notification': bool,
        'sms': bool,
    }
]
def get_offers(self) -> List[avanza.models.offer.Offer]:
1134    def get_offers(self) -> List[Offer]:
1135        """Return current offers"""
1136
1137        return self.__call(HttpMethod.GET, Route.CURRENT_OFFERS_PATH.value)

Return current offers