avanza.avanza

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

Args:

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

retry_with_next_otp: If

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

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

quiet: Do not print a status message if waiting for the next TOTP
    time-step window.
async def subscribe_to_id( self, channel: avanza.constants.ChannelType, id: str, callback: Callable[[str, dict], Any]):
212    async def subscribe_to_id(
213        self, channel: ChannelType, id: str, callback: Callable[[str, dict], Any]
214    ):
215        await self.subscribe_to_ids(channel, [id], callback)
async def subscribe_to_ids( self, channel: avanza.constants.ChannelType, ids: Sequence[str], callback: Callable[[str, dict], Any]):
217    async def subscribe_to_ids(
218        self,
219        channel: ChannelType,
220        ids: Sequence[str],
221        callback: Callable[[str, dict], Any],
222    ):
223        if not callable(callback):
224            raise ValueError("callback parameter has to be a function!")
225
226        if not self._socket._connected:
227            await self._socket.init()
228
229        await self._socket.subscribe_to_ids(channel, ids, callback)
def get_overview(self) -> avanza.models.overview.Overview:
231    def get_overview(self) -> Overview:
232        """Get account and category overviews"""
233        return self.__call(HttpMethod.GET, Route.CATEGORIZED_ACCOUNTS.value)

Get account and category overviews

def get_accounts_positions(self) -> avanza.models.account_posititions.AccountPositions:
235    def get_accounts_positions(self) -> AccountPositions:
236        """Get investment positions for all account"""
237
238        return self.__call(HttpMethod.GET, Route.ACCOUNTS_POSITIONS_PATH.value)

Get investment positions for all account

def get_account_performance_chart_data( self, url_parameters_ids: list[str], time_period: avanza.constants.TimePeriod) -> avanza.models.account_posititions.AccountPositions:
240    def get_account_performance_chart_data(
241        self, url_parameters_ids: list[str], time_period: TimePeriod
242    ) -> AccountPositions:
243        """Get performance chart for accounts.
244
245        Args:
246
247            url_parameters_ids: Scrambled account ids.
248                Can be found in the overview response.
249
250            time_period: Time period to get chart data for
251
252        """
253        return self.__call(
254            HttpMethod.POST,
255            Route.ACCOUNT_PERFORMANCE_CHART_PATH.value,
256            {
257                "scrambledAccountIds": url_parameters_ids,
258                "timePeriod": time_period.value,
259            },
260        )

Get performance chart for accounts.

Args:

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

time_period: Time period to get chart data for
def get_watchlists(self) -> List[avanza.models.watch_list.WatchList]:
262    def get_watchlists(self) -> List[WatchList]:
263        """Get your "Bevakningslistor" """
264        return self.__call(HttpMethod.GET, Route.WATCHLISTS_PATH.value)

Get your "Bevakningslistor"

def add_to_watchlist(self, instrument_id: str, watchlist_id: str) -> None:
266    def add_to_watchlist(self, instrument_id: str, watchlist_id: str) -> None:
267        """Add an instrument to the specified watchlist
268
269        This function returns None if the request was 200 OK,
270        but there is no guarantee that the instrument was added to the list,
271        verify this by calling get_watchlists()
272        """
273        return self.__call(
274            HttpMethod.POST,
275            Route.WATCHLISTS_ADD_PATH.value.format(watchlist_id, instrument_id),
276        )

Add an instrument to the specified watchlist

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

def remove_from_watchlist(self, instrument_id: str, watchlist_id: str) -> None:
278    def remove_from_watchlist(self, instrument_id: str, watchlist_id: str) -> None:
279        """Remove an instrument to the specified watchlist
280
281        This function returns None if the request was 200 OK,
282        but there is no guarantee that the instrument was removed from the list,
283        verify this by calling get_watchlists()
284        """
285        return self.__call(
286            HttpMethod.POST,
287            Route.WATCHLISTS_REMOVE_PATH.value.format(watchlist_id, instrument_id),
288        )

Remove an instrument to the specified watchlist

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

def get_fund_info(self, fund_id: str) -> avanza.models.fund_info.FundInfo:
290    def get_fund_info(self, fund_id: str) -> FundInfo:
291        """Get info about a fund"""
292
293        return self.__call(HttpMethod.GET, Route.FUND_PATH.value.format(fund_id))

Get info about a fund

def get_stock_info(self, stock_id: str) -> avanza.models.stock_info.StockInfo:
295    def get_stock_info(self, stock_id: str) -> StockInfo:
296        """Returns info about a stock"""
297
298        return self.get_instrument(InstrumentType.STOCK, stock_id)

Returns info about a stock

def get_certificate_info( self, certificate_id: str) -> avanza.models.certificate_info.CertificateInfo:
300    def get_certificate_info(self, certificate_id: str) -> CertificateInfo:
301        """Returns info about a certificate"""
302
303        return self.get_instrument(InstrumentType.CERTIFICATE, certificate_id)

Returns info about a certificate

def get_certificate_details( self, certificate_id: str) -> avanza.models.certificate_details.CertificateDetails:
305    def get_certificate_details(self, certificate_id: str) -> CertificateDetails:
306        """Returns additional info about a certificate"""
307
308        return self.get_instrument_details(InstrumentType.CERTIFICATE, certificate_id)

Returns additional info about a certificate

def get_etf_details(self, etf_id: str) -> avanza.models.etf_details.EtfDetails:
310    def get_etf_details(self, etf_id: str) -> EtfDetails:
311        return self.get_instrument_details(InstrumentType.EXCHANGE_TRADED_FUND, etf_id)
def get_warrant_info(self, warrant_id: str) -> avanza.models.warrant_info.WarrantInfo:
313    def get_warrant_info(self, warrant_id: str) -> WarrantInfo:
314        """Returns info about a warrant"""
315
316        return self.get_instrument(InstrumentType.WARRANT, warrant_id)

Returns info about a warrant

def get_index_info(self, index_id: str) -> avanza.models.index_info.IndexInfo:
318    def get_index_info(self, index_id: str) -> IndexInfo:
319        """Returns info about an index"""
320
321        # Works when sending InstrumentType.STOCK, but not InstrumentType.INDEX
322        return self.get_instrument(InstrumentType.STOCK, index_id)

Returns info about an index

def get_analysis(self, instrument_id: str):
324    def get_analysis(self, instrument_id: str):
325        """Returns analysis data for an instrument"""
326
327        return self.__call(
328            HttpMethod.GET, Route.ANALYSIS_PATH.value.format(instrument_id)
329        )

Returns analysis data for an instrument

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

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

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

Get additional instrument info

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

Search for a stock

Args:

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

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

Search for a fund

Args:

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

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

Search for a certificate

Args:

query: can be a ISIN, name or tickerSymbol

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

Search for a warrant

Args:

query: can be a ISIN, name or tickerSymbol

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

Search for a specific instrument

Args:

instrument_type: can be STOCK, FUND, BOND etc

query: can be a ISIN, name or tickerSymbol

limit: maximum number of results to return
def get_order_books( self, order_book_ids: Sequence[str]) -> List[avanza.models.order_book.OrderBook]:
447    def get_order_books(self, order_book_ids: Sequence[str]) -> List[OrderBook]:
448        """Get info about multiple order books"""
449
450        return self.__call(
451            HttpMethod.GET,
452            Route.ORDERBOOK_LIST_PATH.value.format(",".join(order_book_ids)),
453        )

Get info about multiple order books

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

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

def get_deals(self):
464    def get_deals(self):
465        """Get currently active deals"""
466        return self.__call(HttpMethod.GET, Route.DEALS_PATH.value)

Get currently active deals

def get_orders(self):
468    def get_orders(self):
469        """Get currently active orders"""
470        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]:
472    def get_inspiration_lists(self) -> List[InspirationListItem]:
473        """Get all available inspiration lists
474
475        This returns lists similar to the ones found on:
476
477        https://www.avanza.se/aktier/aktieinspiration.html
478
479        https://www.avanza.se/fonder/fondinspiration.html
480
481        """
482        return self.__call(HttpMethod.GET, Route.INSPIRATION_LIST_PATH.value.format(""))

Get all available inspiration lists

This returns lists similar to the ones found on:

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

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

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

Get inspiration list

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

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

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

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

Place an order

Returns:

If the order was successfully placed:

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

If the order was not placed:

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

