tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
   8
   9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  11
  12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH
  13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  18"""
  19
  20# Copyright (c) 2022 Gilmillin Timur Mansurovich
  21#
  22# Licensed under the Apache License, Version 2.0 (the "License");
  23# you may not use this file except in compliance with the License.
  24# You may obtain a copy of the License at
  25#
  26#     http://www.apache.org/licenses/LICENSE-2.0
  27#
  28# Unless required by applicable law or agreed to in writing, software
  29# distributed under the License is distributed on an "AS IS" BASIS,
  30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31# See the License for the specific language governing permissions and
  32# limitations under the License.
  33
  34
  35import sys
  36import os
  37from argparse import ArgumentParser
  38from importlib.metadata import version
  39
  40from dateutil.tz import tzlocal
  41from time import sleep
  42
  43import re
  44import json
  45import requests
  46import traceback as tb
  47from typing import Union
  48
  49from multiprocessing import cpu_count
  50from multiprocessing.pool import ThreadPool
  51import pandas as pd
  52
  53from mako.template import Template  # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
  54from Templates import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  55from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  56from TradeRoutines import *  # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module
  57
  58from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator)
  59from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  60
  61import UniLogger as uLog  # Logger for TKSBrokerAPI
  62
  63
  64# --- Common technical parameters:
  65
  66PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  67uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  68uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  69uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  70
  71__version__ = "1.5"  # The "major.minor" version setup here, but build number define at the build-server only
  72
  73CPU_COUNT = cpu_count()  # host's real CPU count
  74CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  75
  76
  77class TinkoffBrokerServer:
  78    """
  79    This class implements methods to work with Tinkoff broker server.
  80
  81    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  82
  83    About `token`: https://tinkoff.github.io/investAPI/token/
  84    """
  85    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  86        """
  87        Main class init.
  88
  89        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  90        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  91                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  92        :param useCache: use default cache file with raw data to use instead of `iList`.
  93                         True by default. Cache is auto-update if new day has come.
  94                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  95        :param defaultCache: path to default cache file. `dump.json` by default.
  96        """
  97        if token is None or not token:
  98            try:
  99                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 100                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 101
 102            except KeyError:
 103                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 104                raise Exception("Token required")
 105
 106        else:
 107            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 108            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 109
 110        if accountId is None or not accountId:
 111            try:
 112                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 113                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 114
 115            except KeyError:
 116                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 117
 118        else:
 119            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 120            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 121
 122        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 123        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 124
 125        Latest version: https://pypi.org/project/tksbrokerapi/
 126        """
 127
 128        self.aliases = TKS_TICKER_ALIASES
 129        """Some aliases instead official tickers.
 130
 131        See also: `TKSEnums.TKS_TICKER_ALIASES`
 132        """
 133
 134        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 135
 136        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 137
 138        self._ticker = ""
 139        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 140
 141        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 142        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 143
 144        See also: `SearchByTicker()`, `SearchInstruments()`.
 145        """
 146
 147        self._figi = ""
 148        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 149
 150        See also: `SearchByFIGI()`, `SearchInstruments()`.
 151        """
 152
 153        self.depth = 1
 154        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 155
 156        See also: `GetCurrentPrices()`.
 157        """
 158
 159        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 160        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 161
 162        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 163        """
 164
 165        uLogger.debug("Broker API server: {}".format(self.server))
 166
 167        self.timeout = 15
 168        """Server operations timeout in seconds. Default: `15`.
 169
 170        See also: `SendAPIRequest()`.
 171        """
 172
 173        self.headers = {
 174            "Content-Type": "application/json",
 175            "accept": "application/json",
 176            "Authorization": "Bearer {}".format(self.token),
 177            "x-app-name": "Tim55667757.TKSBrokerAPI",
 178        }
 179        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 180
 181        See also: `SendAPIRequest()`.
 182        """
 183
 184        self.body = None
 185        """Request body which send to broker server. Default: `None`.
 186
 187        See also: `SendAPIRequest()`.
 188        """
 189
 190        self.moreDebug = False
 191        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 192
 193        self.useHTMLReports = False
 194        """
 195        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 196        
 197        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 198        """
 199
 200        self.historyFile = None
 201        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 202
 203        See also: `History()`.
 204        """
 205
 206        self.htmlHistoryFile = "index.html"
 207        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 208
 209        See also: `ShowHistoryChart()`.
 210        """
 211
 212        self.instrumentsFile = "instruments.md"
 213        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 214
 215        See also: `ShowInstrumentsInfo()`.
 216        """
 217
 218        self.searchResultsFile = "search-results.md"
 219        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 220
 221        See also: `SearchInstruments()`.
 222        """
 223
 224        self.pricesFile = "prices.md"
 225        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 226
 227        See also: `GetListOfPrices()`.
 228        """
 229
 230        self.infoFile = "info.md"
 231        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 232
 233        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 234        """
 235
 236        self.bondsXLSXFile = "ext-bonds.xlsx"
 237        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 238        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 239
 240        See also: `ExtendBondsData()`.
 241        """
 242
 243        self.calendarFile = "calendar.md"
 244        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 245        
 246        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 247
 248        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 249        """
 250
 251        self.overviewFile = "overview.md"
 252        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 253
 254        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 255        """
 256
 257        self.overviewDigestFile = "overview-digest.md"
 258        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 259
 260        See also: `Overview()` with parameter `details="digest"`.
 261        """
 262
 263        self.overviewPositionsFile = "overview-positions.md"
 264        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 265
 266        See also: `Overview()` with parameter `details="positions"`.
 267        """
 268
 269        self.overviewOrdersFile = "overview-orders.md"
 270        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 271
 272        See also: `Overview()` with parameter `details="orders"`.
 273        """
 274
 275        self.overviewAnalyticsFile = "overview-analytics.md"
 276        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 277
 278        See also: `Overview()` with parameter `details="analytics"`.
 279        """
 280
 281        self.overviewBondsCalendarFile = "overview-calendar.md"
 282        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 283
 284        See also: `Overview()` with parameter `details="calendar"`.
 285        """
 286
 287        self.reportFile = "deals.md"
 288        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 289
 290        See also: `Deals()`.
 291        """
 292
 293        self.withdrawalLimitsFile = "limits.md"
 294        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 295
 296        See also: `OverviewLimits()` and `RequestLimits()`.
 297        """
 298
 299        self.userInfoFile = "user-info.md"
 300        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 301
 302        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 303        """
 304
 305        self.userAccountsFile = "accounts.md"
 306        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 307
 308        See also: `OverviewAccounts()`, `RequestAccounts()`.
 309        """
 310
 311        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 312        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 313
 314        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 315
 316        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 317        """
 318
 319        self.iList = None  # init iList for raw instruments data
 320        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 321        
 322        See also: `Listing()`, `DumpInstruments()`.
 323        """
 324
 325        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 326        if useCache:
 327            if os.path.exists(self.iListDumpFile):
 328                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 329                curTime = datetime.now(tzutc())
 330
 331                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 332                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 333
 334                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 335
 336                else:
 337                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 338
 339                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 340                        os.path.abspath(self.iListDumpFile),
 341                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 342                    ))
 343
 344            else:
 345                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 346                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 347
 348        else:
 349            self.iList = self.Listing()  # request new raw instruments data from broker server
 350            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 351
 352        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 353        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 354
 355        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 356        """
 357
 358    @property
 359    def ticker(self) -> str:
 360        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 361
 362        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 363        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 364
 365        See also: `SearchByTicker()`, `SearchInstruments()`.
 366        """
 367        return self._ticker
 368
 369    @ticker.setter
 370    def ticker(self, value):
 371        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 372
 373        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 374        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 375
 376        See also: `SearchByTicker()`, `SearchInstruments()`.
 377        """
 378        self._ticker = str(value).upper()  # Tickers may be upper case only
 379
 380    @property
 381    def figi(self) -> str:
 382        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 383
 384        See also: `SearchByFIGI()`, `SearchInstruments()`.
 385        """
 386        return self._figi
 387
 388    @figi.setter
 389    def figi(self, value):
 390        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 391
 392        See also: `SearchByFIGI()`, `SearchInstruments()`.
 393        """
 394        self._figi = str(value).upper()  # FIGI may be upper case only
 395
 396    def _ParseJSON(self, rawData="{}") -> dict:
 397        """
 398        Parse JSON from response string.
 399
 400        :param rawData: this is a string with JSON-formatted text.
 401        :return: JSON (dictionary), parsed from server response string.
 402        """
 403        responseJSON = json.loads(rawData) if rawData else {}
 404
 405        if self.moreDebug:
 406            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 407
 408        return responseJSON
 409
 410    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 411        """
 412        Send GET or POST request to broker server and receive JSON object.
 413
 414        self.header: must be defining with dictionary of headers.
 415        self.body: if define then used as request body. None by default.
 416        self.timeout: global request timeout, 15 seconds by default.
 417        :param url: url with REST request.
 418        :param reqType: send "GET" or "POST" request. "GET" by default.
 419        :param retry: how many times retry after first request if an 5xx server errors occurred.
 420        :param pause: sleep time in seconds between retries.
 421        :return: response JSON (dictionary) from broker.
 422        """
 423        if reqType.upper() not in ("GET", "POST"):
 424            uLogger.error("You can define request type: `GET` or `POST`!")
 425            raise Exception("Incorrect value")
 426
 427        if self.moreDebug:
 428            uLogger.debug("Request parameters:")
 429            uLogger.debug("    - REST API URL: {}".format(url))
 430            uLogger.debug("    - request type: {}".format(reqType))
 431            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 432            uLogger.debug("    - body:\n{}".format(self.body))
 433
 434        # fast hack to avoid all operations with some tickers/FIGI
 435        responseJSON = {}
 436        oK = True
 437        for item in self.exclude:
 438            if item in url:
 439                if self.moreDebug:
 440                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 441
 442                oK = False
 443                break
 444
 445        if oK:
 446            counter = 0
 447            response = None
 448            errMsg = ""
 449
 450            while not response and counter <= retry:
 451                if reqType == "GET":
 452                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 453
 454                if reqType == "POST":
 455                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 456
 457                if self.moreDebug:
 458                    uLogger.debug("Response:")
 459                    uLogger.debug("    - status code: {}".format(response.status_code))
 460                    uLogger.debug("    - reason: {}".format(response.reason))
 461                    uLogger.debug("    - body length: {}".format(len(response.text)))
 462                    uLogger.debug("    - headers:\n{}".format(response.headers))
 463
 464                # Server returns some headers:
 465                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 466                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 467                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 468                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 469                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 470                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 471                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 472                    sleep(rateLimitWait)
 473
 474                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 475                if 400 <= response.status_code < 500:
 476                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 477                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 478
 479                    if "code" in response.text and "message" in response.text:
 480                        msgDict = self._ParseJSON(rawData=response.text)
 481                        uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 482
 483                    counter = retry + 1  # do not retry for 4xx errors
 484
 485                if 500 <= response.status_code < 600:
 486                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 487                    uLogger.debug("    - not oK, {}".format(errMsg))
 488
 489                    if "code" in response.text and "message" in response.text:
 490                        errMsgDict = self._ParseJSON(rawData=response.text)
 491                        uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 492
 493                    counter += 1
 494
 495                    if counter <= retry:
 496                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 497                        sleep(pause)
 498
 499            responseJSON = self._ParseJSON(rawData=response.text)
 500
 501            if errMsg:
 502                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 503                uLogger.error("    - not oK, {}".format(errMsg))
 504
 505        return responseJSON
 506
 507    def _IUpdater(self, iType: str) -> tuple:
 508        """
 509        Request instrument by type from server. See available API methods for instruments:
 510        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 511        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 512        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 513        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 514        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 515
 516        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 517        :return: tuple with iType name and list of available instruments of current type for defined user token.
 518        """
 519        result = []
 520
 521        if iType in TKS_INSTRUMENTS:
 522            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 523
 524            # all instruments have the same body in API v2 requests:
 525            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 526            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 527            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 528
 529        return iType, result
 530
 531    def _IWrapper(self, kwargs):
 532        """
 533        Wrapper runs instrument's update method `_IUpdater()`.
 534        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 535        """
 536        return self._IUpdater(**kwargs)
 537
 538    def Listing(self) -> dict:
 539        """
 540        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 541
 542        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 543        """
 544        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 545        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 546
 547        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 548        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 549        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 550
 551        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 552        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 553        poolUpdater.close()
 554
 555        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 556        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 557        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 558
 559        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 560        for iType in iList.keys():
 561            for ticker in iList[iType]:
 562                iList[iType][ticker]["type"] = iType
 563
 564                if "minPriceIncrement" in iList[iType][ticker].keys():
 565                    iList[iType][ticker]["step"] = NanoToFloat(
 566                        iList[iType][ticker]["minPriceIncrement"]["units"],
 567                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 568                    )
 569
 570                else:
 571                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 572
 573        return iList
 574
 575    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 576        """
 577        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 578
 579        See also: `DumpInstruments()`, `Listing()`.
 580
 581        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 582                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 583        """
 584        if self.iListDumpFile is None or not self.iListDumpFile:
 585            uLogger.error("Output name of dump file must be defined!")
 586            raise Exception("Filename required")
 587
 588        if not self.iList or forceUpdate:
 589            self.iList = self.Listing()
 590
 591        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 592
 593        # Save as XLSX with separated sheets for every type of instruments:
 594        with pd.ExcelWriter(
 595                path=xlsxDumpFile,
 596                date_format=TKS_DATE_FORMAT,
 597                datetime_format=TKS_DATE_TIME_FORMAT,
 598                mode="w",
 599        ) as writer:
 600            for iType in TKS_INSTRUMENTS:
 601                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 602                df = df[sorted(df)]  # sorted by column names
 603                df = df.applymap(
 604                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 605                    na_action="ignore",
 606                )  # converting numbers from nano-type to float in every cell
 607                df.to_excel(
 608                    writer,
 609                    sheet_name=iType,
 610                    encoding="UTF-8",
 611                    freeze_panes=(1, 1),
 612                )  # saving as XLSX-file with freeze first row and column as headers
 613
 614        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 615
 616    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 617        """
 618        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 619        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 620
 621        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 622
 623        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 624                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 625        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 626        """
 627        if self.iListDumpFile is None or not self.iListDumpFile:
 628            uLogger.error("Output name of dump file must be defined!")
 629            raise Exception("Filename required")
 630
 631        if not self.iList or forceUpdate:
 632            self.iList = self.Listing()
 633
 634        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 635        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 636            fH.write(jsonDump)
 637
 638        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 639
 640        return jsonDump
 641
 642    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 643        """
 644        Show information about one instrument defined by json data and prints it in Markdown format.
 645
 646        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 647
 648        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 649        :param show: if `True` then also printing information about instrument and its current price.
 650        :return: multilines text in Markdown format with information about one instrument.
 651        """
 652        splitLine = "|                                                             |                                                        |\n"
 653        infoText = ""
 654
 655        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 656            info = [
 657                "# Main information\n\n",
 658                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 659                "| Parameters                                                  | Values                                                 |\n",
 660                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 661                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 662                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 663            ]
 664
 665            if "sector" in iJSON.keys() and iJSON["sector"]:
 666                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 667
 668            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 669                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 670
 671            info.extend([
 672                splitLine,
 673                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 674                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 675            ])
 676
 677            if "isin" in iJSON.keys() and iJSON["isin"]:
 678                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 679
 680            if "classCode" in iJSON.keys():
 681                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 682
 683            info.extend([
 684                splitLine,
 685                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 686                splitLine,
 687                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 688                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 689                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 690            ])
 691
 692            if iJSON["figi"]:
 693                self._figi = iJSON["figi"]
 694                iJSON = iJSON | self.RequestTradingStatus()
 695
 696                info.extend([
 697                    splitLine,
 698                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 699                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 700                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 701                ])
 702
 703            info.append(splitLine)
 704
 705            if "type" in iJSON.keys() and iJSON["type"]:
 706                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 707
 708                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 709                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 710
 711            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 712                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 713
 714            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 715                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 716
 717            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 718                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 719
 720            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 721                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 722
 723            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 724                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 725
 726            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 727                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 728
 729            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 730                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 731
 732            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 733                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 734
 735            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 736                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 737
 738            if "currency" in iJSON.keys():
 739                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 740
 741            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 742                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 743
 744            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 745                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 746
 747            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 748                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 749
 750            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 751                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 752
 753            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 754                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 755
 756            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 757                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 758
 759            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 760                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 761
 762            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 763                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 764
 765            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 766                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 767
 768            iExt = None
 769            if iJSON["type"] == "Bonds":
 770                info.extend([
 771                    splitLine,
 772                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 773                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 774                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 775                        iJSON["nominal"]["currency"],
 776                    )),
 777                ])
 778
 779                if "floatingCouponFlag" in iJSON.keys():
 780                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 781
 782                if "amortizationFlag" in iJSON.keys():
 783                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 784
 785                info.append(splitLine)
 786
 787                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 788                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 789
 790                if iJSON["figi"]:
 791                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 792
 793                    info.extend([
 794                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 795                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 796                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 797                    ])
 798
 799                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 800                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 801                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 802                        iJSON["aciValue"]["currency"]
 803                    )))
 804
 805            if "currentPrice" in iJSON.keys():
 806                info.append(splitLine)
 807
 808                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 809                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 810
 811                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 812                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 813                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 814                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 815                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 816
 817                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 818                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 819
 820                info.extend([
 821                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 822                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 823                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 824                    )),
 825                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 826                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 827                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 828                    )),
 829                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 830                        "{:.2f}%{}".format(
 831                            iJSON["currentPrice"]["changes"],
 832                            " ({}{:.2f} {})".format(
 833                                "+" if bondChangesDelta > 0 else "",
 834                                bondChangesDelta,
 835                                aciCurrency
 836                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 837                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 838                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 839                                currency
 840                            ),
 841                        )
 842                    ),
 843                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 844                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 845                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 846                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 847                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 848                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 849                    )),
 850                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 851                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 852                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 853                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 854                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 855                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 856                    )),
 857                ])
 858
 859            if "lot" in iJSON.keys():
 860                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 861
 862            if "step" in iJSON.keys() and iJSON["step"] != 0:
 863                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 864
 865            # Add bond payment calendar:
 866            if iJSON["type"] == "Bonds":
 867                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 868                info.extend(["\n#", strCalendar])
 869
 870            infoText += "".join(info)
 871
 872            if show:
 873                uLogger.info("{}".format(infoText))
 874
 875            else:
 876                uLogger.debug("{}".format(infoText))
 877
 878            if self.infoFile is not None:
 879                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 880                    fH.write(infoText)
 881
 882                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 883
 884                if self.useHTMLReports:
 885                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 886                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 887                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 888
 889                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 890
 891        return infoText
 892
 893    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 894        """
 895        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 896
 897        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 898        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 899        :return: JSON formatted data with information about instrument.
 900        """
 901        tickerJSON = {}
 902        if self.moreDebug:
 903            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 904
 905        if not self._ticker:
 906            uLogger.warning("self._ticker variable is not be empty!")
 907
 908        else:
 909            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 910                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 911                raise Exception("Instrument not allowed")
 912
 913            if not self.iList:
 914                self.iList = self.Listing()
 915
 916            if self._ticker in self.iList["Shares"].keys():
 917                tickerJSON = self.iList["Shares"][self._ticker]
 918                if self.moreDebug:
 919                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 920
 921            elif self._ticker in self.iList["Currencies"].keys():
 922                tickerJSON = self.iList["Currencies"][self._ticker]
 923                if self.moreDebug:
 924                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 925
 926            elif self._ticker in self.iList["Bonds"].keys():
 927                tickerJSON = self.iList["Bonds"][self._ticker]
 928                if self.moreDebug:
 929                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 930
 931            elif self._ticker in self.iList["Etfs"].keys():
 932                tickerJSON = self.iList["Etfs"][self._ticker]
 933                if self.moreDebug:
 934                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 935
 936            elif self._ticker in self.iList["Futures"].keys():
 937                tickerJSON = self.iList["Futures"][self._ticker]
 938                if self.moreDebug:
 939                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 940
 941        if tickerJSON:
 942            self._figi = tickerJSON["figi"]
 943
 944            if requestPrice:
 945                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 946
 947                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 948                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 949
 950                else:
 951                    tickerJSON["currentPrice"]["changes"] = 0
 952
 953            if show:
 954                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 955
 956        else:
 957            if show:
 958                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 959
 960        return tickerJSON
 961
 962    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 963        """
 964        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 965
 966        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 967        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 968        :return: JSON formatted data with information about instrument.
 969        """
 970        figiJSON = {}
 971        if self.moreDebug:
 972            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 973
 974        if not self._figi:
 975            uLogger.warning("self._figi variable is not be empty!")
 976
 977        else:
 978            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 979                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 980                raise Exception("Instrument not allowed")
 981
 982            if not self.iList:
 983                self.iList = self.Listing()
 984
 985            for item in self.iList["Shares"].keys():
 986                if self._figi == self.iList["Shares"][item]["figi"]:
 987                    figiJSON = self.iList["Shares"][item]
 988
 989                    if self.moreDebug:
 990                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
 991
 992                    break
 993
 994            if not figiJSON:
 995                for item in self.iList["Currencies"].keys():
 996                    if self._figi == self.iList["Currencies"][item]["figi"]:
 997                        figiJSON = self.iList["Currencies"][item]
 998
 999                        if self.moreDebug:
1000                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1001
1002                        break
1003
1004            if not figiJSON:
1005                for item in self.iList["Bonds"].keys():
1006                    if self._figi == self.iList["Bonds"][item]["figi"]:
1007                        figiJSON = self.iList["Bonds"][item]
1008
1009                        if self.moreDebug:
1010                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1011
1012                        break
1013
1014            if not figiJSON:
1015                for item in self.iList["Etfs"].keys():
1016                    if self._figi == self.iList["Etfs"][item]["figi"]:
1017                        figiJSON = self.iList["Etfs"][item]
1018
1019                        if self.moreDebug:
1020                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1021
1022                        break
1023
1024            if not figiJSON:
1025                for item in self.iList["Futures"].keys():
1026                    if self._figi == self.iList["Futures"][item]["figi"]:
1027                        figiJSON = self.iList["Futures"][item]
1028
1029                        if self.moreDebug:
1030                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1031
1032                        break
1033
1034        if figiJSON:
1035            self._figi = figiJSON["figi"]
1036            self._ticker = figiJSON["ticker"]
1037
1038            if requestPrice:
1039                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1040
1041                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1042                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1043
1044                else:
1045                    figiJSON["currentPrice"]["changes"] = 0
1046
1047            if show:
1048                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1049
1050        else:
1051            if show:
1052                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1053
1054        return figiJSON
1055
1056    def GetCurrentPrices(self, show: bool = True) -> dict:
1057        """
1058        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1059        `{"buy": [{"price": 1243.8, "quantity": 193},
1060                  {"price": 1244.0, "quantity": 168},
1061                  {"price": 1244.8, "quantity": 5},
1062                  {"price": 1245.0, "quantity": 61},
1063                  {"price": 1245.4, "quantity": 60}],
1064          "sell": [{"price": 1243.6, "quantity": 8},
1065                   {"price": 1242.6, "quantity": 10},
1066                   {"price": 1242.4, "quantity": 18},
1067                   {"price": 1242.2, "quantity": 50},
1068                   {"price": 1242.0, "quantity": 113}],
1069          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1070        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1071        - sell: list of dicts with Buyers prices,
1072            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1073            - quantity: volume value by current price in lots,
1074        - limitUp: current trade session limit price, maximum,
1075        - limitDown: current trade session limit price, minimum,
1076        - lastPrice: last deal price of the instrument,
1077        - closePrice: previous trade session close price of the instrument.
1078
1079        See also: `SearchByTicker()` and `SearchByFIGI()`.
1080        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1081        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1082
1083        :param show: if `True` then print DOM to log and console.
1084        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1085                 If an error occurred then returns an empty record:
1086                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1087        """
1088        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1089
1090        if self.depth < 1:
1091            uLogger.error("Depth of Market (DOM) must be >=1!")
1092            raise Exception("Incorrect value")
1093
1094        if not (self._ticker or self._figi):
1095            uLogger.error("self._ticker or self._figi variables must be defined!")
1096            raise Exception("Ticker or FIGI required")
1097
1098        if self._ticker and not self._figi:
1099            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1100            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1101
1102        if not self._ticker and self._figi:
1103            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1104            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1105
1106        if not self._figi:
1107            uLogger.error("FIGI is not defined!")
1108            raise Exception("Ticker or FIGI required")
1109
1110        else:
1111            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1112
1113            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1114            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1115            self.body = str({"figi": self._figi, "depth": self.depth})
1116            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1117
1118            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1119                # list of dicts with sellers orders:
1120                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1121
1122                # list of dicts with buyers orders:
1123                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1124
1125                # max price of instrument at this time:
1126                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1127
1128                # min price of instrument at this time:
1129                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1130
1131                # last price of deal with instrument:
1132                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1133
1134                # last close price of instrument:
1135                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1136
1137            else:
1138                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1139                uLogger.debug("Server response: {}".format(pricesResponse))
1140
1141            if show:
1142                if prices["buy"] or prices["sell"]:
1143                    info = [
1144                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1145                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1146                            self._ticker,
1147                            self._figi,
1148                            self.depth,
1149                        ),
1150                        "-" * 60, "\n",
1151                        "             Orders of Buyers | Orders of Sellers\n",
1152                        "-" * 60, "\n",
1153                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1154                        "-" * 60, "\n",
1155                    ]
1156
1157                    if not prices["buy"]:
1158                        info.append("                              | No orders!\n")
1159                        sumBuy = 0
1160
1161                    else:
1162                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1163                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1164                        for item in maxMinSorted:
1165                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1166
1167                    if not prices["sell"]:
1168                        info.append("No orders!                    |\n")
1169                        sumSell = 0
1170
1171                    else:
1172                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1173                        for item in prices["sell"]:
1174                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1175
1176                    info.extend([
1177                        "-" * 60, "\n",
1178                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1179                        "-" * 60, "\n",
1180                    ])
1181
1182                    infoText = "".join(info)
1183
1184                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1185
1186                else:
1187                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1188
1189        return prices
1190
1191    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1192        """
1193        This method get and show information about all available broker instruments for current user account.
1194        If `instrumentsFile` string is not empty then also save information to this file.
1195
1196        :param show: if `True` then print results to console, if `False` — print only to file.
1197        :return: multi-lines string with all available broker instruments
1198        """
1199        if not self.iList:
1200            self.iList = self.Listing()
1201
1202        info = [
1203            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1204            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1205        ]
1206
1207        # add instruments count by type:
1208        for iType in self.iList.keys():
1209            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1210
1211        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1212        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1213
1214        # generating info tables with all instruments by type:
1215        for iType in self.iList.keys():
1216            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1217
1218            for instrument in self.iList[iType].keys():
1219                iName = self.iList[iType][instrument]["name"]  # instrument's name
1220                if len(iName) > 57:
1221                    iName = "{}...".format(iName[:54])  # right trim for a long string
1222
1223                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1224                    self.iList[iType][instrument]["ticker"],
1225                    iName,
1226                    self.iList[iType][instrument]["figi"],
1227                    self.iList[iType][instrument]["currency"],
1228                    self.iList[iType][instrument]["lot"],
1229                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1230                ))
1231
1232        infoText = "".join(info)
1233
1234        if show:
1235            uLogger.info(infoText)
1236
1237        if self.instrumentsFile:
1238            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1239                fH.write(infoText)
1240
1241            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1242
1243            if self.useHTMLReports:
1244                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1245                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1246                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1247
1248                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1249
1250        return infoText
1251
1252    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1253        """
1254        This method search and show information about instruments by part of its ticker, FIGI or name.
1255        If `searchResultsFile` string is not empty then also save information to this file.
1256
1257        :param pattern: string with part of ticker, FIGI or instrument's name.
1258        :param show: if `True` then print results to console, if `False` — return list of result only.
1259        :return: list of dictionaries with all found instruments.
1260        """
1261        if not self.iList:
1262            self.iList = self.Listing()
1263
1264        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1265        compiledPattern = re.compile(pattern, re.IGNORECASE)
1266
1267        for iType in self.iList:
1268            for instrument in self.iList[iType].values():
1269                searchResult = compiledPattern.search(" ".join(
1270                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1271                ))
1272
1273                if searchResult:
1274                    searchResults[iType][instrument["ticker"]] = instrument
1275
1276        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1277        info = [
1278            "# Search results\n\n",
1279            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1280            "* **Search pattern:** [{}]\n".format(pattern),
1281            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1282            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1283        ]
1284        infoShort = info[:]
1285
1286        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1287        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1288        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1289
1290        if resultsLen == 0:
1291            info.append("\nNo results\n")
1292            infoShort.append("\nNo results\n")
1293            uLogger.warning("No results. Try changing your search pattern.")
1294
1295        else:
1296            for iType in searchResults:
1297                iTypeValuesCount = len(searchResults[iType].values())
1298                if iTypeValuesCount > 0:
1299                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1300                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1301
1302                    for instrument in searchResults[iType].values():
1303                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1304                            instrument["type"],
1305                            instrument["ticker"],
1306                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1307                            instrument["figi"],
1308                        ))
1309
1310                    if iTypeValuesCount <= 5:
1311                        infoShort.extend(info[-iTypeValuesCount:])
1312
1313                    else:
1314                        infoShort.extend(info[-5:])
1315                        infoShort.append(skippedLine)
1316
1317        infoText = "".join(info)
1318        infoTextShort = "".join(infoShort)
1319
1320        if show:
1321            uLogger.info(infoTextShort)
1322            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1323
1324        if self.searchResultsFile:
1325            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1326                fH.write(infoText)
1327
1328            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1329
1330            if self.useHTMLReports:
1331                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1332                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1333                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1334
1335                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1336
1337        return searchResults
1338
1339    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1340        """
1341        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1342
1343        :param instruments: list of strings with tickers or FIGIs.
1344        :return: list with unique instrument FIGIs only.
1345        """
1346        requestedInstruments = []
1347        for iName in instruments:
1348            if iName not in self.aliases.keys():
1349                if iName not in requestedInstruments:
1350                    requestedInstruments.append(iName)
1351
1352            else:
1353                if iName not in requestedInstruments:
1354                    if self.aliases[iName] not in requestedInstruments:
1355                        requestedInstruments.append(self.aliases[iName])
1356
1357        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1358
1359        onlyUniqueFIGIs = []
1360        for iName in requestedInstruments:
1361            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1362                continue
1363
1364            self._ticker = iName
1365            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1366
1367            if not iData:
1368                self._ticker = ""
1369                self._figi = iName
1370
1371                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1372
1373                if not iData:
1374                    self._figi = ""
1375                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1376
1377            if iData and iData["figi"] not in onlyUniqueFIGIs:
1378                onlyUniqueFIGIs.append(iData["figi"])
1379
1380        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1381
1382        return onlyUniqueFIGIs
1383
1384    def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1385        """
1386        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1387
1388        See limits: https://tinkoff.github.io/investAPI/limits/
1389
1390        If `pricesFile` string is not empty then also save information to this file.
1391
1392        :param instruments: list of strings with tickers or FIGIs.
1393        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1394        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1395                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1396        """
1397        if instruments is None or not instruments:
1398            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1399            raise Exception("Ticker or FIGI required")
1400
1401        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1402
1403        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1404
1405        iList = []  # trying to get info and current prices about all unique instruments:
1406        for self._figi in onlyUniqueFIGIs:
1407            iData = self.SearchByFIGI(requestPrice=True)
1408            iList.append(iData)
1409
1410        self.ShowListOfPrices(iList, show)
1411
1412        return iList
1413
1414    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1415        """
1416        Show table contains current prices of given instruments.
1417
1418        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1419                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1420        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1421        :return: multilines text in Markdown format as a table contains current prices.
1422        """
1423        infoText = ""
1424
1425        if show or self.pricesFile:
1426            info = [
1427                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1428                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1429                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1430            ]
1431
1432            for item in iList:
1433                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1434                    item["ticker"],
1435                    item["figi"],
1436                    item["type"],
1437                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1438                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1439                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1440                    "{} / {}".format(
1441                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1442                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1443                    ),
1444                    "{} / {}".format(
1445                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1446                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1447                    ),
1448                    item["currency"],
1449                ))
1450
1451            infoText = "".join(info)
1452
1453            if show:
1454                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1455
1456            if self.pricesFile:
1457                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1458                    fH.write(infoText)
1459
1460                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1461
1462                if self.useHTMLReports:
1463                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1464                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1465                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1466
1467                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1468
1469        return infoText
1470
1471    def RequestTradingStatus(self) -> dict:
1472        """
1473        Requesting trading status for the instrument defined by `figi` variable.
1474
1475        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1476
1477        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1478
1479        :return: dictionary with trading status attributes. Response example:
1480                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1481                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1482        """
1483        if self._figi is None or not self._figi:
1484            uLogger.error("Variable `figi` must be defined for using this method!")
1485            raise Exception("FIGI required")
1486
1487        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1488
1489        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1490        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1491        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1492
1493        if self.moreDebug:
1494            uLogger.debug("Records about current trading status successfully received")
1495
1496        return tradingStatus
1497
1498    def RequestPortfolio(self) -> dict:
1499        """
1500        Requesting actual user's portfolio for current `accountId`.
1501
1502        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1503
1504        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1505
1506        :return: dictionary with user's portfolio.
1507        """
1508        if self.accountId is None or not self.accountId:
1509            uLogger.error("Variable `accountId` must be defined for using this method!")
1510            raise Exception("Account ID required")
1511
1512        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1513
1514        self.body = str({"accountId": self.accountId})
1515        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1516        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1517
1518        if self.moreDebug:
1519            uLogger.debug("Records about user's portfolio successfully received")
1520
1521        return rawPortfolio
1522
1523    def RequestPositions(self) -> dict:
1524        """
1525        Requesting open positions by currencies and instruments for current `accountId`.
1526
1527        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1528
1529        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1530
1531        :return: dictionary with open positions by instruments.
1532        """
1533        if self.accountId is None or not self.accountId:
1534            uLogger.error("Variable `accountId` must be defined for using this method!")
1535            raise Exception("Account ID required")
1536
1537        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1538
1539        self.body = str({"accountId": self.accountId})
1540        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1541        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1542
1543        if self.moreDebug:
1544            uLogger.debug("Records about current open positions successfully received")
1545
1546        return rawPositions
1547
1548    def RequestPendingOrders(self) -> list:
1549        """
1550        Requesting current actual pending limit orders for current `accountId`.
1551
1552        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1553
1554        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1555
1556        :return: list of dictionaries with pending limit orders.
1557        """
1558        if self.accountId is None or not self.accountId:
1559            uLogger.error("Variable `accountId` must be defined for using this method!")
1560            raise Exception("Account ID required")
1561
1562        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1563
1564        self.body = str({"accountId": self.accountId})
1565        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1566        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1567
1568        uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1569
1570        return rawOrders
1571
1572    def RequestStopOrders(self) -> list:
1573        """
1574        Requesting current actual stop orders for current `accountId`.
1575
1576        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1577
1578        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1579
1580        :return: list of dictionaries with stop orders.
1581        """
1582        if self.accountId is None or not self.accountId:
1583            uLogger.error("Variable `accountId` must be defined for using this method!")
1584            raise Exception("Account ID required")
1585
1586        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1587
1588        self.body = str({"accountId": self.accountId})
1589        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1590        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1591
1592        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1593
1594        return rawStopOrders
1595
1596    def Overview(self, show: bool = False, details: str = "full") -> dict:
1597        """
1598        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1599        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1600        and `overviewBondsCalendarFile` are defined then also save information to file.
1601
1602        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1603        many requests about the state of the portfolio, and then, based on the received data, a large number
1604        of calculation and statistics are collected.
1605
1606        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1607        :param details: how detailed should the information be?
1608        - `full` — shows full available information about portfolio status (by default),
1609        - `positions` — shows only open positions,
1610        - `orders` — shows only sections of open limits and stop orders.
1611        - `digest` — show a short digest of the portfolio status,
1612        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1613        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1614        :return: dictionary with client's raw portfolio and some statistics.
1615        """
1616        if self.accountId is None or not self.accountId:
1617            uLogger.error("Variable `accountId` must be defined for using this method!")
1618            raise Exception("Account ID required")
1619
1620        view = {
1621            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1622                "headers": {},  # list of dictionaries, response headers without "positions" section
1623                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1624                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1625                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1626                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1627                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1628                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1629                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1630                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1631                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1632            },
1633            "stat": {  # --- some statistics calculated using "raw" sections:
1634                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1635                "availableRUB": 0.,  # available rubles (without other currencies)
1636                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1637                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1638                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1639                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1640                "sharesCostRUB": 0.,  # costs of all shares in RUB
1641                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1642                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1643                "futuresCostRUB": 0.,  # costs of all futures in RUB
1644                "Currencies": [],  # list of dictionaries of all currencies statistics
1645                "Shares": [],  # list of dictionaries of all shares statistics
1646                "Bonds": [],  # list of dictionaries of all bonds statistics
1647                "Etfs": [],  # list of dictionaries of all etfs statistics
1648                "Futures": [],  # list of dictionaries of all futures statistics
1649                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1650                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1651                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1652                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1653                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1654            },
1655            "analytics": {  # --- some analytics of portfolio:
1656                "distrByAssets": {},  # portfolio distribution by assets
1657                "distrByCompanies": {},  # portfolio distribution by companies
1658                "distrBySectors": {},  # portfolio distribution by sectors
1659                "distrByCurrencies": {},  # portfolio distribution by currencies
1660                "distrByCountries": {},  # portfolio distribution by countries
1661                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1662            }
1663        }
1664
1665        details = details.lower()
1666        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1667        if details not in availableDetails:
1668            details = "full"
1669            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1670
1671        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1672
1673        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1674        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1675        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1676        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1677
1678        # save response headers without "positions" section:
1679        for key in portfolioResponse.keys():
1680            if key != "positions":
1681                view["raw"]["headers"][key] = portfolioResponse[key]
1682
1683            else:
1684                continue
1685
1686        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1687        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1688        for item in portfolioResponse["positions"]:
1689            if item["instrumentType"] == "currency":
1690                self._figi = item["figi"]
1691                curr = self.SearchByFIGI(requestPrice=False)
1692
1693                # current price of currency in RUB:
1694                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1695                    "name": curr["name"],
1696                    "currentPrice": NanoToFloat(
1697                        item["currentPrice"]["units"],
1698                        item["currentPrice"]["nano"]
1699                    ),
1700                }
1701
1702                view["raw"]["Currencies"].append(item)
1703
1704            elif item["instrumentType"] == "share":
1705                view["raw"]["Shares"].append(item)
1706
1707            elif item["instrumentType"] == "bond":
1708                view["raw"]["Bonds"].append(item)
1709
1710            elif item["instrumentType"] == "etf":
1711                view["raw"]["Etfs"].append(item)
1712
1713            elif item["instrumentType"] == "futures":
1714                view["raw"]["Futures"].append(item)
1715
1716            else:
1717                continue
1718
1719        # how many volume of currencies (by ISO currency name) are blocked:
1720        for item in view["raw"]["positions"]["blocked"]:
1721            blocked = NanoToFloat(item["units"], item["nano"])
1722            if blocked > 0:
1723                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1724
1725        # how many volume of instruments (by FIGI) are blocked:
1726        for item in view["raw"]["positions"]["securities"]:
1727            blocked = int(item["blocked"])
1728            if blocked > 0:
1729                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1730
1731        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1732
1733        if "rub" in allBlocked.keys():
1734            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1735
1736        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1737        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1738        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1739        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1740        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1741        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1742        view["stat"]["portfolioCostRUB"] = sum([
1743            view["stat"]["allCurrenciesCostRUB"],
1744            view["stat"]["sharesCostRUB"],
1745            view["stat"]["bondsCostRUB"],
1746            view["stat"]["etfsCostRUB"],
1747            view["stat"]["futuresCostRUB"],
1748        ])
1749
1750        # --- calculating some portfolio statistics:
1751        byComp = {}  # distribution by companies
1752        bySect = {}  # distribution by sectors
1753        byCurr = {}  # distribution by currencies (include RUB)
1754        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1755        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1756
1757        for item in portfolioResponse["positions"]:
1758            self._figi = item["figi"]
1759            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1760
1761            if instrument:
1762                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1763                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1764
1765                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1766                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1767
1768                else:
1769                    blocked = 0
1770
1771                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1772                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1773                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1774                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1775                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1776                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1777                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1778                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1779                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1780                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1781                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1782                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1783
1784                statData = {
1785                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1786                    "ticker": instrument["ticker"],  # ticker by FIGI
1787                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1788                    "volume": volume,  # available volume of instrument
1789                    "lots": lots,  # volume in lots of instrument
1790                    "direction": direction,  # direction of an instrument's position: short or long
1791                    "blocked": blocked,  # blocked volume of currency or instrument
1792                    "currentPrice": curPrice,  # current instrument's price in basic asset
1793                    "average": average,  # current average position price
1794                    "cost": cost,  # current cost of all volume of instrument in basic asset
1795                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1796                    "costRUB": costRUB,  # cost of instrument in ruble
1797                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1798                    "profit": profit,  # expected profit at current moment
1799                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1800                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1801                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1802                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1803                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1804                    "step": instrument["step"],  # minimum price increment
1805                }
1806
1807                # adding distribution by unique countries:
1808                if statData["country"] not in byCountry.keys():
1809                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1810
1811                else:
1812                    byCountry[statData["country"]]["cost"] += costRUB
1813                    byCountry[statData["country"]]["percent"] += percentCostRUB
1814
1815                if item["instrumentType"] != "currency":
1816                    # adding distribution by unique companies:
1817                    if statData["name"]:
1818                        if statData["name"] not in byComp.keys():
1819                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1820
1821                        else:
1822                            byComp[statData["name"]]["cost"] += costRUB
1823                            byComp[statData["name"]]["percent"] += percentCostRUB
1824
1825                    # adding distribution by unique sectors:
1826                    if statData["sector"] not in bySect.keys():
1827                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1828
1829                    else:
1830                        bySect[statData["sector"]]["cost"] += costRUB
1831                        bySect[statData["sector"]]["percent"] += percentCostRUB
1832
1833                # adding distribution by unique currencies:
1834                if currency not in byCurr.keys():
1835                    byCurr[currency] = {
1836                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1837                        "cost": costRUB,
1838                        "percent": percentCostRUB
1839                    }
1840
1841                else:
1842                    byCurr[currency]["cost"] += costRUB
1843                    byCurr[currency]["percent"] += percentCostRUB
1844
1845                # saving statistics for every instrument:
1846                if item["instrumentType"] == "currency":
1847                    view["stat"]["Currencies"].append(statData)
1848
1849                    # update dict with free funds for trading (total - blocked) by currencies
1850                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1851                    view["stat"]["funds"][currency] = {
1852                        "total": volume,
1853                        "totalCostRUB": costRUB,  # total volume cost in rubles
1854                        "free": volume - blocked,
1855                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1856                    }
1857
1858                elif item["instrumentType"] == "share":
1859                    view["stat"]["Shares"].append(statData)
1860
1861                elif item["instrumentType"] == "bond":
1862                    view["stat"]["Bonds"].append(statData)
1863
1864                elif item["instrumentType"] == "etf":
1865                    view["stat"]["Etfs"].append(statData)
1866
1867                elif item["instrumentType"] == "Futures":
1868                    view["stat"]["Futures"].append(statData)
1869
1870                else:
1871                    continue
1872
1873        # total changes in Russian Ruble:
1874        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1875        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1876        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1877        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1878        view["stat"]["funds"]["rub"] = {
1879            "total": view["stat"]["availableRUB"],
1880            "totalCostRUB": view["stat"]["availableRUB"],
1881            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1882            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1883        }
1884
1885        # --- pending limit orders sector data:
1886        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1887        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1888
1889        for item in view["raw"]["orders"]:
1890            self._figi = item["figi"]
1891
1892            if item["figi"] not in uniquePendingOrdersFIGIs:
1893                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1894
1895                uniquePendingOrdersFIGIs.append(item["figi"])
1896                uniquePendingOrders[item["figi"]] = instrument
1897
1898            else:
1899                instrument = uniquePendingOrders[item["figi"]]
1900
1901            if instrument:
1902                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1903                orderType = TKS_ORDER_TYPES[item["orderType"]]
1904                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1905                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1906
1907                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1908                if item["direction"] == "ORDER_DIRECTION_BUY":
1909                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1910
1911                else:
1912                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1913
1914                # requested price for order execution:
1915                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1916
1917                # necessary changes in percent to reach target from current price:
1918                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1919
1920                view["stat"]["orders"].append({
1921                    "orderID": item["orderId"],  # orderId number parameter of current order
1922                    "figi": item["figi"],  # FIGI identification
1923                    "ticker": instrument["ticker"],  # ticker name by FIGI
1924                    "lotsRequested": item["lotsRequested"],  # requested lots value
1925                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1926                    "currentPrice": lastPrice,  # current instrument's price for defined action
1927                    "targetPrice": target,  # requested price for order execution in base currency
1928                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1929                    "percentChanges": changes,  # changes in percent to target from current price
1930                    "currency": item["currency"],  # instrument's currency name
1931                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1932                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1933                    "status": orderState,  # order status from TKS_ORDER_STATES
1934                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1935                })
1936
1937        # --- stop orders sector data:
1938        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1939        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1940
1941        for item in view["raw"]["stopOrders"]:
1942            self._figi = item["figi"]
1943
1944            if item["figi"] not in uniqueStopOrdersFIGIs:
1945                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1946
1947                uniqueStopOrdersFIGIs.append(item["figi"])
1948                uniqueStopOrders[item["figi"]] = instrument
1949
1950            else:
1951                instrument = uniqueStopOrders[item["figi"]]
1952
1953            if instrument:
1954                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1955                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1956                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1957
1958                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1959                if "expirationTime" in item.keys():
1960                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1961                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1962
1963                else:
1964                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1965                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1966
1967                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1968                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1969                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1970
1971                else:
1972                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1973
1974                # requested price when stop-order executed:
1975                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1976
1977                # price for limit-order, set up when stop-order executed:
1978                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1979
1980                # necessary changes in percent to reach target from current price:
1981                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1982
1983                view["stat"]["stopOrders"].append({
1984                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1985                    "figi": item["figi"],  # FIGI identification
1986                    "ticker": instrument["ticker"],  # ticker name by FIGI
1987                    "lotsRequested": item["lotsRequested"],  # requested lots value
1988                    "currentPrice": lastPrice,  # current instrument's price for defined action
1989                    "targetPrice": target,  # requested price for stop-order execution in base currency
1990                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1991                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1992                    "percentChanges": changes,  # changes in percent to target from current price
1993                    "currency": item["currency"],  # instrument's currency name
1994                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1995                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1996                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1997                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1998                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1999                })
2000
2001        # --- calculating data for analytics section:
2002        # portfolio distribution by assets:
2003        view["analytics"]["distrByAssets"] = {
2004            "Ruble": {
2005                "uniques": 1,
2006                "cost": view["stat"]["availableRUB"],
2007                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2008            },
2009            "Currencies": {
2010                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2011                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2012                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2013            },
2014            "Shares": {
2015                "uniques": len(view["stat"]["Shares"]),
2016                "cost": view["stat"]["sharesCostRUB"],
2017                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2018            },
2019            "Bonds": {
2020                "uniques": len(view["stat"]["Bonds"]),
2021                "cost": view["stat"]["bondsCostRUB"],
2022                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2023            },
2024            "Etfs": {
2025                "uniques": len(view["stat"]["Etfs"]),
2026                "cost": view["stat"]["etfsCostRUB"],
2027                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2028            },
2029            "Futures": {
2030                "uniques": len(view["stat"]["Futures"]),
2031                "cost": view["stat"]["futuresCostRUB"],
2032                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2033            },
2034        }
2035
2036        # portfolio distribution by companies:
2037        view["analytics"]["distrByCompanies"]["All money cash"] = {
2038            "ticker": "",
2039            "cost": view["stat"]["allCurrenciesCostRUB"],
2040            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2041        }
2042        view["analytics"]["distrByCompanies"].update(byComp)
2043
2044        # portfolio distribution by sectors:
2045        view["analytics"]["distrBySectors"]["All money cash"] = {
2046            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2047            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2048        }
2049        view["analytics"]["distrBySectors"].update(bySect)
2050
2051        # portfolio distribution by currencies:
2052        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2053            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2054
2055            if self.moreDebug:
2056                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2057
2058        view["analytics"]["distrByCurrencies"].update(byCurr)
2059        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2060        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2061
2062        # portfolio distribution by countries:
2063        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2064            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2065
2066            if self.moreDebug:
2067                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2068
2069        view["analytics"]["distrByCountries"].update(byCountry)
2070        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2071        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2072
2073        # --- Prepare text statistics overview in human-readable:
2074        if show:
2075            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2076
2077            # Whatever the value `details`, header not changes:
2078            info = [
2079                "# Client's portfolio\n\n",
2080                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2081                "* **Account ID:** [{}]\n".format(self.accountId),
2082            ]
2083
2084            if details in ["full", "positions", "digest"]:
2085                info.extend([
2086                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2087                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2088                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2089                        view["stat"]["totalChangesRUB"],
2090                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2091                        view["stat"]["totalChangesPercentRUB"],
2092                    ),
2093                ])
2094
2095            if details in ["full", "positions"]:
2096                info.extend([
2097                    "## Open positions\n\n",
2098                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2099                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2100                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2101                        "{:.2f} ({:.2f}) rub".format(
2102                            view["stat"]["availableRUB"],
2103                            view["stat"]["blockedRUB"],
2104                        )
2105                    )
2106                ])
2107
2108                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2109                    return [
2110                        "|                             |                                 |          |              |              |                     |                              |\n",
2111                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2112                            noTradeStr if noTradeStr else typeStr,
2113                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2114                        ),
2115                    ]
2116
2117                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2118                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2119                        "{} [{}]".format(data["ticker"], data["figi"]),
2120                        "{:.2f} ({:.2f}) {}".format(
2121                            data["volume"],
2122                            data["blocked"],
2123                            data["currency"],
2124                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2125                            data["volume"],
2126                            data["blocked"],
2127                        ),
2128                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2129                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2130                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2131                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2132                        "{}{:.2f} {} ({}{:.2f}%)".format(
2133                            "+" if data["profit"] > 0 else "",
2134                            data["profit"], data["baseCurrencyName"],
2135                            "+" if data["percentProfit"] > 0 else "",
2136                            data["percentProfit"],
2137                        ),
2138                    )
2139
2140                # --- Show currencies section:
2141                if view["stat"]["Currencies"]:
2142                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2143                    for item in view["stat"]["Currencies"]:
2144                        info.append(_InfoStr(item, showCurrencyName=True))
2145
2146                else:
2147                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2148
2149                # --- Show shares section:
2150                if view["stat"]["Shares"]:
2151                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2152
2153                    for item in view["stat"]["Shares"]:
2154                        info.append(_InfoStr(item))
2155
2156                else:
2157                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2158
2159                # --- Show bonds section:
2160                if view["stat"]["Bonds"]:
2161                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2162
2163                    for item in view["stat"]["Bonds"]:
2164                        info.append(_InfoStr(item))
2165
2166                else:
2167                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2168
2169                # --- Show etfs section:
2170                if view["stat"]["Etfs"]:
2171                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2172
2173                    for item in view["stat"]["Etfs"]:
2174                        info.append(_InfoStr(item))
2175
2176                else:
2177                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2178
2179                # --- Show futures section:
2180                if view["stat"]["Futures"]:
2181                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2182
2183                    for item in view["stat"]["Futures"]:
2184                        info.append(_InfoStr(item))
2185
2186                else:
2187                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2188
2189            if details in ["full", "orders"]:
2190                # --- Show pending limit orders section:
2191                if view["stat"]["orders"]:
2192                    info.extend([
2193                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2194                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2195                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2196                    ])
2197
2198                    for item in view["stat"]["orders"]:
2199                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2200                            "{} [{}]".format(item["ticker"], item["figi"]),
2201                            item["orderID"],
2202                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2203                            "{} {} ({}{:.2f}%)".format(
2204                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2205                                item["baseCurrencyName"],
2206                                "+" if item["percentChanges"] > 0 else "",
2207                                float(item["percentChanges"]),
2208                            ),
2209                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2210                            item["action"],
2211                            item["type"],
2212                            item["date"],
2213                        ))
2214
2215                else:
2216                    info.append("\n## Total pending limit-orders: [0]\n")
2217
2218                # --- Show stop orders section:
2219                if view["stat"]["stopOrders"]:
2220                    info.extend([
2221                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2222                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2223                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2224                    ])
2225
2226                    for item in view["stat"]["stopOrders"]:
2227                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2228                            "{} [{}]".format(item["ticker"], item["figi"]),
2229                            item["orderID"],
2230                            item["lotsRequested"],
2231                            "{} {} ({}{:.2f}%)".format(
2232                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2233                                item["baseCurrencyName"],
2234                                "+" if item["percentChanges"] > 0 else "",
2235                                float(item["percentChanges"]),
2236                            ),
2237                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2238                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2239                            item["action"],
2240                            item["type"],
2241                            item["expType"],
2242                            item["createDate"],
2243                            item["expDate"],
2244                        ))
2245
2246                else:
2247                    info.append("\n## Total stop-orders: [0]\n")
2248
2249            if details in ["full", "analytics"]:
2250                # -- Show analytics section:
2251                if view["stat"]["portfolioCostRUB"] > 0:
2252                    info.extend([
2253                        "\n# Analytics\n\n"
2254                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2255                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2256                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2257                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2258                            view["stat"]["totalChangesRUB"],
2259                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2260                            view["stat"]["totalChangesPercentRUB"],
2261                        ),
2262                        "\n## Portfolio distribution by assets\n"
2263                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2264                        "|------------------------------------|---------|---------|--------------------|\n",
2265                    ])
2266
2267                    for key in view["analytics"]["distrByAssets"].keys():
2268                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2269                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2270                                key,
2271                                view["analytics"]["distrByAssets"][key]["uniques"],
2272                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2273                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2274                            ))
2275
2276                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2277
2278                    info.extend([
2279                        "\n## Portfolio distribution by companies\n"
2280                        "\n| Company                                      | Percent | Current cost       |\n",
2281                        aSepLine,
2282                    ])
2283
2284                    for company in view["analytics"]["distrByCompanies"].keys():
2285                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2286                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2287                                "{}{}".format(
2288                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2289                                    company,
2290                                ),
2291                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2292                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2293                            ))
2294
2295                    info.extend([
2296                        "\n## Portfolio distribution by sectors\n"
2297                        "\n| Sector                                       | Percent | Current cost       |\n",
2298                        aSepLine,
2299                    ])
2300
2301                    for sector in view["analytics"]["distrBySectors"].keys():
2302                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2303                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2304                                sector,
2305                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2306                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2307                            ))
2308
2309                    info.extend([
2310                        "\n## Portfolio distribution by currencies\n"
2311                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2312                        aSepLine,
2313                    ])
2314
2315                    for curr in view["analytics"]["distrByCurrencies"].keys():
2316                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2317                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2318                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2319                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2320                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2321                            ))
2322
2323                    info.extend([
2324                        "\n## Portfolio distribution by countries\n"
2325                        "\n| Assets by country                            | Percent | Current cost       |\n",
2326                        aSepLine,
2327                    ])
2328
2329                    for country in view["analytics"]["distrByCountries"].keys():
2330                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2331                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2332                                country,
2333                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2334                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2335                            ))
2336
2337            if details in ["full", "calendar"]:
2338                # -- Show bonds payment calendar section:
2339                if view["stat"]["Bonds"]:
2340                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2341                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2342                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2343
2344                else:
2345                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2346
2347            infoText = "".join(info)
2348
2349            uLogger.info(infoText)
2350
2351            if details == "full" and self.overviewFile:
2352                filename = self.overviewFile
2353
2354            elif details == "digest" and self.overviewDigestFile:
2355                filename = self.overviewDigestFile
2356
2357            elif details == "positions" and self.overviewPositionsFile:
2358                filename = self.overviewPositionsFile
2359
2360            elif details == "orders" and self.overviewOrdersFile:
2361                filename = self.overviewOrdersFile
2362
2363            elif details == "analytics" and self.overviewAnalyticsFile:
2364                filename = self.overviewAnalyticsFile
2365
2366            elif details == "calendar" and self.overviewBondsCalendarFile:
2367                filename = self.overviewBondsCalendarFile
2368
2369            else:
2370                filename = ""
2371
2372            if filename:
2373                with open(filename, "w", encoding="UTF-8") as fH:
2374                    fH.write(infoText)
2375
2376                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2377
2378                if self.useHTMLReports:
2379                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2380                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2381                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="", commonCSS=COMMON_CSS, markdown=infoText))
2382
2383                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2384
2385        return view
2386
2387    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2388        """
2389        Returns history operations between two given dates for current `accountId`.
2390        If `reportFile` string is not empty then also save human-readable report.
2391        Shows some statistical data of closed positions.
2392
2393        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2394        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2395        :param show: if `True` then also prints all records to the console.
2396        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2397        :return: original list of dictionaries with history of deals records from API ("operations" key):
2398                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2399                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2400        """
2401        if self.accountId is None or not self.accountId:
2402            uLogger.error("Variable `accountId` must be defined for using this method!")
2403            raise Exception("Account ID required")
2404
2405        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2406
2407        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2408
2409        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2410        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2411        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2412        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2413        customStat = {}  # custom statistics in additional to responseJSON
2414
2415        # --- output report in human-readable format:
2416        if show or self.reportFile:
2417            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2418            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2419            nextDay = ""
2420
2421            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2422
2423            if len(ops) > 0:
2424                customStat = {
2425                    "opsCount": 0,  # total operations count
2426                    "buyCount": 0,  # buy operations
2427                    "sellCount": 0,  # sell operations
2428                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2429                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2430                    "payIn": {"rub": 0.},  # Deposit brokerage account
2431                    "payOut": {"rub": 0.},  # Withdrawals
2432                    "divs": {"rub": 0.},  # Dividends income
2433                    "coupons": {"rub": 0.},  # Coupon's income
2434                    "brokerCom": {"rub": 0.},  # Service commissions
2435                    "serviceCom": {"rub": 0.},  # Service commissions
2436                    "marginCom": {"rub": 0.},  # Margin commissions
2437                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2438                }
2439
2440                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2441                for item in ops:
2442                    if item["state"] == "OPERATION_STATE_EXECUTED":
2443                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2444
2445                        # count buy operations:
2446                        if "_BUY" in item["operationType"]:
2447                            customStat["buyCount"] += 1
2448
2449                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2450                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2451
2452                            else:
2453                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2454
2455                        # count sell operations:
2456                        elif "_SELL" in item["operationType"]:
2457                            customStat["sellCount"] += 1
2458
2459                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2460                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2461
2462                            else:
2463                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2464
2465                        # count incoming operations:
2466                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2467                            if item["payment"]["currency"] in customStat["payIn"].keys():
2468                                customStat["payIn"][item["payment"]["currency"]] += payment
2469
2470                            else:
2471                                customStat["payIn"][item["payment"]["currency"]] = payment
2472
2473                        # count withdrawals operations:
2474                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2475                            if item["payment"]["currency"] in customStat["payOut"].keys():
2476                                customStat["payOut"][item["payment"]["currency"]] += payment
2477
2478                            else:
2479                                customStat["payOut"][item["payment"]["currency"]] = payment
2480
2481                        # count dividends income:
2482                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2483                            if item["payment"]["currency"] in customStat["divs"].keys():
2484                                customStat["divs"][item["payment"]["currency"]] += payment
2485
2486                            else:
2487                                customStat["divs"][item["payment"]["currency"]] = payment
2488
2489                        # count coupon's income:
2490                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2491                            if item["payment"]["currency"] in customStat["coupons"].keys():
2492                                customStat["coupons"][item["payment"]["currency"]] += payment
2493
2494                            else:
2495                                customStat["coupons"][item["payment"]["currency"]] = payment
2496
2497                        # count broker commissions:
2498                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2499                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2500                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2501
2502                            else:
2503                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2504
2505                        # count service commissions:
2506                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2507                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2508                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2509
2510                            else:
2511                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2512
2513                        # count margin commissions:
2514                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2515                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2516                                customStat["marginCom"][item["payment"]["currency"]] += payment
2517
2518                            else:
2519                                customStat["marginCom"][item["payment"]["currency"]] = payment
2520
2521                        # count withholding taxes:
2522                        elif "_TAX" in item["operationType"]:
2523                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2524                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2525
2526                            else:
2527                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2528
2529                        else:
2530                            continue
2531
2532                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2533
2534                # --- view "Actions" lines:
2535                info.extend([
2536                    "| Report sections            |                               |                              |                      |                        |\n",
2537                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2538                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2539                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2540                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2541                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2542                    ),
2543                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2544                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2545                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2546                    ),
2547                ])
2548
2549                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2550                for key in opsKeys:
2551                    if key == "rub":
2552                        continue
2553
2554                    info.extend([
2555                        "|                            |                               | {:<28} |                      |                        |\n".format(
2556                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2557                        ),
2558                        "|                            |                               | {:<28} |                      |                        |\n".format(
2559                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2560                        ),
2561                    ])
2562
2563                info.append(splitLine1)
2564
2565                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2566                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2567                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2568                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2569                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2570                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2571                    )
2572
2573                # --- view "Payments" lines:
2574                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2575                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2576
2577                for key in paymentsKeys:
2578                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2579
2580                info.append(splitLine1)
2581
2582                # --- view "Commissions and taxes" lines:
2583                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2584                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2585
2586                for key in comKeys:
2587                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2588
2589                info.extend([
2590                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2591                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2592                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2593                ])
2594
2595            else:
2596                info.append("Broker returned no operations during this period\n")
2597
2598            # --- view "Operations" section:
2599            for item in ops:
2600                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2601                    continue
2602
2603                else:
2604                    self._figi = item["figi"] if item["figi"] else ""
2605                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2606                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2607
2608                    # group of deals during one day:
2609                    if nextDay and item["date"].split("T")[0] != nextDay:
2610                        info.append(splitLine2)
2611                        nextDay = ""
2612
2613                    else:
2614                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2615
2616                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2617                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2618                        self._figi if self._figi else "—",
2619                        instrument["ticker"] if instrument else "—",
2620                        instrument["type"] if instrument else "—",
2621                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2622                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2623                        TKS_OPERATION_STATES[item["state"]],
2624                        TKS_OPERATION_TYPES[item["operationType"]],
2625                    ))
2626
2627            infoText = "".join(info)
2628
2629            if show:
2630                if self.moreDebug:
2631                    uLogger.debug("Records about history of a client's operations successfully received")
2632
2633                uLogger.info(infoText)
2634
2635            if self.reportFile:
2636                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2637                    fH.write(infoText)
2638
2639                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2640
2641                if self.useHTMLReports:
2642                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2643                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2644                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2645
2646                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2647
2648        return ops, customStat
2649
2650    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2651        """
2652        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2653
2654        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2655        Warning! Broker server used ISO UTC time by default.
2656
2657        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2658        Also, `historyFile` used to update history with `onlyMissing` parameter.
2659
2660        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2661
2662        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2663        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2664        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2665                         `"hour"`, `"day"`. Default: `"hour"`.
2666        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2667                            False by default. Warning! History appends only from last candle to current time
2668                            with always update last candle!
2669        :param csvSep: separator if csv-file is used, `,` by default.
2670        :param show: if `True` then also prints Pandas DataFrame to the console.
2671        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2672                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2673        """
2674        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2675        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2676        history = None  # empty pandas object for history
2677
2678        if interval not in TKS_CANDLE_INTERVALS.keys():
2679            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2680            raise Exception("Incorrect value")
2681
2682        if not (self._ticker or self._figi):
2683            uLogger.error("Ticker or FIGI must be defined!")
2684            raise Exception("Ticker or FIGI required")
2685
2686        if self._ticker and not self._figi:
2687            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2688            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2689
2690        if self._figi and not self._ticker:
2691            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2692            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2693
2694        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2695        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2696        if interval.lower() != "day":
2697            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2698
2699        delta = dtEnd - dtStart  # current UTC time minus last time in file
2700        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2701
2702        # calculate history length in candles:
2703        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2704        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2705            length += 1  # to avoid fraction time
2706
2707        # calculate data blocks count:
2708        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2709
2710        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2711        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2712        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2713        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2714        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2715
2716        tempOld = None  # pandas object for old history, if --only-missing key present
2717        lastTime = None  # datetime object of last old candle in file
2718
2719        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2720            uLogger.debug("--only-missing key present, add only last missing candles...")
2721            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2722
2723            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2724
2725            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2726            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2727            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2728            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2729
2730            # get last datetime object from last string in file or minus 1 delta if file is empty:
2731            if len(tempOld) > 0:
2732                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2733
2734            else:
2735                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2736
2737            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2738
2739        responseJSONs = []  # raw history blocks of data
2740
2741        blockEnd = dtEnd
2742        for item in range(blocks):
2743            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2744            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2745
2746            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2747                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2748            ))
2749
2750            if blockStart == blockEnd:
2751                uLogger.debug("Skipped this zero-length block...")
2752
2753            else:
2754                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2755                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2756                self.body = str({
2757                    "figi": self._figi,
2758                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2759                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2760                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2761                })
2762                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2763
2764                if "code" in responseJSON.keys():
2765                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2766
2767                else:
2768                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2769                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2770
2771                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2772
2773            blockEnd = blockStart
2774
2775        printCount = len(responseJSONs)  # candles to show in console
2776        if responseJSONs:
2777            tempHistory = pd.DataFrame(
2778                data={
2779                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2780                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2781                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2782                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2783                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2784                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2785                    "volume": [int(item["volume"]) for item in responseJSONs],
2786                },
2787                index=range(len(responseJSONs)),
2788                columns=["date", "time", "open", "high", "low", "close", "volume"],
2789            )
2790            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2791            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2792
2793            # append only newest candles to old history if --only-missing key present:
2794            if onlyMissing and tempOld is not None and lastTime is not None:
2795                index = 0  # find start index in tempHistory data:
2796
2797                for i, item in tempHistory.iterrows():
2798                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2799
2800                    if curTime == lastTime:
2801                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2802                        index = i
2803                        printCount = index + 1
2804                        break
2805
2806                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2807
2808            else:
2809                history = tempHistory  # if no `--only-missing` key then load full data from server
2810
2811            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2812
2813        if history is not None and not history.empty:
2814            if show:
2815                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2816                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2817                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2818                ))
2819
2820        else:
2821            uLogger.warning("Received an empty candles history!")
2822
2823        if self.historyFile is not None:
2824            if history is not None and not history.empty:
2825                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2826                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2827
2828            else:
2829                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2830
2831        else:
2832            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2833
2834        return history
2835
2836    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2837        """
2838        Load candles history from csv-file and return Pandas DataFrame object.
2839
2840        See also: `History()` and `ShowHistoryChart()` methods.
2841
2842        :param filePath: path to csv-file to open.
2843        """
2844        loadedHistory = None  # init candles data object
2845
2846        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2847
2848        if os.path.exists(filePath):
2849            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2850
2851            tfStr = self.priceModel.FormattedDelta(
2852                self.priceModel.timeframe,
2853                "{days} days {hours}h {minutes}m {seconds}s",
2854            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2855                self.priceModel.timeframe,
2856                "{hours}h {minutes}m {seconds}s",
2857            )
2858
2859            if loadedHistory is not None and not loadedHistory.empty:
2860                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2861                    len(loadedHistory),
2862                    tfStr,
2863                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2864                )
2865
2866            else:
2867                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2868
2869        else:
2870            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2871
2872        return loadedHistory
2873
2874    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2875        """
2876        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2877
2878        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2879        Default: `index.html` (both for interact and non-interact candlesticks chart).
2880
2881        See also: `History()` and `LoadHistory()` methods.
2882
2883        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2884        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2885                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2886                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2887                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2888        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2889                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2890        """
2891        if isinstance(candles, str):
2892            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2893            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2894
2895        elif isinstance(candles, pd.DataFrame):
2896            self.priceModel.prices = candles  # set candles chain from variable
2897            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2898
2899            if "datetime" not in candles.columns:
2900                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2901
2902        else:
2903            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2904            raise Exception("Incorrect value")
2905
2906        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2907
2908        if interact:
2909            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2910
2911            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2912
2913        else:
2914            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2915
2916            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2917
2918        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2919
2920    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2921        """
2922        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2923        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2924
2925        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2926
2927        :param operation: string "Buy" or "Sell".
2928        :param lots: volume, integer count of lots >= 1.
2929        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2930        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2931        :param expDate: string "Undefined" by default or local date in future,
2932                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2933        :return: JSON with response from broker server.
2934        """
2935        if self.accountId is None or not self.accountId:
2936            uLogger.error("Variable `accountId` must be defined for using this method!")
2937            raise Exception("Account ID required")
2938
2939        if operation is None or not operation or operation not in ("Buy", "Sell"):
2940            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2941            raise Exception("Incorrect value")
2942
2943        if lots is None or lots < 1:
2944            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2945            lots = 1
2946
2947        if tp is None or tp < 0:
2948            tp = 0
2949
2950        if sl is None or sl < 0:
2951            sl = 0
2952
2953        if expDate is None or not expDate:
2954            expDate = "Undefined"
2955
2956        if not (self._ticker or self._figi):
2957            uLogger.error("Ticker or FIGI must be defined!")
2958            raise Exception("Ticker or FIGI required")
2959
2960        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2961        self._ticker = instrument["ticker"]
2962        self._figi = instrument["figi"]
2963
2964        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
2965
2966        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2967        self.body = str({
2968            "figi": self._figi,
2969            "quantity": str(lots),
2970            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2971            "accountId": str(self.accountId),
2972            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2973        })
2974        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2975
2976        if "orderId" in response.keys():
2977            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2978                operation, response["orderId"],
2979                self._ticker, self._figi, lots,
2980                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2981                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2982                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2983            ))
2984
2985            if tp > 0:
2986                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2987
2988            if sl > 0:
2989                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2990
2991        else:
2992            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
2993
2994        return response
2995
2996    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2997        """
2998        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2999        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3000
3001        See also: `Order()` and `Trade()` docstrings.
3002
3003        :param lots: volume, integer count of lots >= 1.
3004        :param tp: float > 0, take profit price of stop-order.
3005        :param sl: float > 0, stop loss price of stop-order.
3006        :param expDate: it's a local date in future.
3007                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3008        :return: JSON with response from broker server.
3009        """
3010        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3011
3012    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3013        """
3014        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3015        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3016
3017        See also: `Order()` and `Trade()` docstrings.
3018
3019        :param lots: volume, integer count of lots >= 1.
3020        :param tp: float > 0, take profit price of stop-order.
3021        :param sl: float > 0, stop loss price of stop-order.
3022        :param expDate: it's a local date in the future.
3023                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3024        :return: JSON with response from broker server.
3025        """
3026        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3027
3028    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3029        """
3030        Close position of given instruments.
3031
3032        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3033        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3034                         This avoids unnecessary downloading data from the server.
3035        """
3036        if instruments is None or not instruments:
3037            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3038            raise Exception("Ticker or FIGI required")
3039
3040        if isinstance(instruments, str):
3041            instruments = [instruments]
3042
3043        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3044        if uniqueInstruments:
3045            if portfolio is None or not portfolio:
3046                portfolio = self.Overview(show=False)
3047
3048            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3049            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3050
3051            for self._figi in uniqueInstruments:
3052                if self._figi not in allOpened:
3053                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3054                    continue
3055
3056                # search open trade info about instrument by ticker:
3057                instrument = {}
3058                for iType in TKS_INSTRUMENTS:
3059                    if instrument:
3060                        break
3061
3062                    for item in portfolio["stat"][iType]:
3063                        if item["figi"] == self._figi:
3064                            instrument = item
3065                            break
3066
3067                if instrument:
3068                    self._ticker = instrument["ticker"]
3069                    self._figi = instrument["figi"]
3070
3071                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3072                        self._ticker,
3073                        self._figi,
3074                        int(instrument["volume"]),
3075                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3076                    ))
3077
3078                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3079
3080                    if tradeLots > 0:
3081                        if instrument["blocked"] > 0:
3082                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3083                                instrument["blocked"],
3084                                self._ticker,
3085                                tradeLots,
3086                            ))
3087
3088                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3089                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3090
3091                    else:
3092                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3093
3094    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3095        """
3096        Close all positions of given instruments with defined type.
3097
3098        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3099        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3100                         This avoids unnecessary downloading data from the server.
3101        """
3102        if iType not in TKS_INSTRUMENTS:
3103            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3104
3105        else:
3106            if portfolio is None or not portfolio:
3107                portfolio = self.Overview(show=False)
3108
3109            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3110            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3111
3112            if tickers and portfolio:
3113                self.CloseTrades(tickers, portfolio)
3114
3115            else:
3116                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3117
3118    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3119        """
3120        Universal method to create market or limit orders with all available parameters for current `accountId`.
3121        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3122
3123        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3124        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3125
3126        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3127        then broker immediately open market order as you can do simple --buy or --sell operations!
3128
3129        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3130        When current price will go up or down to target price value then broker opens a limit order.
3131        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3132
3133        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3134
3135        :param operation: string "Buy" or "Sell".
3136        :param orderType: string "Limit" or "Stop".
3137        :param lots: volume, integer count of lots >= 1.
3138        :param targetPrice: target price > 0. This is open trade price for limit order.
3139        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3140                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3141        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3142                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3143                         Stop loss order always executed by market price.
3144        :param expDate: string "Undefined" by default or local date in future.
3145                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3146                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3147                        A limit order has no expiration date, it lasts until the end of the trading day.
3148        :return: JSON with response from broker server.
3149        """
3150        if self.accountId is None or not self.accountId:
3151            uLogger.error("Variable `accountId` must be defined for using this method!")
3152            raise Exception("Account ID required")
3153
3154        if operation is None or not operation or operation not in ("Buy", "Sell"):
3155            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3156            raise Exception("Incorrect value")
3157
3158        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3159            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3160            raise Exception("Incorrect value")
3161
3162        if lots is None or lots < 1:
3163            uLogger.error("You must define trade volume > 0: integer count of lots!")
3164            raise Exception("Incorrect value")
3165
3166        if targetPrice is None or targetPrice <= 0:
3167            uLogger.error("Target price for limit-order must be greater than 0!")
3168            raise Exception("Incorrect value")
3169
3170        if limitPrice is None or limitPrice <= 0:
3171            limitPrice = targetPrice
3172
3173        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3174            stopType = "Limit"
3175
3176        if expDate is None or not expDate:
3177            expDate = "Undefined"
3178
3179        if not (self._ticker or self._figi):
3180            uLogger.error("Tocker or FIGI must be defined!")
3181            raise Exception("Ticker or FIGI required")
3182
3183        response = {}
3184        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3185        self._ticker = instrument["ticker"]
3186        self._figi = instrument["figi"]
3187
3188        if orderType == "Limit":
3189            uLogger.debug(
3190                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3191                    self._ticker, self._figi,
3192                    operation, lots, targetPrice, instrument["currency"],
3193                ))
3194
3195            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3196            self.body = str({
3197                "figi": self._figi,
3198                "quantity": str(lots),
3199                "price": FloatToNano(targetPrice),
3200                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3201                "accountId": str(self.accountId),
3202                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3203            })
3204            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3205
3206            if "orderId" in response.keys():
3207                uLogger.info(
3208                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3209                        response["orderId"],
3210                        self._ticker, self._figi,
3211                        operation, lots, targetPrice, instrument["currency"],
3212                    ))
3213
3214                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3215                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3216                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3217                            targetPrice, instrument["currency"],
3218                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3219                        ))
3220
3221                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3222                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3223                            targetPrice, instrument["currency"],
3224                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3225                        ))
3226
3227            else:
3228                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3229
3230        if orderType == "Stop":
3231            uLogger.debug(
3232                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3233                    self._ticker, self._figi,
3234                    operation, lots,
3235                    targetPrice, instrument["currency"],
3236                    limitPrice, instrument["currency"],
3237                    stopType, expDate,
3238                ))
3239
3240            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3241            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3242            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3243
3244            body = {
3245                "figi": self._figi,
3246                "quantity": str(lots),
3247                "price": FloatToNano(limitPrice),
3248                "stopPrice": FloatToNano(targetPrice),
3249                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3250                "accountId": str(self.accountId),
3251                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3252                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3253            }
3254
3255            if expDateUTC:
3256                body["expireDate"] = expDateUTC
3257
3258            self.body = str(body)
3259            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3260
3261            if "stopOrderId" in response.keys():
3262                uLogger.info(
3263                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3264                        response["stopOrderId"],
3265                        self._ticker, self._figi,
3266                        operation, lots,
3267                        targetPrice, instrument["currency"],
3268                        limitPrice, instrument["currency"],
3269                        TKS_STOP_ORDER_TYPES[stopOrderType],
3270                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3271                    ))
3272
3273                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3274                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3275                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3276                            targetPrice, instrument["currency"],
3277                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3278                        ))
3279
3280                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3281                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3282                            targetPrice, instrument["currency"],
3283                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3284                        ))
3285
3286            else:
3287                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3288
3289        return response
3290
3291    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3292        """
3293        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3294        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3295        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3296        See also: `Order()` docstring.
3297
3298        :param lots: volume, integer count of lots >= 1.
3299        :param targetPrice: target price > 0. This is open trade price for limit order.
3300        :return: JSON with response from broker server.
3301        """
3302        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3303
3304    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3305        """
3306        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3307        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3308        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3309        target price value then broker opens a limit order. See also: `Order()` docstring.
3310
3311        :param lots: volume, integer count of lots >= 1.
3312        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3313        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3314                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3315        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3316                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3317        :param expDate: string "Undefined" by default or local date in future.
3318                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3319                        This date is converting to UTC format for server.
3320        :return: JSON with response from broker server.
3321        """
3322        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3323
3324    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3325        """
3326        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3327        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3328        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3329        See also: `Order()` docstring.
3330
3331        :param lots: volume, integer count of lots >= 1.
3332        :param targetPrice: target price > 0. This is open trade price for limit order.
3333        :return: JSON with response from broker server.
3334        """
3335        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3336
3337    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3338        """
3339        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3340        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3341        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3342        target price value then broker opens a limit order. See also: `Order()` docstring.
3343
3344        :param lots: volume, integer count of lots >= 1.
3345        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3346        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3347                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3348        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3349                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3350        :param expDate: string "Undefined" by default or local date in future.
3351                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3352                        This date is converting to UTC format for server.
3353        :return: JSON with response from broker server.
3354        """
3355        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3356
3357    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3358        """
3359        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3360
3361        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3362        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3363                             This avoids unnecessary downloading data from the server.
3364        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3365        """
3366        if self.accountId is None or not self.accountId:
3367            uLogger.error("Variable `accountId` must be defined for using this method!")
3368            raise Exception("Account ID required")
3369
3370        if orderIDs:
3371            if allOrdersIDs is None:
3372                rawOrders = self.RequestPendingOrders()
3373                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3374
3375            if allStopOrdersIDs is None:
3376                rawStopOrders = self.RequestStopOrders()
3377                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3378
3379            for orderID in orderIDs:
3380                idInPendingOrders = orderID in allOrdersIDs
3381                idInStopOrders = orderID in allStopOrdersIDs
3382
3383                if not (idInPendingOrders or idInStopOrders):
3384                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3385                    continue
3386
3387                else:
3388                    if idInPendingOrders:
3389                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3390
3391                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3392                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3393                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3394                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3395
3396                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3397                            if self.moreDebug:
3398                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3399
3400                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3401
3402                        else:
3403                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3404
3405                    elif idInStopOrders:
3406                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3407
3408                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3409                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3410                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3411                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3412
3413                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3414                            if self.moreDebug:
3415                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3416
3417                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3418
3419                        else:
3420                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3421
3422                    else:
3423                        continue
3424
3425    def CloseAllOrders(self) -> None:
3426        """
3427        Gets a list of open pending and stop orders and cancel it all.
3428        """
3429        rawOrders = self.RequestPendingOrders()
3430        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3431        lenOrders = len(allOrdersIDs)
3432
3433        rawStopOrders = self.RequestStopOrders()
3434        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3435        lenSOrders = len(allStopOrdersIDs)
3436
3437        if lenOrders > 0 or lenSOrders > 0:
3438            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3439
3440            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3441
3442        else:
3443            uLogger.info("Orders not found, nothing to cancel.")
3444
3445    def CloseAll(self, *args) -> None:
3446        """
3447        Close all available (not blocked) opened trades and orders.
3448
3449        Also, you can select one or more keywords case-insensitive:
3450        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3451
3452        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3453        """
3454        overview = self.Overview(show=False)  # get all open trades info
3455
3456        if len(args) == 0:
3457            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3458            self.CloseAllOrders()  # close all pending and stop orders
3459
3460            for iType in TKS_INSTRUMENTS:
3461                if iType != "Currencies":
3462                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3463
3464        else:
3465            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3466            lowerArgs = [x.lower() for x in args]
3467
3468            if "orders" in lowerArgs:
3469                self.CloseAllOrders()  # close all pending and stop orders
3470
3471            for iType in TKS_INSTRUMENTS:
3472                if iType.lower() in lowerArgs and iType != "Currencies":
3473                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3474
3475    def CloseAllByTicker(self, instrument: str) -> None:
3476        """
3477        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3478
3479        This method searches opened trade and orders of instrument throw all portfolio and then use
3480        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3481
3482        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3483
3484        :param instrument: string with ticker.
3485        """
3486        if instrument is None or not instrument:
3487            uLogger.error("Ticker name must be defined for using this method!")
3488            raise Exception("Ticker required")
3489
3490        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3491
3492        self._ticker = instrument  # try to set instrument as ticker
3493        self._figi = ""
3494
3495        if self.IsInPortfolio(portfolio=overview):
3496            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3497            self.CloseTrades(instruments=[instrument], portfolio=overview)
3498
3499        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3500        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3501
3502        if limitAll and self.IsInLimitOrders(portfolio=overview):
3503            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3504            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3505
3506        if stopAll and self.IsInStopOrders(portfolio=overview):
3507            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3508            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3509
3510    def CloseAllByFIGI(self, instrument: str) -> None:
3511        """
3512        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3513
3514        This method searches opened trade and orders of instrument throw all portfolio and then use
3515        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3516
3517        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3518
3519        :param instrument: string with FIGI id.
3520        """
3521        if instrument is None or not instrument:
3522            uLogger.error("FIGI id must be defined for using this method!")
3523            raise Exception("FIGI required")
3524
3525        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3526
3527        self._ticker = ""
3528        self._figi = instrument  # try to set instrument as FIGI id
3529
3530        if self.IsInPortfolio(portfolio=overview):
3531            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3532            self.CloseTrades(instruments=[instrument], portfolio=overview)
3533
3534        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3535        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3536
3537        if limitAll and self.IsInLimitOrders(portfolio=overview):
3538            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3539            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3540
3541        if stopAll and self.IsInStopOrders(portfolio=overview):
3542            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3543            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3544
3545    @staticmethod
3546    def ParseOrderParameters(operation, **inputParameters):
3547        """
3548        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3549
3550        :param operation: string "Buy" or "Sell".
3551        :param inputParameters: this is dict of strings that looks like this
3552               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3553               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3554               "prices" key: one or more prices to open limit-orders
3555               Counts of values in lots and prices lists must be equals!
3556        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3557        """
3558        # TODO: update order grid work with api v2
3559        pass
3560        # uLogger.debug("Input parameters: {}".format(inputParameters))
3561        #
3562        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3563        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3564        #     raise Exception("Incorrect value")
3565        #
3566        # if "l" in inputParameters.keys():
3567        #     inputParameters["lots"] = inputParameters.pop("l")
3568        #
3569        # if "p" in inputParameters.keys():
3570        #     inputParameters["prices"] = inputParameters.pop("p")
3571        #
3572        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3573        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3574        #     raise Exception("Incorrect value")
3575        #
3576        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3577        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3578        #
3579        # if len(lots) != len(prices):
3580        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3581        #     raise Exception("Incorrect value")
3582        #
3583        # uLogger.debug("Extracted parameters for orders:")
3584        # uLogger.debug("lots = {}".format(lots))
3585        # uLogger.debug("prices = {}".format(prices))
3586        #
3587        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3588        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3589        # uLogger.debug("Order parameters: {}".format(result))
3590        #
3591        # return result
3592
3593    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3594        """
3595        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3596
3597        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3598        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3599        """
3600        result = False
3601        msg = "Instrument not defined!"
3602
3603        if portfolio is None or not portfolio:
3604            portfolio = self.Overview(show=False)
3605
3606        if self._ticker:
3607            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3608            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3609
3610            for iType in TKS_INSTRUMENTS:
3611                for instrument in portfolio["stat"][iType]:
3612                    if instrument["ticker"] == self._ticker:
3613                        result = True
3614                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3615                        break
3616
3617        elif self._figi:
3618            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3619            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3620
3621            for iType in TKS_INSTRUMENTS:
3622                for instrument in portfolio["stat"][iType]:
3623                    if instrument["figi"] == self._figi:
3624                        result = True
3625                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3626                        break
3627
3628        else:
3629            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3630
3631        uLogger.debug(msg)
3632
3633        return result
3634
3635    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3636        """
3637        Returns instrument from the user's portfolio if it presents there.
3638        Instrument must be defined by `ticker` (highly priority) or `figi`.
3639
3640        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3641        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3642        """
3643        result = None
3644        msg = "Instrument not defined!"
3645
3646        if portfolio is None or not portfolio:
3647            portfolio = self.Overview(show=False)
3648
3649        if self._ticker:
3650            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3651            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3652
3653            for iType in TKS_INSTRUMENTS:
3654                for instrument in portfolio["stat"][iType]:
3655                    if instrument["ticker"] == self._ticker:
3656                        result = instrument
3657                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3658                        break
3659
3660        elif self._figi:
3661            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3662            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3663
3664            for iType in TKS_INSTRUMENTS:
3665                for instrument in portfolio["stat"][iType]:
3666                    if instrument["figi"] == self._figi:
3667                        result = instrument
3668                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3669                        break
3670
3671        else:
3672            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3673
3674        uLogger.debug(msg)
3675
3676        return result
3677
3678    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3679        """
3680        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3681
3682        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3683
3684        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3685        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3686        """
3687        result = False
3688        msg = "Instrument not defined!"
3689
3690        if portfolio is None or not portfolio:
3691            portfolio = self.Overview(show=False)
3692
3693        if self._ticker:
3694            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3695            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3696
3697            for instrument in portfolio["stat"]["orders"]:
3698                if instrument["ticker"] == self._ticker:
3699                    result = True
3700                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3701                    break
3702
3703        elif self._figi:
3704            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3705            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3706
3707            for instrument in portfolio["stat"]["orders"]:
3708                if instrument["figi"] == self._figi:
3709                    result = True
3710                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3711                    break
3712
3713        else:
3714            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3715
3716        uLogger.debug(msg)
3717
3718        return result
3719
3720    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3721        """
3722        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3723        Instrument must be defined by `ticker` (highly priority) or `figi`.
3724
3725        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3726
3727        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3728        :return: list with `orderID`s of limit orders.
3729        """
3730        result = []
3731        msg = "Instrument not defined!"
3732
3733        if portfolio is None or not portfolio:
3734            portfolio = self.Overview(show=False)
3735
3736        if self._ticker:
3737            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3738            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3739
3740            for instrument in portfolio["stat"]["orders"]:
3741                if instrument["ticker"] == self._ticker:
3742                    result.append(instrument["orderID"])
3743
3744            if result:
3745                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3746
3747        elif self._figi:
3748            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3749            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3750
3751            for instrument in portfolio["stat"]["orders"]:
3752                if instrument["figi"] == self._figi:
3753                    result.append(instrument["orderID"])
3754
3755            if result:
3756                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3757
3758        else:
3759            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3760
3761        uLogger.debug(msg)
3762
3763        return result
3764
3765    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3766        """
3767        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3768
3769        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3770
3771        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3772        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3773        """
3774        result = False
3775        msg = "Instrument not defined!"
3776
3777        if portfolio is None or not portfolio:
3778            portfolio = self.Overview(show=False)
3779
3780        if self._ticker:
3781            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3782            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3783
3784            for instrument in portfolio["stat"]["stopOrders"]:
3785                if instrument["ticker"] == self._ticker:
3786                    result = True
3787                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3788                    break
3789
3790        elif self._figi:
3791            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3792            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3793
3794            for instrument in portfolio["stat"]["stopOrders"]:
3795                if instrument["figi"] == self._figi:
3796                    result = True
3797                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3798                    break
3799
3800        else:
3801            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3802
3803        uLogger.debug(msg)
3804
3805        return result
3806
3807    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3808        """
3809        Returns list with all `orderID`s of opened stop orders for the instrument.
3810        Instrument must be defined by `ticker` (highly priority) or `figi`.
3811
3812        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3813
3814        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3815        :return: list with `orderID`s of stop orders.
3816        """
3817        result = []
3818        msg = "Instrument not defined!"
3819
3820        if portfolio is None or not portfolio:
3821            portfolio = self.Overview(show=False)
3822
3823        if self._ticker:
3824            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3825            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3826
3827            for instrument in portfolio["stat"]["stopOrders"]:
3828                if instrument["ticker"] == self._ticker:
3829                    result.append(instrument["orderID"])
3830
3831            if result:
3832                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3833
3834        elif self._figi:
3835            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3836            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3837
3838            for instrument in portfolio["stat"]["stopOrders"]:
3839                if instrument["figi"] == self._figi:
3840                    result.append(instrument["orderID"])
3841
3842            if result:
3843                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3844
3845        else:
3846            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3847
3848        uLogger.debug(msg)
3849
3850        return result
3851
3852    def RequestLimits(self) -> dict:
3853        """
3854        Method for obtaining the available funds for withdrawal for current `accountId`.
3855
3856        See also:
3857        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3858        - `OverviewLimits()` method
3859
3860        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3861                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3862                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3863                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3864        """
3865        if self.accountId is None or not self.accountId:
3866            uLogger.error("Variable `accountId` must be defined for using this method!")
3867            raise Exception("Account ID required")
3868
3869        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3870
3871        self.body = str({"accountId": self.accountId})
3872        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3873        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3874
3875        if self.moreDebug:
3876            uLogger.debug("Records about available funds for withdrawal successfully received")
3877
3878        return rawLimits
3879
3880    def OverviewLimits(self, show: bool = False) -> dict:
3881        """
3882        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3883
3884        See also: `RequestLimits()`.
3885
3886        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3887        :return: dict with raw parsed data from server and some calculated statistics about it.
3888        """
3889        if self.accountId is None or not self.accountId:
3890            uLogger.error("Variable `accountId` must be defined for using this method!")
3891            raise Exception("Account ID required")
3892
3893        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3894
3895        view = {
3896            "rawLimits": rawLimits,
3897            "limits": {  # parsed data for every currency:
3898                "money": {  # this is an array of portfolio currency positions
3899                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3900                },
3901                "blocked": {  # this is an array of blocked currency
3902                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3903                },
3904                "blockedGuarantee": {  # this is locked money under collateral for futures
3905                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3906                },
3907            },
3908        }
3909
3910        # --- Prepare text table with limits in human-readable format:
3911        if show:
3912            info = [
3913                "# Withdrawal limits\n\n",
3914                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3915                "* **Account ID:** [{}]\n".format(self.accountId),
3916            ]
3917
3918            if view["limits"]["money"]:
3919                info.extend([
3920                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3921                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3922                ])
3923
3924            else:
3925                info.append("\nNo withdrawal limits\n")
3926
3927            for curr in view["limits"]["money"].keys():
3928                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3929                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3930                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3931
3932                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3933                    "[{}]".format(curr),
3934                    "{:.2f}".format(view["limits"]["money"][curr]),
3935                    "{:.2f}".format(availableMoney),
3936                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3937                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3938                )
3939
3940                if curr == "rub":
3941                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3942
3943                else:
3944                    info.append(infoStr)
3945
3946            infoText = "".join(info)
3947
3948            uLogger.info(infoText)
3949
3950            if self.withdrawalLimitsFile:
3951                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3952                    fH.write(infoText)
3953
3954                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3955
3956                if self.useHTMLReports:
3957                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3958                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3959                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3960
3961                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
3962
3963        return view
3964
3965    def RequestAccounts(self) -> dict:
3966        """
3967        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3968
3969        See also:
3970        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3971        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3972        - `OverviewUserInfo()` method
3973
3974        :return: dict with raw data from server that contains accounts info. Example of dict:
3975                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3976                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3977                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3978                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3979        """
3980        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3981
3982        self.body = str({})
3983        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3984        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3985
3986        if self.moreDebug:
3987            uLogger.debug("Records about available accounts successfully received")
3988
3989        return rawAccounts
3990
3991    def RequestUserInfo(self) -> dict:
3992        """
3993        Method for requesting common user's information.
3994
3995        See also:
3996        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3997        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3998        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3999        - `OverviewUserInfo()` method
4000
4001        :return: dict with raw data from server that contains user's information. Example of dict:
4002                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4003                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4004        """
4005        uLogger.debug("Requesting common user's information. Wait, please...")
4006
4007        self.body = str({})
4008        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4009        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4010
4011        if self.moreDebug:
4012            uLogger.debug("Records about current user successfully received")
4013
4014        return rawUserInfo
4015
4016    def RequestMarginStatus(self, accountId: str = None) -> dict:
4017        """
4018        Method for requesting margin calculation for defined account ID.
4019
4020        See also:
4021        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4022        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4023        - `OverviewUserInfo()` method
4024
4025        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4026        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4027                 Example of responses:
4028                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4029                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4030                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4031                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4032                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4033                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4034        """
4035        if accountId is None or not accountId:
4036            if self.accountId is None or not self.accountId:
4037                uLogger.error("Variable `accountId` must be defined for using this method!")
4038                raise Exception("Account ID required")
4039
4040            else:
4041                accountId = self.accountId  # use `self.accountId` (main ID) by default
4042
4043        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4044
4045        self.body = str({"accountId": accountId})
4046        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4047        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4048
4049        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4050            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4051            rawMargin = {}
4052
4053        else:
4054            if self.moreDebug:
4055                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4056
4057        return rawMargin
4058
4059    def RequestTariffLimits(self) -> dict:
4060        """
4061        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4062
4063        See also:
4064        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4065        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4066        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4067        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4068        - `OverviewUserInfo()` method
4069
4070        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4071                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4072                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4073        """
4074        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4075
4076        self.body = str({})
4077        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4078        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4079
4080        if self.moreDebug:
4081            uLogger.debug("Records with limits of current tariff successfully received")
4082
4083        return rawTariffLimits
4084
4085    def RequestBondCoupons(self, iJSON: dict) -> dict:
4086        """
4087        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4088        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4089        All dates are in UTC timezone.
4090
4091        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4092        Documentation:
4093        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4094        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4095
4096        See also: `ExtendBondsData()`.
4097
4098        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4099                      If raw iJSON is not data of bond then server returns an error [400] with message:
4100                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4101        :return: dictionary with bond payment calendar. Response example
4102                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4103                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4104                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4105                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4106        """
4107        if iJSON["figi"] is None or not iJSON["figi"]:
4108            uLogger.error("FIGI must be defined for using this method!")
4109            raise Exception("FIGI required")
4110
4111        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4112        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4113
4114        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4115            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4116            self._figi,
4117            startDate,
4118            endDate,
4119        ))
4120
4121        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4122        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4123        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4124
4125        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4126            uLogger.warning("Instrument type is not bond!")
4127
4128        else:
4129            if self.moreDebug:
4130                uLogger.debug("Records about bond payment calendar successfully received")
4131
4132        return calendar
4133
4134    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4135        """
4136        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4137        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4138        coupon yields, current yields and some statistics etc.
4139
4140        WARNING! This is too long operation if a lot of bonds requested from broker server.
4141
4142        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4143
4144        :param instruments: list of strings with tickers or FIGIs.
4145        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4146                     for further used by data scientists or stock analytics.
4147        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4148                 In XLSX-file and Pandas DataFrame fields mean:
4149                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4150                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4151        """
4152        if instruments is None or not instruments:
4153            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4154            raise Exception("Ticker or FIGI required")
4155
4156        if isinstance(instruments, str):
4157            instruments = [instruments]
4158
4159        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4160
4161        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4162
4163        iCount = len(uniqueInstruments)
4164        tooLong = iCount >= 20
4165        if tooLong:
4166            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4167
4168        bonds = None
4169        for i, self._figi in enumerate(uniqueInstruments):
4170            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4171
4172            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4173                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4174                rawBond = self.SearchByFIGI(requestPrice=True)
4175
4176                # Widen raw data with UTC current time (iData["actualDateTime"]):
4177                actualDate = datetime.now(tzutc())
4178                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4179
4180                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4181                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4182
4183                # Replace some values with human-readable:
4184                iData["nominalCurrency"] = iData["nominal"]["currency"]
4185                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4186                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4187                iData["aciCurrency"] = iData["aciValue"]["currency"]
4188                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4189                iData["issueSize"] = int(iData["issueSize"])
4190                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4191                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4192                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4193                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4194                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4195                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4196                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4197                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4198                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4199                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4200
4201                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4202                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4203                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4204                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4205                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4206                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4207                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4208                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4209                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4210                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4211                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4212
4213                # Widen raw data with calendar data from `rawCalendar` values:
4214                calendarData = []
4215                if "events" in iData["rawCalendar"].keys():
4216                    for item in iData["rawCalendar"]["events"]:
4217                        calendarData.append({
4218                            "couponDate": item["couponDate"],
4219                            "couponNumber": int(item["couponNumber"]),
4220                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4221                            "payCurrency": item["payOneBond"]["currency"],
4222                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4223                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4224                            "couponStartDate": item["couponStartDate"],
4225                            "couponEndDate": item["couponEndDate"],
4226                            "couponPeriod": item["couponPeriod"],
4227                        })
4228
4229                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4230                    if "maturityDate" not in iData.keys():
4231                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4232
4233                # Widen raw data with Coupon Rate.
4234                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4235                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4236                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4237                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4238
4239                # Widen raw data with Yield to Maturity (YTM) on current date.
4240                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4241                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4242                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4243                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4244                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4245                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4246
4247                iData["calendar"] = calendarData  # adds calendar at the end
4248
4249                # Remove not used data:
4250                iData.pop("uid")
4251                iData.pop("positionUid")
4252                iData.pop("currentPrice")
4253                iData.pop("rawCalendar")
4254
4255                colNames = list(iData.keys())
4256                if bonds is None:
4257                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4258
4259                else:
4260                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4261
4262            else:
4263                uLogger.warning("Instrument is not a bond!")
4264
4265            processed = round(100 * (i + 1) / iCount, 1)
4266            if tooLong and processed % 5 == 0:
4267                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4268
4269            else:
4270                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4271
4272        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4273
4274        # Saving bonds from Pandas DataFrame to XLSX sheet:
4275        if xlsx and self.bondsXLSXFile:
4276            with pd.ExcelWriter(
4277                    path=self.bondsXLSXFile,
4278                    date_format=TKS_DATE_FORMAT,
4279                    datetime_format=TKS_DATE_TIME_FORMAT,
4280                    mode="w",
4281            ) as writer:
4282                bonds.to_excel(
4283                    writer,
4284                    sheet_name="Extended bonds data",
4285                    index=True,
4286                    encoding="UTF-8",
4287                    freeze_panes=(1, 1),
4288                )  # saving as XLSX-file with freeze first row and column as headers
4289
4290            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4291
4292        return bonds
4293
4294    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4295        """
4296        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4297
4298        WARNING! This is too long operation if a lot of bonds requested from broker server.
4299
4300        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4301
4302        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4303                        extended information about bonds: main info, current prices, bond payment calendar,
4304                        coupon yields, current yields and some statistics etc.
4305                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4306        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4307                     for further used by data scientists or stock analytics.
4308        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4309        """
4310        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4311            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4312
4313        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4314
4315        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4316        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4317        calendar = None
4318        for bond in extBonds.iterrows():
4319            for item in bond[1]["calendar"]:
4320                cData = {
4321                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4322                    "couponDate": item["couponDate"],
4323                    "figi": bond[1]["figi"],
4324                    "ticker": bond[1]["ticker"],
4325                    "name": bond[1]["name"],
4326                    "couponNumber": item["couponNumber"],
4327                    "payOneBond": item["payOneBond"],
4328                    "payCurrency": item["payCurrency"],
4329                    "couponType": item["couponType"],
4330                    "couponPeriod": item["couponPeriod"],
4331                    "fixDate": item["fixDate"],
4332                    "couponStartDate": item["couponStartDate"],
4333                    "couponEndDate": item["couponEndDate"],
4334                }
4335
4336                if calendar is None:
4337                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4338
4339                else:
4340                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4341
4342        if calendar is not None:
4343            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4344
4345            # Saving calendar from Pandas DataFrame to XLSX sheet:
4346            if xlsx:
4347                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4348
4349                with pd.ExcelWriter(
4350                        path=xlsxCalendarFile,
4351                        date_format=TKS_DATE_FORMAT,
4352                        datetime_format=TKS_DATE_TIME_FORMAT,
4353                        mode="w",
4354                ) as writer:
4355                    humanReadable = calendar.copy(deep=True)
4356                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4357                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4358                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4359                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4360                    humanReadable.columns = colNames  # human-readable column names
4361
4362                    humanReadable.to_excel(
4363                        writer,
4364                        sheet_name="Bond payments calendar",
4365                        index=False,
4366                        encoding="UTF-8",
4367                        freeze_panes=(1, 2),
4368                    )  # saving as XLSX-file with freeze first row and column as headers
4369
4370                    del humanReadable  # release df in memory
4371
4372                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4373
4374        return calendar
4375
4376    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4377        """
4378        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4379        Also, creates Markdown file with calendar data, `calendar.md` by default.
4380
4381        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4382
4383        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4384                        extended information about bonds: main info, current prices, bond payment calendar,
4385                        coupon yields, current yields and some statistics etc.
4386                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4387        :param show: if `True` then also printing bonds payment calendar to the console,
4388                     otherwise save to file `calendarFile` only. `False` by default.
4389        :return: multilines text in Markdown format with bonds payment calendar as a table.
4390        """
4391        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4392            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4393
4394        infoText = "# Bond payments calendar\n\n"
4395
4396        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4397
4398        if not (calendar is None or calendar.empty):
4399            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4400
4401            info = [
4402                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4403                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4404                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4405            ]
4406
4407            newMonth = False
4408            notOneBond = calendar["figi"].nunique() > 1
4409            for i, bond in enumerate(calendar.iterrows()):
4410                if newMonth and notOneBond:
4411                    info.append(splitLine)
4412
4413                info.append(
4414                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4415                        "  √" if bond[1]["paid"] else "  —",
4416                        bond[1]["couponDate"].split("T")[0],
4417                        bond[1]["figi"],
4418                        bond[1]["ticker"],
4419                        bond[1]["couponNumber"],
4420                        "{} {}".format(
4421                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4422                            bond[1]["payCurrency"],
4423                        ),
4424                        bond[1]["couponType"],
4425                        bond[1]["couponPeriod"],
4426                        bond[1]["fixDate"].split("T")[0],
4427                    )
4428                )
4429
4430                if i < len(calendar.values) - 1:
4431                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4432                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4433                    newMonth = False if curDate.month == nextDate.month else True
4434
4435                else:
4436                    newMonth = False
4437
4438            infoText += "".join(info)
4439
4440            if show:
4441                uLogger.info("{}".format(infoText))
4442
4443            if self.calendarFile is not None:
4444                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4445                    fH.write(infoText)
4446
4447                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4448
4449                if self.useHTMLReports:
4450                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4451                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4452                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4453
4454                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4455
4456        else:
4457            infoText += "No data\n"
4458
4459        return infoText
4460
4461    def OverviewAccounts(self, show: bool = False) -> dict:
4462        """
4463        Method for parsing and show simple table with all available user accounts.
4464
4465        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4466
4467        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4468        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4469                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4470                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4471                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4472                                                        "closed": "—", "access": "Full access" }, ...}}`
4473        """
4474        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4475
4476        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4477        accounts = {
4478            item["id"]: {
4479                "type": TKS_ACCOUNT_TYPES[item["type"]],
4480                "name": item["name"],
4481                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4482                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4483                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4484                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4485            } for item in rawAccounts["accounts"]
4486        }
4487
4488        # Raw and parsed data with some fields replaced in "stat" section:
4489        view = {
4490            "rawAccounts": rawAccounts,
4491            "stat": accounts,
4492        }
4493
4494        # --- Prepare simple text table with only accounts data in human-readable format:
4495        if show:
4496            info = [
4497                "# User accounts\n\n",
4498                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4499                "| Account ID   | Type                      | Status                    | Name                           |\n",
4500                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4501            ]
4502
4503            for account in view["stat"].keys():
4504                info.extend([
4505                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4506                        account,
4507                        view["stat"][account]["type"],
4508                        view["stat"][account]["status"],
4509                        view["stat"][account]["name"],
4510                    )
4511                ])
4512
4513            infoText = "".join(info)
4514
4515            uLogger.info(infoText)
4516
4517            if self.userAccountsFile:
4518                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4519                    fH.write(infoText)
4520
4521                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4522
4523                if self.useHTMLReports:
4524                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4525                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4526                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4527
4528                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4529
4530        return view
4531
4532    def OverviewUserInfo(self, show: bool = False) -> dict:
4533        """
4534        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4535
4536        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4537
4538        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4539        :return: dict with raw parsed data from server and some calculated statistics about it.
4540        """
4541        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4542        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4543        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4544        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4545        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4546        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4547
4548        # This is dict with parsed common user data:
4549        userInfo = {
4550            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4551            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4552            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4553            "tariff": rawUserInfo["tariff"],
4554        }
4555
4556        # This is an array of dict with parsed margin statuses for every account IDs:
4557        margins = {}
4558        for accountId in accounts.keys():
4559            if rawMargins[accountId]:
4560                margins[accountId] = {
4561                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4562                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4563                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4564                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4565                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4566                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4567                }
4568
4569            else:
4570                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4571
4572        unary = {}  # unary-connection limits
4573        for item in rawTariffLimits["unaryLimits"]:
4574            if item["limitPerMinute"] in unary.keys():
4575                unary[item["limitPerMinute"]].extend(item["methods"])
4576
4577            else:
4578                unary[item["limitPerMinute"]] = item["methods"]
4579
4580        stream = {}  # stream-connection limits
4581        for item in rawTariffLimits["streamLimits"]:
4582            if item["limit"] in stream.keys():
4583                stream[item["limit"]].extend(item["streams"])
4584
4585            else:
4586                stream[item["limit"]] = item["streams"]
4587
4588        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4589        limits = {
4590            "unary": unary,
4591            "stream": stream,
4592        }
4593
4594        # Raw and parsed data as an output result:
4595        view = {
4596            "rawUserInfo": rawUserInfo,
4597            "rawAccounts": rawAccounts,
4598            "rawMargins": rawMargins,
4599            "rawTariffLimits": rawTariffLimits,
4600            "stat": {
4601                "userInfo": userInfo,
4602                "accounts": accounts,
4603                "margins": margins,
4604                "limits": limits,
4605            },
4606        }
4607
4608        # --- Prepare text table with user information in human-readable format:
4609        if show:
4610            info = [
4611                "# Full user information\n\n",
4612                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4613                "## Common information\n\n",
4614                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4615                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4616                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4617                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4618                "\n## User accounts\n\n",
4619            ]
4620
4621            for account in view["stat"]["accounts"].keys():
4622                info.extend([
4623                    "### ID: [{}]\n\n".format(account),
4624                    "| Parameters           | Values                                                       |\n",
4625                    "|----------------------|--------------------------------------------------------------|\n",
4626                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4627                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4628                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4629                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4630                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4631                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4632                ])
4633
4634                if margins[account]:
4635                    info.extend([
4636                        "| Margin status:       | Enabled                                                      |\n",
4637                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4638                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4639                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4640                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4641                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4642                    ])
4643
4644                else:
4645                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4646
4647            info.extend([
4648                "\n## Current user tariff limits\n",
4649                "\n### See also\n",
4650                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4651                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4652                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4653                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4654                "\n### Unary limits\n",
4655            ])
4656
4657            if unary:
4658                for key, values in sorted(unary.items()):
4659                    info.append("\n* Max requests per minute: {}\n".format(key))
4660
4661                    for value in values:
4662                        info.append("  - {}\n".format(value))
4663
4664            else:
4665                info.append("\nNot available\n")
4666
4667            info.append("\n### Stream limits\n")
4668
4669            if stream:
4670                for key, values in sorted(stream.items()):
4671                    info.append("\n* Max stream connections: {}\n".format(key))
4672
4673                    for value in values:
4674                        info.append("  - {}\n".format(value))
4675
4676            else:
4677                info.append("\nNot available\n")
4678
4679            infoText = "".join(info)
4680
4681            uLogger.info(infoText)
4682
4683            if self.userInfoFile:
4684                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4685                    fH.write(infoText)
4686
4687                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4688
4689                if self.useHTMLReports:
4690                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4691                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4692                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4693
4694                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4695
4696        return view
4697
4698
4699class Args:
4700    """
4701    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4702    """
4703    def __init__(self, **kwargs):
4704        self.__dict__.update(kwargs)
4705
4706    def __getattr__(self, item):
4707        return None
4708
4709
4710def ParseArgs():
4711    """This function get and parse command line keys."""
4712    parser = ArgumentParser()  # command-line string parser
4713
4714    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4715    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4716
4717    # --- options:
4718
4719    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4720    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4721    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4722
4723    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4724    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4725
4726    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4727    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4728
4729    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4730    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4731
4732    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4733    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4734    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4735
4736    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4737    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4738
4739    # --- commands:
4740
4741    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4742
4743    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4744    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4745    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4746    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4747    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4748    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4749    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4750    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4751
4752    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4753    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4754    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4755    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4756    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4757    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4758
4759    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4760    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4761    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4762    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4763
4764    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4765    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4766    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4767
4768    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4769    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4770    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4771    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4772    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4773    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4774    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4775
4776    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4777    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4778    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4779    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4780    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4781
4782    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4783    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4784    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4785
4786    cmdArgs = parser.parse_args()
4787    return cmdArgs
4788
4789
4790def Main(**kwargs):
4791    """
4792    Main function for work with TKSBrokerAPI in the console.
4793
4794    See examples:
4795    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4796    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4797    """
4798    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4799
4800    if args.debug_level:
4801        uLogger.level = 10  # always debug level by default
4802        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4803
4804    exitCode = 0
4805    start = datetime.now(tzutc())
4806    uLogger.debug("=-" * 50)
4807    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4808        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4809        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4810    ))
4811
4812    # trying to calculate full current version:
4813    buildVersion = __version__
4814    try:
4815        v = version("tksbrokerapi")
4816        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4817
4818    except Exception:
4819        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4820
4821    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4822    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4823
4824    try:
4825        if args.version:
4826            print("TKSBrokerAPI {}".format(buildVersion))
4827            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4828
4829        else:
4830            # Init class for trading with Tinkoff Broker:
4831            trader = TinkoffBrokerServer(
4832                token=args.token,
4833                accountId=args.account_id,
4834                useCache=not args.no_cache,
4835            )
4836
4837            # --- set some options:
4838
4839            if args.more:
4840                trader.moreDebug = True
4841                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4842
4843            if args.html:
4844                trader.useHTMLReports = True
4845
4846            if args.ticker:
4847                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4848
4849                if ticker in trader.aliasesKeys:
4850                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4851
4852                else:
4853                    trader.ticker = ticker
4854
4855            if args.figi:
4856                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4857
4858            if args.depth is not None:
4859                trader.depth = args.depth
4860
4861            # --- do one command:
4862
4863            if args.list:
4864                if args.output is not None:
4865                    trader.instrumentsFile = args.output
4866
4867                trader.ShowInstrumentsInfo(show=True)
4868
4869            elif args.list_xlsx:
4870                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4871
4872            elif args.bonds_xlsx is not None:
4873                if args.output is not None:
4874                    trader.bondsXLSXFile = args.output
4875
4876                if len(args.bonds_xlsx) == 0:
4877                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4878
4879                else:
4880                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4881
4882            elif args.search:
4883                if args.output is not None:
4884                    trader.searchResultsFile = args.output
4885
4886                trader.SearchInstruments(pattern=args.search[0], show=True)
4887
4888            elif args.info:
4889                if not (args.ticker or args.figi):
4890                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4891                    raise Exception("Ticker or FIGI required")
4892
4893                if args.output is not None:
4894                    trader.infoFile = args.output
4895
4896                if args.ticker:
4897                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4898
4899                else:
4900                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4901
4902            elif args.calendar is not None:
4903                if args.output is not None:
4904                    trader.calendarFile = args.output
4905
4906                if len(args.calendar) == 0:
4907                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4908
4909                else:
4910                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4911
4912                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4913
4914            elif args.price:
4915                if not (args.ticker or args.figi):
4916                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4917                    raise Exception("Ticker or FIGI required")
4918
4919                trader.GetCurrentPrices(show=True)
4920
4921            elif args.prices is not None:
4922                if args.output is not None:
4923                    trader.pricesFile = args.output
4924
4925                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4926
4927            elif args.overview:
4928                if args.output is not None:
4929                    trader.overviewFile = args.output
4930
4931                trader.Overview(show=True, details="full")
4932
4933            elif args.overview_digest:
4934                if args.output is not None:
4935                    trader.overviewDigestFile = args.output
4936
4937                trader.Overview(show=True, details="digest")
4938
4939            elif args.overview_positions:
4940                if args.output is not None:
4941                    trader.overviewPositionsFile = args.output
4942
4943                trader.Overview(show=True, details="positions")
4944
4945            elif args.overview_orders:
4946                if args.output is not None:
4947                    trader.overviewOrdersFile = args.output
4948
4949                trader.Overview(show=True, details="orders")
4950
4951            elif args.overview_analytics:
4952                if args.output is not None:
4953                    trader.overviewAnalyticsFile = args.output
4954
4955                trader.Overview(show=True, details="analytics")
4956
4957            elif args.overview_calendar:
4958                if args.output is not None:
4959                    trader.overviewAnalyticsFile = args.output
4960
4961                trader.Overview(show=True, details="calendar")
4962
4963            elif args.deals is not None:
4964                if args.output is not None:
4965                    trader.reportFile = args.output
4966
4967                if 0 <= len(args.deals) < 3:
4968                    trader.Deals(
4969                        start=args.deals[0] if len(args.deals) >= 1 else None,
4970                        end=args.deals[1] if len(args.deals) == 2 else None,
4971                        show=True,  # Always show deals report in console
4972                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4973                    )
4974
4975                else:
4976                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4977                    raise Exception("Incorrect value")
4978
4979            elif args.history is not None:
4980                if args.output is not None:
4981                    trader.historyFile = args.output
4982
4983                if 0 <= len(args.history) < 3:
4984                    dataReceived = trader.History(
4985                        start=args.history[0] if len(args.history) >= 1 else None,
4986                        end=args.history[1] if len(args.history) == 2 else None,
4987                        interval="hour" if args.interval is None or not args.interval else args.interval,
4988                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4989                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4990                        show=True,  # shows all downloaded candles in console
4991                    )
4992
4993                    if args.render_chart is not None and dataReceived is not None:
4994                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4995
4996                        trader.ShowHistoryChart(
4997                            candles=dataReceived,
4998                            interact=iChart,
4999                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5000                        )
5001
5002                else:
5003                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5004                    raise Exception("Incorrect value")
5005
5006            elif args.load_history is not None:
5007                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5008
5009                if args.render_chart is not None and histData is not None:
5010                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5011                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5012
5013                    trader.ShowHistoryChart(
5014                        candles=histData,
5015                        interact=iChart,
5016                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5017                    )
5018
5019            elif args.trade is not None:
5020                if 1 <= len(args.trade) <= 5:
5021                    trader.Trade(
5022                        operation=args.trade[0],
5023                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5024                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5025                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5026                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5027                    )
5028
5029                else:
5030                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5031
5032            elif args.buy is not None:
5033                if 0 <= len(args.buy) <= 4:
5034                    trader.Buy(
5035                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5036                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5037                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5038                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5039                    )
5040
5041                else:
5042                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5043
5044            elif args.sell is not None:
5045                if 0 <= len(args.sell) <= 4:
5046                    trader.Sell(
5047                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5048                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5049                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5050                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5051                    )
5052
5053                else:
5054                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5055
5056            elif args.order:
5057                if 4 <= len(args.order) <= 7:
5058                    trader.Order(
5059                        operation=args.order[0],
5060                        orderType=args.order[1],
5061                        lots=int(args.order[2]),
5062                        targetPrice=float(args.order[3]),
5063                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5064                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5065                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5066                    )
5067
5068                else:
5069                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5070
5071            elif args.buy_limit:
5072                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5073
5074            elif args.sell_limit:
5075                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5076
5077            elif args.buy_stop:
5078                if 2 <= len(args.buy_stop) <= 7:
5079                    trader.BuyStop(
5080                        lots=int(args.buy_stop[0]),
5081                        targetPrice=float(args.buy_stop[1]),
5082                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5083                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5084                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5085                    )
5086
5087                else:
5088                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5089
5090            elif args.sell_stop:
5091                if 2 <= len(args.sell_stop) <= 7:
5092                    trader.SellStop(
5093                        lots=int(args.sell_stop[0]),
5094                        targetPrice=float(args.sell_stop[1]),
5095                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5096                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5097                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5098                    )
5099
5100                else:
5101                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5102
5103            # elif args.buy_order_grid is not None:
5104            #     # update order grid work with api v2
5105            #     if len(args.buy_order_grid) == 2:
5106            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5107            #
5108            #         for order in orderParams:
5109            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5110            #
5111            #     else:
5112            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5113            #
5114            # elif args.sell_order_grid is not None:
5115            #     # update order grid work with api v2
5116            #     if len(args.sell_order_grid) >= 2:
5117            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5118            #
5119            #         for order in orderParams:
5120            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5121            #
5122            #     else:
5123            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5124
5125            elif args.close_order is not None:
5126                trader.CloseOrders(args.close_order)  # close only one order
5127
5128            elif args.close_orders is not None:
5129                trader.CloseOrders(args.close_orders)  # close list of orders
5130
5131            elif args.close_trade:
5132                if not (args.ticker or args.figi):
5133                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5134                    raise Exception("Ticker or FIGI required")
5135
5136                if args.ticker:
5137                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5138
5139                else:
5140                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5141
5142            elif args.close_trades is not None:
5143                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5144
5145            elif args.close_all is not None:
5146                if args.ticker:
5147                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5148
5149                elif args.figi:
5150                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5151
5152                else:
5153                    trader.CloseAll(*args.close_all)
5154
5155            elif args.limits:
5156                if args.output is not None:
5157                    trader.withdrawalLimitsFile = args.output
5158
5159                trader.OverviewLimits(show=True)
5160
5161            elif args.user_info:
5162                if args.output is not None:
5163                    trader.userInfoFile = args.output
5164
5165                trader.OverviewUserInfo(show=True)
5166
5167            elif args.account:
5168                if args.output is not None:
5169                    trader.userAccountsFile = args.output
5170
5171                trader.OverviewAccounts(show=True)
5172
5173            else:
5174                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5175                raise Exception("There is no command to execute")
5176
5177    except Exception:
5178        trace = tb.format_exc()
5179        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5180            if e in trace:
5181                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5182                break
5183
5184        uLogger.debug(trace)
5185        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5186        exitCode = 255  # an error occurred, must be open a ticket for this issue
5187
5188    finally:
5189        finish = datetime.now(tzutc())
5190
5191        if exitCode == 0:
5192            if args.more:
5193                uLogger.debug("All operations were finished success (summary code is 0).")
5194
5195        else:
5196            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5197                os.path.abspath(uLog.defaultLogFile), exitCode,
5198            ))
5199
5200        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5201        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5202            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5203            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5204        ))
5205        uLogger.debug("=-" * 50)
5206
5207        if not kwargs:
5208            sys.exit(exitCode)
5209
5210        else:
5211            return exitCode
5212
5213
5214if __name__ == "__main__":
5215    Main()
class TinkoffBrokerServer:
  78class TinkoffBrokerServer:
  79    """
  80    This class implements methods to work with Tinkoff broker server.
  81
  82    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  83
  84    About `token`: https://tinkoff.github.io/investAPI/token/
  85    """
  86    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  87        """
  88        Main class init.
  89
  90        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  91        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  92                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  93        :param useCache: use default cache file with raw data to use instead of `iList`.
  94                         True by default. Cache is auto-update if new day has come.
  95                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  96        :param defaultCache: path to default cache file. `dump.json` by default.
  97        """
  98        if token is None or not token:
  99            try:
 100                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 101                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 102
 103            except KeyError:
 104                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 105                raise Exception("Token required")
 106
 107        else:
 108            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 109            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 110
 111        if accountId is None or not accountId:
 112            try:
 113                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 114                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 115
 116            except KeyError:
 117                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 118
 119        else:
 120            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 121            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 122
 123        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 124        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 125
 126        Latest version: https://pypi.org/project/tksbrokerapi/
 127        """
 128
 129        self.aliases = TKS_TICKER_ALIASES
 130        """Some aliases instead official tickers.
 131
 132        See also: `TKSEnums.TKS_TICKER_ALIASES`
 133        """
 134
 135        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 136
 137        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 138
 139        self._ticker = ""
 140        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 141
 142        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 143        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 144
 145        See also: `SearchByTicker()`, `SearchInstruments()`.
 146        """
 147
 148        self._figi = ""
 149        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 150
 151        See also: `SearchByFIGI()`, `SearchInstruments()`.
 152        """
 153
 154        self.depth = 1
 155        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 156
 157        See also: `GetCurrentPrices()`.
 158        """
 159
 160        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 161        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 162
 163        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 164        """
 165
 166        uLogger.debug("Broker API server: {}".format(self.server))
 167
 168        self.timeout = 15
 169        """Server operations timeout in seconds. Default: `15`.
 170
 171        See also: `SendAPIRequest()`.
 172        """
 173
 174        self.headers = {
 175            "Content-Type": "application/json",
 176            "accept": "application/json",
 177            "Authorization": "Bearer {}".format(self.token),
 178            "x-app-name": "Tim55667757.TKSBrokerAPI",
 179        }
 180        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 181
 182        See also: `SendAPIRequest()`.
 183        """
 184
 185        self.body = None
 186        """Request body which send to broker server. Default: `None`.
 187
 188        See also: `SendAPIRequest()`.
 189        """
 190
 191        self.moreDebug = False
 192        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 193
 194        self.useHTMLReports = False
 195        """
 196        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 197        
 198        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 199        """
 200
 201        self.historyFile = None
 202        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 203
 204        See also: `History()`.
 205        """
 206
 207        self.htmlHistoryFile = "index.html"
 208        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 209
 210        See also: `ShowHistoryChart()`.
 211        """
 212
 213        self.instrumentsFile = "instruments.md"
 214        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 215
 216        See also: `ShowInstrumentsInfo()`.
 217        """
 218
 219        self.searchResultsFile = "search-results.md"
 220        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 221
 222        See also: `SearchInstruments()`.
 223        """
 224
 225        self.pricesFile = "prices.md"
 226        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 227
 228        See also: `GetListOfPrices()`.
 229        """
 230
 231        self.infoFile = "info.md"
 232        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 233
 234        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 235        """
 236
 237        self.bondsXLSXFile = "ext-bonds.xlsx"
 238        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 239        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 240
 241        See also: `ExtendBondsData()`.
 242        """
 243
 244        self.calendarFile = "calendar.md"
 245        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 246        
 247        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 248
 249        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 250        """
 251
 252        self.overviewFile = "overview.md"
 253        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 254
 255        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 256        """
 257
 258        self.overviewDigestFile = "overview-digest.md"
 259        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 260
 261        See also: `Overview()` with parameter `details="digest"`.
 262        """
 263
 264        self.overviewPositionsFile = "overview-positions.md"
 265        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 266
 267        See also: `Overview()` with parameter `details="positions"`.
 268        """
 269
 270        self.overviewOrdersFile = "overview-orders.md"
 271        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 272
 273        See also: `Overview()` with parameter `details="orders"`.
 274        """
 275
 276        self.overviewAnalyticsFile = "overview-analytics.md"
 277        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 278
 279        See also: `Overview()` with parameter `details="analytics"`.
 280        """
 281
 282        self.overviewBondsCalendarFile = "overview-calendar.md"
 283        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 284
 285        See also: `Overview()` with parameter `details="calendar"`.
 286        """
 287
 288        self.reportFile = "deals.md"
 289        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 290
 291        See also: `Deals()`.
 292        """
 293
 294        self.withdrawalLimitsFile = "limits.md"
 295        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 296
 297        See also: `OverviewLimits()` and `RequestLimits()`.
 298        """
 299
 300        self.userInfoFile = "user-info.md"
 301        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 302
 303        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 304        """
 305
 306        self.userAccountsFile = "accounts.md"
 307        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 308
 309        See also: `OverviewAccounts()`, `RequestAccounts()`.
 310        """
 311
 312        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 313        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 314
 315        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 316
 317        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 318        """
 319
 320        self.iList = None  # init iList for raw instruments data
 321        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 322        
 323        See also: `Listing()`, `DumpInstruments()`.
 324        """
 325
 326        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 327        if useCache:
 328            if os.path.exists(self.iListDumpFile):
 329                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 330                curTime = datetime.now(tzutc())
 331
 332                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 333                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 334
 335                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 336
 337                else:
 338                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 339
 340                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 341                        os.path.abspath(self.iListDumpFile),
 342                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 343                    ))
 344
 345            else:
 346                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 347                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 348
 349        else:
 350            self.iList = self.Listing()  # request new raw instruments data from broker server
 351            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 352
 353        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 354        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 355
 356        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 357        """
 358
 359    @property
 360    def ticker(self) -> str:
 361        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 362
 363        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 364        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 365
 366        See also: `SearchByTicker()`, `SearchInstruments()`.
 367        """
 368        return self._ticker
 369
 370    @ticker.setter
 371    def ticker(self, value):
 372        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 373
 374        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 375        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 376
 377        See also: `SearchByTicker()`, `SearchInstruments()`.
 378        """
 379        self._ticker = str(value).upper()  # Tickers may be upper case only
 380
 381    @property
 382    def figi(self) -> str:
 383        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 384
 385        See also: `SearchByFIGI()`, `SearchInstruments()`.
 386        """
 387        return self._figi
 388
 389    @figi.setter
 390    def figi(self, value):
 391        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 392
 393        See also: `SearchByFIGI()`, `SearchInstruments()`.
 394        """
 395        self._figi = str(value).upper()  # FIGI may be upper case only
 396
 397    def _ParseJSON(self, rawData="{}") -> dict:
 398        """
 399        Parse JSON from response string.
 400
 401        :param rawData: this is a string with JSON-formatted text.
 402        :return: JSON (dictionary), parsed from server response string.
 403        """
 404        responseJSON = json.loads(rawData) if rawData else {}
 405
 406        if self.moreDebug:
 407            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 408
 409        return responseJSON
 410
 411    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 412        """
 413        Send GET or POST request to broker server and receive JSON object.
 414
 415        self.header: must be defining with dictionary of headers.
 416        self.body: if define then used as request body. None by default.
 417        self.timeout: global request timeout, 15 seconds by default.
 418        :param url: url with REST request.
 419        :param reqType: send "GET" or "POST" request. "GET" by default.
 420        :param retry: how many times retry after first request if an 5xx server errors occurred.
 421        :param pause: sleep time in seconds between retries.
 422        :return: response JSON (dictionary) from broker.
 423        """
 424        if reqType.upper() not in ("GET", "POST"):
 425            uLogger.error("You can define request type: `GET` or `POST`!")
 426            raise Exception("Incorrect value")
 427
 428        if self.moreDebug:
 429            uLogger.debug("Request parameters:")
 430            uLogger.debug("    - REST API URL: {}".format(url))
 431            uLogger.debug("    - request type: {}".format(reqType))
 432            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 433            uLogger.debug("    - body:\n{}".format(self.body))
 434
 435        # fast hack to avoid all operations with some tickers/FIGI
 436        responseJSON = {}
 437        oK = True
 438        for item in self.exclude:
 439            if item in url:
 440                if self.moreDebug:
 441                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 442
 443                oK = False
 444                break
 445
 446        if oK:
 447            counter = 0
 448            response = None
 449            errMsg = ""
 450
 451            while not response and counter <= retry:
 452                if reqType == "GET":
 453                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 454
 455                if reqType == "POST":
 456                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 457
 458                if self.moreDebug:
 459                    uLogger.debug("Response:")
 460                    uLogger.debug("    - status code: {}".format(response.status_code))
 461                    uLogger.debug("    - reason: {}".format(response.reason))
 462                    uLogger.debug("    - body length: {}".format(len(response.text)))
 463                    uLogger.debug("    - headers:\n{}".format(response.headers))
 464
 465                # Server returns some headers:
 466                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 467                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 468                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 469                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 470                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 471                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 472                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 473                    sleep(rateLimitWait)
 474
 475                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 476                if 400 <= response.status_code < 500:
 477                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 478                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 479
 480                    if "code" in response.text and "message" in response.text:
 481                        msgDict = self._ParseJSON(rawData=response.text)
 482                        uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 483
 484                    counter = retry + 1  # do not retry for 4xx errors
 485
 486                if 500 <= response.status_code < 600:
 487                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 488                    uLogger.debug("    - not oK, {}".format(errMsg))
 489
 490                    if "code" in response.text and "message" in response.text:
 491                        errMsgDict = self._ParseJSON(rawData=response.text)
 492                        uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 493
 494                    counter += 1
 495
 496                    if counter <= retry:
 497                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 498                        sleep(pause)
 499
 500            responseJSON = self._ParseJSON(rawData=response.text)
 501
 502            if errMsg:
 503                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 504                uLogger.error("    - not oK, {}".format(errMsg))
 505
 506        return responseJSON
 507
 508    def _IUpdater(self, iType: str) -> tuple:
 509        """
 510        Request instrument by type from server. See available API methods for instruments:
 511        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 512        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 513        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 514        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 515        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 516
 517        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 518        :return: tuple with iType name and list of available instruments of current type for defined user token.
 519        """
 520        result = []
 521
 522        if iType in TKS_INSTRUMENTS:
 523            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 524
 525            # all instruments have the same body in API v2 requests:
 526            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 527            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 528            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 529
 530        return iType, result
 531
 532    def _IWrapper(self, kwargs):
 533        """
 534        Wrapper runs instrument's update method `_IUpdater()`.
 535        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 536        """
 537        return self._IUpdater(**kwargs)
 538
 539    def Listing(self) -> dict:
 540        """
 541        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 542
 543        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 544        """
 545        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 546        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 547
 548        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 549        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 550        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 551
 552        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 553        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 554        poolUpdater.close()
 555
 556        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 557        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 558        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 559
 560        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 561        for iType in iList.keys():
 562            for ticker in iList[iType]:
 563                iList[iType][ticker]["type"] = iType
 564
 565                if "minPriceIncrement" in iList[iType][ticker].keys():
 566                    iList[iType][ticker]["step"] = NanoToFloat(
 567                        iList[iType][ticker]["minPriceIncrement"]["units"],
 568                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 569                    )
 570
 571                else:
 572                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 573
 574        return iList
 575
 576    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 577        """
 578        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 579
 580        See also: `DumpInstruments()`, `Listing()`.
 581
 582        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 583                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 584        """
 585        if self.iListDumpFile is None or not self.iListDumpFile:
 586            uLogger.error("Output name of dump file must be defined!")
 587            raise Exception("Filename required")
 588
 589        if not self.iList or forceUpdate:
 590            self.iList = self.Listing()
 591
 592        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 593
 594        # Save as XLSX with separated sheets for every type of instruments:
 595        with pd.ExcelWriter(
 596                path=xlsxDumpFile,
 597                date_format=TKS_DATE_FORMAT,
 598                datetime_format=TKS_DATE_TIME_FORMAT,
 599                mode="w",
 600        ) as writer:
 601            for iType in TKS_INSTRUMENTS:
 602                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 603                df = df[sorted(df)]  # sorted by column names
 604                df = df.applymap(
 605                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 606                    na_action="ignore",
 607                )  # converting numbers from nano-type to float in every cell
 608                df.to_excel(
 609                    writer,
 610                    sheet_name=iType,
 611                    encoding="UTF-8",
 612                    freeze_panes=(1, 1),
 613                )  # saving as XLSX-file with freeze first row and column as headers
 614
 615        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 616
 617    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 618        """
 619        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 620        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 621
 622        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 623
 624        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 625                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 626        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 627        """
 628        if self.iListDumpFile is None or not self.iListDumpFile:
 629            uLogger.error("Output name of dump file must be defined!")
 630            raise Exception("Filename required")
 631
 632        if not self.iList or forceUpdate:
 633            self.iList = self.Listing()
 634
 635        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 636        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 637            fH.write(jsonDump)
 638
 639        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 640
 641        return jsonDump
 642
 643    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 644        """
 645        Show information about one instrument defined by json data and prints it in Markdown format.
 646
 647        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 648
 649        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 650        :param show: if `True` then also printing information about instrument and its current price.
 651        :return: multilines text in Markdown format with information about one instrument.
 652        """
 653        splitLine = "|                                                             |                                                        |\n"
 654        infoText = ""
 655
 656        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 657            info = [
 658                "# Main information\n\n",
 659                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 660                "| Parameters                                                  | Values                                                 |\n",
 661                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 662                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 663                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 664            ]
 665
 666            if "sector" in iJSON.keys() and iJSON["sector"]:
 667                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 668
 669            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 670                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 671
 672            info.extend([
 673                splitLine,
 674                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 675                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 676            ])
 677
 678            if "isin" in iJSON.keys() and iJSON["isin"]:
 679                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 680
 681            if "classCode" in iJSON.keys():
 682                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 683
 684            info.extend([
 685                splitLine,
 686                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 687                splitLine,
 688                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 689                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 690                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 691            ])
 692
 693            if iJSON["figi"]:
 694                self._figi = iJSON["figi"]
 695                iJSON = iJSON | self.RequestTradingStatus()
 696
 697                info.extend([
 698                    splitLine,
 699                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 700                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 701                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 702                ])
 703
 704            info.append(splitLine)
 705
 706            if "type" in iJSON.keys() and iJSON["type"]:
 707                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 708
 709                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 710                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 711
 712            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 713                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 714
 715            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 716                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 717
 718            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 719                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 720
 721            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 722                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 723
 724            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 725                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 726
 727            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 728                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 729
 730            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 731                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 732
 733            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 734                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 735
 736            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 737                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 738
 739            if "currency" in iJSON.keys():
 740                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 741
 742            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 743                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 744
 745            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 746                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 747
 748            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 749                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 750
 751            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 752                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 753
 754            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 755                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 756
 757            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 758                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 759
 760            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 761                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 762
 763            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 764                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 765
 766            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 767                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 768
 769            iExt = None
 770            if iJSON["type"] == "Bonds":
 771                info.extend([
 772                    splitLine,
 773                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 774                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 775                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 776                        iJSON["nominal"]["currency"],
 777                    )),
 778                ])
 779
 780                if "floatingCouponFlag" in iJSON.keys():
 781                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 782
 783                if "amortizationFlag" in iJSON.keys():
 784                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 785
 786                info.append(splitLine)
 787
 788                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 789                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 790
 791                if iJSON["figi"]:
 792                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 793
 794                    info.extend([
 795                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 796                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 797                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 798                    ])
 799
 800                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 801                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 802                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 803                        iJSON["aciValue"]["currency"]
 804                    )))
 805
 806            if "currentPrice" in iJSON.keys():
 807                info.append(splitLine)
 808
 809                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 810                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 811
 812                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 813                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 814                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 815                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 816                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 817
 818                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 819                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 820
 821                info.extend([
 822                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 823                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 824                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 825                    )),
 826                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 827                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 828                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 829                    )),
 830                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 831                        "{:.2f}%{}".format(
 832                            iJSON["currentPrice"]["changes"],
 833                            " ({}{:.2f} {})".format(
 834                                "+" if bondChangesDelta > 0 else "",
 835                                bondChangesDelta,
 836                                aciCurrency
 837                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 838                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 839                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 840                                currency
 841                            ),
 842                        )
 843                    ),
 844                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 845                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 846                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 847                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 848                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 849                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 850                    )),
 851                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 852                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 853                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 854                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 855                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 856                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 857                    )),
 858                ])
 859
 860            if "lot" in iJSON.keys():
 861                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 862
 863            if "step" in iJSON.keys() and iJSON["step"] != 0:
 864                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 865
 866            # Add bond payment calendar:
 867            if iJSON["type"] == "Bonds":
 868                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 869                info.extend(["\n#", strCalendar])
 870
 871            infoText += "".join(info)
 872
 873            if show:
 874                uLogger.info("{}".format(infoText))
 875
 876            else:
 877                uLogger.debug("{}".format(infoText))
 878
 879            if self.infoFile is not None:
 880                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 881                    fH.write(infoText)
 882
 883                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 884
 885                if self.useHTMLReports:
 886                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 887                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 888                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 889
 890                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 891
 892        return infoText
 893
 894    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 895        """
 896        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 897
 898        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 899        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 900        :return: JSON formatted data with information about instrument.
 901        """
 902        tickerJSON = {}
 903        if self.moreDebug:
 904            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 905
 906        if not self._ticker:
 907            uLogger.warning("self._ticker variable is not be empty!")
 908
 909        else:
 910            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 911                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 912                raise Exception("Instrument not allowed")
 913
 914            if not self.iList:
 915                self.iList = self.Listing()
 916
 917            if self._ticker in self.iList["Shares"].keys():
 918                tickerJSON = self.iList["Shares"][self._ticker]
 919                if self.moreDebug:
 920                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 921
 922            elif self._ticker in self.iList["Currencies"].keys():
 923                tickerJSON = self.iList["Currencies"][self._ticker]
 924                if self.moreDebug:
 925                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 926
 927            elif self._ticker in self.iList["Bonds"].keys():
 928                tickerJSON = self.iList["Bonds"][self._ticker]
 929                if self.moreDebug:
 930                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 931
 932            elif self._ticker in self.iList["Etfs"].keys():
 933                tickerJSON = self.iList["Etfs"][self._ticker]
 934                if self.moreDebug:
 935                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 936
 937            elif self._ticker in self.iList["Futures"].keys():
 938                tickerJSON = self.iList["Futures"][self._ticker]
 939                if self.moreDebug:
 940                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 941
 942        if tickerJSON:
 943            self._figi = tickerJSON["figi"]
 944
 945            if requestPrice:
 946                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 947
 948                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 949                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 950
 951                else:
 952                    tickerJSON["currentPrice"]["changes"] = 0
 953
 954            if show:
 955                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 956
 957        else:
 958            if show:
 959                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 960
 961        return tickerJSON
 962
 963    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 964        """
 965        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 966
 967        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 968        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 969        :return: JSON formatted data with information about instrument.
 970        """
 971        figiJSON = {}
 972        if self.moreDebug:
 973            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 974
 975        if not self._figi:
 976            uLogger.warning("self._figi variable is not be empty!")
 977
 978        else:
 979            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 980                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 981                raise Exception("Instrument not allowed")
 982
 983            if not self.iList:
 984                self.iList = self.Listing()
 985
 986            for item in self.iList["Shares"].keys():
 987                if self._figi == self.iList["Shares"][item]["figi"]:
 988                    figiJSON = self.iList["Shares"][item]
 989
 990                    if self.moreDebug:
 991                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
 992
 993                    break
 994
 995            if not figiJSON:
 996                for item in self.iList["Currencies"].keys():
 997                    if self._figi == self.iList["Currencies"][item]["figi"]:
 998                        figiJSON = self.iList["Currencies"][item]
 999
1000                        if self.moreDebug:
1001                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1002
1003                        break
1004
1005            if not figiJSON:
1006                for item in self.iList["Bonds"].keys():
1007                    if self._figi == self.iList["Bonds"][item]["figi"]:
1008                        figiJSON = self.iList["Bonds"][item]
1009
1010                        if self.moreDebug:
1011                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1012
1013                        break
1014
1015            if not figiJSON:
1016                for item in self.iList["Etfs"].keys():
1017                    if self._figi == self.iList["Etfs"][item]["figi"]:
1018                        figiJSON = self.iList["Etfs"][item]
1019
1020                        if self.moreDebug:
1021                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1022
1023                        break
1024
1025            if not figiJSON:
1026                for item in self.iList["Futures"].keys():
1027                    if self._figi == self.iList["Futures"][item]["figi"]:
1028                        figiJSON = self.iList["Futures"][item]
1029
1030                        if self.moreDebug:
1031                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1032
1033                        break
1034
1035        if figiJSON:
1036            self._figi = figiJSON["figi"]
1037            self._ticker = figiJSON["ticker"]
1038
1039            if requestPrice:
1040                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1041
1042                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1043                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1044
1045                else:
1046                    figiJSON["currentPrice"]["changes"] = 0
1047
1048            if show:
1049                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1050
1051        else:
1052            if show:
1053                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1054
1055        return figiJSON
1056
1057    def GetCurrentPrices(self, show: bool = True) -> dict:
1058        """
1059        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1060        `{"buy": [{"price": 1243.8, "quantity": 193},
1061                  {"price": 1244.0, "quantity": 168},
1062                  {"price": 1244.8, "quantity": 5},
1063                  {"price": 1245.0, "quantity": 61},
1064                  {"price": 1245.4, "quantity": 60}],
1065          "sell": [{"price": 1243.6, "quantity": 8},
1066                   {"price": 1242.6, "quantity": 10},
1067                   {"price": 1242.4, "quantity": 18},
1068                   {"price": 1242.2, "quantity": 50},
1069                   {"price": 1242.0, "quantity": 113}],
1070          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1071        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1072        - sell: list of dicts with Buyers prices,
1073            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1074            - quantity: volume value by current price in lots,
1075        - limitUp: current trade session limit price, maximum,
1076        - limitDown: current trade session limit price, minimum,
1077        - lastPrice: last deal price of the instrument,
1078        - closePrice: previous trade session close price of the instrument.
1079
1080        See also: `SearchByTicker()` and `SearchByFIGI()`.
1081        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1082        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1083
1084        :param show: if `True` then print DOM to log and console.
1085        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1086                 If an error occurred then returns an empty record:
1087                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1088        """
1089        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1090
1091        if self.depth < 1:
1092            uLogger.error("Depth of Market (DOM) must be >=1!")
1093            raise Exception("Incorrect value")
1094
1095        if not (self._ticker or self._figi):
1096            uLogger.error("self._ticker or self._figi variables must be defined!")
1097            raise Exception("Ticker or FIGI required")
1098
1099        if self._ticker and not self._figi:
1100            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1101            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1102
1103        if not self._ticker and self._figi:
1104            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1105            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1106
1107        if not self._figi:
1108            uLogger.error("FIGI is not defined!")
1109            raise Exception("Ticker or FIGI required")
1110
1111        else:
1112            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1113
1114            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1115            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1116            self.body = str({"figi": self._figi, "depth": self.depth})
1117            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1118
1119            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1120                # list of dicts with sellers orders:
1121                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1122
1123                # list of dicts with buyers orders:
1124                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1125
1126                # max price of instrument at this time:
1127                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1128
1129                # min price of instrument at this time:
1130                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1131
1132                # last price of deal with instrument:
1133                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1134
1135                # last close price of instrument:
1136                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1137
1138            else:
1139                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1140                uLogger.debug("Server response: {}".format(pricesResponse))
1141
1142            if show:
1143                if prices["buy"] or prices["sell"]:
1144                    info = [
1145                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1146                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1147                            self._ticker,
1148                            self._figi,
1149                            self.depth,
1150                        ),
1151                        "-" * 60, "\n",
1152                        "             Orders of Buyers | Orders of Sellers\n",
1153                        "-" * 60, "\n",
1154                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1155                        "-" * 60, "\n",
1156                    ]
1157
1158                    if not prices["buy"]:
1159                        info.append("                              | No orders!\n")
1160                        sumBuy = 0
1161
1162                    else:
1163                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1164                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1165                        for item in maxMinSorted:
1166                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1167
1168                    if not prices["sell"]:
1169                        info.append("No orders!                    |\n")
1170                        sumSell = 0
1171
1172                    else:
1173                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1174                        for item in prices["sell"]:
1175                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1176
1177                    info.extend([
1178                        "-" * 60, "\n",
1179                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1180                        "-" * 60, "\n",
1181                    ])
1182
1183                    infoText = "".join(info)
1184
1185                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1186
1187                else:
1188                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1189
1190        return prices
1191
1192    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1193        """
1194        This method get and show information about all available broker instruments for current user account.
1195        If `instrumentsFile` string is not empty then also save information to this file.
1196
1197        :param show: if `True` then print results to console, if `False` — print only to file.
1198        :return: multi-lines string with all available broker instruments
1199        """
1200        if not self.iList:
1201            self.iList = self.Listing()
1202
1203        info = [
1204            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1205            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1206        ]
1207
1208        # add instruments count by type:
1209        for iType in self.iList.keys():
1210            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1211
1212        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1213        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1214
1215        # generating info tables with all instruments by type:
1216        for iType in self.iList.keys():
1217            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1218
1219            for instrument in self.iList[iType].keys():
1220                iName = self.iList[iType][instrument]["name"]  # instrument's name
1221                if len(iName) > 57:
1222                    iName = "{}...".format(iName[:54])  # right trim for a long string
1223
1224                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1225                    self.iList[iType][instrument]["ticker"],
1226                    iName,
1227                    self.iList[iType][instrument]["figi"],
1228                    self.iList[iType][instrument]["currency"],
1229                    self.iList[iType][instrument]["lot"],
1230                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1231                ))
1232
1233        infoText = "".join(info)
1234
1235        if show:
1236            uLogger.info(infoText)
1237
1238        if self.instrumentsFile:
1239            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1240                fH.write(infoText)
1241
1242            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1243
1244            if self.useHTMLReports:
1245                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1246                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1247                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1248
1249                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1250
1251        return infoText
1252
1253    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1254        """
1255        This method search and show information about instruments by part of its ticker, FIGI or name.
1256        If `searchResultsFile` string is not empty then also save information to this file.
1257
1258        :param pattern: string with part of ticker, FIGI or instrument's name.
1259        :param show: if `True` then print results to console, if `False` — return list of result only.
1260        :return: list of dictionaries with all found instruments.
1261        """
1262        if not self.iList:
1263            self.iList = self.Listing()
1264
1265        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1266        compiledPattern = re.compile(pattern, re.IGNORECASE)
1267
1268        for iType in self.iList:
1269            for instrument in self.iList[iType].values():
1270                searchResult = compiledPattern.search(" ".join(
1271                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1272                ))
1273
1274                if searchResult:
1275                    searchResults[iType][instrument["ticker"]] = instrument
1276
1277        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1278        info = [
1279            "# Search results\n\n",
1280            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1281            "* **Search pattern:** [{}]\n".format(pattern),
1282            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1283            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1284        ]
1285        infoShort = info[:]
1286
1287        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1288        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1289        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1290
1291        if resultsLen == 0:
1292            info.append("\nNo results\n")
1293            infoShort.append("\nNo results\n")
1294            uLogger.warning("No results. Try changing your search pattern.")
1295
1296        else:
1297            for iType in searchResults:
1298                iTypeValuesCount = len(searchResults[iType].values())
1299                if iTypeValuesCount > 0:
1300                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1301                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1302
1303                    for instrument in searchResults[iType].values():
1304                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1305                            instrument["type"],
1306                            instrument["ticker"],
1307                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1308                            instrument["figi"],
1309                        ))
1310
1311                    if iTypeValuesCount <= 5:
1312                        infoShort.extend(info[-iTypeValuesCount:])
1313
1314                    else:
1315                        infoShort.extend(info[-5:])
1316                        infoShort.append(skippedLine)
1317
1318        infoText = "".join(info)
1319        infoTextShort = "".join(infoShort)
1320
1321        if show:
1322            uLogger.info(infoTextShort)
1323            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1324
1325        if self.searchResultsFile:
1326            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1327                fH.write(infoText)
1328
1329            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1330
1331            if self.useHTMLReports:
1332                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1333                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1334                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1335
1336                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1337
1338        return searchResults
1339
1340    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1341        """
1342        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1343
1344        :param instruments: list of strings with tickers or FIGIs.
1345        :return: list with unique instrument FIGIs only.
1346        """
1347        requestedInstruments = []
1348        for iName in instruments:
1349            if iName not in self.aliases.keys():
1350                if iName not in requestedInstruments:
1351                    requestedInstruments.append(iName)
1352
1353            else:
1354                if iName not in requestedInstruments:
1355                    if self.aliases[iName] not in requestedInstruments:
1356                        requestedInstruments.append(self.aliases[iName])
1357
1358        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1359
1360        onlyUniqueFIGIs = []
1361        for iName in requestedInstruments:
1362            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1363                continue
1364
1365            self._ticker = iName
1366            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1367
1368            if not iData:
1369                self._ticker = ""
1370                self._figi = iName
1371
1372                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1373
1374                if not iData:
1375                    self._figi = ""
1376                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1377
1378            if iData and iData["figi"] not in onlyUniqueFIGIs:
1379                onlyUniqueFIGIs.append(iData["figi"])
1380
1381        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1382
1383        return onlyUniqueFIGIs
1384
1385    def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1386        """
1387        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1388
1389        See limits: https://tinkoff.github.io/investAPI/limits/
1390
1391        If `pricesFile` string is not empty then also save information to this file.
1392
1393        :param instruments: list of strings with tickers or FIGIs.
1394        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1395        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1396                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1397        """
1398        if instruments is None or not instruments:
1399            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1400            raise Exception("Ticker or FIGI required")
1401
1402        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1403
1404        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1405
1406        iList = []  # trying to get info and current prices about all unique instruments:
1407        for self._figi in onlyUniqueFIGIs:
1408            iData = self.SearchByFIGI(requestPrice=True)
1409            iList.append(iData)
1410
1411        self.ShowListOfPrices(iList, show)
1412
1413        return iList
1414
1415    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1416        """
1417        Show table contains current prices of given instruments.
1418
1419        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1420                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1421        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1422        :return: multilines text in Markdown format as a table contains current prices.
1423        """
1424        infoText = ""
1425
1426        if show or self.pricesFile:
1427            info = [
1428                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1429                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1430                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1431            ]
1432
1433            for item in iList:
1434                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1435                    item["ticker"],
1436                    item["figi"],
1437                    item["type"],
1438                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1439                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1440                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1441                    "{} / {}".format(
1442                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1443                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1444                    ),
1445                    "{} / {}".format(
1446                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1447                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1448                    ),
1449                    item["currency"],
1450                ))
1451
1452            infoText = "".join(info)
1453
1454            if show:
1455                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1456
1457            if self.pricesFile:
1458                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1459                    fH.write(infoText)
1460
1461                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1462
1463                if self.useHTMLReports:
1464                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1465                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1466                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1467
1468                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1469
1470        return infoText
1471
1472    def RequestTradingStatus(self) -> dict:
1473        """
1474        Requesting trading status for the instrument defined by `figi` variable.
1475
1476        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1477
1478        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1479
1480        :return: dictionary with trading status attributes. Response example:
1481                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1482                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1483        """
1484        if self._figi is None or not self._figi:
1485            uLogger.error("Variable `figi` must be defined for using this method!")
1486            raise Exception("FIGI required")
1487
1488        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1489
1490        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1491        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1492        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1493
1494        if self.moreDebug:
1495            uLogger.debug("Records about current trading status successfully received")
1496
1497        return tradingStatus
1498
1499    def RequestPortfolio(self) -> dict:
1500        """
1501        Requesting actual user's portfolio for current `accountId`.
1502
1503        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1504
1505        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1506
1507        :return: dictionary with user's portfolio.
1508        """
1509        if self.accountId is None or not self.accountId:
1510            uLogger.error("Variable `accountId` must be defined for using this method!")
1511            raise Exception("Account ID required")
1512
1513        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1514
1515        self.body = str({"accountId": self.accountId})
1516        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1517        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1518
1519        if self.moreDebug:
1520            uLogger.debug("Records about user's portfolio successfully received")
1521
1522        return rawPortfolio
1523
1524    def RequestPositions(self) -> dict:
1525        """
1526        Requesting open positions by currencies and instruments for current `accountId`.
1527
1528        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1529
1530        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1531
1532        :return: dictionary with open positions by instruments.
1533        """
1534        if self.accountId is None or not self.accountId:
1535            uLogger.error("Variable `accountId` must be defined for using this method!")
1536            raise Exception("Account ID required")
1537
1538        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1539
1540        self.body = str({"accountId": self.accountId})
1541        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1542        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1543
1544        if self.moreDebug:
1545            uLogger.debug("Records about current open positions successfully received")
1546
1547        return rawPositions
1548
1549    def RequestPendingOrders(self) -> list:
1550        """
1551        Requesting current actual pending limit orders for current `accountId`.
1552
1553        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1554
1555        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1556
1557        :return: list of dictionaries with pending limit orders.
1558        """
1559        if self.accountId is None or not self.accountId:
1560            uLogger.error("Variable `accountId` must be defined for using this method!")
1561            raise Exception("Account ID required")
1562
1563        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1564
1565        self.body = str({"accountId": self.accountId})
1566        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1567        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1568
1569        uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1570
1571        return rawOrders
1572
1573    def RequestStopOrders(self) -> list:
1574        """
1575        Requesting current actual stop orders for current `accountId`.
1576
1577        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1578
1579        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1580
1581        :return: list of dictionaries with stop orders.
1582        """
1583        if self.accountId is None or not self.accountId:
1584            uLogger.error("Variable `accountId` must be defined for using this method!")
1585            raise Exception("Account ID required")
1586
1587        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1588
1589        self.body = str({"accountId": self.accountId})
1590        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1591        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1592
1593        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1594
1595        return rawStopOrders
1596
1597    def Overview(self, show: bool = False, details: str = "full") -> dict:
1598        """
1599        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1600        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1601        and `overviewBondsCalendarFile` are defined then also save information to file.
1602
1603        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1604        many requests about the state of the portfolio, and then, based on the received data, a large number
1605        of calculation and statistics are collected.
1606
1607        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1608        :param details: how detailed should the information be?
1609        - `full` — shows full available information about portfolio status (by default),
1610        - `positions` — shows only open positions,
1611        - `orders` — shows only sections of open limits and stop orders.
1612        - `digest` — show a short digest of the portfolio status,
1613        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1614        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1615        :return: dictionary with client's raw portfolio and some statistics.
1616        """
1617        if self.accountId is None or not self.accountId:
1618            uLogger.error("Variable `accountId` must be defined for using this method!")
1619            raise Exception("Account ID required")
1620
1621        view = {
1622            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1623                "headers": {},  # list of dictionaries, response headers without "positions" section
1624                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1625                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1626                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1627                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1628                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1629                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1630                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1631                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1632                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1633            },
1634            "stat": {  # --- some statistics calculated using "raw" sections:
1635                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1636                "availableRUB": 0.,  # available rubles (without other currencies)
1637                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1638                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1639                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1640                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1641                "sharesCostRUB": 0.,  # costs of all shares in RUB
1642                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1643                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1644                "futuresCostRUB": 0.,  # costs of all futures in RUB
1645                "Currencies": [],  # list of dictionaries of all currencies statistics
1646                "Shares": [],  # list of dictionaries of all shares statistics
1647                "Bonds": [],  # list of dictionaries of all bonds statistics
1648                "Etfs": [],  # list of dictionaries of all etfs statistics
1649                "Futures": [],  # list of dictionaries of all futures statistics
1650                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1651                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1652                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1653                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1654                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1655            },
1656            "analytics": {  # --- some analytics of portfolio:
1657                "distrByAssets": {},  # portfolio distribution by assets
1658                "distrByCompanies": {},  # portfolio distribution by companies
1659                "distrBySectors": {},  # portfolio distribution by sectors
1660                "distrByCurrencies": {},  # portfolio distribution by currencies
1661                "distrByCountries": {},  # portfolio distribution by countries
1662                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1663            }
1664        }
1665
1666        details = details.lower()
1667        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1668        if details not in availableDetails:
1669            details = "full"
1670            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1671
1672        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1673
1674        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1675        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1676        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1677        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1678
1679        # save response headers without "positions" section:
1680        for key in portfolioResponse.keys():
1681            if key != "positions":
1682                view["raw"]["headers"][key] = portfolioResponse[key]
1683
1684            else:
1685                continue
1686
1687        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1688        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1689        for item in portfolioResponse["positions"]:
1690            if item["instrumentType"] == "currency":
1691                self._figi = item["figi"]
1692                curr = self.SearchByFIGI(requestPrice=False)
1693
1694                # current price of currency in RUB:
1695                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1696                    "name": curr["name"],
1697                    "currentPrice": NanoToFloat(
1698                        item["currentPrice"]["units"],
1699                        item["currentPrice"]["nano"]
1700                    ),
1701                }
1702
1703                view["raw"]["Currencies"].append(item)
1704
1705            elif item["instrumentType"] == "share":
1706                view["raw"]["Shares"].append(item)
1707
1708            elif item["instrumentType"] == "bond":
1709                view["raw"]["Bonds"].append(item)
1710
1711            elif item["instrumentType"] == "etf":
1712                view["raw"]["Etfs"].append(item)
1713
1714            elif item["instrumentType"] == "futures":
1715                view["raw"]["Futures"].append(item)
1716
1717            else:
1718                continue
1719
1720        # how many volume of currencies (by ISO currency name) are blocked:
1721        for item in view["raw"]["positions"]["blocked"]:
1722            blocked = NanoToFloat(item["units"], item["nano"])
1723            if blocked > 0:
1724                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1725
1726        # how many volume of instruments (by FIGI) are blocked:
1727        for item in view["raw"]["positions"]["securities"]:
1728            blocked = int(item["blocked"])
1729            if blocked > 0:
1730                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1731
1732        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1733
1734        if "rub" in allBlocked.keys():
1735            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1736
1737        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1738        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1739        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1740        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1741        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1742        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1743        view["stat"]["portfolioCostRUB"] = sum([
1744            view["stat"]["allCurrenciesCostRUB"],
1745            view["stat"]["sharesCostRUB"],
1746            view["stat"]["bondsCostRUB"],
1747            view["stat"]["etfsCostRUB"],
1748            view["stat"]["futuresCostRUB"],
1749        ])
1750
1751        # --- calculating some portfolio statistics:
1752        byComp = {}  # distribution by companies
1753        bySect = {}  # distribution by sectors
1754        byCurr = {}  # distribution by currencies (include RUB)
1755        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1756        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1757
1758        for item in portfolioResponse["positions"]:
1759            self._figi = item["figi"]
1760            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1761
1762            if instrument:
1763                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1764                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1765
1766                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1767                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1768
1769                else:
1770                    blocked = 0
1771
1772                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1773                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1774                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1775                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1776                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1777                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1778                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1779                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1780                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1781                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1782                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1783                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1784
1785                statData = {
1786                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1787                    "ticker": instrument["ticker"],  # ticker by FIGI
1788                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1789                    "volume": volume,  # available volume of instrument
1790                    "lots": lots,  # volume in lots of instrument
1791                    "direction": direction,  # direction of an instrument's position: short or long
1792                    "blocked": blocked,  # blocked volume of currency or instrument
1793                    "currentPrice": curPrice,  # current instrument's price in basic asset
1794                    "average": average,  # current average position price
1795                    "cost": cost,  # current cost of all volume of instrument in basic asset
1796                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1797                    "costRUB": costRUB,  # cost of instrument in ruble
1798                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1799                    "profit": profit,  # expected profit at current moment
1800                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1801                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1802                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1803                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1804                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1805                    "step": instrument["step"],  # minimum price increment
1806                }
1807
1808                # adding distribution by unique countries:
1809                if statData["country"] not in byCountry.keys():
1810                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1811
1812                else:
1813                    byCountry[statData["country"]]["cost"] += costRUB
1814                    byCountry[statData["country"]]["percent"] += percentCostRUB
1815
1816                if item["instrumentType"] != "currency":
1817                    # adding distribution by unique companies:
1818                    if statData["name"]:
1819                        if statData["name"] not in byComp.keys():
1820                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1821
1822                        else:
1823                            byComp[statData["name"]]["cost"] += costRUB
1824                            byComp[statData["name"]]["percent"] += percentCostRUB
1825
1826                    # adding distribution by unique sectors:
1827                    if statData["sector"] not in bySect.keys():
1828                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1829
1830                    else:
1831                        bySect[statData["sector"]]["cost"] += costRUB
1832                        bySect[statData["sector"]]["percent"] += percentCostRUB
1833
1834                # adding distribution by unique currencies:
1835                if currency not in byCurr.keys():
1836                    byCurr[currency] = {
1837                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1838                        "cost": costRUB,
1839                        "percent": percentCostRUB
1840                    }
1841
1842                else:
1843                    byCurr[currency]["cost"] += costRUB
1844                    byCurr[currency]["percent"] += percentCostRUB
1845
1846                # saving statistics for every instrument:
1847                if item["instrumentType"] == "currency":
1848                    view["stat"]["Currencies"].append(statData)
1849
1850                    # update dict with free funds for trading (total - blocked) by currencies
1851                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1852                    view["stat"]["funds"][currency] = {
1853                        "total": volume,
1854                        "totalCostRUB": costRUB,  # total volume cost in rubles
1855                        "free": volume - blocked,
1856                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1857                    }
1858
1859                elif item["instrumentType"] == "share":
1860                    view["stat"]["Shares"].append(statData)
1861
1862                elif item["instrumentType"] == "bond":
1863                    view["stat"]["Bonds"].append(statData)
1864
1865                elif item["instrumentType"] == "etf":
1866                    view["stat"]["Etfs"].append(statData)
1867
1868                elif item["instrumentType"] == "Futures":
1869                    view["stat"]["Futures"].append(statData)
1870
1871                else:
1872                    continue
1873
1874        # total changes in Russian Ruble:
1875        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1876        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1877        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1878        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1879        view["stat"]["funds"]["rub"] = {
1880            "total": view["stat"]["availableRUB"],
1881            "totalCostRUB": view["stat"]["availableRUB"],
1882            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1883            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1884        }
1885
1886        # --- pending limit orders sector data:
1887        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1888        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1889
1890        for item in view["raw"]["orders"]:
1891            self._figi = item["figi"]
1892
1893            if item["figi"] not in uniquePendingOrdersFIGIs:
1894                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1895
1896                uniquePendingOrdersFIGIs.append(item["figi"])
1897                uniquePendingOrders[item["figi"]] = instrument
1898
1899            else:
1900                instrument = uniquePendingOrders[item["figi"]]
1901
1902            if instrument:
1903                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1904                orderType = TKS_ORDER_TYPES[item["orderType"]]
1905                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1906                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1907
1908                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1909                if item["direction"] == "ORDER_DIRECTION_BUY":
1910                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1911
1912                else:
1913                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1914
1915                # requested price for order execution:
1916                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1917
1918                # necessary changes in percent to reach target from current price:
1919                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1920
1921                view["stat"]["orders"].append({
1922                    "orderID": item["orderId"],  # orderId number parameter of current order
1923                    "figi": item["figi"],  # FIGI identification
1924                    "ticker": instrument["ticker"],  # ticker name by FIGI
1925                    "lotsRequested": item["lotsRequested"],  # requested lots value
1926                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1927                    "currentPrice": lastPrice,  # current instrument's price for defined action
1928                    "targetPrice": target,  # requested price for order execution in base currency
1929                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1930                    "percentChanges": changes,  # changes in percent to target from current price
1931                    "currency": item["currency"],  # instrument's currency name
1932                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1933                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1934                    "status": orderState,  # order status from TKS_ORDER_STATES
1935                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1936                })
1937
1938        # --- stop orders sector data:
1939        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1940        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1941
1942        for item in view["raw"]["stopOrders"]:
1943            self._figi = item["figi"]
1944
1945            if item["figi"] not in uniqueStopOrdersFIGIs:
1946                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1947
1948                uniqueStopOrdersFIGIs.append(item["figi"])
1949                uniqueStopOrders[item["figi"]] = instrument
1950
1951            else:
1952                instrument = uniqueStopOrders[item["figi"]]
1953
1954            if instrument:
1955                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1956                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1957                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1958
1959                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1960                if "expirationTime" in item.keys():
1961                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1962                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1963
1964                else:
1965                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1966                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1967
1968                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1969                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1970                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1971
1972                else:
1973                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1974
1975                # requested price when stop-order executed:
1976                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1977
1978                # price for limit-order, set up when stop-order executed:
1979                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1980
1981                # necessary changes in percent to reach target from current price:
1982                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1983
1984                view["stat"]["stopOrders"].append({
1985                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1986                    "figi": item["figi"],  # FIGI identification
1987                    "ticker": instrument["ticker"],  # ticker name by FIGI
1988                    "lotsRequested": item["lotsRequested"],  # requested lots value
1989                    "currentPrice": lastPrice,  # current instrument's price for defined action
1990                    "targetPrice": target,  # requested price for stop-order execution in base currency
1991                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1992                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1993                    "percentChanges": changes,  # changes in percent to target from current price
1994                    "currency": item["currency"],  # instrument's currency name
1995                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1996                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1997                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1998                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1999                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2000                })
2001
2002        # --- calculating data for analytics section:
2003        # portfolio distribution by assets:
2004        view["analytics"]["distrByAssets"] = {
2005            "Ruble": {
2006                "uniques": 1,
2007                "cost": view["stat"]["availableRUB"],
2008                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2009            },
2010            "Currencies": {
2011                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2012                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2013                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2014            },
2015            "Shares": {
2016                "uniques": len(view["stat"]["Shares"]),
2017                "cost": view["stat"]["sharesCostRUB"],
2018                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2019            },
2020            "Bonds": {
2021                "uniques": len(view["stat"]["Bonds"]),
2022                "cost": view["stat"]["bondsCostRUB"],
2023                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2024            },
2025            "Etfs": {
2026                "uniques": len(view["stat"]["Etfs"]),
2027                "cost": view["stat"]["etfsCostRUB"],
2028                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2029            },
2030            "Futures": {
2031                "uniques": len(view["stat"]["Futures"]),
2032                "cost": view["stat"]["futuresCostRUB"],
2033                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2034            },
2035        }
2036
2037        # portfolio distribution by companies:
2038        view["analytics"]["distrByCompanies"]["All money cash"] = {
2039            "ticker": "",
2040            "cost": view["stat"]["allCurrenciesCostRUB"],
2041            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2042        }
2043        view["analytics"]["distrByCompanies"].update(byComp)
2044
2045        # portfolio distribution by sectors:
2046        view["analytics"]["distrBySectors"]["All money cash"] = {
2047            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2048            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2049        }
2050        view["analytics"]["distrBySectors"].update(bySect)
2051
2052        # portfolio distribution by currencies:
2053        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2054            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2055
2056            if self.moreDebug:
2057                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2058
2059        view["analytics"]["distrByCurrencies"].update(byCurr)
2060        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2061        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2062
2063        # portfolio distribution by countries:
2064        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2065            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2066
2067            if self.moreDebug:
2068                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2069
2070        view["analytics"]["distrByCountries"].update(byCountry)
2071        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2072        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2073
2074        # --- Prepare text statistics overview in human-readable:
2075        if show:
2076            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2077
2078            # Whatever the value `details`, header not changes:
2079            info = [
2080                "# Client's portfolio\n\n",
2081                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2082                "* **Account ID:** [{}]\n".format(self.accountId),
2083            ]
2084
2085            if details in ["full", "positions", "digest"]:
2086                info.extend([
2087                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2088                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2089                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2090                        view["stat"]["totalChangesRUB"],
2091                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2092                        view["stat"]["totalChangesPercentRUB"],
2093                    ),
2094                ])
2095
2096            if details in ["full", "positions"]:
2097                info.extend([
2098                    "## Open positions\n\n",
2099                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2100                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2101                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2102                        "{:.2f} ({:.2f}) rub".format(
2103                            view["stat"]["availableRUB"],
2104                            view["stat"]["blockedRUB"],
2105                        )
2106                    )
2107                ])
2108
2109                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2110                    return [
2111                        "|                             |                                 |          |              |              |                     |                              |\n",
2112                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2113                            noTradeStr if noTradeStr else typeStr,
2114                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2115                        ),
2116                    ]
2117
2118                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2119                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2120                        "{} [{}]".format(data["ticker"], data["figi"]),
2121                        "{:.2f} ({:.2f}) {}".format(
2122                            data["volume"],
2123                            data["blocked"],
2124                            data["currency"],
2125                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2126                            data["volume"],
2127                            data["blocked"],
2128                        ),
2129                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2130                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2131                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2132                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2133                        "{}{:.2f} {} ({}{:.2f}%)".format(
2134                            "+" if data["profit"] > 0 else "",
2135                            data["profit"], data["baseCurrencyName"],
2136                            "+" if data["percentProfit"] > 0 else "",
2137                            data["percentProfit"],
2138                        ),
2139                    )
2140
2141                # --- Show currencies section:
2142                if view["stat"]["Currencies"]:
2143                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2144                    for item in view["stat"]["Currencies"]:
2145                        info.append(_InfoStr(item, showCurrencyName=True))
2146
2147                else:
2148                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2149
2150                # --- Show shares section:
2151                if view["stat"]["Shares"]:
2152                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2153
2154                    for item in view["stat"]["Shares"]:
2155                        info.append(_InfoStr(item))
2156
2157                else:
2158                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2159
2160                # --- Show bonds section:
2161                if view["stat"]["Bonds"]:
2162                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2163
2164                    for item in view["stat"]["Bonds"]:
2165                        info.append(_InfoStr(item))
2166
2167                else:
2168                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2169
2170                # --- Show etfs section:
2171                if view["stat"]["Etfs"]:
2172                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2173
2174                    for item in view["stat"]["Etfs"]:
2175                        info.append(_InfoStr(item))
2176
2177                else:
2178                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2179
2180                # --- Show futures section:
2181                if view["stat"]["Futures"]:
2182                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2183
2184                    for item in view["stat"]["Futures"]:
2185                        info.append(_InfoStr(item))
2186
2187                else:
2188                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2189
2190            if details in ["full", "orders"]:
2191                # --- Show pending limit orders section:
2192                if view["stat"]["orders"]:
2193                    info.extend([
2194                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2195                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2196                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2197                    ])
2198
2199                    for item in view["stat"]["orders"]:
2200                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2201                            "{} [{}]".format(item["ticker"], item["figi"]),
2202                            item["orderID"],
2203                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2204                            "{} {} ({}{:.2f}%)".format(
2205                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2206                                item["baseCurrencyName"],
2207                                "+" if item["percentChanges"] > 0 else "",
2208                                float(item["percentChanges"]),
2209                            ),
2210                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2211                            item["action"],
2212                            item["type"],
2213                            item["date"],
2214                        ))
2215
2216                else:
2217                    info.append("\n## Total pending limit-orders: [0]\n")
2218
2219                # --- Show stop orders section:
2220                if view["stat"]["stopOrders"]:
2221                    info.extend([
2222                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2223                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2224                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2225                    ])
2226
2227                    for item in view["stat"]["stopOrders"]:
2228                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2229                            "{} [{}]".format(item["ticker"], item["figi"]),
2230                            item["orderID"],
2231                            item["lotsRequested"],
2232                            "{} {} ({}{:.2f}%)".format(
2233                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2234                                item["baseCurrencyName"],
2235                                "+" if item["percentChanges"] > 0 else "",
2236                                float(item["percentChanges"]),
2237                            ),
2238                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2239                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2240                            item["action"],
2241                            item["type"],
2242                            item["expType"],
2243                            item["createDate"],
2244                            item["expDate"],
2245                        ))
2246
2247                else:
2248                    info.append("\n## Total stop-orders: [0]\n")
2249
2250            if details in ["full", "analytics"]:
2251                # -- Show analytics section:
2252                if view["stat"]["portfolioCostRUB"] > 0:
2253                    info.extend([
2254                        "\n# Analytics\n\n"
2255                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2256                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2257                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2258                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2259                            view["stat"]["totalChangesRUB"],
2260                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2261                            view["stat"]["totalChangesPercentRUB"],
2262                        ),
2263                        "\n## Portfolio distribution by assets\n"
2264                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2265                        "|------------------------------------|---------|---------|--------------------|\n",
2266                    ])
2267
2268                    for key in view["analytics"]["distrByAssets"].keys():
2269                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2270                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2271                                key,
2272                                view["analytics"]["distrByAssets"][key]["uniques"],
2273                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2274                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2275                            ))
2276
2277                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2278
2279                    info.extend([
2280                        "\n## Portfolio distribution by companies\n"
2281                        "\n| Company                                      | Percent | Current cost       |\n",
2282                        aSepLine,
2283                    ])
2284
2285                    for company in view["analytics"]["distrByCompanies"].keys():
2286                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2287                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2288                                "{}{}".format(
2289                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2290                                    company,
2291                                ),
2292                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2293                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2294                            ))
2295
2296                    info.extend([
2297                        "\n## Portfolio distribution by sectors\n"
2298                        "\n| Sector                                       | Percent | Current cost       |\n",
2299                        aSepLine,
2300                    ])
2301
2302                    for sector in view["analytics"]["distrBySectors"].keys():
2303                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2304                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2305                                sector,
2306                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2307                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2308                            ))
2309
2310                    info.extend([
2311                        "\n## Portfolio distribution by currencies\n"
2312                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2313                        aSepLine,
2314                    ])
2315
2316                    for curr in view["analytics"]["distrByCurrencies"].keys():
2317                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2318                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2319                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2320                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2321                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2322                            ))
2323
2324                    info.extend([
2325                        "\n## Portfolio distribution by countries\n"
2326                        "\n| Assets by country                            | Percent | Current cost       |\n",
2327                        aSepLine,
2328                    ])
2329
2330                    for country in view["analytics"]["distrByCountries"].keys():
2331                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2332                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2333                                country,
2334                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2335                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2336                            ))
2337
2338            if details in ["full", "calendar"]:
2339                # -- Show bonds payment calendar section:
2340                if view["stat"]["Bonds"]:
2341                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2342                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2343                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2344
2345                else:
2346                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2347
2348            infoText = "".join(info)
2349
2350            uLogger.info(infoText)
2351
2352            if details == "full" and self.overviewFile:
2353                filename = self.overviewFile
2354
2355            elif details == "digest" and self.overviewDigestFile:
2356                filename = self.overviewDigestFile
2357
2358            elif details == "positions" and self.overviewPositionsFile:
2359                filename = self.overviewPositionsFile
2360
2361            elif details == "orders" and self.overviewOrdersFile:
2362                filename = self.overviewOrdersFile
2363
2364            elif details == "analytics" and self.overviewAnalyticsFile:
2365                filename = self.overviewAnalyticsFile
2366
2367            elif details == "calendar" and self.overviewBondsCalendarFile:
2368                filename = self.overviewBondsCalendarFile
2369
2370            else:
2371                filename = ""
2372
2373            if filename:
2374                with open(filename, "w", encoding="UTF-8") as fH:
2375                    fH.write(infoText)
2376
2377                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2378
2379                if self.useHTMLReports:
2380                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2381                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2382                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="", commonCSS=COMMON_CSS, markdown=infoText))
2383
2384                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2385
2386        return view
2387
2388    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2389        """
2390        Returns history operations between two given dates for current `accountId`.
2391        If `reportFile` string is not empty then also save human-readable report.
2392        Shows some statistical data of closed positions.
2393
2394        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2395        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2396        :param show: if `True` then also prints all records to the console.
2397        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2398        :return: original list of dictionaries with history of deals records from API ("operations" key):
2399                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2400                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2401        """
2402        if self.accountId is None or not self.accountId:
2403            uLogger.error("Variable `accountId` must be defined for using this method!")
2404            raise Exception("Account ID required")
2405
2406        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2407
2408        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2409
2410        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2411        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2412        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2413        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2414        customStat = {}  # custom statistics in additional to responseJSON
2415
2416        # --- output report in human-readable format:
2417        if show or self.reportFile:
2418            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2419            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2420            nextDay = ""
2421
2422            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2423
2424            if len(ops) > 0:
2425                customStat = {
2426                    "opsCount": 0,  # total operations count
2427                    "buyCount": 0,  # buy operations
2428                    "sellCount": 0,  # sell operations
2429                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2430                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2431                    "payIn": {"rub": 0.},  # Deposit brokerage account
2432                    "payOut": {"rub": 0.},  # Withdrawals
2433                    "divs": {"rub": 0.},  # Dividends income
2434                    "coupons": {"rub": 0.},  # Coupon's income
2435                    "brokerCom": {"rub": 0.},  # Service commissions
2436                    "serviceCom": {"rub": 0.},  # Service commissions
2437                    "marginCom": {"rub": 0.},  # Margin commissions
2438                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2439                }
2440
2441                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2442                for item in ops:
2443                    if item["state"] == "OPERATION_STATE_EXECUTED":
2444                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2445
2446                        # count buy operations:
2447                        if "_BUY" in item["operationType"]:
2448                            customStat["buyCount"] += 1
2449
2450                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2451                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2452
2453                            else:
2454                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2455
2456                        # count sell operations:
2457                        elif "_SELL" in item["operationType"]:
2458                            customStat["sellCount"] += 1
2459
2460                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2461                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2462
2463                            else:
2464                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2465
2466                        # count incoming operations:
2467                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2468                            if item["payment"]["currency"] in customStat["payIn"].keys():
2469                                customStat["payIn"][item["payment"]["currency"]] += payment
2470
2471                            else:
2472                                customStat["payIn"][item["payment"]["currency"]] = payment
2473
2474                        # count withdrawals operations:
2475                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2476                            if item["payment"]["currency"] in customStat["payOut"].keys():
2477                                customStat["payOut"][item["payment"]["currency"]] += payment
2478
2479                            else:
2480                                customStat["payOut"][item["payment"]["currency"]] = payment
2481
2482                        # count dividends income:
2483                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2484                            if item["payment"]["currency"] in customStat["divs"].keys():
2485                                customStat["divs"][item["payment"]["currency"]] += payment
2486
2487                            else:
2488                                customStat["divs"][item["payment"]["currency"]] = payment
2489
2490                        # count coupon's income:
2491                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2492                            if item["payment"]["currency"] in customStat["coupons"].keys():
2493                                customStat["coupons"][item["payment"]["currency"]] += payment
2494
2495                            else:
2496                                customStat["coupons"][item["payment"]["currency"]] = payment
2497
2498                        # count broker commissions:
2499                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2500                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2501                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2502
2503                            else:
2504                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2505
2506                        # count service commissions:
2507                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2508                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2509                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2510
2511                            else:
2512                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2513
2514                        # count margin commissions:
2515                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2516                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2517                                customStat["marginCom"][item["payment"]["currency"]] += payment
2518
2519                            else:
2520                                customStat["marginCom"][item["payment"]["currency"]] = payment
2521
2522                        # count withholding taxes:
2523                        elif "_TAX" in item["operationType"]:
2524                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2525                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2526
2527                            else:
2528                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2529
2530                        else:
2531                            continue
2532
2533                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2534
2535                # --- view "Actions" lines:
2536                info.extend([
2537                    "| Report sections            |                               |                              |                      |                        |\n",
2538                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2539                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2540                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2541                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2542                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2543                    ),
2544                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2545                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2546                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2547                    ),
2548                ])
2549
2550                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2551                for key in opsKeys:
2552                    if key == "rub":
2553                        continue
2554
2555                    info.extend([
2556                        "|                            |                               | {:<28} |                      |                        |\n".format(
2557                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2558                        ),
2559                        "|                            |                               | {:<28} |                      |                        |\n".format(
2560                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2561                        ),
2562                    ])
2563
2564                info.append(splitLine1)
2565
2566                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2567                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2568                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2569                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2570                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2571                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2572                    )
2573
2574                # --- view "Payments" lines:
2575                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2576                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2577
2578                for key in paymentsKeys:
2579                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2580
2581                info.append(splitLine1)
2582
2583                # --- view "Commissions and taxes" lines:
2584                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2585                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2586
2587                for key in comKeys:
2588                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2589
2590                info.extend([
2591                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2592                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2593                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2594                ])
2595
2596            else:
2597                info.append("Broker returned no operations during this period\n")
2598
2599            # --- view "Operations" section:
2600            for item in ops:
2601                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2602                    continue
2603
2604                else:
2605                    self._figi = item["figi"] if item["figi"] else ""
2606                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2607                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2608
2609                    # group of deals during one day:
2610                    if nextDay and item["date"].split("T")[0] != nextDay:
2611                        info.append(splitLine2)
2612                        nextDay = ""
2613
2614                    else:
2615                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2616
2617                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2618                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2619                        self._figi if self._figi else "—",
2620                        instrument["ticker"] if instrument else "—",
2621                        instrument["type"] if instrument else "—",
2622                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2623                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2624                        TKS_OPERATION_STATES[item["state"]],
2625                        TKS_OPERATION_TYPES[item["operationType"]],
2626                    ))
2627
2628            infoText = "".join(info)
2629
2630            if show:
2631                if self.moreDebug:
2632                    uLogger.debug("Records about history of a client's operations successfully received")
2633
2634                uLogger.info(infoText)
2635
2636            if self.reportFile:
2637                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2638                    fH.write(infoText)
2639
2640                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2641
2642                if self.useHTMLReports:
2643                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2644                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2645                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2646
2647                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2648
2649        return ops, customStat
2650
2651    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2652        """
2653        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2654
2655        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2656        Warning! Broker server used ISO UTC time by default.
2657
2658        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2659        Also, `historyFile` used to update history with `onlyMissing` parameter.
2660
2661        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2662
2663        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2664        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2665        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2666                         `"hour"`, `"day"`. Default: `"hour"`.
2667        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2668                            False by default. Warning! History appends only from last candle to current time
2669                            with always update last candle!
2670        :param csvSep: separator if csv-file is used, `,` by default.
2671        :param show: if `True` then also prints Pandas DataFrame to the console.
2672        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2673                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2674        """
2675        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2676        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2677        history = None  # empty pandas object for history
2678
2679        if interval not in TKS_CANDLE_INTERVALS.keys():
2680            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2681            raise Exception("Incorrect value")
2682
2683        if not (self._ticker or self._figi):
2684            uLogger.error("Ticker or FIGI must be defined!")
2685            raise Exception("Ticker or FIGI required")
2686
2687        if self._ticker and not self._figi:
2688            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2689            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2690
2691        if self._figi and not self._ticker:
2692            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2693            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2694
2695        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2696        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2697        if interval.lower() != "day":
2698            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2699
2700        delta = dtEnd - dtStart  # current UTC time minus last time in file
2701        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2702
2703        # calculate history length in candles:
2704        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2705        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2706            length += 1  # to avoid fraction time
2707
2708        # calculate data blocks count:
2709        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2710
2711        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2712        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2713        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2714        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2715        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2716
2717        tempOld = None  # pandas object for old history, if --only-missing key present
2718        lastTime = None  # datetime object of last old candle in file
2719
2720        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2721            uLogger.debug("--only-missing key present, add only last missing candles...")
2722            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2723
2724            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2725
2726            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2727            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2728            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2729            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2730
2731            # get last datetime object from last string in file or minus 1 delta if file is empty:
2732            if len(tempOld) > 0:
2733                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2734
2735            else:
2736                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2737
2738            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2739
2740        responseJSONs = []  # raw history blocks of data
2741
2742        blockEnd = dtEnd
2743        for item in range(blocks):
2744            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2745            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2746
2747            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2748                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2749            ))
2750
2751            if blockStart == blockEnd:
2752                uLogger.debug("Skipped this zero-length block...")
2753
2754            else:
2755                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2756                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2757                self.body = str({
2758                    "figi": self._figi,
2759                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2760                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2761                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2762                })
2763                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2764
2765                if "code" in responseJSON.keys():
2766                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2767
2768                else:
2769                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2770                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2771
2772                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2773
2774            blockEnd = blockStart
2775
2776        printCount = len(responseJSONs)  # candles to show in console
2777        if responseJSONs:
2778            tempHistory = pd.DataFrame(
2779                data={
2780                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2781                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2782                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2783                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2784                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2785                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2786                    "volume": [int(item["volume"]) for item in responseJSONs],
2787                },
2788                index=range(len(responseJSONs)),
2789                columns=["date", "time", "open", "high", "low", "close", "volume"],
2790            )
2791            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2792            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2793
2794            # append only newest candles to old history if --only-missing key present:
2795            if onlyMissing and tempOld is not None and lastTime is not None:
2796                index = 0  # find start index in tempHistory data:
2797
2798                for i, item in tempHistory.iterrows():
2799                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2800
2801                    if curTime == lastTime:
2802                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2803                        index = i
2804                        printCount = index + 1
2805                        break
2806
2807                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2808
2809            else:
2810                history = tempHistory  # if no `--only-missing` key then load full data from server
2811
2812            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2813
2814        if history is not None and not history.empty:
2815            if show:
2816                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2817                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2818                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2819                ))
2820
2821        else:
2822            uLogger.warning("Received an empty candles history!")
2823
2824        if self.historyFile is not None:
2825            if history is not None and not history.empty:
2826                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2827                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2828
2829            else:
2830                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2831
2832        else:
2833            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2834
2835        return history
2836
2837    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2838        """
2839        Load candles history from csv-file and return Pandas DataFrame object.
2840
2841        See also: `History()` and `ShowHistoryChart()` methods.
2842
2843        :param filePath: path to csv-file to open.
2844        """
2845        loadedHistory = None  # init candles data object
2846
2847        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2848
2849        if os.path.exists(filePath):
2850            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2851
2852            tfStr = self.priceModel.FormattedDelta(
2853                self.priceModel.timeframe,
2854                "{days} days {hours}h {minutes}m {seconds}s",
2855            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2856                self.priceModel.timeframe,
2857                "{hours}h {minutes}m {seconds}s",
2858            )
2859
2860            if loadedHistory is not None and not loadedHistory.empty:
2861                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2862                    len(loadedHistory),
2863                    tfStr,
2864                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2865                )
2866
2867            else:
2868                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2869
2870        else:
2871            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2872
2873        return loadedHistory
2874
2875    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2876        """
2877        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2878
2879        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2880        Default: `index.html` (both for interact and non-interact candlesticks chart).
2881
2882        See also: `History()` and `LoadHistory()` methods.
2883
2884        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2885        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2886                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2887                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2888                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2889        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2890                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2891        """
2892        if isinstance(candles, str):
2893            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2894            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2895
2896        elif isinstance(candles, pd.DataFrame):
2897            self.priceModel.prices = candles  # set candles chain from variable
2898            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2899
2900            if "datetime" not in candles.columns:
2901                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2902
2903        else:
2904            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2905            raise Exception("Incorrect value")
2906
2907        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2908
2909        if interact:
2910            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2911
2912            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2913
2914        else:
2915            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2916
2917            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2918
2919        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2920
2921    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2922        """
2923        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2924        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2925
2926        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2927
2928        :param operation: string "Buy" or "Sell".
2929        :param lots: volume, integer count of lots >= 1.
2930        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2931        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2932        :param expDate: string "Undefined" by default or local date in future,
2933                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2934        :return: JSON with response from broker server.
2935        """
2936        if self.accountId is None or not self.accountId:
2937            uLogger.error("Variable `accountId` must be defined for using this method!")
2938            raise Exception("Account ID required")
2939
2940        if operation is None or not operation or operation not in ("Buy", "Sell"):
2941            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2942            raise Exception("Incorrect value")
2943
2944        if lots is None or lots < 1:
2945            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2946            lots = 1
2947
2948        if tp is None or tp < 0:
2949            tp = 0
2950
2951        if sl is None or sl < 0:
2952            sl = 0
2953
2954        if expDate is None or not expDate:
2955            expDate = "Undefined"
2956
2957        if not (self._ticker or self._figi):
2958            uLogger.error("Ticker or FIGI must be defined!")
2959            raise Exception("Ticker or FIGI required")
2960
2961        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2962        self._ticker = instrument["ticker"]
2963        self._figi = instrument["figi"]
2964
2965        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
2966
2967        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2968        self.body = str({
2969            "figi": self._figi,
2970            "quantity": str(lots),
2971            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2972            "accountId": str(self.accountId),
2973            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2974        })
2975        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2976
2977        if "orderId" in response.keys():
2978            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2979                operation, response["orderId"],
2980                self._ticker, self._figi, lots,
2981                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2982                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2983                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2984            ))
2985
2986            if tp > 0:
2987                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2988
2989            if sl > 0:
2990                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2991
2992        else:
2993            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
2994
2995        return response
2996
2997    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2998        """
2999        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3000        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3001
3002        See also: `Order()` and `Trade()` docstrings.
3003
3004        :param lots: volume, integer count of lots >= 1.
3005        :param tp: float > 0, take profit price of stop-order.
3006        :param sl: float > 0, stop loss price of stop-order.
3007        :param expDate: it's a local date in future.
3008                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3009        :return: JSON with response from broker server.
3010        """
3011        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3012
3013    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3014        """
3015        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3016        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3017
3018        See also: `Order()` and `Trade()` docstrings.
3019
3020        :param lots: volume, integer count of lots >= 1.
3021        :param tp: float > 0, take profit price of stop-order.
3022        :param sl: float > 0, stop loss price of stop-order.
3023        :param expDate: it's a local date in the future.
3024                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3025        :return: JSON with response from broker server.
3026        """
3027        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3028
3029    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3030        """
3031        Close position of given instruments.
3032
3033        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3034        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3035                         This avoids unnecessary downloading data from the server.
3036        """
3037        if instruments is None or not instruments:
3038            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3039            raise Exception("Ticker or FIGI required")
3040
3041        if isinstance(instruments, str):
3042            instruments = [instruments]
3043
3044        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3045        if uniqueInstruments:
3046            if portfolio is None or not portfolio:
3047                portfolio = self.Overview(show=False)
3048
3049            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3050            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3051
3052            for self._figi in uniqueInstruments:
3053                if self._figi not in allOpened:
3054                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3055                    continue
3056
3057                # search open trade info about instrument by ticker:
3058                instrument = {}
3059                for iType in TKS_INSTRUMENTS:
3060                    if instrument:
3061                        break
3062
3063                    for item in portfolio["stat"][iType]:
3064                        if item["figi"] == self._figi:
3065                            instrument = item
3066                            break
3067
3068                if instrument:
3069                    self._ticker = instrument["ticker"]
3070                    self._figi = instrument["figi"]
3071
3072                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3073                        self._ticker,
3074                        self._figi,
3075                        int(instrument["volume"]),
3076                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3077                    ))
3078
3079                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3080
3081                    if tradeLots > 0:
3082                        if instrument["blocked"] > 0:
3083                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3084                                instrument["blocked"],
3085                                self._ticker,
3086                                tradeLots,
3087                            ))
3088
3089                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3090                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3091
3092                    else:
3093                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3094
3095    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3096        """
3097        Close all positions of given instruments with defined type.
3098
3099        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3100        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3101                         This avoids unnecessary downloading data from the server.
3102        """
3103        if iType not in TKS_INSTRUMENTS:
3104            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3105
3106        else:
3107            if portfolio is None or not portfolio:
3108                portfolio = self.Overview(show=False)
3109
3110            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3111            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3112
3113            if tickers and portfolio:
3114                self.CloseTrades(tickers, portfolio)
3115
3116            else:
3117                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3118
3119    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3120        """
3121        Universal method to create market or limit orders with all available parameters for current `accountId`.
3122        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3123
3124        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3125        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3126
3127        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3128        then broker immediately open market order as you can do simple --buy or --sell operations!
3129
3130        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3131        When current price will go up or down to target price value then broker opens a limit order.
3132        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3133
3134        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3135
3136        :param operation: string "Buy" or "Sell".
3137        :param orderType: string "Limit" or "Stop".
3138        :param lots: volume, integer count of lots >= 1.
3139        :param targetPrice: target price > 0. This is open trade price for limit order.
3140        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3141                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3142        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3143                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3144                         Stop loss order always executed by market price.
3145        :param expDate: string "Undefined" by default or local date in future.
3146                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3147                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3148                        A limit order has no expiration date, it lasts until the end of the trading day.
3149        :return: JSON with response from broker server.
3150        """
3151        if self.accountId is None or not self.accountId:
3152            uLogger.error("Variable `accountId` must be defined for using this method!")
3153            raise Exception("Account ID required")
3154
3155        if operation is None or not operation or operation not in ("Buy", "Sell"):
3156            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3157            raise Exception("Incorrect value")
3158
3159        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3160            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3161            raise Exception("Incorrect value")
3162
3163        if lots is None or lots < 1:
3164            uLogger.error("You must define trade volume > 0: integer count of lots!")
3165            raise Exception("Incorrect value")
3166
3167        if targetPrice is None or targetPrice <= 0:
3168            uLogger.error("Target price for limit-order must be greater than 0!")
3169            raise Exception("Incorrect value")
3170
3171        if limitPrice is None or limitPrice <= 0:
3172            limitPrice = targetPrice
3173
3174        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3175            stopType = "Limit"
3176
3177        if expDate is None or not expDate:
3178            expDate = "Undefined"
3179
3180        if not (self._ticker or self._figi):
3181            uLogger.error("Tocker or FIGI must be defined!")
3182            raise Exception("Ticker or FIGI required")
3183
3184        response = {}
3185        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3186        self._ticker = instrument["ticker"]
3187        self._figi = instrument["figi"]
3188
3189        if orderType == "Limit":
3190            uLogger.debug(
3191                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3192                    self._ticker, self._figi,
3193                    operation, lots, targetPrice, instrument["currency"],
3194                ))
3195
3196            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3197            self.body = str({
3198                "figi": self._figi,
3199                "quantity": str(lots),
3200                "price": FloatToNano(targetPrice),
3201                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3202                "accountId": str(self.accountId),
3203                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3204            })
3205            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3206
3207            if "orderId" in response.keys():
3208                uLogger.info(
3209                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3210                        response["orderId"],
3211                        self._ticker, self._figi,
3212                        operation, lots, targetPrice, instrument["currency"],
3213                    ))
3214
3215                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3216                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3217                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3218                            targetPrice, instrument["currency"],
3219                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3220                        ))
3221
3222                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3223                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3224                            targetPrice, instrument["currency"],
3225                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3226                        ))
3227
3228            else:
3229                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3230
3231        if orderType == "Stop":
3232            uLogger.debug(
3233                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3234                    self._ticker, self._figi,
3235                    operation, lots,
3236                    targetPrice, instrument["currency"],
3237                    limitPrice, instrument["currency"],
3238                    stopType, expDate,
3239                ))
3240
3241            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3242            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3243            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3244
3245            body = {
3246                "figi": self._figi,
3247                "quantity": str(lots),
3248                "price": FloatToNano(limitPrice),
3249                "stopPrice": FloatToNano(targetPrice),
3250                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3251                "accountId": str(self.accountId),
3252                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3253                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3254            }
3255
3256            if expDateUTC:
3257                body["expireDate"] = expDateUTC
3258
3259            self.body = str(body)
3260            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3261
3262            if "stopOrderId" in response.keys():
3263                uLogger.info(
3264                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3265                        response["stopOrderId"],
3266                        self._ticker, self._figi,
3267                        operation, lots,
3268                        targetPrice, instrument["currency"],
3269                        limitPrice, instrument["currency"],
3270                        TKS_STOP_ORDER_TYPES[stopOrderType],
3271                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3272                    ))
3273
3274                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3275                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3276                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3277                            targetPrice, instrument["currency"],
3278                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3279                        ))
3280
3281                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3282                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3283                            targetPrice, instrument["currency"],
3284                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3285                        ))
3286
3287            else:
3288                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3289
3290        return response
3291
3292    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3293        """
3294        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3295        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3296        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3297        See also: `Order()` docstring.
3298
3299        :param lots: volume, integer count of lots >= 1.
3300        :param targetPrice: target price > 0. This is open trade price for limit order.
3301        :return: JSON with response from broker server.
3302        """
3303        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3304
3305    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3306        """
3307        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3308        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3309        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3310        target price value then broker opens a limit order. See also: `Order()` docstring.
3311
3312        :param lots: volume, integer count of lots >= 1.
3313        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3314        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3315                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3316        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3317                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3318        :param expDate: string "Undefined" by default or local date in future.
3319                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3320                        This date is converting to UTC format for server.
3321        :return: JSON with response from broker server.
3322        """
3323        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3324
3325    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3326        """
3327        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3328        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3329        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3330        See also: `Order()` docstring.
3331
3332        :param lots: volume, integer count of lots >= 1.
3333        :param targetPrice: target price > 0. This is open trade price for limit order.
3334        :return: JSON with response from broker server.
3335        """
3336        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3337
3338    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3339        """
3340        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3341        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3342        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3343        target price value then broker opens a limit order. See also: `Order()` docstring.
3344
3345        :param lots: volume, integer count of lots >= 1.
3346        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3347        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3348                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3349        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3350                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3351        :param expDate: string "Undefined" by default or local date in future.
3352                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3353                        This date is converting to UTC format for server.
3354        :return: JSON with response from broker server.
3355        """
3356        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3357
3358    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3359        """
3360        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3361
3362        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3363        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3364                             This avoids unnecessary downloading data from the server.
3365        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3366        """
3367        if self.accountId is None or not self.accountId:
3368            uLogger.error("Variable `accountId` must be defined for using this method!")
3369            raise Exception("Account ID required")
3370
3371        if orderIDs:
3372            if allOrdersIDs is None:
3373                rawOrders = self.RequestPendingOrders()
3374                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3375
3376            if allStopOrdersIDs is None:
3377                rawStopOrders = self.RequestStopOrders()
3378                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3379
3380            for orderID in orderIDs:
3381                idInPendingOrders = orderID in allOrdersIDs
3382                idInStopOrders = orderID in allStopOrdersIDs
3383
3384                if not (idInPendingOrders or idInStopOrders):
3385                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3386                    continue
3387
3388                else:
3389                    if idInPendingOrders:
3390                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3391
3392                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3393                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3394                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3395                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3396
3397                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3398                            if self.moreDebug:
3399                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3400
3401                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3402
3403                        else:
3404                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3405
3406                    elif idInStopOrders:
3407                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3408
3409                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3410                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3411                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3412                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3413
3414                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3415                            if self.moreDebug:
3416                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3417
3418                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3419
3420                        else:
3421                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3422
3423                    else:
3424                        continue
3425
3426    def CloseAllOrders(self) -> None:
3427        """
3428        Gets a list of open pending and stop orders and cancel it all.
3429        """
3430        rawOrders = self.RequestPendingOrders()
3431        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3432        lenOrders = len(allOrdersIDs)
3433
3434        rawStopOrders = self.RequestStopOrders()
3435        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3436        lenSOrders = len(allStopOrdersIDs)
3437
3438        if lenOrders > 0 or lenSOrders > 0:
3439            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3440
3441            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3442
3443        else:
3444            uLogger.info("Orders not found, nothing to cancel.")
3445
3446    def CloseAll(self, *args) -> None:
3447        """
3448        Close all available (not blocked) opened trades and orders.
3449
3450        Also, you can select one or more keywords case-insensitive:
3451        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3452
3453        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3454        """
3455        overview = self.Overview(show=False)  # get all open trades info
3456
3457        if len(args) == 0:
3458            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3459            self.CloseAllOrders()  # close all pending and stop orders
3460
3461            for iType in TKS_INSTRUMENTS:
3462                if iType != "Currencies":
3463                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3464
3465        else:
3466            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3467            lowerArgs = [x.lower() for x in args]
3468
3469            if "orders" in lowerArgs:
3470                self.CloseAllOrders()  # close all pending and stop orders
3471
3472            for iType in TKS_INSTRUMENTS:
3473                if iType.lower() in lowerArgs and iType != "Currencies":
3474                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3475
3476    def CloseAllByTicker(self, instrument: str) -> None:
3477        """
3478        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3479
3480        This method searches opened trade and orders of instrument throw all portfolio and then use
3481        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3482
3483        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3484
3485        :param instrument: string with ticker.
3486        """
3487        if instrument is None or not instrument:
3488            uLogger.error("Ticker name must be defined for using this method!")
3489            raise Exception("Ticker required")
3490
3491        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3492
3493        self._ticker = instrument  # try to set instrument as ticker
3494        self._figi = ""
3495
3496        if self.IsInPortfolio(portfolio=overview):
3497            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3498            self.CloseTrades(instruments=[instrument], portfolio=overview)
3499
3500        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3501        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3502
3503        if limitAll and self.IsInLimitOrders(portfolio=overview):
3504            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3505            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3506
3507        if stopAll and self.IsInStopOrders(portfolio=overview):
3508            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3509            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3510
3511    def CloseAllByFIGI(self, instrument: str) -> None:
3512        """
3513        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3514
3515        This method searches opened trade and orders of instrument throw all portfolio and then use
3516        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3517
3518        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3519
3520        :param instrument: string with FIGI id.
3521        """
3522        if instrument is None or not instrument:
3523            uLogger.error("FIGI id must be defined for using this method!")
3524            raise Exception("FIGI required")
3525
3526        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3527
3528        self._ticker = ""
3529        self._figi = instrument  # try to set instrument as FIGI id
3530
3531        if self.IsInPortfolio(portfolio=overview):
3532            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3533            self.CloseTrades(instruments=[instrument], portfolio=overview)
3534
3535        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3536        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3537
3538        if limitAll and self.IsInLimitOrders(portfolio=overview):
3539            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3540            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3541
3542        if stopAll and self.IsInStopOrders(portfolio=overview):
3543            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3544            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3545
3546    @staticmethod
3547    def ParseOrderParameters(operation, **inputParameters):
3548        """
3549        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3550
3551        :param operation: string "Buy" or "Sell".
3552        :param inputParameters: this is dict of strings that looks like this
3553               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3554               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3555               "prices" key: one or more prices to open limit-orders
3556               Counts of values in lots and prices lists must be equals!
3557        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3558        """
3559        # TODO: update order grid work with api v2
3560        pass
3561        # uLogger.debug("Input parameters: {}".format(inputParameters))
3562        #
3563        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3564        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3565        #     raise Exception("Incorrect value")
3566        #
3567        # if "l" in inputParameters.keys():
3568        #     inputParameters["lots"] = inputParameters.pop("l")
3569        #
3570        # if "p" in inputParameters.keys():
3571        #     inputParameters["prices"] = inputParameters.pop("p")
3572        #
3573        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3574        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3575        #     raise Exception("Incorrect value")
3576        #
3577        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3578        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3579        #
3580        # if len(lots) != len(prices):
3581        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3582        #     raise Exception("Incorrect value")
3583        #
3584        # uLogger.debug("Extracted parameters for orders:")
3585        # uLogger.debug("lots = {}".format(lots))
3586        # uLogger.debug("prices = {}".format(prices))
3587        #
3588        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3589        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3590        # uLogger.debug("Order parameters: {}".format(result))
3591        #
3592        # return result
3593
3594    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3595        """
3596        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3597
3598        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3599        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3600        """
3601        result = False
3602        msg = "Instrument not defined!"
3603
3604        if portfolio is None or not portfolio:
3605            portfolio = self.Overview(show=False)
3606
3607        if self._ticker:
3608            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3609            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3610
3611            for iType in TKS_INSTRUMENTS:
3612                for instrument in portfolio["stat"][iType]:
3613                    if instrument["ticker"] == self._ticker:
3614                        result = True
3615                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3616                        break
3617
3618        elif self._figi:
3619            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3620            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3621
3622            for iType in TKS_INSTRUMENTS:
3623                for instrument in portfolio["stat"][iType]:
3624                    if instrument["figi"] == self._figi:
3625                        result = True
3626                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3627                        break
3628
3629        else:
3630            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3631
3632        uLogger.debug(msg)
3633
3634        return result
3635
3636    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3637        """
3638        Returns instrument from the user's portfolio if it presents there.
3639        Instrument must be defined by `ticker` (highly priority) or `figi`.
3640
3641        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3642        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3643        """
3644        result = None
3645        msg = "Instrument not defined!"
3646
3647        if portfolio is None or not portfolio:
3648            portfolio = self.Overview(show=False)
3649
3650        if self._ticker:
3651            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3652            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3653
3654            for iType in TKS_INSTRUMENTS:
3655                for instrument in portfolio["stat"][iType]:
3656                    if instrument["ticker"] == self._ticker:
3657                        result = instrument
3658                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3659                        break
3660
3661        elif self._figi:
3662            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3663            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3664
3665            for iType in TKS_INSTRUMENTS:
3666                for instrument in portfolio["stat"][iType]:
3667                    if instrument["figi"] == self._figi:
3668                        result = instrument
3669                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3670                        break
3671
3672        else:
3673            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3674
3675        uLogger.debug(msg)
3676
3677        return result
3678
3679    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3680        """
3681        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3682
3683        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3684
3685        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3686        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3687        """
3688        result = False
3689        msg = "Instrument not defined!"
3690
3691        if portfolio is None or not portfolio:
3692            portfolio = self.Overview(show=False)
3693
3694        if self._ticker:
3695            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3696            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3697
3698            for instrument in portfolio["stat"]["orders"]:
3699                if instrument["ticker"] == self._ticker:
3700                    result = True
3701                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3702                    break
3703
3704        elif self._figi:
3705            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3706            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3707
3708            for instrument in portfolio["stat"]["orders"]:
3709                if instrument["figi"] == self._figi:
3710                    result = True
3711                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3712                    break
3713
3714        else:
3715            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3716
3717        uLogger.debug(msg)
3718
3719        return result
3720
3721    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3722        """
3723        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3724        Instrument must be defined by `ticker` (highly priority) or `figi`.
3725
3726        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3727
3728        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3729        :return: list with `orderID`s of limit orders.
3730        """
3731        result = []
3732        msg = "Instrument not defined!"
3733
3734        if portfolio is None or not portfolio:
3735            portfolio = self.Overview(show=False)
3736
3737        if self._ticker:
3738            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3739            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3740
3741            for instrument in portfolio["stat"]["orders"]:
3742                if instrument["ticker"] == self._ticker:
3743                    result.append(instrument["orderID"])
3744
3745            if result:
3746                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3747
3748        elif self._figi:
3749            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3750            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3751
3752            for instrument in portfolio["stat"]["orders"]:
3753                if instrument["figi"] == self._figi:
3754                    result.append(instrument["orderID"])
3755
3756            if result:
3757                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3758
3759        else:
3760            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3761
3762        uLogger.debug(msg)
3763
3764        return result
3765
3766    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3767        """
3768        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3769
3770        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3771
3772        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3773        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3774        """
3775        result = False
3776        msg = "Instrument not defined!"
3777
3778        if portfolio is None or not portfolio:
3779            portfolio = self.Overview(show=False)
3780
3781        if self._ticker:
3782            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3783            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3784
3785            for instrument in portfolio["stat"]["stopOrders"]:
3786                if instrument["ticker"] == self._ticker:
3787                    result = True
3788                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3789                    break
3790
3791        elif self._figi:
3792            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3793            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3794
3795            for instrument in portfolio["stat"]["stopOrders"]:
3796                if instrument["figi"] == self._figi:
3797                    result = True
3798                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3799                    break
3800
3801        else:
3802            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3803
3804        uLogger.debug(msg)
3805
3806        return result
3807
3808    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3809        """
3810        Returns list with all `orderID`s of opened stop orders for the instrument.
3811        Instrument must be defined by `ticker` (highly priority) or `figi`.
3812
3813        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3814
3815        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3816        :return: list with `orderID`s of stop orders.
3817        """
3818        result = []
3819        msg = "Instrument not defined!"
3820
3821        if portfolio is None or not portfolio:
3822            portfolio = self.Overview(show=False)
3823
3824        if self._ticker:
3825            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3826            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3827
3828            for instrument in portfolio["stat"]["stopOrders"]:
3829                if instrument["ticker"] == self._ticker:
3830                    result.append(instrument["orderID"])
3831
3832            if result:
3833                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3834
3835        elif self._figi:
3836            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3837            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3838
3839            for instrument in portfolio["stat"]["stopOrders"]:
3840                if instrument["figi"] == self._figi:
3841                    result.append(instrument["orderID"])
3842
3843            if result:
3844                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3845
3846        else:
3847            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3848
3849        uLogger.debug(msg)
3850
3851        return result
3852
3853    def RequestLimits(self) -> dict:
3854        """
3855        Method for obtaining the available funds for withdrawal for current `accountId`.
3856
3857        See also:
3858        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3859        - `OverviewLimits()` method
3860
3861        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3862                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3863                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3864                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3865        """
3866        if self.accountId is None or not self.accountId:
3867            uLogger.error("Variable `accountId` must be defined for using this method!")
3868            raise Exception("Account ID required")
3869
3870        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3871
3872        self.body = str({"accountId": self.accountId})
3873        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3874        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3875
3876        if self.moreDebug:
3877            uLogger.debug("Records about available funds for withdrawal successfully received")
3878
3879        return rawLimits
3880
3881    def OverviewLimits(self, show: bool = False) -> dict:
3882        """
3883        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3884
3885        See also: `RequestLimits()`.
3886
3887        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3888        :return: dict with raw parsed data from server and some calculated statistics about it.
3889        """
3890        if self.accountId is None or not self.accountId:
3891            uLogger.error("Variable `accountId` must be defined for using this method!")
3892            raise Exception("Account ID required")
3893
3894        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3895
3896        view = {
3897            "rawLimits": rawLimits,
3898            "limits": {  # parsed data for every currency:
3899                "money": {  # this is an array of portfolio currency positions
3900                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3901                },
3902                "blocked": {  # this is an array of blocked currency
3903                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3904                },
3905                "blockedGuarantee": {  # this is locked money under collateral for futures
3906                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3907                },
3908            },
3909        }
3910
3911        # --- Prepare text table with limits in human-readable format:
3912        if show:
3913            info = [
3914                "# Withdrawal limits\n\n",
3915                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3916                "* **Account ID:** [{}]\n".format(self.accountId),
3917            ]
3918
3919            if view["limits"]["money"]:
3920                info.extend([
3921                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3922                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3923                ])
3924
3925            else:
3926                info.append("\nNo withdrawal limits\n")
3927
3928            for curr in view["limits"]["money"].keys():
3929                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3930                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3931                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3932
3933                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3934                    "[{}]".format(curr),
3935                    "{:.2f}".format(view["limits"]["money"][curr]),
3936                    "{:.2f}".format(availableMoney),
3937                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3938                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3939                )
3940
3941                if curr == "rub":
3942                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3943
3944                else:
3945                    info.append(infoStr)
3946
3947            infoText = "".join(info)
3948
3949            uLogger.info(infoText)
3950
3951            if self.withdrawalLimitsFile:
3952                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3953                    fH.write(infoText)
3954
3955                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3956
3957                if self.useHTMLReports:
3958                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3959                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3960                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3961
3962                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
3963
3964        return view
3965
3966    def RequestAccounts(self) -> dict:
3967        """
3968        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3969
3970        See also:
3971        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3972        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3973        - `OverviewUserInfo()` method
3974
3975        :return: dict with raw data from server that contains accounts info. Example of dict:
3976                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3977                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3978                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3979                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3980        """
3981        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3982
3983        self.body = str({})
3984        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3985        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3986
3987        if self.moreDebug:
3988            uLogger.debug("Records about available accounts successfully received")
3989
3990        return rawAccounts
3991
3992    def RequestUserInfo(self) -> dict:
3993        """
3994        Method for requesting common user's information.
3995
3996        See also:
3997        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3998        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3999        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4000        - `OverviewUserInfo()` method
4001
4002        :return: dict with raw data from server that contains user's information. Example of dict:
4003                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4004                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4005        """
4006        uLogger.debug("Requesting common user's information. Wait, please...")
4007
4008        self.body = str({})
4009        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4010        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4011
4012        if self.moreDebug:
4013            uLogger.debug("Records about current user successfully received")
4014
4015        return rawUserInfo
4016
4017    def RequestMarginStatus(self, accountId: str = None) -> dict:
4018        """
4019        Method for requesting margin calculation for defined account ID.
4020
4021        See also:
4022        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4023        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4024        - `OverviewUserInfo()` method
4025
4026        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4027        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4028                 Example of responses:
4029                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4030                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4031                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4032                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4033                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4034                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4035        """
4036        if accountId is None or not accountId:
4037            if self.accountId is None or not self.accountId:
4038                uLogger.error("Variable `accountId` must be defined for using this method!")
4039                raise Exception("Account ID required")
4040
4041            else:
4042                accountId = self.accountId  # use `self.accountId` (main ID) by default
4043
4044        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4045
4046        self.body = str({"accountId": accountId})
4047        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4048        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4049
4050        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4051            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4052            rawMargin = {}
4053
4054        else:
4055            if self.moreDebug:
4056                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4057
4058        return rawMargin
4059
4060    def RequestTariffLimits(self) -> dict:
4061        """
4062        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4063
4064        See also:
4065        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4066        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4067        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4068        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4069        - `OverviewUserInfo()` method
4070
4071        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4072                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4073                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4074        """
4075        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4076
4077        self.body = str({})
4078        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4079        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4080
4081        if self.moreDebug:
4082            uLogger.debug("Records with limits of current tariff successfully received")
4083
4084        return rawTariffLimits
4085
4086    def RequestBondCoupons(self, iJSON: dict) -> dict:
4087        """
4088        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4089        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4090        All dates are in UTC timezone.
4091
4092        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4093        Documentation:
4094        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4095        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4096
4097        See also: `ExtendBondsData()`.
4098
4099        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4100                      If raw iJSON is not data of bond then server returns an error [400] with message:
4101                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4102        :return: dictionary with bond payment calendar. Response example
4103                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4104                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4105                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4106                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4107        """
4108        if iJSON["figi"] is None or not iJSON["figi"]:
4109            uLogger.error("FIGI must be defined for using this method!")
4110            raise Exception("FIGI required")
4111
4112        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4113        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4114
4115        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4116            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4117            self._figi,
4118            startDate,
4119            endDate,
4120        ))
4121
4122        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4123        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4124        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4125
4126        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4127            uLogger.warning("Instrument type is not bond!")
4128
4129        else:
4130            if self.moreDebug:
4131                uLogger.debug("Records about bond payment calendar successfully received")
4132
4133        return calendar
4134
4135    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4136        """
4137        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4138        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4139        coupon yields, current yields and some statistics etc.
4140
4141        WARNING! This is too long operation if a lot of bonds requested from broker server.
4142
4143        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4144
4145        :param instruments: list of strings with tickers or FIGIs.
4146        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4147                     for further used by data scientists or stock analytics.
4148        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4149                 In XLSX-file and Pandas DataFrame fields mean:
4150                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4151                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4152        """
4153        if instruments is None or not instruments:
4154            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4155            raise Exception("Ticker or FIGI required")
4156
4157        if isinstance(instruments, str):
4158            instruments = [instruments]
4159
4160        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4161
4162        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4163
4164        iCount = len(uniqueInstruments)
4165        tooLong = iCount >= 20
4166        if tooLong:
4167            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4168
4169        bonds = None
4170        for i, self._figi in enumerate(uniqueInstruments):
4171            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4172
4173            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4174                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4175                rawBond = self.SearchByFIGI(requestPrice=True)
4176
4177                # Widen raw data with UTC current time (iData["actualDateTime"]):
4178                actualDate = datetime.now(tzutc())
4179                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4180
4181                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4182                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4183
4184                # Replace some values with human-readable:
4185                iData["nominalCurrency"] = iData["nominal"]["currency"]
4186                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4187                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4188                iData["aciCurrency"] = iData["aciValue"]["currency"]
4189                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4190                iData["issueSize"] = int(iData["issueSize"])
4191                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4192                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4193                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4194                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4195                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4196                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4197                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4198                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4199                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4200                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4201
4202                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4203                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4204                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4205                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4206                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4207                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4208                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4209                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4210                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4211                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4212                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4213
4214                # Widen raw data with calendar data from `rawCalendar` values:
4215                calendarData = []
4216                if "events" in iData["rawCalendar"].keys():
4217                    for item in iData["rawCalendar"]["events"]:
4218                        calendarData.append({
4219                            "couponDate": item["couponDate"],
4220                            "couponNumber": int(item["couponNumber"]),
4221                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4222                            "payCurrency": item["payOneBond"]["currency"],
4223                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4224                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4225                            "couponStartDate": item["couponStartDate"],
4226                            "couponEndDate": item["couponEndDate"],
4227                            "couponPeriod": item["couponPeriod"],
4228                        })
4229
4230                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4231                    if "maturityDate" not in iData.keys():
4232                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4233
4234                # Widen raw data with Coupon Rate.
4235                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4236                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4237                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4238                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4239
4240                # Widen raw data with Yield to Maturity (YTM) on current date.
4241                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4242                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4243                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4244                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4245                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4246                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4247
4248                iData["calendar"] = calendarData  # adds calendar at the end
4249
4250                # Remove not used data:
4251                iData.pop("uid")
4252                iData.pop("positionUid")
4253                iData.pop("currentPrice")
4254                iData.pop("rawCalendar")
4255
4256                colNames = list(iData.keys())
4257                if bonds is None:
4258                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4259
4260                else:
4261                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4262
4263            else:
4264                uLogger.warning("Instrument is not a bond!")
4265
4266            processed = round(100 * (i + 1) / iCount, 1)
4267            if tooLong and processed % 5 == 0:
4268                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4269
4270            else:
4271                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4272
4273        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4274
4275        # Saving bonds from Pandas DataFrame to XLSX sheet:
4276        if xlsx and self.bondsXLSXFile:
4277            with pd.ExcelWriter(
4278                    path=self.bondsXLSXFile,
4279                    date_format=TKS_DATE_FORMAT,
4280                    datetime_format=TKS_DATE_TIME_FORMAT,
4281                    mode="w",
4282            ) as writer:
4283                bonds.to_excel(
4284                    writer,
4285                    sheet_name="Extended bonds data",
4286                    index=True,
4287                    encoding="UTF-8",
4288                    freeze_panes=(1, 1),
4289                )  # saving as XLSX-file with freeze first row and column as headers
4290
4291            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4292
4293        return bonds
4294
4295    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4296        """
4297        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4298
4299        WARNING! This is too long operation if a lot of bonds requested from broker server.
4300
4301        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4302
4303        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4304                        extended information about bonds: main info, current prices, bond payment calendar,
4305                        coupon yields, current yields and some statistics etc.
4306                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4307        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4308                     for further used by data scientists or stock analytics.
4309        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4310        """
4311        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4312            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4313
4314        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4315
4316        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4317        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4318        calendar = None
4319        for bond in extBonds.iterrows():
4320            for item in bond[1]["calendar"]:
4321                cData = {
4322                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4323                    "couponDate": item["couponDate"],
4324                    "figi": bond[1]["figi"],
4325                    "ticker": bond[1]["ticker"],
4326                    "name": bond[1]["name"],
4327                    "couponNumber": item["couponNumber"],
4328                    "payOneBond": item["payOneBond"],
4329                    "payCurrency": item["payCurrency"],
4330                    "couponType": item["couponType"],
4331                    "couponPeriod": item["couponPeriod"],
4332                    "fixDate": item["fixDate"],
4333                    "couponStartDate": item["couponStartDate"],
4334                    "couponEndDate": item["couponEndDate"],
4335                }
4336
4337                if calendar is None:
4338                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4339
4340                else:
4341                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4342
4343        if calendar is not None:
4344            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4345
4346            # Saving calendar from Pandas DataFrame to XLSX sheet:
4347            if xlsx:
4348                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4349
4350                with pd.ExcelWriter(
4351                        path=xlsxCalendarFile,
4352                        date_format=TKS_DATE_FORMAT,
4353                        datetime_format=TKS_DATE_TIME_FORMAT,
4354                        mode="w",
4355                ) as writer:
4356                    humanReadable = calendar.copy(deep=True)
4357                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4358                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4359                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4360                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4361                    humanReadable.columns = colNames  # human-readable column names
4362
4363                    humanReadable.to_excel(
4364                        writer,
4365                        sheet_name="Bond payments calendar",
4366                        index=False,
4367                        encoding="UTF-8",
4368                        freeze_panes=(1, 2),
4369                    )  # saving as XLSX-file with freeze first row and column as headers
4370
4371                    del humanReadable  # release df in memory
4372
4373                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4374
4375        return calendar
4376
4377    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4378        """
4379        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4380        Also, creates Markdown file with calendar data, `calendar.md` by default.
4381
4382        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4383
4384        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4385                        extended information about bonds: main info, current prices, bond payment calendar,
4386                        coupon yields, current yields and some statistics etc.
4387                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4388        :param show: if `True` then also printing bonds payment calendar to the console,
4389                     otherwise save to file `calendarFile` only. `False` by default.
4390        :return: multilines text in Markdown format with bonds payment calendar as a table.
4391        """
4392        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4393            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4394
4395        infoText = "# Bond payments calendar\n\n"
4396
4397        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4398
4399        if not (calendar is None or calendar.empty):
4400            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4401
4402            info = [
4403                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4404                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4405                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4406            ]
4407
4408            newMonth = False
4409            notOneBond = calendar["figi"].nunique() > 1
4410            for i, bond in enumerate(calendar.iterrows()):
4411                if newMonth and notOneBond:
4412                    info.append(splitLine)
4413
4414                info.append(
4415                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4416                        "  √" if bond[1]["paid"] else "  —",
4417                        bond[1]["couponDate"].split("T")[0],
4418                        bond[1]["figi"],
4419                        bond[1]["ticker"],
4420                        bond[1]["couponNumber"],
4421                        "{} {}".format(
4422                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4423                            bond[1]["payCurrency"],
4424                        ),
4425                        bond[1]["couponType"],
4426                        bond[1]["couponPeriod"],
4427                        bond[1]["fixDate"].split("T")[0],
4428                    )
4429                )
4430
4431                if i < len(calendar.values) - 1:
4432                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4433                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4434                    newMonth = False if curDate.month == nextDate.month else True
4435
4436                else:
4437                    newMonth = False
4438
4439            infoText += "".join(info)
4440
4441            if show:
4442                uLogger.info("{}".format(infoText))
4443
4444            if self.calendarFile is not None:
4445                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4446                    fH.write(infoText)
4447
4448                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4449
4450                if self.useHTMLReports:
4451                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4452                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4453                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4454
4455                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4456
4457        else:
4458            infoText += "No data\n"
4459
4460        return infoText
4461
4462    def OverviewAccounts(self, show: bool = False) -> dict:
4463        """
4464        Method for parsing and show simple table with all available user accounts.
4465
4466        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4467
4468        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4469        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4470                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4471                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4472                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4473                                                        "closed": "—", "access": "Full access" }, ...}}`
4474        """
4475        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4476
4477        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4478        accounts = {
4479            item["id"]: {
4480                "type": TKS_ACCOUNT_TYPES[item["type"]],
4481                "name": item["name"],
4482                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4483                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4484                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4485                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4486            } for item in rawAccounts["accounts"]
4487        }
4488
4489        # Raw and parsed data with some fields replaced in "stat" section:
4490        view = {
4491            "rawAccounts": rawAccounts,
4492            "stat": accounts,
4493        }
4494
4495        # --- Prepare simple text table with only accounts data in human-readable format:
4496        if show:
4497            info = [
4498                "# User accounts\n\n",
4499                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4500                "| Account ID   | Type                      | Status                    | Name                           |\n",
4501                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4502            ]
4503
4504            for account in view["stat"].keys():
4505                info.extend([
4506                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4507                        account,
4508                        view["stat"][account]["type"],
4509                        view["stat"][account]["status"],
4510                        view["stat"][account]["name"],
4511                    )
4512                ])
4513
4514            infoText = "".join(info)
4515
4516            uLogger.info(infoText)
4517
4518            if self.userAccountsFile:
4519                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4520                    fH.write(infoText)
4521
4522                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4523
4524                if self.useHTMLReports:
4525                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4526                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4527                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4528
4529                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4530
4531        return view
4532
4533    def OverviewUserInfo(self, show: bool = False) -> dict:
4534        """
4535        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4536
4537        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4538
4539        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4540        :return: dict with raw parsed data from server and some calculated statistics about it.
4541        """
4542        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4543        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4544        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4545        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4546        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4547        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4548
4549        # This is dict with parsed common user data:
4550        userInfo = {
4551            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4552            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4553            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4554            "tariff": rawUserInfo["tariff"],
4555        }
4556
4557        # This is an array of dict with parsed margin statuses for every account IDs:
4558        margins = {}
4559        for accountId in accounts.keys():
4560            if rawMargins[accountId]:
4561                margins[accountId] = {
4562                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4563                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4564                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4565                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4566                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4567                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4568                }
4569
4570            else:
4571                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4572
4573        unary = {}  # unary-connection limits
4574        for item in rawTariffLimits["unaryLimits"]:
4575            if item["limitPerMinute"] in unary.keys():
4576                unary[item["limitPerMinute"]].extend(item["methods"])
4577
4578            else:
4579                unary[item["limitPerMinute"]] = item["methods"]
4580
4581        stream = {}  # stream-connection limits
4582        for item in rawTariffLimits["streamLimits"]:
4583            if item["limit"] in stream.keys():
4584                stream[item["limit"]].extend(item["streams"])
4585
4586            else:
4587                stream[item["limit"]] = item["streams"]
4588
4589        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4590        limits = {
4591            "unary": unary,
4592            "stream": stream,
4593        }
4594
4595        # Raw and parsed data as an output result:
4596        view = {
4597            "rawUserInfo": rawUserInfo,
4598            "rawAccounts": rawAccounts,
4599            "rawMargins": rawMargins,
4600            "rawTariffLimits": rawTariffLimits,
4601            "stat": {
4602                "userInfo": userInfo,
4603                "accounts": accounts,
4604                "margins": margins,
4605                "limits": limits,
4606            },
4607        }
4608
4609        # --- Prepare text table with user information in human-readable format:
4610        if show:
4611            info = [
4612                "# Full user information\n\n",
4613                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4614                "## Common information\n\n",
4615                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4616                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4617                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4618                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4619                "\n## User accounts\n\n",
4620            ]
4621
4622            for account in view["stat"]["accounts"].keys():
4623                info.extend([
4624                    "### ID: [{}]\n\n".format(account),
4625                    "| Parameters           | Values                                                       |\n",
4626                    "|----------------------|--------------------------------------------------------------|\n",
4627                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4628                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4629                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4630                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4631                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4632                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4633                ])
4634
4635                if margins[account]:
4636                    info.extend([
4637                        "| Margin status:       | Enabled                                                      |\n",
4638                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4639                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4640                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4641                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4642                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4643                    ])
4644
4645                else:
4646                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4647
4648            info.extend([
4649                "\n## Current user tariff limits\n",
4650                "\n### See also\n",
4651                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4652                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4653                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4654                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4655                "\n### Unary limits\n",
4656            ])
4657
4658            if unary:
4659                for key, values in sorted(unary.items()):
4660                    info.append("\n* Max requests per minute: {}\n".format(key))
4661
4662                    for value in values:
4663                        info.append("  - {}\n".format(value))
4664
4665            else:
4666                info.append("\nNot available\n")
4667
4668            info.append("\n### Stream limits\n")
4669
4670            if stream:
4671                for key, values in sorted(stream.items()):
4672                    info.append("\n* Max stream connections: {}\n".format(key))
4673
4674                    for value in values:
4675                        info.append("  - {}\n".format(value))
4676
4677            else:
4678                info.append("\nNot available\n")
4679
4680            infoText = "".join(info)
4681
4682            uLogger.info(infoText)
4683
4684            if self.userInfoFile:
4685                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4686                    fH.write(infoText)
4687
4688                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4689
4690                if self.useHTMLReports:
4691                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4692                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4693                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4694
4695                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4696
4697        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
 86    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 87        """
 88        Main class init.
 89
 90        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 91        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 92                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 93        :param useCache: use default cache file with raw data to use instead of `iList`.
 94                         True by default. Cache is auto-update if new day has come.
 95                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 96        :param defaultCache: path to default cache file. `dump.json` by default.
 97        """
 98        if token is None or not token:
 99            try:
100                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
101                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
102
103            except KeyError:
104                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
105                raise Exception("Token required")
106
107        else:
108            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
109            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
110
111        if accountId is None or not accountId:
112            try:
113                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
114                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
115
116            except KeyError:
117                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
118
119        else:
120            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
121            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
122
123        self.version = __version__  # duplicate here used TKSBrokerAPI main version
124        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
125
126        Latest version: https://pypi.org/project/tksbrokerapi/
127        """
128
129        self.aliases = TKS_TICKER_ALIASES
130        """Some aliases instead official tickers.
131
132        See also: `TKSEnums.TKS_TICKER_ALIASES`
133        """
134
135        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
136
137        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
138
139        self._ticker = ""
140        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
141
142        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
143        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
144
145        See also: `SearchByTicker()`, `SearchInstruments()`.
146        """
147
148        self._figi = ""
149        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
150
151        See also: `SearchByFIGI()`, `SearchInstruments()`.
152        """
153
154        self.depth = 1
155        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
156
157        See also: `GetCurrentPrices()`.
158        """
159
160        self.server = r"https://invest-public-api.tinkoff.ru/rest"
161        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
162
163        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
164        """
165
166        uLogger.debug("Broker API server: {}".format(self.server))
167
168        self.timeout = 15
169        """Server operations timeout in seconds. Default: `15`.
170
171        See also: `SendAPIRequest()`.
172        """
173
174        self.headers = {
175            "Content-Type": "application/json",
176            "accept": "application/json",
177            "Authorization": "Bearer {}".format(self.token),
178            "x-app-name": "Tim55667757.TKSBrokerAPI",
179        }
180        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
181
182        See also: `SendAPIRequest()`.
183        """
184
185        self.body = None
186        """Request body which send to broker server. Default: `None`.
187
188        See also: `SendAPIRequest()`.
189        """
190
191        self.moreDebug = False
192        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
193
194        self.useHTMLReports = False
195        """
196        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
197        
198        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
199        """
200
201        self.historyFile = None
202        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
203
204        See also: `History()`.
205        """
206
207        self.htmlHistoryFile = "index.html"
208        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
209
210        See also: `ShowHistoryChart()`.
211        """
212
213        self.instrumentsFile = "instruments.md"
214        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
215
216        See also: `ShowInstrumentsInfo()`.
217        """
218
219        self.searchResultsFile = "search-results.md"
220        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
221
222        See also: `SearchInstruments()`.
223        """
224
225        self.pricesFile = "prices.md"
226        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
227
228        See also: `GetListOfPrices()`.
229        """
230
231        self.infoFile = "info.md"
232        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
233
234        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
235        """
236
237        self.bondsXLSXFile = "ext-bonds.xlsx"
238        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
239        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
240
241        See also: `ExtendBondsData()`.
242        """
243
244        self.calendarFile = "calendar.md"
245        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
246        
247        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
248
249        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
250        """
251
252        self.overviewFile = "overview.md"
253        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
254
255        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
256        """
257
258        self.overviewDigestFile = "overview-digest.md"
259        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
260
261        See also: `Overview()` with parameter `details="digest"`.
262        """
263
264        self.overviewPositionsFile = "overview-positions.md"
265        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
266
267        See also: `Overview()` with parameter `details="positions"`.
268        """
269
270        self.overviewOrdersFile = "overview-orders.md"
271        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
272
273        See also: `Overview()` with parameter `details="orders"`.
274        """
275
276        self.overviewAnalyticsFile = "overview-analytics.md"
277        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
278
279        See also: `Overview()` with parameter `details="analytics"`.
280        """
281
282        self.overviewBondsCalendarFile = "overview-calendar.md"
283        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
284
285        See also: `Overview()` with parameter `details="calendar"`.
286        """
287
288        self.reportFile = "deals.md"
289        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
290
291        See also: `Deals()`.
292        """
293
294        self.withdrawalLimitsFile = "limits.md"
295        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
296
297        See also: `OverviewLimits()` and `RequestLimits()`.
298        """
299
300        self.userInfoFile = "user-info.md"
301        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
302
303        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
304        """
305
306        self.userAccountsFile = "accounts.md"
307        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
308
309        See also: `OverviewAccounts()`, `RequestAccounts()`.
310        """
311
312        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
313        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
314
315        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
316
317        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
318        """
319
320        self.iList = None  # init iList for raw instruments data
321        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
322        
323        See also: `Listing()`, `DumpInstruments()`.
324        """
325
326        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
327        if useCache:
328            if os.path.exists(self.iListDumpFile):
329                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
330                curTime = datetime.now(tzutc())
331
332                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
333                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
334
335                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
336
337                else:
338                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
339
340                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
341                        os.path.abspath(self.iListDumpFile),
342                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
343                    ))
344
345            else:
346                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
347                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
348
349        else:
350            self.iList = self.Listing()  # request new raw instruments data from broker server
351            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
352
353        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
354        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
355
356        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
357        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

useHTMLReports

If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.

See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

overviewBondsCalendarFile

Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.

See also: Overview() with parameter details="calendar".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

ticker: str

Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.

Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi: str

Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.

See also: SearchByFIGI(), SearchInstruments().

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
411    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
412        """
413        Send GET or POST request to broker server and receive JSON object.
414
415        self.header: must be defining with dictionary of headers.
416        self.body: if define then used as request body. None by default.
417        self.timeout: global request timeout, 15 seconds by default.
418        :param url: url with REST request.
419        :param reqType: send "GET" or "POST" request. "GET" by default.
420        :param retry: how many times retry after first request if an 5xx server errors occurred.
421        :param pause: sleep time in seconds between retries.
422        :return: response JSON (dictionary) from broker.
423        """
424        if reqType.upper() not in ("GET", "POST"):
425            uLogger.error("You can define request type: `GET` or `POST`!")
426            raise Exception("Incorrect value")
427
428        if self.moreDebug:
429            uLogger.debug("Request parameters:")
430            uLogger.debug("    - REST API URL: {}".format(url))
431            uLogger.debug("    - request type: {}".format(reqType))
432            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
433            uLogger.debug("    - body:\n{}".format(self.body))
434
435        # fast hack to avoid all operations with some tickers/FIGI
436        responseJSON = {}
437        oK = True
438        for item in self.exclude:
439            if item in url:
440                if self.moreDebug:
441                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
442
443                oK = False
444                break
445
446        if oK:
447            counter = 0
448            response = None
449            errMsg = ""
450
451            while not response and counter <= retry:
452                if reqType == "GET":
453                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
454
455                if reqType == "POST":
456                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
457
458                if self.moreDebug:
459                    uLogger.debug("Response:")
460                    uLogger.debug("    - status code: {}".format(response.status_code))
461                    uLogger.debug("    - reason: {}".format(response.reason))
462                    uLogger.debug("    - body length: {}".format(len(response.text)))
463                    uLogger.debug("    - headers:\n{}".format(response.headers))
464
465                # Server returns some headers:
466                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
467                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
468                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
469                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
470                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
471                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
472                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
473                    sleep(rateLimitWait)
474
475                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
476                if 400 <= response.status_code < 500:
477                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
478                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
479
480                    if "code" in response.text and "message" in response.text:
481                        msgDict = self._ParseJSON(rawData=response.text)
482                        uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
483
484                    counter = retry + 1  # do not retry for 4xx errors
485
486                if 500 <= response.status_code < 600:
487                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
488                    uLogger.debug("    - not oK, {}".format(errMsg))
489
490                    if "code" in response.text and "message" in response.text:
491                        errMsgDict = self._ParseJSON(rawData=response.text)
492                        uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
493
494                    counter += 1
495
496                    if counter <= retry:
497                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
498                        sleep(pause)
499
500            responseJSON = self._ParseJSON(rawData=response.text)
501
502            if errMsg:
503                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
504                uLogger.error("    - not oK, {}".format(errMsg))
505
506        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
539    def Listing(self) -> dict:
540        """
541        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
542
543        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
544        """
545        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
546        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
547
548        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
549        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
550        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
551
552        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
553        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
554        poolUpdater.close()
555
556        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
557        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
558        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
559
560        # calculate minimum price increment (step) for all instruments and set up instrument's type:
561        for iType in iList.keys():
562            for ticker in iList[iType]:
563                iList[iType][ticker]["type"] = iType
564
565                if "minPriceIncrement" in iList[iType][ticker].keys():
566                    iList[iType][ticker]["step"] = NanoToFloat(
567                        iList[iType][ticker]["minPriceIncrement"]["units"],
568                        iList[iType][ticker]["minPriceIncrement"]["nano"],
569                    )
570
571                else:
572                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
573
574        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
576    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
577        """
578        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
579
580        See also: `DumpInstruments()`, `Listing()`.
581
582        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
583                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
584        """
585        if self.iListDumpFile is None or not self.iListDumpFile:
586            uLogger.error("Output name of dump file must be defined!")
587            raise Exception("Filename required")
588
589        if not self.iList or forceUpdate:
590            self.iList = self.Listing()
591
592        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
593
594        # Save as XLSX with separated sheets for every type of instruments:
595        with pd.ExcelWriter(
596                path=xlsxDumpFile,
597                date_format=TKS_DATE_FORMAT,
598                datetime_format=TKS_DATE_TIME_FORMAT,
599                mode="w",
600        ) as writer:
601            for iType in TKS_INSTRUMENTS:
602                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
603                df = df[sorted(df)]  # sorted by column names
604                df = df.applymap(
605                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
606                    na_action="ignore",
607                )  # converting numbers from nano-type to float in every cell
608                df.to_excel(
609                    writer,
610                    sheet_name=iType,
611                    encoding="UTF-8",
612                    freeze_panes=(1, 1),
613                )  # saving as XLSX-file with freeze first row and column as headers
614
615        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
617    def DumpInstruments(self, forceUpdate: bool = True) -> str:
618        """
619        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
620        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
621
622        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
623
624        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
625                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
626        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
627        """
628        if self.iListDumpFile is None or not self.iListDumpFile:
629            uLogger.error("Output name of dump file must be defined!")
630            raise Exception("Filename required")
631
632        if not self.iList or forceUpdate:
633            self.iList = self.Listing()
634
635        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
636        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
637            fH.write(jsonDump)
638
639        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
640
641        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
643    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
644        """
645        Show information about one instrument defined by json data and prints it in Markdown format.
646
647        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
648
649        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
650        :param show: if `True` then also printing information about instrument and its current price.
651        :return: multilines text in Markdown format with information about one instrument.
652        """
653        splitLine = "|                                                             |                                                        |\n"
654        infoText = ""
655
656        if iJSON is not None and iJSON and isinstance(iJSON, dict):
657            info = [
658                "# Main information\n\n",
659                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
660                "| Parameters                                                  | Values                                                 |\n",
661                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
662                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
663                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
664            ]
665
666            if "sector" in iJSON.keys() and iJSON["sector"]:
667                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
668
669            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
670                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
671
672            info.extend([
673                splitLine,
674                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
675                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
676            ])
677
678            if "isin" in iJSON.keys() and iJSON["isin"]:
679                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
680
681            if "classCode" in iJSON.keys():
682                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
683
684            info.extend([
685                splitLine,
686                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
687                splitLine,
688                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
689                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
690                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
691            ])
692
693            if iJSON["figi"]:
694                self._figi = iJSON["figi"]
695                iJSON = iJSON | self.RequestTradingStatus()
696
697                info.extend([
698                    splitLine,
699                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
700                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
701                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
702                ])
703
704            info.append(splitLine)
705
706            if "type" in iJSON.keys() and iJSON["type"]:
707                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
708
709                if "shareType" in iJSON.keys() and iJSON["shareType"]:
710                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
711
712            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
713                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
714
715            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
716                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
717
718            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
719                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
720
721            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
722                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
723
724            if "focusType" in iJSON.keys() and iJSON["focusType"]:
725                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
726
727            if "assetType" in iJSON.keys() and iJSON["assetType"]:
728                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
729
730            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
731                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
732
733            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
734                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
735
736            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
737                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
738
739            if "currency" in iJSON.keys():
740                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
741
742            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
743                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
744
745            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
746                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
747
748            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
749                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
750
751            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
752                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
753
754            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
755                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
756
757            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
758                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
759
760            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
761                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
762
763            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
764                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
765
766            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
767                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
768
769            iExt = None
770            if iJSON["type"] == "Bonds":
771                info.extend([
772                    splitLine,
773                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
774                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
775                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
776                        iJSON["nominal"]["currency"],
777                    )),
778                ])
779
780                if "floatingCouponFlag" in iJSON.keys():
781                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
782
783                if "amortizationFlag" in iJSON.keys():
784                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
785
786                info.append(splitLine)
787
788                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
789                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
790
791                if iJSON["figi"]:
792                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
793
794                    info.extend([
795                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
796                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
797                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
798                    ])
799
800                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
801                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
802                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
803                        iJSON["aciValue"]["currency"]
804                    )))
805
806            if "currentPrice" in iJSON.keys():
807                info.append(splitLine)
808
809                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
810                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
811
812                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
813                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
814                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
815                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
816                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
817
818                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
819                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
820
821                info.extend([
822                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
823                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
824                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
825                    )),
826                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
827                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
828                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
829                    )),
830                    "| Changes between last deal price and last close              | {:<54} |\n".format(
831                        "{:.2f}%{}".format(
832                            iJSON["currentPrice"]["changes"],
833                            " ({}{:.2f} {})".format(
834                                "+" if bondChangesDelta > 0 else "",
835                                bondChangesDelta,
836                                aciCurrency
837                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
838                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
839                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
840                                currency
841                            ),
842                        )
843                    ),
844                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
845                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
846                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
847                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
848                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
849                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
850                    )),
851                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
852                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
853                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
854                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
855                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
856                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
857                    )),
858                ])
859
860            if "lot" in iJSON.keys():
861                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
862
863            if "step" in iJSON.keys() and iJSON["step"] != 0:
864                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
865
866            # Add bond payment calendar:
867            if iJSON["type"] == "Bonds":
868                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
869                info.extend(["\n#", strCalendar])
870
871            infoText += "".join(info)
872
873            if show:
874                uLogger.info("{}".format(infoText))
875
876            else:
877                uLogger.debug("{}".format(infoText))
878
879            if self.infoFile is not None:
880                with open(self.infoFile, "w", encoding="UTF-8") as fH:
881                    fH.write(infoText)
882
883                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
884
885                if self.useHTMLReports:
886                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
887                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
888                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
889
890                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
891
892        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self._ticker]
  • show: if True then also printing information about instrument and its current price.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
894    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
895        """
896        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
897
898        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
899        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
900        :return: JSON formatted data with information about instrument.
901        """
902        tickerJSON = {}
903        if self.moreDebug:
904            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
905
906        if not self._ticker:
907            uLogger.warning("self._ticker variable is not be empty!")
908
909        else:
910            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
911                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
912                raise Exception("Instrument not allowed")
913
914            if not self.iList:
915                self.iList = self.Listing()
916
917            if self._ticker in self.iList["Shares"].keys():
918                tickerJSON = self.iList["Shares"][self._ticker]
919                if self.moreDebug:
920                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
921
922            elif self._ticker in self.iList["Currencies"].keys():
923                tickerJSON = self.iList["Currencies"][self._ticker]
924                if self.moreDebug:
925                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
926
927            elif self._ticker in self.iList["Bonds"].keys():
928                tickerJSON = self.iList["Bonds"][self._ticker]
929                if self.moreDebug:
930                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
931
932            elif self._ticker in self.iList["Etfs"].keys():
933                tickerJSON = self.iList["Etfs"][self._ticker]
934                if self.moreDebug:
935                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
936
937            elif self._ticker in self.iList["Futures"].keys():
938                tickerJSON = self.iList["Futures"][self._ticker]
939                if self.moreDebug:
940                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
941
942        if tickerJSON:
943            self._figi = tickerJSON["figi"]
944
945            if requestPrice:
946                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
947
948                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
949                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
950
951                else:
952                    tickerJSON["currentPrice"]["changes"] = 0
953
954            if show:
955                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
956
957        else:
958            if show:
959                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
960
961        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 963    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 964        """
 965        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 966
 967        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 968        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 969        :return: JSON formatted data with information about instrument.
 970        """
 971        figiJSON = {}
 972        if self.moreDebug:
 973            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 974
 975        if not self._figi:
 976            uLogger.warning("self._figi variable is not be empty!")
 977
 978        else:
 979            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 980                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 981                raise Exception("Instrument not allowed")
 982
 983            if not self.iList:
 984                self.iList = self.Listing()
 985
 986            for item in self.iList["Shares"].keys():
 987                if self._figi == self.iList["Shares"][item]["figi"]:
 988                    figiJSON = self.iList["Shares"][item]
 989
 990                    if self.moreDebug:
 991                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
 992
 993                    break
 994
 995            if not figiJSON:
 996                for item in self.iList["Currencies"].keys():
 997                    if self._figi == self.iList["Currencies"][item]["figi"]:
 998                        figiJSON = self.iList["Currencies"][item]
 999
1000                        if self.moreDebug:
1001                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1002
1003                        break
1004
1005            if not figiJSON:
1006                for item in self.iList["Bonds"].keys():
1007                    if self._figi == self.iList["Bonds"][item]["figi"]:
1008                        figiJSON = self.iList["Bonds"][item]
1009
1010                        if self.moreDebug:
1011                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1012
1013                        break
1014
1015            if not figiJSON:
1016                for item in self.iList["Etfs"].keys():
1017                    if self._figi == self.iList["Etfs"][item]["figi"]:
1018                        figiJSON = self.iList["Etfs"][item]
1019
1020                        if self.moreDebug:
1021                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1022
1023                        break
1024
1025            if not figiJSON:
1026                for item in self.iList["Futures"].keys():
1027                    if self._figi == self.iList["Futures"][item]["figi"]:
1028                        figiJSON = self.iList["Futures"][item]
1029
1030                        if self.moreDebug:
1031                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1032
1033                        break
1034
1035        if figiJSON:
1036            self._figi = figiJSON["figi"]
1037            self._ticker = figiJSON["ticker"]
1038
1039            if requestPrice:
1040                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1041
1042                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1043                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1044
1045                else:
1046                    figiJSON["currentPrice"]["changes"] = 0
1047
1048            if show:
1049                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1050
1051        else:
1052            if show:
1053                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1054
1055        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1057    def GetCurrentPrices(self, show: bool = True) -> dict:
1058        """
1059        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1060        `{"buy": [{"price": 1243.8, "quantity": 193},
1061                  {"price": 1244.0, "quantity": 168},
1062                  {"price": 1244.8, "quantity": 5},
1063                  {"price": 1245.0, "quantity": 61},
1064                  {"price": 1245.4, "quantity": 60}],
1065          "sell": [{"price": 1243.6, "quantity": 8},
1066                   {"price": 1242.6, "quantity": 10},
1067                   {"price": 1242.4, "quantity": 18},
1068                   {"price": 1242.2, "quantity": 50},
1069                   {"price": 1242.0, "quantity": 113}],
1070          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1071        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1072        - sell: list of dicts with Buyers prices,
1073            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1074            - quantity: volume value by current price in lots,
1075        - limitUp: current trade session limit price, maximum,
1076        - limitDown: current trade session limit price, minimum,
1077        - lastPrice: last deal price of the instrument,
1078        - closePrice: previous trade session close price of the instrument.
1079
1080        See also: `SearchByTicker()` and `SearchByFIGI()`.
1081        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1082        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1083
1084        :param show: if `True` then print DOM to log and console.
1085        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1086                 If an error occurred then returns an empty record:
1087                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1088        """
1089        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1090
1091        if self.depth < 1:
1092            uLogger.error("Depth of Market (DOM) must be >=1!")
1093            raise Exception("Incorrect value")
1094
1095        if not (self._ticker or self._figi):
1096            uLogger.error("self._ticker or self._figi variables must be defined!")
1097            raise Exception("Ticker or FIGI required")
1098
1099        if self._ticker and not self._figi:
1100            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1101            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1102
1103        if not self._ticker and self._figi:
1104            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1105            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1106
1107        if not self._figi:
1108            uLogger.error("FIGI is not defined!")
1109            raise Exception("Ticker or FIGI required")
1110
1111        else:
1112            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1113
1114            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1115            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1116            self.body = str({"figi": self._figi, "depth": self.depth})
1117            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1118
1119            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1120                # list of dicts with sellers orders:
1121                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1122
1123                # list of dicts with buyers orders:
1124                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1125
1126                # max price of instrument at this time:
1127                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1128
1129                # min price of instrument at this time:
1130                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1131
1132                # last price of deal with instrument:
1133                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1134
1135                # last close price of instrument:
1136                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1137
1138            else:
1139                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1140                uLogger.debug("Server response: {}".format(pricesResponse))
1141
1142            if show:
1143                if prices["buy"] or prices["sell"]:
1144                    info = [
1145                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1146                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1147                            self._ticker,
1148                            self._figi,
1149                            self.depth,
1150                        ),
1151                        "-" * 60, "\n",
1152                        "             Orders of Buyers | Orders of Sellers\n",
1153                        "-" * 60, "\n",
1154                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1155                        "-" * 60, "\n",
1156                    ]
1157
1158                    if not prices["buy"]:
1159                        info.append("                              | No orders!\n")
1160                        sumBuy = 0
1161
1162                    else:
1163                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1164                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1165                        for item in maxMinSorted:
1166                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1167
1168                    if not prices["sell"]:
1169                        info.append("No orders!                    |\n")
1170                        sumSell = 0
1171
1172                    else:
1173                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1174                        for item in prices["sell"]:
1175                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1176
1177                    info.extend([
1178                        "-" * 60, "\n",
1179                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1180                        "-" * 60, "\n",
1181                    ])
1182
1183                    infoText = "".join(info)
1184
1185                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1186
1187                else:
1188                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1189
1190        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True) -> str:
1192    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1193        """
1194        This method get and show information about all available broker instruments for current user account.
1195        If `instrumentsFile` string is not empty then also save information to this file.
1196
1197        :param show: if `True` then print results to console, if `False` — print only to file.
1198        :return: multi-lines string with all available broker instruments
1199        """
1200        if not self.iList:
1201            self.iList = self.Listing()
1202
1203        info = [
1204            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1205            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1206        ]
1207
1208        # add instruments count by type:
1209        for iType in self.iList.keys():
1210            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1211
1212        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1213        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1214
1215        # generating info tables with all instruments by type:
1216        for iType in self.iList.keys():
1217            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1218
1219            for instrument in self.iList[iType].keys():
1220                iName = self.iList[iType][instrument]["name"]  # instrument's name
1221                if len(iName) > 57:
1222                    iName = "{}...".format(iName[:54])  # right trim for a long string
1223
1224                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1225                    self.iList[iType][instrument]["ticker"],
1226                    iName,
1227                    self.iList[iType][instrument]["figi"],
1228                    self.iList[iType][instrument]["currency"],
1229                    self.iList[iType][instrument]["lot"],
1230                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1231                ))
1232
1233        infoText = "".join(info)
1234
1235        if show:
1236            uLogger.info(infoText)
1237
1238        if self.instrumentsFile:
1239            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1240                fH.write(infoText)
1241
1242            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1243
1244            if self.useHTMLReports:
1245                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1246                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1247                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1248
1249                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1250
1251        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False — print only to file.
Returns

multi-lines string with all available broker instruments

def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1253    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1254        """
1255        This method search and show information about instruments by part of its ticker, FIGI or name.
1256        If `searchResultsFile` string is not empty then also save information to this file.
1257
1258        :param pattern: string with part of ticker, FIGI or instrument's name.
1259        :param show: if `True` then print results to console, if `False` — return list of result only.
1260        :return: list of dictionaries with all found instruments.
1261        """
1262        if not self.iList:
1263            self.iList = self.Listing()
1264
1265        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1266        compiledPattern = re.compile(pattern, re.IGNORECASE)
1267
1268        for iType in self.iList:
1269            for instrument in self.iList[iType].values():
1270                searchResult = compiledPattern.search(" ".join(
1271                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1272                ))
1273
1274                if searchResult:
1275                    searchResults[iType][instrument["ticker"]] = instrument
1276
1277        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1278        info = [
1279            "# Search results\n\n",
1280            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1281            "* **Search pattern:** [{}]\n".format(pattern),
1282            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1283            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1284        ]
1285        infoShort = info[:]
1286
1287        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1288        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1289        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1290
1291        if resultsLen == 0:
1292            info.append("\nNo results\n")
1293            infoShort.append("\nNo results\n")
1294            uLogger.warning("No results. Try changing your search pattern.")
1295
1296        else:
1297            for iType in searchResults:
1298                iTypeValuesCount = len(searchResults[iType].values())
1299                if iTypeValuesCount > 0:
1300                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1301                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1302
1303                    for instrument in searchResults[iType].values():
1304                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1305                            instrument["type"],
1306                            instrument["ticker"],
1307                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1308                            instrument["figi"],
1309                        ))
1310
1311                    if iTypeValuesCount <= 5:
1312                        infoShort.extend(info[-iTypeValuesCount:])
1313
1314                    else:
1315                        infoShort.extend(info[-5:])
1316                        infoShort.append(skippedLine)
1317
1318        infoText = "".join(info)
1319        infoTextShort = "".join(infoShort)
1320
1321        if show:
1322            uLogger.info(infoTextShort)
1323            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1324
1325        if self.searchResultsFile:
1326            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1327                fH.write(infoText)
1328
1329            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1330
1331            if self.useHTMLReports:
1332                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1333                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1334                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1335
1336                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1337
1338        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False — return list of result only.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1340    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1341        """
1342        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1343
1344        :param instruments: list of strings with tickers or FIGIs.
1345        :return: list with unique instrument FIGIs only.
1346        """
1347        requestedInstruments = []
1348        for iName in instruments:
1349            if iName not in self.aliases.keys():
1350                if iName not in requestedInstruments:
1351                    requestedInstruments.append(iName)
1352
1353            else:
1354                if iName not in requestedInstruments:
1355                    if self.aliases[iName] not in requestedInstruments:
1356                        requestedInstruments.append(self.aliases[iName])
1357
1358        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1359
1360        onlyUniqueFIGIs = []
1361        for iName in requestedInstruments:
1362            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1363                continue
1364
1365            self._ticker = iName
1366            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1367
1368            if not iData:
1369                self._ticker = ""
1370                self._figi = iName
1371
1372                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1373
1374                if not iData:
1375                    self._figi = ""
1376                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1377
1378            if iData and iData["figi"] not in onlyUniqueFIGIs:
1379                onlyUniqueFIGIs.append(iData["figi"])
1380
1381        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1382
1383        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1385    def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1386        """
1387        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1388
1389        See limits: https://tinkoff.github.io/investAPI/limits/
1390
1391        If `pricesFile` string is not empty then also save information to this file.
1392
1393        :param instruments: list of strings with tickers or FIGIs.
1394        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1395        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1396                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1397        """
1398        if instruments is None or not instruments:
1399            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1400            raise Exception("Ticker or FIGI required")
1401
1402        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1403
1404        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1405
1406        iList = []  # trying to get info and current prices about all unique instruments:
1407        for self._figi in onlyUniqueFIGIs:
1408            iData = self.SearchByFIGI(requestPrice=True)
1409            iList.append(iData)
1410
1411        self.ShowListOfPrices(iList, show)
1412
1413        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!

See limits: https://tinkoff.github.io/investAPI/limits/

If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1415    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1416        """
1417        Show table contains current prices of given instruments.
1418
1419        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1420                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1421        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1422        :return: multilines text in Markdown format as a table contains current prices.
1423        """
1424        infoText = ""
1425
1426        if show or self.pricesFile:
1427            info = [
1428                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1429                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1430                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1431            ]
1432
1433            for item in iList:
1434                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1435                    item["ticker"],
1436                    item["figi"],
1437                    item["type"],
1438                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1439                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1440                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1441                    "{} / {}".format(
1442                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1443                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1444                    ),
1445                    "{} / {}".format(
1446                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1447                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1448                    ),
1449                    item["currency"],
1450                ))
1451
1452            infoText = "".join(info)
1453
1454            if show:
1455                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1456
1457            if self.pricesFile:
1458                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1459                    fH.write(infoText)
1460
1461                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1462
1463                if self.useHTMLReports:
1464                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1465                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1466                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1467
1468                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1469
1470        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1472    def RequestTradingStatus(self) -> dict:
1473        """
1474        Requesting trading status for the instrument defined by `figi` variable.
1475
1476        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1477
1478        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1479
1480        :return: dictionary with trading status attributes. Response example:
1481                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1482                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1483        """
1484        if self._figi is None or not self._figi:
1485            uLogger.error("Variable `figi` must be defined for using this method!")
1486            raise Exception("FIGI required")
1487
1488        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1489
1490        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1491        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1492        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1493
1494        if self.moreDebug:
1495            uLogger.debug("Records about current trading status successfully received")
1496
1497        return tradingStatus

Requesting trading status for the instrument defined by figi variable.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus

Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1499    def RequestPortfolio(self) -> dict:
1500        """
1501        Requesting actual user's portfolio for current `accountId`.
1502
1503        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1504
1505        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1506
1507        :return: dictionary with user's portfolio.
1508        """
1509        if self.accountId is None or not self.accountId:
1510            uLogger.error("Variable `accountId` must be defined for using this method!")
1511            raise Exception("Account ID required")
1512
1513        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1514
1515        self.body = str({"accountId": self.accountId})
1516        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1517        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1518
1519        if self.moreDebug:
1520            uLogger.debug("Records about user's portfolio successfully received")
1521
1522        return rawPortfolio

Requesting actual user's portfolio for current accountId.

REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio

Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1524    def RequestPositions(self) -> dict:
1525        """
1526        Requesting open positions by currencies and instruments for current `accountId`.
1527
1528        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1529
1530        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1531
1532        :return: dictionary with open positions by instruments.
1533        """
1534        if self.accountId is None or not self.accountId:
1535            uLogger.error("Variable `accountId` must be defined for using this method!")
1536            raise Exception("Account ID required")
1537
1538        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1539
1540        self.body = str({"accountId": self.accountId})
1541        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1542        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1543
1544        if self.moreDebug:
1545            uLogger.debug("Records about current open positions successfully received")
1546
1547        return rawPositions

Requesting open positions by currencies and instruments for current accountId.

REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions

Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1549    def RequestPendingOrders(self) -> list:
1550        """
1551        Requesting current actual pending limit orders for current `accountId`.
1552
1553        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1554
1555        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1556
1557        :return: list of dictionaries with pending limit orders.
1558        """
1559        if self.accountId is None or not self.accountId:
1560            uLogger.error("Variable `accountId` must be defined for using this method!")
1561            raise Exception("Account ID required")
1562
1563        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1564
1565        self.body = str({"accountId": self.accountId})
1566        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1567        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1568
1569        uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1570
1571        return rawOrders

Requesting current actual pending limit orders for current accountId.

REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders

Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending limit orders.

def RequestStopOrders(self) -> list:
1573    def RequestStopOrders(self) -> list:
1574        """
1575        Requesting current actual stop orders for current `accountId`.
1576
1577        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1578
1579        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1580
1581        :return: list of dictionaries with stop orders.
1582        """
1583        if self.accountId is None or not self.accountId:
1584            uLogger.error("Variable `accountId` must be defined for using this method!")
1585            raise Exception("Account ID required")
1586
1587        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1588
1589        self.body = str({"accountId": self.accountId})
1590        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1591        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1592
1593        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1594
1595        return rawStopOrders

Requesting current actual stop orders for current accountId.

REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders

Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full') -> dict:
1597    def Overview(self, show: bool = False, details: str = "full") -> dict:
1598        """
1599        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1600        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1601        and `overviewBondsCalendarFile` are defined then also save information to file.
1602
1603        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1604        many requests about the state of the portfolio, and then, based on the received data, a large number
1605        of calculation and statistics are collected.
1606
1607        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1608        :param details: how detailed should the information be?
1609        - `full` — shows full available information about portfolio status (by default),
1610        - `positions` — shows only open positions,
1611        - `orders` — shows only sections of open limits and stop orders.
1612        - `digest` — show a short digest of the portfolio status,
1613        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1614        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1615        :return: dictionary with client's raw portfolio and some statistics.
1616        """
1617        if self.accountId is None or not self.accountId:
1618            uLogger.error("Variable `accountId` must be defined for using this method!")
1619            raise Exception("Account ID required")
1620
1621        view = {
1622            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1623                "headers": {},  # list of dictionaries, response headers without "positions" section
1624                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1625                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1626                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1627                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1628                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1629                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1630                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1631                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1632                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1633            },
1634            "stat": {  # --- some statistics calculated using "raw" sections:
1635                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1636                "availableRUB": 0.,  # available rubles (without other currencies)
1637                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1638                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1639                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1640                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1641                "sharesCostRUB": 0.,  # costs of all shares in RUB
1642                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1643                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1644                "futuresCostRUB": 0.,  # costs of all futures in RUB
1645                "Currencies": [],  # list of dictionaries of all currencies statistics
1646                "Shares": [],  # list of dictionaries of all shares statistics
1647                "Bonds": [],  # list of dictionaries of all bonds statistics
1648                "Etfs": [],  # list of dictionaries of all etfs statistics
1649                "Futures": [],  # list of dictionaries of all futures statistics
1650                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1651                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1652                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1653                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1654                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1655            },
1656            "analytics": {  # --- some analytics of portfolio:
1657                "distrByAssets": {},  # portfolio distribution by assets
1658                "distrByCompanies": {},  # portfolio distribution by companies
1659                "distrBySectors": {},  # portfolio distribution by sectors
1660                "distrByCurrencies": {},  # portfolio distribution by currencies
1661                "distrByCountries": {},  # portfolio distribution by countries
1662                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1663            }
1664        }
1665
1666        details = details.lower()
1667        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1668        if details not in availableDetails:
1669            details = "full"
1670            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1671
1672        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1673
1674        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1675        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1676        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1677        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1678
1679        # save response headers without "positions" section:
1680        for key in portfolioResponse.keys():
1681            if key != "positions":
1682                view["raw"]["headers"][key] = portfolioResponse[key]
1683
1684            else:
1685                continue
1686
1687        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1688        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1689        for item in portfolioResponse["positions"]:
1690            if item["instrumentType"] == "currency":
1691                self._figi = item["figi"]
1692                curr = self.SearchByFIGI(requestPrice=False)
1693
1694                # current price of currency in RUB:
1695                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1696                    "name": curr["name"],
1697                    "currentPrice": NanoToFloat(
1698                        item["currentPrice"]["units"],
1699                        item["currentPrice"]["nano"]
1700                    ),
1701                }
1702
1703                view["raw"]["Currencies"].append(item)
1704
1705            elif item["instrumentType"] == "share":
1706                view["raw"]["Shares"].append(item)
1707
1708            elif item["instrumentType"] == "bond":
1709                view["raw"]["Bonds"].append(item)
1710
1711            elif item["instrumentType"] == "etf":
1712                view["raw"]["Etfs"].append(item)
1713
1714            elif item["instrumentType"] == "futures":
1715                view["raw"]["Futures"].append(item)
1716
1717            else:
1718                continue
1719
1720        # how many volume of currencies (by ISO currency name) are blocked:
1721        for item in view["raw"]["positions"]["blocked"]:
1722            blocked = NanoToFloat(item["units"], item["nano"])
1723            if blocked > 0:
1724                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1725
1726        # how many volume of instruments (by FIGI) are blocked:
1727        for item in view["raw"]["positions"]["securities"]:
1728            blocked = int(item["blocked"])
1729            if blocked > 0:
1730                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1731
1732        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1733
1734        if "rub" in allBlocked.keys():
1735            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1736
1737        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1738        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1739        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1740        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1741        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1742        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1743        view["stat"]["portfolioCostRUB"] = sum([
1744            view["stat"]["allCurrenciesCostRUB"],
1745            view["stat"]["sharesCostRUB"],
1746            view["stat"]["bondsCostRUB"],
1747            view["stat"]["etfsCostRUB"],
1748            view["stat"]["futuresCostRUB"],
1749        ])
1750
1751        # --- calculating some portfolio statistics:
1752        byComp = {}  # distribution by companies
1753        bySect = {}  # distribution by sectors
1754        byCurr = {}  # distribution by currencies (include RUB)
1755        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1756        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1757
1758        for item in portfolioResponse["positions"]:
1759            self._figi = item["figi"]
1760            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1761
1762            if instrument:
1763                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1764                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1765
1766                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1767                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1768
1769                else:
1770                    blocked = 0
1771
1772                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1773                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1774                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1775                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1776                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1777                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1778                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1779                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1780                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1781                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1782                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1783                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1784
1785                statData = {
1786                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1787                    "ticker": instrument["ticker"],  # ticker by FIGI
1788                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1789                    "volume": volume,  # available volume of instrument
1790                    "lots": lots,  # volume in lots of instrument
1791                    "direction": direction,  # direction of an instrument's position: short or long
1792                    "blocked": blocked,  # blocked volume of currency or instrument
1793                    "currentPrice": curPrice,  # current instrument's price in basic asset
1794                    "average": average,  # current average position price
1795                    "cost": cost,  # current cost of all volume of instrument in basic asset
1796                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1797                    "costRUB": costRUB,  # cost of instrument in ruble
1798                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1799                    "profit": profit,  # expected profit at current moment
1800                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1801                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1802                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1803                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1804                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1805                    "step": instrument["step"],  # minimum price increment
1806                }
1807
1808                # adding distribution by unique countries:
1809                if statData["country"] not in byCountry.keys():
1810                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1811
1812                else:
1813                    byCountry[statData["country"]]["cost"] += costRUB
1814                    byCountry[statData["country"]]["percent"] += percentCostRUB
1815
1816                if item["instrumentType"] != "currency":
1817                    # adding distribution by unique companies:
1818                    if statData["name"]:
1819                        if statData["name"] not in byComp.keys():
1820                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1821
1822                        else:
1823                            byComp[statData["name"]]["cost"] += costRUB
1824                            byComp[statData["name"]]["percent"] += percentCostRUB
1825
1826                    # adding distribution by unique sectors:
1827                    if statData["sector"] not in bySect.keys():
1828                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1829
1830                    else:
1831                        bySect[statData["sector"]]["cost"] += costRUB
1832                        bySect[statData["sector"]]["percent"] += percentCostRUB
1833
1834                # adding distribution by unique currencies:
1835                if currency not in byCurr.keys():
1836                    byCurr[currency] = {
1837                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1838                        "cost": costRUB,
1839                        "percent": percentCostRUB
1840                    }
1841
1842                else:
1843                    byCurr[currency]["cost"] += costRUB
1844                    byCurr[currency]["percent"] += percentCostRUB
1845
1846                # saving statistics for every instrument:
1847                if item["instrumentType"] == "currency":
1848                    view["stat"]["Currencies"].append(statData)
1849
1850                    # update dict with free funds for trading (total - blocked) by currencies
1851                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1852                    view["stat"]["funds"][currency] = {
1853                        "total": volume,
1854                        "totalCostRUB": costRUB,  # total volume cost in rubles
1855                        "free": volume - blocked,
1856                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1857                    }
1858
1859                elif item["instrumentType"] == "share":
1860                    view["stat"]["Shares"].append(statData)
1861
1862                elif item["instrumentType"] == "bond":
1863                    view["stat"]["Bonds"].append(statData)
1864
1865                elif item["instrumentType"] == "etf":
1866                    view["stat"]["Etfs"].append(statData)
1867
1868                elif item["instrumentType"] == "Futures":
1869                    view["stat"]["Futures"].append(statData)
1870
1871                else:
1872                    continue
1873
1874        # total changes in Russian Ruble:
1875        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1876        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1877        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1878        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1879        view["stat"]["funds"]["rub"] = {
1880            "total": view["stat"]["availableRUB"],
1881            "totalCostRUB": view["stat"]["availableRUB"],
1882            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1883            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1884        }
1885
1886        # --- pending limit orders sector data:
1887        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1888        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1889
1890        for item in view["raw"]["orders"]:
1891            self._figi = item["figi"]
1892
1893            if item["figi"] not in uniquePendingOrdersFIGIs:
1894                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1895
1896                uniquePendingOrdersFIGIs.append(item["figi"])
1897                uniquePendingOrders[item["figi"]] = instrument
1898
1899            else:
1900                instrument = uniquePendingOrders[item["figi"]]
1901
1902            if instrument:
1903                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1904                orderType = TKS_ORDER_TYPES[item["orderType"]]
1905                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1906                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1907
1908                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1909                if item["direction"] == "ORDER_DIRECTION_BUY":
1910                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1911
1912                else:
1913                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1914
1915                # requested price for order execution:
1916                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1917
1918                # necessary changes in percent to reach target from current price:
1919                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1920
1921                view["stat"]["orders"].append({
1922                    "orderID": item["orderId"],  # orderId number parameter of current order
1923                    "figi": item["figi"],  # FIGI identification
1924                    "ticker": instrument["ticker"],  # ticker name by FIGI
1925                    "lotsRequested": item["lotsRequested"],  # requested lots value
1926                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1927                    "currentPrice": lastPrice,  # current instrument's price for defined action
1928                    "targetPrice": target,  # requested price for order execution in base currency
1929                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1930                    "percentChanges": changes,  # changes in percent to target from current price
1931                    "currency": item["currency"],  # instrument's currency name
1932                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1933                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1934                    "status": orderState,  # order status from TKS_ORDER_STATES
1935                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1936                })
1937
1938        # --- stop orders sector data:
1939        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1940        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1941
1942        for item in view["raw"]["stopOrders"]:
1943            self._figi = item["figi"]
1944
1945            if item["figi"] not in uniqueStopOrdersFIGIs:
1946                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1947
1948                uniqueStopOrdersFIGIs.append(item["figi"])
1949                uniqueStopOrders[item["figi"]] = instrument
1950
1951            else:
1952                instrument = uniqueStopOrders[item["figi"]]
1953
1954            if instrument:
1955                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1956                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1957                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1958
1959                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1960                if "expirationTime" in item.keys():
1961                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1962                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1963
1964                else:
1965                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1966                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1967
1968                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1969                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1970                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1971
1972                else:
1973                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1974
1975                # requested price when stop-order executed:
1976                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1977
1978                # price for limit-order, set up when stop-order executed:
1979                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1980
1981                # necessary changes in percent to reach target from current price:
1982                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1983
1984                view["stat"]["stopOrders"].append({
1985                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1986                    "figi": item["figi"],  # FIGI identification
1987                    "ticker": instrument["ticker"],  # ticker name by FIGI
1988                    "lotsRequested": item["lotsRequested"],  # requested lots value
1989                    "currentPrice": lastPrice,  # current instrument's price for defined action
1990                    "targetPrice": target,  # requested price for stop-order execution in base currency
1991                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1992                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1993                    "percentChanges": changes,  # changes in percent to target from current price
1994                    "currency": item["currency"],  # instrument's currency name
1995                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1996                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1997                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1998                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1999                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2000                })
2001
2002        # --- calculating data for analytics section:
2003        # portfolio distribution by assets:
2004        view["analytics"]["distrByAssets"] = {
2005            "Ruble": {
2006                "uniques": 1,
2007                "cost": view["stat"]["availableRUB"],
2008                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2009            },
2010            "Currencies": {
2011                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2012                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2013                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2014            },
2015            "Shares": {
2016                "uniques": len(view["stat"]["Shares"]),
2017                "cost": view["stat"]["sharesCostRUB"],
2018                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2019            },
2020            "Bonds": {
2021                "uniques": len(view["stat"]["Bonds"]),
2022                "cost": view["stat"]["bondsCostRUB"],
2023                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2024            },
2025            "Etfs": {
2026                "uniques": len(view["stat"]["Etfs"]),
2027                "cost": view["stat"]["etfsCostRUB"],
2028                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2029            },
2030            "Futures": {
2031                "uniques": len(view["stat"]["Futures"]),
2032                "cost": view["stat"]["futuresCostRUB"],
2033                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2034            },
2035        }
2036
2037        # portfolio distribution by companies:
2038        view["analytics"]["distrByCompanies"]["All money cash"] = {
2039            "ticker": "",
2040            "cost": view["stat"]["allCurrenciesCostRUB"],
2041            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2042        }
2043        view["analytics"]["distrByCompanies"].update(byComp)
2044
2045        # portfolio distribution by sectors:
2046        view["analytics"]["distrBySectors"]["All money cash"] = {
2047            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2048            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2049        }
2050        view["analytics"]["distrBySectors"].update(bySect)
2051
2052        # portfolio distribution by currencies:
2053        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2054            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2055
2056            if self.moreDebug:
2057                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2058
2059        view["analytics"]["distrByCurrencies"].update(byCurr)
2060        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2061        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2062
2063        # portfolio distribution by countries:
2064        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2065            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2066
2067            if self.moreDebug:
2068                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2069
2070        view["analytics"]["distrByCountries"].update(byCountry)
2071        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2072        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2073
2074        # --- Prepare text statistics overview in human-readable:
2075        if show:
2076            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2077
2078            # Whatever the value `details`, header not changes:
2079            info = [
2080                "# Client's portfolio\n\n",
2081                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2082                "* **Account ID:** [{}]\n".format(self.accountId),
2083            ]
2084
2085            if details in ["full", "positions", "digest"]:
2086                info.extend([
2087                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2088                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2089                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2090                        view["stat"]["totalChangesRUB"],
2091                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2092                        view["stat"]["totalChangesPercentRUB"],
2093                    ),
2094                ])
2095
2096            if details in ["full", "positions"]:
2097                info.extend([
2098                    "## Open positions\n\n",
2099                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2100                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2101                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2102                        "{:.2f} ({:.2f}) rub".format(
2103                            view["stat"]["availableRUB"],
2104                            view["stat"]["blockedRUB"],
2105                        )
2106                    )
2107                ])
2108
2109                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2110                    return [
2111                        "|                             |                                 |          |              |              |                     |                              |\n",
2112                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2113                            noTradeStr if noTradeStr else typeStr,
2114                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2115                        ),
2116                    ]
2117
2118                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2119                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2120                        "{} [{}]".format(data["ticker"], data["figi"]),
2121                        "{:.2f} ({:.2f}) {}".format(
2122                            data["volume"],
2123                            data["blocked"],
2124                            data["currency"],
2125                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2126                            data["volume"],
2127                            data["blocked"],
2128                        ),
2129                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2130                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2131                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2132                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2133                        "{}{:.2f} {} ({}{:.2f}%)".format(
2134                            "+" if data["profit"] > 0 else "",
2135                            data["profit"], data["baseCurrencyName"],
2136                            "+" if data["percentProfit"] > 0 else "",
2137                            data["percentProfit"],
2138                        ),
2139                    )
2140
2141                # --- Show currencies section:
2142                if view["stat"]["Currencies"]:
2143                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2144                    for item in view["stat"]["Currencies"]:
2145                        info.append(_InfoStr(item, showCurrencyName=True))
2146
2147                else:
2148                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2149
2150                # --- Show shares section:
2151                if view["stat"]["Shares"]:
2152                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2153
2154                    for item in view["stat"]["Shares"]:
2155                        info.append(_InfoStr(item))
2156
2157                else:
2158                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2159
2160                # --- Show bonds section:
2161                if view["stat"]["Bonds"]:
2162                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2163
2164                    for item in view["stat"]["Bonds"]:
2165                        info.append(_InfoStr(item))
2166
2167                else:
2168                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2169
2170                # --- Show etfs section:
2171                if view["stat"]["Etfs"]:
2172                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2173
2174                    for item in view["stat"]["Etfs"]:
2175                        info.append(_InfoStr(item))
2176
2177                else:
2178                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2179
2180                # --- Show futures section:
2181                if view["stat"]["Futures"]:
2182                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2183
2184                    for item in view["stat"]["Futures"]:
2185                        info.append(_InfoStr(item))
2186
2187                else:
2188                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2189
2190            if details in ["full", "orders"]:
2191                # --- Show pending limit orders section:
2192                if view["stat"]["orders"]:
2193                    info.extend([
2194                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2195                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2196                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2197                    ])
2198
2199                    for item in view["stat"]["orders"]:
2200                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2201                            "{} [{}]".format(item["ticker"], item["figi"]),
2202                            item["orderID"],
2203                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2204                            "{} {} ({}{:.2f}%)".format(
2205                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2206                                item["baseCurrencyName"],
2207                                "+" if item["percentChanges"] > 0 else "",
2208                                float(item["percentChanges"]),
2209                            ),
2210                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2211                            item["action"],
2212                            item["type"],
2213                            item["date"],
2214                        ))
2215
2216                else:
2217                    info.append("\n## Total pending limit-orders: [0]\n")
2218
2219                # --- Show stop orders section:
2220                if view["stat"]["stopOrders"]:
2221                    info.extend([
2222                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2223                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2224                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2225                    ])
2226
2227                    for item in view["stat"]["stopOrders"]:
2228                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2229                            "{} [{}]".format(item["ticker"], item["figi"]),
2230                            item["orderID"],
2231                            item["lotsRequested"],
2232                            "{} {} ({}{:.2f}%)".format(
2233                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2234                                item["baseCurrencyName"],
2235                                "+" if item["percentChanges"] > 0 else "",
2236                                float(item["percentChanges"]),
2237                            ),
2238                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2239                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2240                            item["action"],
2241                            item["type"],
2242                            item["expType"],
2243                            item["createDate"],
2244                            item["expDate"],
2245                        ))
2246
2247                else:
2248                    info.append("\n## Total stop-orders: [0]\n")
2249
2250            if details in ["full", "analytics"]:
2251                # -- Show analytics section:
2252                if view["stat"]["portfolioCostRUB"] > 0:
2253                    info.extend([
2254                        "\n# Analytics\n\n"
2255                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2256                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2257                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2258                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2259                            view["stat"]["totalChangesRUB"],
2260                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2261                            view["stat"]["totalChangesPercentRUB"],
2262                        ),
2263                        "\n## Portfolio distribution by assets\n"
2264                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2265                        "|------------------------------------|---------|---------|--------------------|\n",
2266                    ])
2267
2268                    for key in view["analytics"]["distrByAssets"].keys():
2269                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2270                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2271                                key,
2272                                view["analytics"]["distrByAssets"][key]["uniques"],
2273                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2274                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2275                            ))
2276
2277                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2278
2279                    info.extend([
2280                        "\n## Portfolio distribution by companies\n"
2281                        "\n| Company                                      | Percent | Current cost       |\n",
2282                        aSepLine,
2283                    ])
2284
2285                    for company in view["analytics"]["distrByCompanies"].keys():
2286                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2287                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2288                                "{}{}".format(
2289                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2290                                    company,
2291                                ),
2292                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2293                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2294                            ))
2295
2296                    info.extend([
2297                        "\n## Portfolio distribution by sectors\n"
2298                        "\n| Sector                                       | Percent | Current cost       |\n",
2299                        aSepLine,
2300                    ])
2301
2302                    for sector in view["analytics"]["distrBySectors"].keys():
2303                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2304                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2305                                sector,
2306                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2307                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2308                            ))
2309
2310                    info.extend([
2311                        "\n## Portfolio distribution by currencies\n"
2312                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2313                        aSepLine,
2314                    ])
2315
2316                    for curr in view["analytics"]["distrByCurrencies"].keys():
2317                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2318                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2319                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2320                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2321                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2322                            ))
2323
2324                    info.extend([
2325                        "\n## Portfolio distribution by countries\n"
2326                        "\n| Assets by country                            | Percent | Current cost       |\n",
2327                        aSepLine,
2328                    ])
2329
2330                    for country in view["analytics"]["distrByCountries"].keys():
2331                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2332                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2333                                country,
2334                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2335                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2336                            ))
2337
2338            if details in ["full", "calendar"]:
2339                # -- Show bonds payment calendar section:
2340                if view["stat"]["Bonds"]:
2341                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2342                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2343                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2344
2345                else:
2346                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2347
2348            infoText = "".join(info)
2349
2350            uLogger.info(infoText)
2351
2352            if details == "full" and self.overviewFile:
2353                filename = self.overviewFile
2354
2355            elif details == "digest" and self.overviewDigestFile:
2356                filename = self.overviewDigestFile
2357
2358            elif details == "positions" and self.overviewPositionsFile:
2359                filename = self.overviewPositionsFile
2360
2361            elif details == "orders" and self.overviewOrdersFile:
2362                filename = self.overviewOrdersFile
2363
2364            elif details == "analytics" and self.overviewAnalyticsFile:
2365                filename = self.overviewAnalyticsFile
2366
2367            elif details == "calendar" and self.overviewBondsCalendarFile:
2368                filename = self.overviewBondsCalendarFile
2369
2370            else:
2371                filename = ""
2372
2373            if filename:
2374                with open(filename, "w", encoding="UTF-8") as fH:
2375                    fH.write(infoText)
2376
2377                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2378
2379                if self.useHTMLReports:
2380                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2381                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2382                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="", commonCSS=COMMON_CSS, markdown=infoText))
2383
2384                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2385
2386        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile and overviewBondsCalendarFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be?
    • full — shows full available information about portfolio status (by default),
    • positions — shows only open positions,
    • orders — shows only sections of open limits and stop orders.
    • digest — show a short digest of the portfolio status,
    • analytics — shows only the analytics section and the distribution of the portfolio by various categories,
    • calendar — shows only the bonds calendar section (if these present in portfolio),
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2388    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2389        """
2390        Returns history operations between two given dates for current `accountId`.
2391        If `reportFile` string is not empty then also save human-readable report.
2392        Shows some statistical data of closed positions.
2393
2394        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2395        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2396        :param show: if `True` then also prints all records to the console.
2397        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2398        :return: original list of dictionaries with history of deals records from API ("operations" key):
2399                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2400                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2401        """
2402        if self.accountId is None or not self.accountId:
2403            uLogger.error("Variable `accountId` must be defined for using this method!")
2404            raise Exception("Account ID required")
2405
2406        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2407
2408        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2409
2410        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2411        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2412        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2413        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2414        customStat = {}  # custom statistics in additional to responseJSON
2415
2416        # --- output report in human-readable format:
2417        if show or self.reportFile:
2418            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2419            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2420            nextDay = ""
2421
2422            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2423
2424            if len(ops) > 0:
2425                customStat = {
2426                    "opsCount": 0,  # total operations count
2427                    "buyCount": 0,  # buy operations
2428                    "sellCount": 0,  # sell operations
2429                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2430                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2431                    "payIn": {"rub": 0.},  # Deposit brokerage account
2432                    "payOut": {"rub": 0.},  # Withdrawals
2433                    "divs": {"rub": 0.},  # Dividends income
2434                    "coupons": {"rub": 0.},  # Coupon's income
2435                    "brokerCom": {"rub": 0.},  # Service commissions
2436                    "serviceCom": {"rub": 0.},  # Service commissions
2437                    "marginCom": {"rub": 0.},  # Margin commissions
2438                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2439                }
2440
2441                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2442                for item in ops:
2443                    if item["state"] == "OPERATION_STATE_EXECUTED":
2444                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2445
2446                        # count buy operations:
2447                        if "_BUY" in item["operationType"]:
2448                            customStat["buyCount"] += 1
2449
2450                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2451                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2452
2453                            else:
2454                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2455
2456                        # count sell operations:
2457                        elif "_SELL" in item["operationType"]:
2458                            customStat["sellCount"] += 1
2459
2460                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2461                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2462
2463                            else:
2464                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2465
2466                        # count incoming operations:
2467                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2468                            if item["payment"]["currency"] in customStat["payIn"].keys():
2469                                customStat["payIn"][item["payment"]["currency"]] += payment
2470
2471                            else:
2472                                customStat["payIn"][item["payment"]["currency"]] = payment
2473
2474                        # count withdrawals operations:
2475                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2476                            if item["payment"]["currency"] in customStat["payOut"].keys():
2477                                customStat["payOut"][item["payment"]["currency"]] += payment
2478
2479                            else:
2480                                customStat["payOut"][item["payment"]["currency"]] = payment
2481
2482                        # count dividends income:
2483                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2484                            if item["payment"]["currency"] in customStat["divs"].keys():
2485                                customStat["divs"][item["payment"]["currency"]] += payment
2486
2487                            else:
2488                                customStat["divs"][item["payment"]["currency"]] = payment
2489
2490                        # count coupon's income:
2491                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2492                            if item["payment"]["currency"] in customStat["coupons"].keys():
2493                                customStat["coupons"][item["payment"]["currency"]] += payment
2494
2495                            else:
2496                                customStat["coupons"][item["payment"]["currency"]] = payment
2497
2498                        # count broker commissions:
2499                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2500                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2501                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2502
2503                            else:
2504                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2505
2506                        # count service commissions:
2507                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2508                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2509                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2510
2511                            else:
2512                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2513
2514                        # count margin commissions:
2515                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2516                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2517                                customStat["marginCom"][item["payment"]["currency"]] += payment
2518
2519                            else:
2520                                customStat["marginCom"][item["payment"]["currency"]] = payment
2521
2522                        # count withholding taxes:
2523                        elif "_TAX" in item["operationType"]:
2524                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2525                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2526
2527                            else:
2528                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2529
2530                        else:
2531                            continue
2532
2533                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2534
2535                # --- view "Actions" lines:
2536                info.extend([
2537                    "| Report sections            |                               |                              |                      |                        |\n",
2538                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2539                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2540                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2541                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2542                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2543                    ),
2544                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2545                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2546                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2547                    ),
2548                ])
2549
2550                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2551                for key in opsKeys:
2552                    if key == "rub":
2553                        continue
2554
2555                    info.extend([
2556                        "|                            |                               | {:<28} |                      |                        |\n".format(
2557                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2558                        ),
2559                        "|                            |                               | {:<28} |                      |                        |\n".format(
2560                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2561                        ),
2562                    ])
2563
2564                info.append(splitLine1)
2565
2566                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2567                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2568                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2569                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2570                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2571                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2572                    )
2573
2574                # --- view "Payments" lines:
2575                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2576                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2577
2578                for key in paymentsKeys:
2579                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2580
2581                info.append(splitLine1)
2582
2583                # --- view "Commissions and taxes" lines:
2584                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2585                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2586
2587                for key in comKeys:
2588                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2589
2590                info.extend([
2591                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2592                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2593                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2594                ])
2595
2596            else:
2597                info.append("Broker returned no operations during this period\n")
2598
2599            # --- view "Operations" section:
2600            for item in ops:
2601                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2602                    continue
2603
2604                else:
2605                    self._figi = item["figi"] if item["figi"] else ""
2606                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2607                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2608
2609                    # group of deals during one day:
2610                    if nextDay and item["date"].split("T")[0] != nextDay:
2611                        info.append(splitLine2)
2612                        nextDay = ""
2613
2614                    else:
2615                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2616
2617                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2618                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2619                        self._figi if self._figi else "—",
2620                        instrument["ticker"] if instrument else "—",
2621                        instrument["type"] if instrument else "—",
2622                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2623                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2624                        TKS_OPERATION_STATES[item["state"]],
2625                        TKS_OPERATION_TYPES[item["operationType"]],
2626                    ))
2627
2628            infoText = "".join(info)
2629
2630            if show:
2631                if self.moreDebug:
2632                    uLogger.debug("Records about history of a client's operations successfully received")
2633
2634                uLogger.info(infoText)
2635
2636            if self.reportFile:
2637                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2638                    fH.write(infoText)
2639
2640                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2641
2642                if self.useHTMLReports:
2643                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2644                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2645                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2646
2647                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2648
2649        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False) -> pandas.core.frame.DataFrame:
2651    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2652        """
2653        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2654
2655        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2656        Warning! Broker server used ISO UTC time by default.
2657
2658        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2659        Also, `historyFile` used to update history with `onlyMissing` parameter.
2660
2661        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2662
2663        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2664        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2665        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2666                         `"hour"`, `"day"`. Default: `"hour"`.
2667        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2668                            False by default. Warning! History appends only from last candle to current time
2669                            with always update last candle!
2670        :param csvSep: separator if csv-file is used, `,` by default.
2671        :param show: if `True` then also prints Pandas DataFrame to the console.
2672        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2673                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2674        """
2675        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2676        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2677        history = None  # empty pandas object for history
2678
2679        if interval not in TKS_CANDLE_INTERVALS.keys():
2680            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2681            raise Exception("Incorrect value")
2682
2683        if not (self._ticker or self._figi):
2684            uLogger.error("Ticker or FIGI must be defined!")
2685            raise Exception("Ticker or FIGI required")
2686
2687        if self._ticker and not self._figi:
2688            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2689            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2690
2691        if self._figi and not self._ticker:
2692            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2693            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2694
2695        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2696        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2697        if interval.lower() != "day":
2698            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2699
2700        delta = dtEnd - dtStart  # current UTC time minus last time in file
2701        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2702
2703        # calculate history length in candles:
2704        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2705        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2706            length += 1  # to avoid fraction time
2707
2708        # calculate data blocks count:
2709        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2710
2711        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2712        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2713        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2714        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2715        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2716
2717        tempOld = None  # pandas object for old history, if --only-missing key present
2718        lastTime = None  # datetime object of last old candle in file
2719
2720        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2721            uLogger.debug("--only-missing key present, add only last missing candles...")
2722            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2723
2724            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2725
2726            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2727            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2728            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2729            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2730
2731            # get last datetime object from last string in file or minus 1 delta if file is empty:
2732            if len(tempOld) > 0:
2733                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2734
2735            else:
2736                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2737
2738            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2739
2740        responseJSONs = []  # raw history blocks of data
2741
2742        blockEnd = dtEnd
2743        for item in range(blocks):
2744            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2745            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2746
2747            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2748                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2749            ))
2750
2751            if blockStart == blockEnd:
2752                uLogger.debug("Skipped this zero-length block...")
2753
2754            else:
2755                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2756                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2757                self.body = str({
2758                    "figi": self._figi,
2759                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2760                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2761                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2762                })
2763                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2764
2765                if "code" in responseJSON.keys():
2766                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2767
2768                else:
2769                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2770                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2771
2772                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2773
2774            blockEnd = blockStart
2775
2776        printCount = len(responseJSONs)  # candles to show in console
2777        if responseJSONs:
2778            tempHistory = pd.DataFrame(
2779                data={
2780                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2781                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2782                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2783                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2784                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2785                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2786                    "volume": [int(item["volume"]) for item in responseJSONs],
2787                },
2788                index=range(len(responseJSONs)),
2789                columns=["date", "time", "open", "high", "low", "close", "volume"],
2790            )
2791            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2792            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2793
2794            # append only newest candles to old history if --only-missing key present:
2795            if onlyMissing and tempOld is not None and lastTime is not None:
2796                index = 0  # find start index in tempHistory data:
2797
2798                for i, item in tempHistory.iterrows():
2799                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2800
2801                    if curTime == lastTime:
2802                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2803                        index = i
2804                        printCount = index + 1
2805                        break
2806
2807                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2808
2809            else:
2810                history = tempHistory  # if no `--only-missing` key then load full data from server
2811
2812            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2813
2814        if history is not None and not history.empty:
2815            if show:
2816                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2817                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2818                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2819                ))
2820
2821        else:
2822            uLogger.warning("Received an empty candles history!")
2823
2824        if self.historyFile is not None:
2825            if history is not None and not history.empty:
2826                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2827                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2828
2829            else:
2830                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2831
2832        else:
2833            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2834
2835        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2837    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2838        """
2839        Load candles history from csv-file and return Pandas DataFrame object.
2840
2841        See also: `History()` and `ShowHistoryChart()` methods.
2842
2843        :param filePath: path to csv-file to open.
2844        """
2845        loadedHistory = None  # init candles data object
2846
2847        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2848
2849        if os.path.exists(filePath):
2850            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2851
2852            tfStr = self.priceModel.FormattedDelta(
2853                self.priceModel.timeframe,
2854                "{days} days {hours}h {minutes}m {seconds}s",
2855            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2856                self.priceModel.timeframe,
2857                "{hours}h {minutes}m {seconds}s",
2858            )
2859
2860            if loadedHistory is not None and not loadedHistory.empty:
2861                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2862                    len(loadedHistory),
2863                    tfStr,
2864                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2865                )
2866
2867            else:
2868                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2869
2870        else:
2871            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2872
2873        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2875    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2876        """
2877        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2878
2879        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2880        Default: `index.html` (both for interact and non-interact candlesticks chart).
2881
2882        See also: `History()` and `LoadHistory()` methods.
2883
2884        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2885        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2886                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2887                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2888                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2889        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2890                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2891        """
2892        if isinstance(candles, str):
2893            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2894            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2895
2896        elif isinstance(candles, pd.DataFrame):
2897            self.priceModel.prices = candles  # set candles chain from variable
2898            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2899
2900            if "datetime" not in candles.columns:
2901                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2902
2903        else:
2904            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2905            raise Exception("Incorrect value")
2906
2907        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2908
2909        if interact:
2910            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2911
2912            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2913
2914        else:
2915            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2916
2917            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2918
2919        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2921    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2922        """
2923        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2924        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2925
2926        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2927
2928        :param operation: string "Buy" or "Sell".
2929        :param lots: volume, integer count of lots >= 1.
2930        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2931        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2932        :param expDate: string "Undefined" by default or local date in future,
2933                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2934        :return: JSON with response from broker server.
2935        """
2936        if self.accountId is None or not self.accountId:
2937            uLogger.error("Variable `accountId` must be defined for using this method!")
2938            raise Exception("Account ID required")
2939
2940        if operation is None or not operation or operation not in ("Buy", "Sell"):
2941            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2942            raise Exception("Incorrect value")
2943
2944        if lots is None or lots < 1:
2945            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2946            lots = 1
2947
2948        if tp is None or tp < 0:
2949            tp = 0
2950
2951        if sl is None or sl < 0:
2952            sl = 0
2953
2954        if expDate is None or not expDate:
2955            expDate = "Undefined"
2956
2957        if not (self._ticker or self._figi):
2958            uLogger.error("Ticker or FIGI must be defined!")
2959            raise Exception("Ticker or FIGI required")
2960
2961        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2962        self._ticker = instrument["ticker"]
2963        self._figi = instrument["figi"]
2964
2965        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
2966
2967        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2968        self.body = str({
2969            "figi": self._figi,
2970            "quantity": str(lots),
2971            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2972            "accountId": str(self.accountId),
2973            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2974        })
2975        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2976
2977        if "orderId" in response.keys():
2978            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2979                operation, response["orderId"],
2980                self._ticker, self._figi, lots,
2981                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2982                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2983                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2984            ))
2985
2986            if tp > 0:
2987                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2988
2989            if sl > 0:
2990                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2991
2992        else:
2993            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
2994
2995        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2997    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2998        """
2999        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3000        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3001
3002        See also: `Order()` and `Trade()` docstrings.
3003
3004        :param lots: volume, integer count of lots >= 1.
3005        :param tp: float > 0, take profit price of stop-order.
3006        :param sl: float > 0, stop loss price of stop-order.
3007        :param expDate: it's a local date in future.
3008                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3009        :return: JSON with response from broker server.
3010        """
3011        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3013    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3014        """
3015        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3016        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3017
3018        See also: `Order()` and `Trade()` docstrings.
3019
3020        :param lots: volume, integer count of lots >= 1.
3021        :param tp: float > 0, take profit price of stop-order.
3022        :param sl: float > 0, stop loss price of stop-order.
3023        :param expDate: it's a local date in the future.
3024                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3025        :return: JSON with response from broker server.
3026        """
3027        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3029    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3030        """
3031        Close position of given instruments.
3032
3033        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3034        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3035                         This avoids unnecessary downloading data from the server.
3036        """
3037        if instruments is None or not instruments:
3038            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3039            raise Exception("Ticker or FIGI required")
3040
3041        if isinstance(instruments, str):
3042            instruments = [instruments]
3043
3044        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3045        if uniqueInstruments:
3046            if portfolio is None or not portfolio:
3047                portfolio = self.Overview(show=False)
3048
3049            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3050            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3051
3052            for self._figi in uniqueInstruments:
3053                if self._figi not in allOpened:
3054                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3055                    continue
3056
3057                # search open trade info about instrument by ticker:
3058                instrument = {}
3059                for iType in TKS_INSTRUMENTS:
3060                    if instrument:
3061                        break
3062
3063                    for item in portfolio["stat"][iType]:
3064                        if item["figi"] == self._figi:
3065                            instrument = item
3066                            break
3067
3068                if instrument:
3069                    self._ticker = instrument["ticker"]
3070                    self._figi = instrument["figi"]
3071
3072                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3073                        self._ticker,
3074                        self._figi,
3075                        int(instrument["volume"]),
3076                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3077                    ))
3078
3079                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3080
3081                    if tradeLots > 0:
3082                        if instrument["blocked"] > 0:
3083                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3084                                instrument["blocked"],
3085                                self._ticker,
3086                                tradeLots,
3087                            ))
3088
3089                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3090                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3091
3092                    else:
3093                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3095    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3096        """
3097        Close all positions of given instruments with defined type.
3098
3099        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3100        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3101                         This avoids unnecessary downloading data from the server.
3102        """
3103        if iType not in TKS_INSTRUMENTS:
3104            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3105
3106        else:
3107            if portfolio is None or not portfolio:
3108                portfolio = self.Overview(show=False)
3109
3110            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3111            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3112
3113            if tickers and portfolio:
3114                self.CloseTrades(tickers, portfolio)
3115
3116            else:
3117                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3119    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3120        """
3121        Universal method to create market or limit orders with all available parameters for current `accountId`.
3122        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3123
3124        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3125        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3126
3127        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3128        then broker immediately open market order as you can do simple --buy or --sell operations!
3129
3130        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3131        When current price will go up or down to target price value then broker opens a limit order.
3132        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3133
3134        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3135
3136        :param operation: string "Buy" or "Sell".
3137        :param orderType: string "Limit" or "Stop".
3138        :param lots: volume, integer count of lots >= 1.
3139        :param targetPrice: target price > 0. This is open trade price for limit order.
3140        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3141                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3142        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3143                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3144                         Stop loss order always executed by market price.
3145        :param expDate: string "Undefined" by default or local date in future.
3146                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3147                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3148                        A limit order has no expiration date, it lasts until the end of the trading day.
3149        :return: JSON with response from broker server.
3150        """
3151        if self.accountId is None or not self.accountId:
3152            uLogger.error("Variable `accountId` must be defined for using this method!")
3153            raise Exception("Account ID required")
3154
3155        if operation is None or not operation or operation not in ("Buy", "Sell"):
3156            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3157            raise Exception("Incorrect value")
3158
3159        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3160            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3161            raise Exception("Incorrect value")
3162
3163        if lots is None or lots < 1:
3164            uLogger.error("You must define trade volume > 0: integer count of lots!")
3165            raise Exception("Incorrect value")
3166
3167        if targetPrice is None or targetPrice <= 0:
3168            uLogger.error("Target price for limit-order must be greater than 0!")
3169            raise Exception("Incorrect value")
3170
3171        if limitPrice is None or limitPrice <= 0:
3172            limitPrice = targetPrice
3173
3174        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3175            stopType = "Limit"
3176
3177        if expDate is None or not expDate:
3178            expDate = "Undefined"
3179
3180        if not (self._ticker or self._figi):
3181            uLogger.error("Tocker or FIGI must be defined!")
3182            raise Exception("Ticker or FIGI required")
3183
3184        response = {}
3185        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3186        self._ticker = instrument["ticker"]
3187        self._figi = instrument["figi"]
3188
3189        if orderType == "Limit":
3190            uLogger.debug(
3191                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3192                    self._ticker, self._figi,
3193                    operation, lots, targetPrice, instrument["currency"],
3194                ))
3195
3196            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3197            self.body = str({
3198                "figi": self._figi,
3199                "quantity": str(lots),
3200                "price": FloatToNano(targetPrice),
3201                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3202                "accountId": str(self.accountId),
3203                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3204            })
3205            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3206
3207            if "orderId" in response.keys():
3208                uLogger.info(
3209                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3210                        response["orderId"],
3211                        self._ticker, self._figi,
3212                        operation, lots, targetPrice, instrument["currency"],
3213                    ))
3214
3215                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3216                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3217                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3218                            targetPrice, instrument["currency"],
3219                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3220                        ))
3221
3222                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3223                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3224                            targetPrice, instrument["currency"],
3225                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3226                        ))
3227
3228            else:
3229                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3230
3231        if orderType == "Stop":
3232            uLogger.debug(
3233                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3234                    self._ticker, self._figi,
3235                    operation, lots,
3236                    targetPrice, instrument["currency"],
3237                    limitPrice, instrument["currency"],
3238                    stopType, expDate,
3239                ))
3240
3241            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3242            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3243            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3244
3245            body = {
3246                "figi": self._figi,
3247                "quantity": str(lots),
3248                "price": FloatToNano(limitPrice),
3249                "stopPrice": FloatToNano(targetPrice),
3250                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3251                "accountId": str(self.accountId),
3252                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3253                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3254            }
3255
3256            if expDateUTC:
3257                body["expireDate"] = expDateUTC
3258
3259            self.body = str(body)
3260            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3261
3262            if "stopOrderId" in response.keys():
3263                uLogger.info(
3264                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3265                        response["stopOrderId"],
3266                        self._ticker, self._figi,
3267                        operation, lots,
3268                        targetPrice, instrument["currency"],
3269                        limitPrice, instrument["currency"],
3270                        TKS_STOP_ORDER_TYPES[stopOrderType],
3271                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3272                    ))
3273
3274                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3275                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3276                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3277                            targetPrice, instrument["currency"],
3278                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3279                        ))
3280
3281                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3282                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3283                            targetPrice, instrument["currency"],
3284                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3285                        ))
3286
3287            else:
3288                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3289
3290        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3292    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3293        """
3294        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3295        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3296        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3297        See also: `Order()` docstring.
3298
3299        :param lots: volume, integer count of lots >= 1.
3300        :param targetPrice: target price > 0. This is open trade price for limit order.
3301        :return: JSON with response from broker server.
3302        """
3303        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3305    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3306        """
3307        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3308        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3309        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3310        target price value then broker opens a limit order. See also: `Order()` docstring.
3311
3312        :param lots: volume, integer count of lots >= 1.
3313        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3314        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3315                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3316        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3317                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3318        :param expDate: string "Undefined" by default or local date in future.
3319                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3320                        This date is converting to UTC format for server.
3321        :return: JSON with response from broker server.
3322        """
3323        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3325    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3326        """
3327        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3328        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3329        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3330        See also: `Order()` docstring.
3331
3332        :param lots: volume, integer count of lots >= 1.
3333        :param targetPrice: target price > 0. This is open trade price for limit order.
3334        :return: JSON with response from broker server.
3335        """
3336        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3338    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3339        """
3340        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3341        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3342        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3343        target price value then broker opens a limit order. See also: `Order()` docstring.
3344
3345        :param lots: volume, integer count of lots >= 1.
3346        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3347        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3348                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3349        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3350                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3351        :param expDate: string "Undefined" by default or local date in future.
3352                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3353                        This date is converting to UTC format for server.
3354        :return: JSON with response from broker server.
3355        """
3356        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3358    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3359        """
3360        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3361
3362        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3363        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3364                             This avoids unnecessary downloading data from the server.
3365        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3366        """
3367        if self.accountId is None or not self.accountId:
3368            uLogger.error("Variable `accountId` must be defined for using this method!")
3369            raise Exception("Account ID required")
3370
3371        if orderIDs:
3372            if allOrdersIDs is None:
3373                rawOrders = self.RequestPendingOrders()
3374                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3375
3376            if allStopOrdersIDs is None:
3377                rawStopOrders = self.RequestStopOrders()
3378                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3379
3380            for orderID in orderIDs:
3381                idInPendingOrders = orderID in allOrdersIDs
3382                idInStopOrders = orderID in allStopOrdersIDs
3383
3384                if not (idInPendingOrders or idInStopOrders):
3385                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3386                    continue
3387
3388                else:
3389                    if idInPendingOrders:
3390                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3391
3392                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3393                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3394                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3395                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3396
3397                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3398                            if self.moreDebug:
3399                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3400
3401                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3402
3403                        else:
3404                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3405
3406                    elif idInStopOrders:
3407                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3408
3409                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3410                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3411                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3412                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3413
3414                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3415                            if self.moreDebug:
3416                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3417
3418                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3419
3420                        else:
3421                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3422
3423                    else:
3424                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3426    def CloseAllOrders(self) -> None:
3427        """
3428        Gets a list of open pending and stop orders and cancel it all.
3429        """
3430        rawOrders = self.RequestPendingOrders()
3431        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3432        lenOrders = len(allOrdersIDs)
3433
3434        rawStopOrders = self.RequestStopOrders()
3435        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3436        lenSOrders = len(allStopOrdersIDs)
3437
3438        if lenOrders > 0 or lenSOrders > 0:
3439            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3440
3441            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3442
3443        else:
3444            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3446    def CloseAll(self, *args) -> None:
3447        """
3448        Close all available (not blocked) opened trades and orders.
3449
3450        Also, you can select one or more keywords case-insensitive:
3451        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3452
3453        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3454        """
3455        overview = self.Overview(show=False)  # get all open trades info
3456
3457        if len(args) == 0:
3458            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3459            self.CloseAllOrders()  # close all pending and stop orders
3460
3461            for iType in TKS_INSTRUMENTS:
3462                if iType != "Currencies":
3463                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3464
3465        else:
3466            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3467            lowerArgs = [x.lower() for x in args]
3468
3469            if "orders" in lowerArgs:
3470                self.CloseAllOrders()  # close all pending and stop orders
3471
3472            for iType in TKS_INSTRUMENTS:
3473                if iType.lower() in lowerArgs and iType != "Currencies":
3474                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

def CloseAllByTicker(self, instrument: str) -> None:
3476    def CloseAllByTicker(self, instrument: str) -> None:
3477        """
3478        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3479
3480        This method searches opened trade and orders of instrument throw all portfolio and then use
3481        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3482
3483        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3484
3485        :param instrument: string with ticker.
3486        """
3487        if instrument is None or not instrument:
3488            uLogger.error("Ticker name must be defined for using this method!")
3489            raise Exception("Ticker required")
3490
3491        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3492
3493        self._ticker = instrument  # try to set instrument as ticker
3494        self._figi = ""
3495
3496        if self.IsInPortfolio(portfolio=overview):
3497            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3498            self.CloseTrades(instruments=[instrument], portfolio=overview)
3499
3500        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3501        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3502
3503        if limitAll and self.IsInLimitOrders(portfolio=overview):
3504            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3505            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3506
3507        if stopAll and self.IsInStopOrders(portfolio=overview):
3508            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3509            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)

Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with ticker.
def CloseAllByFIGI(self, instrument: str) -> None:
3511    def CloseAllByFIGI(self, instrument: str) -> None:
3512        """
3513        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3514
3515        This method searches opened trade and orders of instrument throw all portfolio and then use
3516        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3517
3518        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3519
3520        :param instrument: string with FIGI id.
3521        """
3522        if instrument is None or not instrument:
3523            uLogger.error("FIGI id must be defined for using this method!")
3524            raise Exception("FIGI required")
3525
3526        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3527
3528        self._ticker = ""
3529        self._figi = instrument  # try to set instrument as FIGI id
3530
3531        if self.IsInPortfolio(portfolio=overview):
3532            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3533            self.CloseTrades(instruments=[instrument], portfolio=overview)
3534
3535        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3536        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3537
3538        if limitAll and self.IsInLimitOrders(portfolio=overview):
3539            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3540            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3541
3542        if stopAll and self.IsInStopOrders(portfolio=overview):
3543            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3544            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)

Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with FIGI id.
@staticmethod
def ParseOrderParameters(operation, **inputParameters):
3546    @staticmethod
3547    def ParseOrderParameters(operation, **inputParameters):
3548        """
3549        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3550
3551        :param operation: string "Buy" or "Sell".
3552        :param inputParameters: this is dict of strings that looks like this
3553               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3554               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3555               "prices" key: one or more prices to open limit-orders
3556               Counts of values in lots and prices lists must be equals!
3557        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3558        """
3559        # TODO: update order grid work with api v2
3560        pass
3561        # uLogger.debug("Input parameters: {}".format(inputParameters))
3562        #
3563        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3564        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3565        #     raise Exception("Incorrect value")
3566        #
3567        # if "l" in inputParameters.keys():
3568        #     inputParameters["lots"] = inputParameters.pop("l")
3569        #
3570        # if "p" in inputParameters.keys():
3571        #     inputParameters["prices"] = inputParameters.pop("p")
3572        #
3573        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3574        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3575        #     raise Exception("Incorrect value")
3576        #
3577        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3578        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3579        #
3580        # if len(lots) != len(prices):
3581        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3582        #     raise Exception("Incorrect value")
3583        #
3584        # uLogger.debug("Extracted parameters for orders:")
3585        # uLogger.debug("lots = {}".format(lots))
3586        # uLogger.debug("prices = {}".format(prices))
3587        #
3588        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3589        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3590        # uLogger.debug("Order parameters: {}".format(result))
3591        #
3592        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3594    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3595        """
3596        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3597
3598        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3599        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3600        """
3601        result = False
3602        msg = "Instrument not defined!"
3603
3604        if portfolio is None or not portfolio:
3605            portfolio = self.Overview(show=False)
3606
3607        if self._ticker:
3608            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3609            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3610
3611            for iType in TKS_INSTRUMENTS:
3612                for instrument in portfolio["stat"][iType]:
3613                    if instrument["ticker"] == self._ticker:
3614                        result = True
3615                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3616                        break
3617
3618        elif self._figi:
3619            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3620            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3621
3622            for iType in TKS_INSTRUMENTS:
3623                for instrument in portfolio["stat"][iType]:
3624                    if instrument["figi"] == self._figi:
3625                        result = True
3626                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3627                        break
3628
3629        else:
3630            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3631
3632        uLogger.debug(msg)
3633
3634        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3636    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3637        """
3638        Returns instrument from the user's portfolio if it presents there.
3639        Instrument must be defined by `ticker` (highly priority) or `figi`.
3640
3641        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3642        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3643        """
3644        result = None
3645        msg = "Instrument not defined!"
3646
3647        if portfolio is None or not portfolio:
3648            portfolio = self.Overview(show=False)
3649
3650        if self._ticker:
3651            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3652            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3653
3654            for iType in TKS_INSTRUMENTS:
3655                for instrument in portfolio["stat"][iType]:
3656                    if instrument["ticker"] == self._ticker:
3657                        result = instrument
3658                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3659                        break
3660
3661        elif self._figi:
3662            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3663            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3664
3665            for iType in TKS_INSTRUMENTS:
3666                for instrument in portfolio["stat"][iType]:
3667                    if instrument["figi"] == self._figi:
3668                        result = instrument
3669                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3670                        break
3671
3672        else:
3673            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3674
3675        uLogger.debug(msg)
3676
3677        return result

Returns instrument from the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3679    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3680        """
3681        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3682
3683        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3684
3685        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3686        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3687        """
3688        result = False
3689        msg = "Instrument not defined!"
3690
3691        if portfolio is None or not portfolio:
3692            portfolio = self.Overview(show=False)
3693
3694        if self._ticker:
3695            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3696            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3697
3698            for instrument in portfolio["stat"]["orders"]:
3699                if instrument["ticker"] == self._ticker:
3700                    result = True
3701                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3702                    break
3703
3704        elif self._figi:
3705            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3706            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3707
3708            for instrument in portfolio["stat"]["orders"]:
3709                if instrument["figi"] == self._figi:
3710                    result = True
3711                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3712                    break
3713
3714        else:
3715            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3716
3717        uLogger.debug(msg)
3718
3719        return result

Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if limit orders list contains some limit orders for the instrument, False otherwise.

def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3721    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3722        """
3723        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3724        Instrument must be defined by `ticker` (highly priority) or `figi`.
3725
3726        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3727
3728        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3729        :return: list with `orderID`s of limit orders.
3730        """
3731        result = []
3732        msg = "Instrument not defined!"
3733
3734        if portfolio is None or not portfolio:
3735            portfolio = self.Overview(show=False)
3736
3737        if self._ticker:
3738            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3739            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3740
3741            for instrument in portfolio["stat"]["orders"]:
3742                if instrument["ticker"] == self._ticker:
3743                    result.append(instrument["orderID"])
3744
3745            if result:
3746                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3747
3748        elif self._figi:
3749            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3750            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3751
3752            for instrument in portfolio["stat"]["orders"]:
3753                if instrument["figi"] == self._figi:
3754                    result.append(instrument["orderID"])
3755
3756            if result:
3757                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3758
3759        else:
3760            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3761
3762        uLogger.debug(msg)
3763
3764        return result

Returns list with all orderIDs of opened pending limit orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of limit orders.

def IsInStopOrders(self, portfolio: dict = None) -> bool:
3766    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3767        """
3768        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3769
3770        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3771
3772        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3773        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3774        """
3775        result = False
3776        msg = "Instrument not defined!"
3777
3778        if portfolio is None or not portfolio:
3779            portfolio = self.Overview(show=False)
3780
3781        if self._ticker:
3782            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3783            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3784
3785            for instrument in portfolio["stat"]["stopOrders"]:
3786                if instrument["ticker"] == self._ticker:
3787                    result = True
3788                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3789                    break
3790
3791        elif self._figi:
3792            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3793            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3794
3795            for instrument in portfolio["stat"]["stopOrders"]:
3796                if instrument["figi"] == self._figi:
3797                    result = True
3798                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3799                    break
3800
3801        else:
3802            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3803
3804        uLogger.debug(msg)
3805
3806        return result

Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if stop orders list contains some stop orders for the instrument, False otherwise.

def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3808    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3809        """
3810        Returns list with all `orderID`s of opened stop orders for the instrument.
3811        Instrument must be defined by `ticker` (highly priority) or `figi`.
3812
3813        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3814
3815        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3816        :return: list with `orderID`s of stop orders.
3817        """
3818        result = []
3819        msg = "Instrument not defined!"
3820
3821        if portfolio is None or not portfolio:
3822            portfolio = self.Overview(show=False)
3823
3824        if self._ticker:
3825            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3826            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3827
3828            for instrument in portfolio["stat"]["stopOrders"]:
3829                if instrument["ticker"] == self._ticker:
3830                    result.append(instrument["orderID"])
3831
3832            if result:
3833                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3834
3835        elif self._figi:
3836            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3837            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3838
3839            for instrument in portfolio["stat"]["stopOrders"]:
3840                if instrument["figi"] == self._figi:
3841                    result.append(instrument["orderID"])
3842
3843            if result:
3844                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3845
3846        else:
3847            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3848
3849        uLogger.debug(msg)
3850
3851        return result

Returns list with all orderIDs of opened stop orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of stop orders.

def RequestLimits(self) -> dict:
3853    def RequestLimits(self) -> dict:
3854        """
3855        Method for obtaining the available funds for withdrawal for current `accountId`.
3856
3857        See also:
3858        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3859        - `OverviewLimits()` method
3860
3861        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3862                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3863                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3864                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3865        """
3866        if self.accountId is None or not self.accountId:
3867            uLogger.error("Variable `accountId` must be defined for using this method!")
3868            raise Exception("Account ID required")
3869
3870        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3871
3872        self.body = str({"accountId": self.accountId})
3873        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3874        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3875
3876        if self.moreDebug:
3877            uLogger.debug("Records about available funds for withdrawal successfully received")
3878
3879        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False) -> dict:
3881    def OverviewLimits(self, show: bool = False) -> dict:
3882        """
3883        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3884
3885        See also: `RequestLimits()`.
3886
3887        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3888        :return: dict with raw parsed data from server and some calculated statistics about it.
3889        """
3890        if self.accountId is None or not self.accountId:
3891            uLogger.error("Variable `accountId` must be defined for using this method!")
3892            raise Exception("Account ID required")
3893
3894        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3895
3896        view = {
3897            "rawLimits": rawLimits,
3898            "limits": {  # parsed data for every currency:
3899                "money": {  # this is an array of portfolio currency positions
3900                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3901                },
3902                "blocked": {  # this is an array of blocked currency
3903                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3904                },
3905                "blockedGuarantee": {  # this is locked money under collateral for futures
3906                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3907                },
3908            },
3909        }
3910
3911        # --- Prepare text table with limits in human-readable format:
3912        if show:
3913            info = [
3914                "# Withdrawal limits\n\n",
3915                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3916                "* **Account ID:** [{}]\n".format(self.accountId),
3917            ]
3918
3919            if view["limits"]["money"]:
3920                info.extend([
3921                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3922                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3923                ])
3924
3925            else:
3926                info.append("\nNo withdrawal limits\n")
3927
3928            for curr in view["limits"]["money"].keys():
3929                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3930                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3931                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3932
3933                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3934                    "[{}]".format(curr),
3935                    "{:.2f}".format(view["limits"]["money"][curr]),
3936                    "{:.2f}".format(availableMoney),
3937                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3938                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3939                )
3940
3941                if curr == "rub":
3942                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3943
3944                else:
3945                    info.append(infoStr)
3946
3947            infoText = "".join(info)
3948
3949            uLogger.info(infoText)
3950
3951            if self.withdrawalLimitsFile:
3952                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3953                    fH.write(infoText)
3954
3955                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3956
3957                if self.useHTMLReports:
3958                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3959                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3960                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3961
3962                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
3963
3964        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3966    def RequestAccounts(self) -> dict:
3967        """
3968        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3969
3970        See also:
3971        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3972        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3973        - `OverviewUserInfo()` method
3974
3975        :return: dict with raw data from server that contains accounts info. Example of dict:
3976                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3977                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3978                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3979                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3980        """
3981        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3982
3983        self.body = str({})
3984        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3985        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3986
3987        if self.moreDebug:
3988            uLogger.debug("Records about available accounts successfully received")
3989
3990        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
3992    def RequestUserInfo(self) -> dict:
3993        """
3994        Method for requesting common user's information.
3995
3996        See also:
3997        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3998        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3999        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4000        - `OverviewUserInfo()` method
4001
4002        :return: dict with raw data from server that contains user's information. Example of dict:
4003                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4004                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4005        """
4006        uLogger.debug("Requesting common user's information. Wait, please...")
4007
4008        self.body = str({})
4009        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4010        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4011
4012        if self.moreDebug:
4013            uLogger.debug("Records about current user successfully received")
4014
4015        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
4017    def RequestMarginStatus(self, accountId: str = None) -> dict:
4018        """
4019        Method for requesting margin calculation for defined account ID.
4020
4021        See also:
4022        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4023        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4024        - `OverviewUserInfo()` method
4025
4026        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4027        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4028                 Example of responses:
4029                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4030                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4031                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4032                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4033                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4034                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4035        """
4036        if accountId is None or not accountId:
4037            if self.accountId is None or not self.accountId:
4038                uLogger.error("Variable `accountId` must be defined for using this method!")
4039                raise Exception("Account ID required")
4040
4041            else:
4042                accountId = self.accountId  # use `self.accountId` (main ID) by default
4043
4044        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4045
4046        self.body = str({"accountId": accountId})
4047        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4048        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4049
4050        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4051            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4052            rawMargin = {}
4053
4054        else:
4055            if self.moreDebug:
4056                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4057
4058        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
4060    def RequestTariffLimits(self) -> dict:
4061        """
4062        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4063
4064        See also:
4065        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4066        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4067        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4068        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4069        - `OverviewUserInfo()` method
4070
4071        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4072                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4073                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4074        """
4075        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4076
4077        self.body = str({})
4078        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4079        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4080
4081        if self.moreDebug:
4082            uLogger.debug("Records with limits of current tariff successfully received")
4083
4084        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
4086    def RequestBondCoupons(self, iJSON: dict) -> dict:
4087        """
4088        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4089        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4090        All dates are in UTC timezone.
4091
4092        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4093        Documentation:
4094        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4095        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4096
4097        See also: `ExtendBondsData()`.
4098
4099        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4100                      If raw iJSON is not data of bond then server returns an error [400] with message:
4101                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4102        :return: dictionary with bond payment calendar. Response example
4103                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4104                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4105                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4106                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4107        """
4108        if iJSON["figi"] is None or not iJSON["figi"]:
4109            uLogger.error("FIGI must be defined for using this method!")
4110            raise Exception("FIGI required")
4111
4112        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4113        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4114
4115        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4116            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4117            self._figi,
4118            startDate,
4119            endDate,
4120        ))
4121
4122        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4123        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4124        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4125
4126        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4127            uLogger.warning("Instrument type is not bond!")
4128
4129        else:
4130            if self.moreDebug:
4131                uLogger.debug("Records about bond payment calendar successfully received")
4132
4133        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self._ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
4135    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4136        """
4137        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4138        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4139        coupon yields, current yields and some statistics etc.
4140
4141        WARNING! This is too long operation if a lot of bonds requested from broker server.
4142
4143        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4144
4145        :param instruments: list of strings with tickers or FIGIs.
4146        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4147                     for further used by data scientists or stock analytics.
4148        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4149                 In XLSX-file and Pandas DataFrame fields mean:
4150                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4151                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4152        """
4153        if instruments is None or not instruments:
4154            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4155            raise Exception("Ticker or FIGI required")
4156
4157        if isinstance(instruments, str):
4158            instruments = [instruments]
4159
4160        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4161
4162        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4163
4164        iCount = len(uniqueInstruments)
4165        tooLong = iCount >= 20
4166        if tooLong:
4167            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4168
4169        bonds = None
4170        for i, self._figi in enumerate(uniqueInstruments):
4171            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4172
4173            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4174                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4175                rawBond = self.SearchByFIGI(requestPrice=True)
4176
4177                # Widen raw data with UTC current time (iData["actualDateTime"]):
4178                actualDate = datetime.now(tzutc())
4179                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4180
4181                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4182                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4183
4184                # Replace some values with human-readable:
4185                iData["nominalCurrency"] = iData["nominal"]["currency"]
4186                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4187                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4188                iData["aciCurrency"] = iData["aciValue"]["currency"]
4189                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4190                iData["issueSize"] = int(iData["issueSize"])
4191                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4192                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4193                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4194                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4195                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4196                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4197                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4198                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4199                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4200                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4201
4202                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4203                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4204                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4205                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4206                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4207                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4208                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4209                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4210                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4211                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4212                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4213
4214                # Widen raw data with calendar data from `rawCalendar` values:
4215                calendarData = []
4216                if "events" in iData["rawCalendar"].keys():
4217                    for item in iData["rawCalendar"]["events"]:
4218                        calendarData.append({
4219                            "couponDate": item["couponDate"],
4220                            "couponNumber": int(item["couponNumber"]),
4221                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4222                            "payCurrency": item["payOneBond"]["currency"],
4223                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4224                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4225                            "couponStartDate": item["couponStartDate"],
4226                            "couponEndDate": item["couponEndDate"],
4227                            "couponPeriod": item["couponPeriod"],
4228                        })
4229
4230                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4231                    if "maturityDate" not in iData.keys():
4232                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4233
4234                # Widen raw data with Coupon Rate.
4235                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4236                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4237                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4238                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4239
4240                # Widen raw data with Yield to Maturity (YTM) on current date.
4241                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4242                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4243                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4244                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4245                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4246                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4247
4248                iData["calendar"] = calendarData  # adds calendar at the end
4249
4250                # Remove not used data:
4251                iData.pop("uid")
4252                iData.pop("positionUid")
4253                iData.pop("currentPrice")
4254                iData.pop("rawCalendar")
4255
4256                colNames = list(iData.keys())
4257                if bonds is None:
4258                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4259
4260                else:
4261                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4262
4263            else:
4264                uLogger.warning("Instrument is not a bond!")
4265
4266            processed = round(100 * (i + 1) / iCount, 1)
4267            if tooLong and processed % 5 == 0:
4268                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4269
4270            else:
4271                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4272
4273        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4274
4275        # Saving bonds from Pandas DataFrame to XLSX sheet:
4276        if xlsx and self.bondsXLSXFile:
4277            with pd.ExcelWriter(
4278                    path=self.bondsXLSXFile,
4279                    date_format=TKS_DATE_FORMAT,
4280                    datetime_format=TKS_DATE_TIME_FORMAT,
4281                    mode="w",
4282            ) as writer:
4283                bonds.to_excel(
4284                    writer,
4285                    sheet_name="Extended bonds data",
4286                    index=True,
4287                    encoding="UTF-8",
4288                    freeze_panes=(1, 1),
4289                )  # saving as XLSX-file with freeze first row and column as headers
4290
4291            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4292
4293        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
4295    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4296        """
4297        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4298
4299        WARNING! This is too long operation if a lot of bonds requested from broker server.
4300
4301        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4302
4303        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4304                        extended information about bonds: main info, current prices, bond payment calendar,
4305                        coupon yields, current yields and some statistics etc.
4306                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4307        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4308                     for further used by data scientists or stock analytics.
4309        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4310        """
4311        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4312            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4313
4314        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4315
4316        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4317        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4318        calendar = None
4319        for bond in extBonds.iterrows():
4320            for item in bond[1]["calendar"]:
4321                cData = {
4322                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4323                    "couponDate": item["couponDate"],
4324                    "figi": bond[1]["figi"],
4325                    "ticker": bond[1]["ticker"],
4326                    "name": bond[1]["name"],
4327                    "couponNumber": item["couponNumber"],
4328                    "payOneBond": item["payOneBond"],
4329                    "payCurrency": item["payCurrency"],
4330                    "couponType": item["couponType"],
4331                    "couponPeriod": item["couponPeriod"],
4332                    "fixDate": item["fixDate"],
4333                    "couponStartDate": item["couponStartDate"],
4334                    "couponEndDate": item["couponEndDate"],
4335                }
4336
4337                if calendar is None:
4338                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4339
4340                else:
4341                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4342
4343        if calendar is not None:
4344            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4345
4346            # Saving calendar from Pandas DataFrame to XLSX sheet:
4347            if xlsx:
4348                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4349
4350                with pd.ExcelWriter(
4351                        path=xlsxCalendarFile,
4352                        date_format=TKS_DATE_FORMAT,
4353                        datetime_format=TKS_DATE_TIME_FORMAT,
4354                        mode="w",
4355                ) as writer:
4356                    humanReadable = calendar.copy(deep=True)
4357                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4358                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4359                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4360                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4361                    humanReadable.columns = colNames  # human-readable column names
4362
4363                    humanReadable.to_excel(
4364                        writer,
4365                        sheet_name="Bond payments calendar",
4366                        index=False,
4367                        encoding="UTF-8",
4368                        freeze_panes=(1, 2),
4369                    )  # saving as XLSX-file with freeze first row and column as headers
4370
4371                    del humanReadable  # release df in memory
4372
4373                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4374
4375        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar(self, extBonds: pandas.core.frame.DataFrame, show: bool = True) -> str:
4377    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4378        """
4379        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4380        Also, creates Markdown file with calendar data, `calendar.md` by default.
4381
4382        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4383
4384        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4385                        extended information about bonds: main info, current prices, bond payment calendar,
4386                        coupon yields, current yields and some statistics etc.
4387                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4388        :param show: if `True` then also printing bonds payment calendar to the console,
4389                     otherwise save to file `calendarFile` only. `False` by default.
4390        :return: multilines text in Markdown format with bonds payment calendar as a table.
4391        """
4392        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4393            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4394
4395        infoText = "# Bond payments calendar\n\n"
4396
4397        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4398
4399        if not (calendar is None or calendar.empty):
4400            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4401
4402            info = [
4403                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4404                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4405                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4406            ]
4407
4408            newMonth = False
4409            notOneBond = calendar["figi"].nunique() > 1
4410            for i, bond in enumerate(calendar.iterrows()):
4411                if newMonth and notOneBond:
4412                    info.append(splitLine)
4413
4414                info.append(
4415                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4416                        "  √" if bond[1]["paid"] else "  —",
4417                        bond[1]["couponDate"].split("T")[0],
4418                        bond[1]["figi"],
4419                        bond[1]["ticker"],
4420                        bond[1]["couponNumber"],
4421                        "{} {}".format(
4422                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4423                            bond[1]["payCurrency"],
4424                        ),
4425                        bond[1]["couponType"],
4426                        bond[1]["couponPeriod"],
4427                        bond[1]["fixDate"].split("T")[0],
4428                    )
4429                )
4430
4431                if i < len(calendar.values) - 1:
4432                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4433                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4434                    newMonth = False if curDate.month == nextDate.month else True
4435
4436                else:
4437                    newMonth = False
4438
4439            infoText += "".join(info)
4440
4441            if show:
4442                uLogger.info("{}".format(infoText))
4443
4444            if self.calendarFile is not None:
4445                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4446                    fH.write(infoText)
4447
4448                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4449
4450                if self.useHTMLReports:
4451                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4452                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4453                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4454
4455                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4456
4457        else:
4458            infoText += "No data\n"
4459
4460        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False) -> dict:
4462    def OverviewAccounts(self, show: bool = False) -> dict:
4463        """
4464        Method for parsing and show simple table with all available user accounts.
4465
4466        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4467
4468        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4469        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4470                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4471                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4472                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4473                                                        "closed": "—", "access": "Full access" }, ...}}`
4474        """
4475        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4476
4477        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4478        accounts = {
4479            item["id"]: {
4480                "type": TKS_ACCOUNT_TYPES[item["type"]],
4481                "name": item["name"],
4482                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4483                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4484                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4485                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4486            } for item in rawAccounts["accounts"]
4487        }
4488
4489        # Raw and parsed data with some fields replaced in "stat" section:
4490        view = {
4491            "rawAccounts": rawAccounts,
4492            "stat": accounts,
4493        }
4494
4495        # --- Prepare simple text table with only accounts data in human-readable format:
4496        if show:
4497            info = [
4498                "# User accounts\n\n",
4499                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4500                "| Account ID   | Type                      | Status                    | Name                           |\n",
4501                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4502            ]
4503
4504            for account in view["stat"].keys():
4505                info.extend([
4506                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4507                        account,
4508                        view["stat"][account]["type"],
4509                        view["stat"][account]["status"],
4510                        view["stat"][account]["name"],
4511                    )
4512                ])
4513
4514            infoText = "".join(info)
4515
4516            uLogger.info(infoText)
4517
4518            if self.userAccountsFile:
4519                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4520                    fH.write(infoText)
4521
4522                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4523
4524                if self.useHTMLReports:
4525                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4526                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4527                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4528
4529                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4530
4531        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False) -> dict:
4533    def OverviewUserInfo(self, show: bool = False) -> dict:
4534        """
4535        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4536
4537        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4538
4539        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4540        :return: dict with raw parsed data from server and some calculated statistics about it.
4541        """
4542        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4543        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4544        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4545        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4546        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4547        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4548
4549        # This is dict with parsed common user data:
4550        userInfo = {
4551            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4552            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4553            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4554            "tariff": rawUserInfo["tariff"],
4555        }
4556
4557        # This is an array of dict with parsed margin statuses for every account IDs:
4558        margins = {}
4559        for accountId in accounts.keys():
4560            if rawMargins[accountId]:
4561                margins[accountId] = {
4562                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4563                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4564                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4565                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4566                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4567                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4568                }
4569
4570            else:
4571                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4572
4573        unary = {}  # unary-connection limits
4574        for item in rawTariffLimits["unaryLimits"]:
4575            if item["limitPerMinute"] in unary.keys():
4576                unary[item["limitPerMinute"]].extend(item["methods"])
4577
4578            else:
4579                unary[item["limitPerMinute"]] = item["methods"]
4580
4581        stream = {}  # stream-connection limits
4582        for item in rawTariffLimits["streamLimits"]:
4583            if item["limit"] in stream.keys():
4584                stream[item["limit"]].extend(item["streams"])
4585
4586            else:
4587                stream[item["limit"]] = item["streams"]
4588
4589        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4590        limits = {
4591            "unary": unary,
4592            "stream": stream,
4593        }
4594
4595        # Raw and parsed data as an output result:
4596        view = {
4597            "rawUserInfo": rawUserInfo,
4598            "rawAccounts": rawAccounts,
4599            "rawMargins": rawMargins,
4600            "rawTariffLimits": rawTariffLimits,
4601            "stat": {
4602                "userInfo": userInfo,
4603                "accounts": accounts,
4604                "margins": margins,
4605                "limits": limits,
4606            },
4607        }
4608
4609        # --- Prepare text table with user information in human-readable format:
4610        if show:
4611            info = [
4612                "# Full user information\n\n",
4613                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4614                "## Common information\n\n",
4615                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4616                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4617                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4618                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4619                "\n## User accounts\n\n",
4620            ]
4621
4622            for account in view["stat"]["accounts"].keys():
4623                info.extend([
4624                    "### ID: [{}]\n\n".format(account),
4625                    "| Parameters           | Values                                                       |\n",
4626                    "|----------------------|--------------------------------------------------------------|\n",
4627                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4628                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4629                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4630                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4631                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4632                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4633                ])
4634
4635                if margins[account]:
4636                    info.extend([
4637                        "| Margin status:       | Enabled                                                      |\n",
4638                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4639                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4640                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4641                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4642                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4643                    ])
4644
4645                else:
4646                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4647
4648            info.extend([
4649                "\n## Current user tariff limits\n",
4650                "\n### See also\n",
4651                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4652                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4653                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4654                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4655                "\n### Unary limits\n",
4656            ])
4657
4658            if unary:
4659                for key, values in sorted(unary.items()):
4660                    info.append("\n* Max requests per minute: {}\n".format(key))
4661
4662                    for value in values:
4663                        info.append("  - {}\n".format(value))
4664
4665            else:
4666                info.append("\nNot available\n")
4667
4668            info.append("\n### Stream limits\n")
4669
4670            if stream:
4671                for key, values in sorted(stream.items()):
4672                    info.append("\n* Max stream connections: {}\n".format(key))
4673
4674                    for value in values:
4675                        info.append("  - {}\n".format(value))
4676
4677            else:
4678                info.append("\nNot available\n")
4679
4680            infoText = "".join(info)
4681
4682            uLogger.info(infoText)
4683
4684            if self.userInfoFile:
4685                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4686                    fH.write(infoText)
4687
4688                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4689
4690                if self.useHTMLReports:
4691                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4692                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4693                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4694
4695                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4696
4697        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4700class Args:
4701    """
4702    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4703    """
4704    def __init__(self, **kwargs):
4705        self.__dict__.update(kwargs)
4706
4707    def __getattr__(self, item):
4708        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4704    def __init__(self, **kwargs):
4705        self.__dict__.update(kwargs)
def ParseArgs():
4711def ParseArgs():
4712    """This function get and parse command line keys."""
4713    parser = ArgumentParser()  # command-line string parser
4714
4715    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4716    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4717
4718    # --- options:
4719
4720    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4721    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4722    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4723
4724    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4725    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4726
4727    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4728    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4729
4730    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4731    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4732
4733    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4734    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4735    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4736
4737    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4738    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4739
4740    # --- commands:
4741
4742    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4743
4744    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4745    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4746    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4747    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4748    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4749    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4750    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4751    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4752
4753    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4754    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4755    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4756    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4757    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4758    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4759
4760    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4761    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4762    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4763    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4764
4765    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4766    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4767    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4768
4769    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4770    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4771    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4772    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4773    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4774    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4775    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4776
4777    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4778    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4779    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4780    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4781    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4782
4783    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4784    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4785    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4786
4787    cmdArgs = parser.parse_args()
4788    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs):
4791def Main(**kwargs):
4792    """
4793    Main function for work with TKSBrokerAPI in the console.
4794
4795    See examples:
4796    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4797    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4798    """
4799    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4800
4801    if args.debug_level:
4802        uLogger.level = 10  # always debug level by default
4803        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4804
4805    exitCode = 0
4806    start = datetime.now(tzutc())
4807    uLogger.debug("=-" * 50)
4808    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4809        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4810        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4811    ))
4812
4813    # trying to calculate full current version:
4814    buildVersion = __version__
4815    try:
4816        v = version("tksbrokerapi")
4817        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4818
4819    except Exception:
4820        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4821
4822    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4823    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4824
4825    try:
4826        if args.version:
4827            print("TKSBrokerAPI {}".format(buildVersion))
4828            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4829
4830        else:
4831            # Init class for trading with Tinkoff Broker:
4832            trader = TinkoffBrokerServer(
4833                token=args.token,
4834                accountId=args.account_id,
4835                useCache=not args.no_cache,
4836            )
4837
4838            # --- set some options:
4839
4840            if args.more:
4841                trader.moreDebug = True
4842                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4843
4844            if args.html:
4845                trader.useHTMLReports = True
4846
4847            if args.ticker:
4848                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4849
4850                if ticker in trader.aliasesKeys:
4851                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4852
4853                else:
4854                    trader.ticker = ticker
4855
4856            if args.figi:
4857                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4858
4859            if args.depth is not None:
4860                trader.depth = args.depth
4861
4862            # --- do one command:
4863
4864            if args.list:
4865                if args.output is not None:
4866                    trader.instrumentsFile = args.output
4867
4868                trader.ShowInstrumentsInfo(show=True)
4869
4870            elif args.list_xlsx:
4871                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4872
4873            elif args.bonds_xlsx is not None:
4874                if args.output is not None:
4875                    trader.bondsXLSXFile = args.output
4876
4877                if len(args.bonds_xlsx) == 0:
4878                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4879
4880                else:
4881                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4882
4883            elif args.search:
4884                if args.output is not None:
4885                    trader.searchResultsFile = args.output
4886
4887                trader.SearchInstruments(pattern=args.search[0], show=True)
4888
4889            elif args.info:
4890                if not (args.ticker or args.figi):
4891                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4892                    raise Exception("Ticker or FIGI required")
4893
4894                if args.output is not None:
4895                    trader.infoFile = args.output
4896
4897                if args.ticker:
4898                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4899
4900                else:
4901                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4902
4903            elif args.calendar is not None:
4904                if args.output is not None:
4905                    trader.calendarFile = args.output
4906
4907                if len(args.calendar) == 0:
4908                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4909
4910                else:
4911                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4912
4913                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4914
4915            elif args.price:
4916                if not (args.ticker or args.figi):
4917                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4918                    raise Exception("Ticker or FIGI required")
4919
4920                trader.GetCurrentPrices(show=True)
4921
4922            elif args.prices is not None:
4923                if args.output is not None:
4924                    trader.pricesFile = args.output
4925
4926                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4927
4928            elif args.overview:
4929                if args.output is not None:
4930                    trader.overviewFile = args.output
4931
4932                trader.Overview(show=True, details="full")
4933
4934            elif args.overview_digest:
4935                if args.output is not None:
4936                    trader.overviewDigestFile = args.output
4937
4938                trader.Overview(show=True, details="digest")
4939
4940            elif args.overview_positions:
4941                if args.output is not None:
4942                    trader.overviewPositionsFile = args.output
4943
4944                trader.Overview(show=True, details="positions")
4945
4946            elif args.overview_orders:
4947                if args.output is not None:
4948                    trader.overviewOrdersFile = args.output
4949
4950                trader.Overview(show=True, details="orders")
4951
4952            elif args.overview_analytics:
4953                if args.output is not None:
4954                    trader.overviewAnalyticsFile = args.output
4955
4956                trader.Overview(show=True, details="analytics")
4957
4958            elif args.overview_calendar:
4959                if args.output is not None:
4960                    trader.overviewAnalyticsFile = args.output
4961
4962                trader.Overview(show=True, details="calendar")
4963
4964            elif args.deals is not None:
4965                if args.output is not None:
4966                    trader.reportFile = args.output
4967
4968                if 0 <= len(args.deals) < 3:
4969                    trader.Deals(
4970                        start=args.deals[0] if len(args.deals) >= 1 else None,
4971                        end=args.deals[1] if len(args.deals) == 2 else None,
4972                        show=True,  # Always show deals report in console
4973                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4974                    )
4975
4976                else:
4977                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4978                    raise Exception("Incorrect value")
4979
4980            elif args.history is not None:
4981                if args.output is not None:
4982                    trader.historyFile = args.output
4983
4984                if 0 <= len(args.history) < 3:
4985                    dataReceived = trader.History(
4986                        start=args.history[0] if len(args.history) >= 1 else None,
4987                        end=args.history[1] if len(args.history) == 2 else None,
4988                        interval="hour" if args.interval is None or not args.interval else args.interval,
4989                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4990                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4991                        show=True,  # shows all downloaded candles in console
4992                    )
4993
4994                    if args.render_chart is not None and dataReceived is not None:
4995                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4996
4997                        trader.ShowHistoryChart(
4998                            candles=dataReceived,
4999                            interact=iChart,
5000                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5001                        )
5002
5003                else:
5004                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5005                    raise Exception("Incorrect value")
5006
5007            elif args.load_history is not None:
5008                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5009
5010                if args.render_chart is not None and histData is not None:
5011                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5012                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5013
5014                    trader.ShowHistoryChart(
5015                        candles=histData,
5016                        interact=iChart,
5017                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5018                    )
5019
5020            elif args.trade is not None:
5021                if 1 <= len(args.trade) <= 5:
5022                    trader.Trade(
5023                        operation=args.trade[0],
5024                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5025                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5026                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5027                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5028                    )
5029
5030                else:
5031                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5032
5033            elif args.buy is not None:
5034                if 0 <= len(args.buy) <= 4:
5035                    trader.Buy(
5036                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5037                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5038                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5039                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5040                    )
5041
5042                else:
5043                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5044
5045            elif args.sell is not None:
5046                if 0 <= len(args.sell) <= 4:
5047                    trader.Sell(
5048                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5049                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5050                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5051                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5052                    )
5053
5054                else:
5055                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5056
5057            elif args.order:
5058                if 4 <= len(args.order) <= 7:
5059                    trader.Order(
5060                        operation=args.order[0],
5061                        orderType=args.order[1],
5062                        lots=int(args.order[2]),
5063                        targetPrice=float(args.order[3]),
5064                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5065                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5066                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5067                    )
5068
5069                else:
5070                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5071
5072            elif args.buy_limit:
5073                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5074
5075            elif args.sell_limit:
5076                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5077
5078            elif args.buy_stop:
5079                if 2 <= len(args.buy_stop) <= 7:
5080                    trader.BuyStop(
5081                        lots=int(args.buy_stop[0]),
5082                        targetPrice=float(args.buy_stop[1]),
5083                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5084                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5085                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5086                    )
5087
5088                else:
5089                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5090
5091            elif args.sell_stop:
5092                if 2 <= len(args.sell_stop) <= 7:
5093                    trader.SellStop(
5094                        lots=int(args.sell_stop[0]),
5095                        targetPrice=float(args.sell_stop[1]),
5096                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5097                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5098                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5099                    )
5100
5101                else:
5102                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5103
5104            # elif args.buy_order_grid is not None:
5105            #     # update order grid work with api v2
5106            #     if len(args.buy_order_grid) == 2:
5107            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5108            #
5109            #         for order in orderParams:
5110            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5111            #
5112            #     else:
5113            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5114            #
5115            # elif args.sell_order_grid is not None:
5116            #     # update order grid work with api v2
5117            #     if len(args.sell_order_grid) >= 2:
5118            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5119            #
5120            #         for order in orderParams:
5121            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5122            #
5123            #     else:
5124            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5125
5126            elif args.close_order is not None:
5127                trader.CloseOrders(args.close_order)  # close only one order
5128
5129            elif args.close_orders is not None:
5130                trader.CloseOrders(args.close_orders)  # close list of orders
5131
5132            elif args.close_trade:
5133                if not (args.ticker or args.figi):
5134                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5135                    raise Exception("Ticker or FIGI required")
5136
5137                if args.ticker:
5138                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5139
5140                else:
5141                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5142
5143            elif args.close_trades is not None:
5144                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5145
5146            elif args.close_all is not None:
5147                if args.ticker:
5148                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5149
5150                elif args.figi:
5151                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5152
5153                else:
5154                    trader.CloseAll(*args.close_all)
5155
5156            elif args.limits:
5157                if args.output is not None:
5158                    trader.withdrawalLimitsFile = args.output
5159
5160                trader.OverviewLimits(show=True)
5161
5162            elif args.user_info:
5163                if args.output is not None:
5164                    trader.userInfoFile = args.output
5165
5166                trader.OverviewUserInfo(show=True)
5167
5168            elif args.account:
5169                if args.output is not None:
5170                    trader.userAccountsFile = args.output
5171
5172                trader.OverviewAccounts(show=True)
5173
5174            else:
5175                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5176                raise Exception("There is no command to execute")
5177
5178    except Exception:
5179        trace = tb.format_exc()
5180        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5181            if e in trace:
5182                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5183                break
5184
5185        uLogger.debug(trace)
5186        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5187        exitCode = 255  # an error occurred, must be open a ticket for this issue
5188
5189    finally:
5190        finish = datetime.now(tzutc())
5191
5192        if exitCode == 0:
5193            if args.more:
5194                uLogger.debug("All operations were finished success (summary code is 0).")
5195
5196        else:
5197            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5198                os.path.abspath(uLog.defaultLogFile), exitCode,
5199            ))
5200
5201        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5202        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5203            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5204            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5205        ))
5206        uLogger.debug("=-" * 50)
5207
5208        if not kwargs:
5209            sys.exit(exitCode)
5210
5211        else:
5212            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: