avanza.avanza

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

Args:

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

retry_with_next_otp: If

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

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

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

Get account and category overviews

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

Get investment positions for all account

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

Get performance chart for accounts.

Args:

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

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

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

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

Get your "Bevakningslistor"

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

Add an instrument to the specified watchlist

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

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

Remove an instrument to the specified watchlist

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

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

Get info about a fund

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

Returns info about a stock

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

Returns info about a certificate

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

Returns additional info about a certificate

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

Returns info about a warrant

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

Returns info about an index

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

Returns analysis data for an instrument

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

Returns latest news data for an instrument

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

Returns latest forum posts for an instrument

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

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

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

Get additional instrument info

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

Search for a stock

Args:

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

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

Search for a fund

Args:

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

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

Search for a certificate

Args:

query: can be a ISIN, name or tickerSymbol

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

Search for a warrant

Args:

query: can be a ISIN, name or tickerSymbol

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

Search for a specific instrument

Args:

instrument_type: can be STOCK, FUND, BOND etc

query: can be a ISIN, name or tickerSymbol

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

Get info about an orderbook

def get_market_data(self, order_book_id: str) -> avanza.models.market_data.MarketData:
450    def get_market_data(self, order_book_id: str)-> MarketData:
451        """Marketdata includes current order depth and quote."""
452        return self.__call(
453            HttpMethod.GET,
454            Route.MARKETDATA_PATH.value.format(order_book_id)
455        )

Marketdata includes current order depth and quote.

def get_insights_report( self, account_ids: list[str], time_period: avanza.constants.InsightsReportTimePeriod) -> avanza.models.insights_report.InsightsReport:
457    def get_insights_report(
458        self, account_ids: list[str], time_period: InsightsReportTimePeriod
459    ) -> InsightsReport:
460        """Get report about the development of your owned positions during the specified timeperiod"""
461        return self.__call(
462            HttpMethod.POST,
463            Route.INSIGHTS_PATH.value,
464            options={"timePeriod": time_period.value, "accountIds": account_ids},
465        )

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

def get_deals(self):
467    def get_deals(self):
468        """Get currently active deals"""
469        return self.__call(HttpMethod.GET, Route.DEALS_PATH.value)

Get currently active deals

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

Get all available inspiration lists

This returns lists similar to the ones found on:

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

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

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

Get inspiration list

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

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

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

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

Place an order

Returns:

If the order was successfully placed:

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

If the order was not placed:

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

Place a buy order for a fund

Returns:

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

Place a sell order for a fund

Returns:

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

Place an stop loss order

Args:

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

account_id: A valid account id.

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

stop_loss_trigger: The stop loss trigger type.

stop_loss_order_event: The stop loss order event type.

Returns:

If the order was successfully placed:

{
    status: 'SUCCESS',
    stoplossOrderId: str
}

If the order was not placed:

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

Update an existing order

Returns:

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

Get an existing order

Returns:

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

Get open stop losses

Returns:

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

delete a stop loss order

Args:

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

account_id: A valid account id.

Returns: Nothing

def delete_order(self, account_id: str, order_id: str):
792    def delete_order(self, account_id: str, order_id: str):
793        """Delete an existing order
794
795        Returns:
796
797            {
798                messages: str,
799                orderId: str,
800                parameters: List[str],
801                orderRequestStatus: str
802            }
803        """
804        return self.__call(
805            HttpMethod.POST,
806            Route.ORDER_DELETE_PATH.value,
807            {"accountId": account_id, "orderId": order_id},
808        )

Delete an existing order

Returns:

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

Get monthly savings at avanza for specific account

Returns:

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

Get your monthly savings at Avanza

Returns:

{
    'monthlySavings': [{
        'account': {
            'id': str,
            'name': str,
            'type': str
        },
        'amount': float,
        'cash': {'amount': float, 'percent': float},
        'externalAccount': {
            'accountNumber': str,
            'bankName': str,
            'clearingNumber': str
        },
        'fundDistributions': [{
            'amount': float,
            'orderbook': {
                'buyable': bool,
                'id': str,
                'name': str
            },
            'percent': float
        },
        'id': str,
        'name': str,
        'purchaseDay': int,
        'status': str,
        'transferDay': int
    }],
    'totalAmount': float
}
def pause_monthly_saving(self, account_id: str, monthly_savings_id: str):
892    def pause_monthly_saving(self, account_id: str, monthly_savings_id: str):
893        """Pause an active monthly saving
894
895        Returns:
896            'OK'
897
898        """
899
900        return self.__call(
901            HttpMethod.PUT,
902            Route.MONTHLY_SAVINGS_PAUSE_PATH.value.format(
903                account_id, monthly_savings_id
904            ),
905        )

Pause an active monthly saving

Returns: 'OK'

def resume_monthly_saving(self, account_id: str, monthly_savings_id: str):
907    def resume_monthly_saving(self, account_id: str, monthly_savings_id: str):
908        """Resume a paused monthly saving
909
910        Returns:
911            'OK'
912
913        """
914
915        return self.__call(
916            HttpMethod.PUT,
917            Route.MONTHLY_SAVINGS_RESUME_PATH.value.format(
918                account_id, monthly_savings_id
919            ),
920        )

Resume a paused monthly saving

Returns: 'OK'

def delete_monthly_saving(self, account_id: str, monthly_savings_id: str) -> None:
922    def delete_monthly_saving(self, account_id: str, monthly_savings_id: str) -> None:
923        """Deletes a monthly saving
924
925        Returns:
926            None
927
928        """
929
930        return self.__call(
931            HttpMethod.DELETE,
932            Route.MONTHLY_SAVINGS_REMOVE_PATH.value.format(
933                account_id, monthly_savings_id
934            ),
935        )

Deletes a monthly saving

Returns: None

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

Create a monthly saving at Avanza

Args:

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

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

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

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

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

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

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

Returns:

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

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

Get transactions, optionally apply criteria.

Args:

transaction_types: One or more transaction types.

transactions_from: Fetch transactions from this date.

transactions_to: Fetch transactions to this date.

isin: Only fetch transactions for specified isin.

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

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

Returns:

[
    {
        'alertId': str,
        'accountId': str,
        'price': float,
        'validUntil': str,
        'direction': str,
        'email': bool,
        'notification': bool,
        'sms': bool,
    }
]
def get_price_alert(self, order_book_id: str) -> List[avanza.models.price_alert.PriceAlert]:
1109    def get_price_alert(self, order_book_id: str) -> List[PriceAlert]:
1110        """Gets all the price alerts for the specified orderbook"""
1111
1112        return self.__call(
1113            HttpMethod.GET,
1114            Route.PRICE_ALERT_PATH.value.format(order_book_id),
1115        )

Gets all the price alerts for the specified orderbook

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

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

Returns:

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

Return current offers