Place a buy order for a fund

Returns:

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

Place a sell order for a fund

Returns:

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

Place an stop loss order

Args:

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

account_id: A valid account id.

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

stop_loss_trigger: The stop loss trigger type.

stop_loss_order_event: The stop loss order event type.

Returns:

If the order was successfully placed:

{
    status: 'SUCCESS',
    stoplossOrderId: str
}

If the order was not placed:

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

Update an existing order

Returns:

{
    orderRequestStatus: str,
    message: str,
    parameters: List[str],
    orderId: str
}
def get_order(self, account_id: str, order_id: str):
692    def get_order(self, account_id: str, order_id: str):
693        """Get an existing order
694
695        Returns:
696
697            {
698                'account': {
699                    'buyingPower': float,
700                    'id': str,
701                    'name': str,
702                    'totalBalance': float,
703                    'type': str
704                },
705                'brokerTradeSummary': {
706                    'items': [{
707                        'brokerCode': str,
708                        'buyVolume': int,
709                        'netBuyVolume': int,
710                        'sellVolume': int
711                    }],
712                    'orderbookId': str
713                },
714                'customer': {
715                    'courtageClass': str,
716                    'showCourtageClassInfoOnOrderPage': bool
717                },
718                'firstTradableDate': str,
719                'hasInstrumentKnowledge': bool,
720                'hasInvestmentFees': {'buy': bool, 'sell': bool},
721                'hasShortSellKnowledge': bool,
722                'lastTradableDate': str,
723                'latestTrades': [{
724                    'buyer': str,
725                    'cancelled': bool,
726                    'dealTime': str,
727                    'matchedOnMarket': bool,
728                    'price': float,
729                    'seller': str,
730                    'volume': int
731                }],
732                'marketMakerExpected': bool,
733                'marketTrades': bool,
734                'order': {
735                    'orderCondition': str,
736                    'orderType': str,
737                    'price': float,
738                    'validUntil': str,
739                    'volume': int
740                },
741                'orderDepthLevels': List,
742                'orderDepthReceivedTime': str,
743                'orderbook': {
744                    'change': float,
745                    'changePercent': float,
746                    'currency': str,
747                    'exchangeRate': float,
748                    'flagCode': str,
749                    'highestPrice': float,
750                    'id': str,
751                    'lastPrice': float,
752                    'lastPriceUpdated': str,
753                    'lowestPrice': float,
754                    'name': str,
755                    'positionVolume': float,
756                    'tickerSymbol': str,
757                    'totalValueTraded': float,
758                    'totalVolumeTraded': float,
759                    'tradable': bool,
760                    'tradingUnit': int,
761                    'type': str,
762                    'volumeFactor': float
763                },
764                'tickSizeRules': [{
765                    'maxPrice': int,
766                    'minPrice': int,
767                    'tickSize': int
768                }],
769                'untradableDates': List[str]
770            }
771        """
772
773        return self.__call(
774            HttpMethod.GET,
775            Route.ORDER_GET_PATH.value.format(
776                # Have tried this with three different instrument types
777                # (STOCK, FUND, CERTIFICATE)
778                # and it only seems to work when sending the instrument type
779                # as STOCK
780                InstrumentType.STOCK.value,
781                account_id,
782                order_id,
783            ),
784        )

Get an existing order

Returns:

{
    'account': {
        'buyingPower': float,
        'id': str,
        'name': str,
        'totalBalance': float,
        'type': str
    },
    'brokerTradeSummary': {
        'items': [{
            'brokerCode': str,
            'buyVolume': int,
            'netBuyVolume': int,
            'sellVolume': int
        }],
        'orderbookId': str
    },
    'customer': {
        'courtageClass': str,
        'showCourtageClassInfoOnOrderPage': bool
    },
    'firstTradableDate': str,
    'hasInstrumentKnowledge': bool,
    'hasInvestmentFees': {'buy': bool, 'sell': bool},
    'hasShortSellKnowledge': bool,
    'lastTradableDate': str,
    'latestTrades': [{
        'buyer': str,
        'cancelled': bool,
        'dealTime': str,
        'matchedOnMarket': bool,
        'price': float,
        'seller': str,
        'volume': int
    }],
    'marketMakerExpected': bool,
    'marketTrades': bool,
    'order': {
        'orderCondition': str,
        'orderType': str,
        'price': float,
        'validUntil': str,
        'volume': int
    },
    'orderDepthLevels': List,
    'orderDepthReceivedTime': str,
    'orderbook': {
        'change': float,
        'changePercent': float,
        'currency': str,
        'exchangeRate': float,
        'flagCode': str,
        'highestPrice': float,
        'id': str,
        'lastPrice': float,
        'lastPriceUpdated': str,
        'lowestPrice': float,
        'name': str,
        'positionVolume': float,
        'tickerSymbol': str,
        'totalValueTraded': float,
        'totalVolumeTraded': float,
        'tradable': bool,
        'tradingUnit': int,
        'type': str,
        'volumeFactor': float
    },
    'tickSizeRules': [{
        'maxPrice': int,
        'minPrice': int,
        'tickSize': int
    }],
    'untradableDates': List[str]
}
def get_all_stop_losses(self):
786    def get_all_stop_losses(self):
787        """Get open stop losses
788
789        Returns:
790
791            [{
792                "id": str,
793                "status": str,
794                "account": {
795                    "id": str,
796                    "name": str,
797                    "type": str,
798                    "urlParameterId": str
799                },
800                "orderbook": {
801                    "id": str,
802                    "name": str,
803                    "countryCode": str,
804                    "currency": str,
805                    "shortName": str,
806                    "type": str
807                },
808                "hasExcludingChildren": bool,
809                "message": str,
810                "trigger": {
811                    "value": int,
812                    "type": str,
813                    "validUntil": str,
814                    "valueType": str
815                },
816                "order": {
817                    "type": str,
818                    "price": int,
819                    "volume": int,
820                    "shortSellingAllowed": bool,
821                    "validDays": int,
822                    "priceType": str,
823                    "priceDecimalPrecision": 0
824                },
825                "editable": bool,
826                "deletable": bool
827            }]
828        """
829        return self.__call(HttpMethod.GET, Route.STOP_LOSS_PATH.value)

Get open stop losses

Returns:

[{
    "id": str,
    "status": str,
    "account": {
        "id": str,
        "name": str,
        "type": str,
        "urlParameterId": str
    },
    "orderbook": {
        "id": str,
        "name": str,
        "countryCode": str,
        "currency": str,
        "shortName": str,
        "type": str
    },
    "hasExcludingChildren": bool,
    "message": str,
    "trigger": {
        "value": int,
        "type": str,
        "validUntil": str,
        "valueType": str
    },
    "order": {
        "type": str,
        "price": int,
        "volume": int,
        "shortSellingAllowed": bool,
        "validDays": int,
        "priceType": str,
        "priceDecimalPrecision": 0
    },
    "editable": bool,
    "deletable": bool
}]
def delete_stop_loss_order(self, account_id: str, stop_loss_id: str):
831    def delete_stop_loss_order(self, account_id: str, stop_loss_id: str):
832        """delete a stop loss order
833
834        Args:
835
836            stop_loss_id: The id of the stop loss order to delete.
837
838            account_id: A valid account id.
839
840        Returns:
841            Nothing
842        """
843
844        return self.__call(
845            HttpMethod.DELETE,
846            Route.ORDER_DELETE_STOP_LOSS_PATH.value.format(
847                account_id,
848                stop_loss_id,
849            ),
850        )

delete a stop loss order

Args:

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

account_id: A valid account id.

Returns: Nothing

def delete_order(self, account_id: str, order_id: str):
852    def delete_order(self, account_id: str, order_id: str):
853        """Delete an existing order
854
855        Returns:
856
857            {
858                messages: str,
859                orderId: str,
860                parameters: List[str],
861                orderRequestStatus: str
862            }
863        """
864        return self.__call(
865            HttpMethod.POST,
866            Route.ORDER_DELETE_PATH.value,
867            {"accountId": account_id, "orderId": order_id},
868        )

Delete an existing order

Returns:

