TKSBrokerAPI

Форк
0
/
TKSBrokerAPI.py 
5215 строк · 294.6 Кб
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,
6
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
7
from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
8

9
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
10
the 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

35
import sys
36
import os
37
from argparse import ArgumentParser
38
from importlib.metadata import version
39

40
from dateutil.tz import tzlocal
41
from time import sleep
42

43
import re
44
import json
45
import requests
46
import traceback as tb
47
from typing import Union
48

49
from multiprocessing import cpu_count
50
from multiprocessing.pool import ThreadPool
51
import pandas as pd
52

53
from mako.template import Template  # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
54
from Templates import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
55
from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
56
from TradeRoutines import *  # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module
57

58
from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator)
59
from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
60

61
import UniLogger as uLog  # Logger for TKSBrokerAPI
62

63

64
# --- Common technical parameters:
65

66
PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
67
uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
68
uLogger.level = 10  # debug level by default for TKSBrokerAPI module
69
uLogger.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

73
CPU_COUNT = cpu_count()  # host's real CPU count
74
CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
75

76

77
class 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

4699
class 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

4710
def 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

4790
def 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

5214
if __name__ == "__main__":
5215
    Main()
5216

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.