{
    messages: str,
    orderId: str,
    parameters: List[str],
    orderRequestStatus: str
}
def get_monthly_savings_by_account_id(self, account_id: str):
870    def get_monthly_savings_by_account_id(self, account_id: str):
871        """Get monthly savings at avanza for specific account
872
873        Returns:
874
875            {
876                'monthlySavings': [{
877                    'account': {
878                        'id': str,
879                        'name': str,
880                        'type': str
881                    },
882                    'amount': float,
883                    'cash': {'amount': float, 'percent': float},
884                    'externalAccount': {
885                        'accountNumber': str,
886                        'bankName': str,
887                        'clearingNumber': str
888                    },
889                    'fundDistributions': [{
890                        'amount': float,
891                        'orderbook': {
892                            'buyable': bool,
893                            'id': str,
894                            'name': str
895                        },
896                        'percent': float
897                    },
898                    'id': str,
899                    'name': str,
900                    'purchaseDay': int,
901                    'status': str,
902                    'transferDay': int
903                }],
904                'totalAmount': float
905            }
906        """
907
908        return self.__call(
909            HttpMethod.GET, Route.MONTHLY_SAVINGS_PATH.value.format(account_id)
910        )

Get monthly savings at avanza for specific account

Returns:

{
    'monthlySavings': [{
        'account': {
            'id': str,
            'name': str,
            'type': str
        },
        'amount': float,
        'cash': {'amount': float, 'percent': float},
        'externalAccount': {
            'accountNumber': str,
            'bankName': str,
            'clearingNumber': str
        },
        'fundDistributions': [{
            'amount': float,
            'orderbook': {
                'buyable': bool,
                'id': str,
                'name': str
            },
            'percent': float
        },
        'id': str,
        'name': str,
        'purchaseDay': int,
        'status': str,
        'transferDay': int
    }],
    'totalAmount': float
}
def get_all_monthly_savings(self):
912    def get_all_monthly_savings(self):
913        """Get your monthly savings at Avanza
914
915        Returns:
916
917            {
918                'monthlySavings': [{
919                    'account': {
920                        'id': str,
921                        'name': str,
922                        'type': str
923                    },
924                    'amount': float,
925                    'cash': {'amount': float, 'percent': float},
926                    'externalAccount': {
927                        'accountNumber': str,
928                        'bankName': str,
929                        'clearingNumber': str
930                    },
931                    'fundDistributions': [{
932                        'amount': float,
933                        'orderbook': {
934                            'buyable': bool,
935                            'id': str,
936                            'name': str
937                        },
938                        'percent': float
939                    },
940                    'id': str,
941                    'name': str,
942                    'purchaseDay': int,
943                    'status': str,
944                    'transferDay': int
945                }],
946                'totalAmount': float
947            }
948        """
949
950        return self.__call(HttpMethod.GET, Route.MONTHLY_SAVINGS_PATH.value.format(""))

Get your monthly savings at Avanza

Returns:

{
    'monthlySavings': [{
        'account': {
            'id': str,
            'name': str,
            'type': str
        },
        'amount': float,
        'cash': {'amount': float, 'percent': float},
        'externalAccount': {
            'accountNumber': str,
            'bankName': str,
            'clearingNumber': str
        },
        'fundDistributions': [{
            'amount': float,
            'orderbook': {
                'buyable': bool,
                'id': str,
                'name': str
            },
            'percent': float
        },
        'id': str,
        'name': str,
        'purchaseDay': int,
        'status': str,
        'transferDay': int
    }],
    'totalAmount': float
}
def pause_monthly_saving(self, account_id: str, monthly_savings_id: str):
952    def pause_monthly_saving(self, account_id: str, monthly_savings_id: str):
953        """Pause an active monthly saving
954
955        Returns:
956            'OK'
957
958        """
959
960        return self.__call(
961            HttpMethod.PUT,
962            Route.MONTHLY_SAVINGS_PAUSE_PATH.value.format(
963                account_id, monthly_savings_id
964            ),
965        )

Pause an active monthly saving

Returns: 'OK'

def resume_monthly_saving(self, account_id: str, monthly_savings_id: str):
967    def resume_monthly_saving(self, account_id: str, monthly_savings_id: str):
968        """Resume a paused monthly saving
969
970        Returns:
971            'OK'
972
973        """
974
975        return self.__call(
976            HttpMethod.PUT,
977            Route.MONTHLY_SAVINGS_RESUME_PATH.value.format(
978                account_id, monthly_savings_id
979            ),
980        )

Resume a paused monthly saving

Returns: 'OK'

def delete_monthly_saving(self, account_id: str, monthly_savings_id: str) -> None:
982    def delete_monthly_saving(self, account_id: str, monthly_savings_id: str) -> None:
983        """Deletes a monthly saving
984
985        Returns:
986            None
987
988        """
989
990        return self.__call(
991            HttpMethod.DELETE,
992            Route.MONTHLY_SAVINGS_REMOVE_PATH.value.format(
993                account_id, monthly_savings_id
994            ),
995        )

Deletes a monthly saving

Returns: None

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]):
 997    def create_monthly_saving(
 998        self,
 999        account_id: str,
1000        amount: int,
1001        transfer_day_of_month: int,
1002        purchase_day_of_month: int,
1003        clearing_and_account_number: str,
1004        fund_distribution: Dict[str, int],
1005    ):
1006        """Create a monthly saving at Avanza
1007
1008        Args:
1009
1010            account_id: The Avanza account to which the withdrawn money should be transferred to
1011
1012            amount: minimum amount 100 (SEK)
1013                the amount that should be withdrawn from the external account every month
1014
1015            transfer_day_of_month: valid range (1-31)
1016                when the money should be withdrawn from the external account
1017
1018            purchase_day_of_month: valid range (1-31)
1019                when the funds should be purchased,
1020                must occur after the transfer_day_of_month
1021
1022            clearing_and_account_number: The external account from which the money for the monthly savings should be withdrawn from,
1023                has to be formatted as follows 'XXXX-XXXXXXXXX'
1024
1025            fund_distrubution: the percentage distribution of the funds
1026                The key is the funds id and the value is the distribution of the amount in a whole percentage
1027                The sum of the percentages has to total 100
1028
1029                Examples:
1030                    {'41567': 100}
1031                    {'41567': 50, '878733': 50}
1032                    {'41567': 25, '878733': 75}
1033
1034        Returns:
1035
1036            {
1037                'monthlySavingId': str,
1038                'status': str
1039            }
1040
1041            monthlySavingId has the following format: 'XX^XXXXXXXXXXXXX^XXXXXX'
1042            status should have the value 'ACCEPTED' if the monthly saving was created successfully
1043        """
1044
1045        if not 1 <= transfer_day_of_month <= 31:
1046            raise ValueError(
1047                "transfer_day_of_month is outside the valid range of (1-31)"
1048            )
1049
1050        if not 1 <= purchase_day_of_month <= 31:
1051            raise ValueError(
1052                "purchase_day_of_month is outside the valid range of (1-31)"
1053            )
1054
1055        if transfer_day_of_month >= purchase_day_of_month:
1056            raise ValueError(
1057                "transfer_day_of_month must occur before purchase_day_of_month"
1058            )
1059
1060        if len(fund_distribution) == 0:
1061            raise ValueError("No founds were specified in the fund_distribution")
1062
1063        if sum(fund_distribution.values()) != 100:
1064            raise ValueError("The fund_distribution values must total 100")
1065
1066        return self.__call(
1067            HttpMethod.POST,
1068            Route.MONTHLY_SAVINGS_CREATE_PATH.value.format(account_id),
1069            {
1070                "amount": amount,
1071                "autogiro": {
1072                    "dayOfMonth": transfer_day_of_month,
1073                    "externalClearingAndAccount": clearing_and_account_number,
1074                },
1075                "fundDistribution": {
1076                    "dayOfMonth": purchase_day_of_month,
1077                    "fundDistributions": fund_distribution,
1078                },
1079            },
1080        )

Create a monthly saving at Avanza

Args:

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

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

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

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

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

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

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

Returns:

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

monthlySavingId has the following format: 'XX^XXXXXXXXXXXXX^XXXXXX'
status should have the value 'ACCEPTED' if the monthly saving was created successfully
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:
1082    def get_transactions_details(
1083        self,
1084        transaction_details_types: Optional[Sequence[TransactionsDetailsType]] = [],
1085        transactions_from: Optional[date] = None,
1086        transactions_to: Optional[date] = None,
1087        isin: Optional[str] = None,
1088        max_elements: Optional[int] = 1000,
1089    ) -> Transactions:
1090        """Get transactions, optionally apply criteria.
1091
1092        Args:
1093
1094            transaction_types: One or more transaction types.
1095
1096            transactions_from: Fetch transactions from this date.
1097
1098            transactions_to: Fetch transactions to this date.
1099
1100            isin: Only fetch transactions for specified isin.
1101
1102            max_elements: Limit result to N transactions.
1103        """
1104        options = {}
1105        options["maxElements"] = max_elements
1106
1107        if transaction_details_types:
1108            options["transactionTypes"] = ",".join(
1109                [type.value for type in transaction_details_types]
1110            )
1111        if transactions_from:
1112            options["from"] = transactions_from.isoformat()
1113        if transactions_to:
1114            options["to"] = transactions_to.isoformat()
1115        if isin:
1116            options["isin"] = isin
1117
1118        return self.__call(
1119            HttpMethod.GET, Route.TRANSACTIONS_DETAILS_PATH.value, options
1120        )

Get transactions, optionally apply criteria.

Args:

transaction_types: One or more transaction types.

transactions_from: Fetch transactions from this date.

transactions_to: Fetch transactions to this date.

isin: Only fetch transactions for specified isin.

max_elements: Limit result to N transactions.
def get_note_as_pdf(self, url_parameter_id: str, note_id: str):
1122    def get_note_as_pdf(self, url_parameter_id: str, note_id: str):
1123        return self.__call(
1124            HttpMethod.GET,
1125            Route.NOTE_PATH.value.format(url_parameter_id, note_id),
1126            return_content=True,
1127        )
def set_price_alert( self, order_book_id: str, price: float, valid_until: datetime.date, notification: bool = True, email: bool = False, sms: bool = False):
1129    def set_price_alert(
1130        self,
1131        order_book_id: str,
1132        price: float,
1133        valid_until: date,
1134        notification: bool = True,
1135        email: bool = False,
1136        sms: bool = False,
1137    ):
1138        """
1139        Sets a price alert for the specified orderbook and returns all the existing alerts.
1140
1141        Returns:
1142
1143            [
1144                {
1145                    'alertId': str,
1146                    'accountId': str,
1147                    'price': float,
1148                    'validUntil': str,
1149                    'direction': str,
1150                    'email': bool,
1151                    'notification': bool,
1152                    'sms': bool,
1153                }
1154            ]
1155        """
1156
1157        return self.__call(
1158            HttpMethod.POST,
1159            Route.PRICE_ALERT_PATH.value.format(order_book_id),
1160            {
1161                "price": price,
1162                "validUntil": valid_until.isoformat(),
1163                "notification": notification,
1164                "email": email,
1165                "sms": sms,
1166            },
1167        )

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

Returns:

[
    {
        'alertId': str,
        'accountId': str,
        'price': float,
        'validUntil': str,
        'direction': str,
        'email': bool,
        'notification': bool,
        'sms': bool,
    }
]
def get_price_alert(self, order_book_id: str) -> List[avanza.models.price_alert.PriceAlert]:
1169    def get_price_alert(self, order_book_id: str) -> List[PriceAlert]:
1170        """Gets all the price alerts for the specified orderbook"""
1171
1172        return self.__call(
1173            HttpMethod.GET,
1174            Route.PRICE_ALERT_PATH.value.format(order_book_id),
1175        )

Gets all the price alerts for the specified orderbook

def delete_price_alert(self, order_book_id: str, alert_id: str):
1177    def delete_price_alert(self, order_book_id: str, alert_id: str):
1178        """
1179        Deletes a price alert from the specified orderbook and returns the remaining alerts.
1180
1181        Returns:
1182
1183            [
1184                {
1185                    'alertId': str,
1186                    'accountId': str,
1187                    'price': float,
1188                    'validUntil': str,
1189                    'direction': str,
1190                    'email': bool,
1191                    'notification': bool,
1192                    'sms': bool,
1193                }
1194            ]
1195        """
1196        return self.__call(
1197            HttpMethod.DELETE,
1198            Route.PRICE_ALERT_PATH.value.format(order_book_id, alert_id)
1199            + f"/{alert_id}",
1200        )

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

Returns:

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

Return current offers