tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import
.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See CLI examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from dateutil.tz import tzlocal 41from time import sleep 42 43import re 44import json 45import requests 46import traceback as tb 47from typing import Union 48 49from multiprocessing import cpu_count 50from multiprocessing.pool import ThreadPool 51import pandas as pd 52 53from mako.template import Template # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 54from Templates import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 55from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 56from TradeRoutines import * # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module 57 58from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator) 59from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 60 61import UniLogger as uLog # Logger for TKSBrokerAPI 62 63 64# --- Common technical parameters: 65 66PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 67uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 68uLogger.level = 10 # debug level by default for TKSBrokerAPI module 69uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 70 71__version__ = "1.5" # The "major.minor" version setup here, but build number define at the build-server only 72 73CPU_COUNT = cpu_count() # host's real CPU count 74CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 75 76 77class TinkoffBrokerServer: 78 """ 79 This class implements methods to work with Tinkoff broker server. 80 81 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 82 83 About `token`: https://tinkoff.github.io/investAPI/token/ 84 """ 85 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 86 """ 87 Main class init. 88 89 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 90 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 91 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 92 :param useCache: use default cache file with raw data to use instead of `iList`. 93 True by default. Cache is auto-update if new day has come. 94 If you don't want to use cache and always updates raw data then set `useCache=False`. 95 :param defaultCache: path to default cache file. `dump.json` by default. 96 """ 97 if token is None or not token: 98 try: 99 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 100 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 101 102 except KeyError: 103 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 104 raise Exception("Token required") 105 106 else: 107 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 108 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 109 110 if accountId is None or not accountId: 111 try: 112 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 113 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 114 115 except KeyError: 116 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 117 118 else: 119 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 120 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 121 122 self.version = __version__ # duplicate here used TKSBrokerAPI main version 123 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 124 125 Latest version: https://pypi.org/project/tksbrokerapi/ 126 """ 127 128 self.aliases = TKS_TICKER_ALIASES 129 """Some aliases instead official tickers. 130 131 See also: `TKSEnums.TKS_TICKER_ALIASES` 132 """ 133 134 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 135 136 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 137 138 self._ticker = "" 139 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 140 141 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 142 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 143 144 See also: `SearchByTicker()`, `SearchInstruments()`. 145 """ 146 147 self._figi = "" 148 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 149 150 See also: `SearchByFIGI()`, `SearchInstruments()`. 151 """ 152 153 self.depth = 1 154 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 155 156 See also: `GetCurrentPrices()`. 157 """ 158 159 self.server = r"https://invest-public-api.tinkoff.ru/rest" 160 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 161 162 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 163 """ 164 165 uLogger.debug("Broker API server: {}".format(self.server)) 166 167 self.timeout = 15 168 """Server operations timeout in seconds. Default: `15`. 169 170 See also: `SendAPIRequest()`. 171 """ 172 173 self.headers = { 174 "Content-Type": "application/json", 175 "accept": "application/json", 176 "Authorization": "Bearer {}".format(self.token), 177 "x-app-name": "Tim55667757.TKSBrokerAPI", 178 } 179 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 180 181 See also: `SendAPIRequest()`. 182 """ 183 184 self.body = None 185 """Request body which send to broker server. Default: `None`. 186 187 See also: `SendAPIRequest()`. 188 """ 189 190 self.moreDebug = False 191 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 192 193 self.useHTMLReports = False 194 """ 195 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 196 197 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 198 """ 199 200 self.historyFile = None 201 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 202 203 See also: `History()`. 204 """ 205 206 self.htmlHistoryFile = "index.html" 207 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 208 209 See also: `ShowHistoryChart()`. 210 """ 211 212 self.instrumentsFile = "instruments.md" 213 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 214 215 See also: `ShowInstrumentsInfo()`. 216 """ 217 218 self.searchResultsFile = "search-results.md" 219 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 220 221 See also: `SearchInstruments()`. 222 """ 223 224 self.pricesFile = "prices.md" 225 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 226 227 See also: `GetListOfPrices()`. 228 """ 229 230 self.infoFile = "info.md" 231 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 232 233 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 234 """ 235 236 self.bondsXLSXFile = "ext-bonds.xlsx" 237 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 238 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 239 240 See also: `ExtendBondsData()`. 241 """ 242 243 self.calendarFile = "calendar.md" 244 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 245 246 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 247 248 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 249 """ 250 251 self.overviewFile = "overview.md" 252 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 253 254 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 255 """ 256 257 self.overviewDigestFile = "overview-digest.md" 258 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 259 260 See also: `Overview()` with parameter `details="digest"`. 261 """ 262 263 self.overviewPositionsFile = "overview-positions.md" 264 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 265 266 See also: `Overview()` with parameter `details="positions"`. 267 """ 268 269 self.overviewOrdersFile = "overview-orders.md" 270 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 271 272 See also: `Overview()` with parameter `details="orders"`. 273 """ 274 275 self.overviewAnalyticsFile = "overview-analytics.md" 276 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 277 278 See also: `Overview()` with parameter `details="analytics"`. 279 """ 280 281 self.overviewBondsCalendarFile = "overview-calendar.md" 282 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 283 284 See also: `Overview()` with parameter `details="calendar"`. 285 """ 286 287 self.reportFile = "deals.md" 288 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 289 290 See also: `Deals()`. 291 """ 292 293 self.withdrawalLimitsFile = "limits.md" 294 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 295 296 See also: `OverviewLimits()` and `RequestLimits()`. 297 """ 298 299 self.userInfoFile = "user-info.md" 300 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 301 302 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 303 """ 304 305 self.userAccountsFile = "accounts.md" 306 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 307 308 See also: `OverviewAccounts()`, `RequestAccounts()`. 309 """ 310 311 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 312 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 313 314 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 315 316 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 317 """ 318 319 self.iList = None # init iList for raw instruments data 320 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 321 322 See also: `Listing()`, `DumpInstruments()`. 323 """ 324 325 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 326 if useCache: 327 if os.path.exists(self.iListDumpFile): 328 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 329 curTime = datetime.now(tzutc()) 330 331 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 332 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 333 334 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 335 336 else: 337 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 338 339 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 340 os.path.abspath(self.iListDumpFile), 341 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 342 )) 343 344 else: 345 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 346 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 347 348 else: 349 self.iList = self.Listing() # request new raw instruments data from broker server 350 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 351 352 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 353 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 354 355 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 356 """ 357 358 @property 359 def ticker(self) -> str: 360 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 361 362 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 363 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 364 365 See also: `SearchByTicker()`, `SearchInstruments()`. 366 """ 367 return self._ticker 368 369 @ticker.setter 370 def ticker(self, value): 371 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 372 373 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 374 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 375 376 See also: `SearchByTicker()`, `SearchInstruments()`. 377 """ 378 self._ticker = str(value).upper() # Tickers may be upper case only 379 380 @property 381 def figi(self) -> str: 382 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 383 384 See also: `SearchByFIGI()`, `SearchInstruments()`. 385 """ 386 return self._figi 387 388 @figi.setter 389 def figi(self, value): 390 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 391 392 See also: `SearchByFIGI()`, `SearchInstruments()`. 393 """ 394 self._figi = str(value).upper() # FIGI may be upper case only 395 396 def _ParseJSON(self, rawData="{}") -> dict: 397 """ 398 Parse JSON from response string. 399 400 :param rawData: this is a string with JSON-formatted text. 401 :return: JSON (dictionary), parsed from server response string. 402 """ 403 responseJSON = json.loads(rawData) if rawData else {} 404 405 if self.moreDebug: 406 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 407 408 return responseJSON 409 410 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 411 """ 412 Send GET or POST request to broker server and receive JSON object. 413 414 self.header: must be defining with dictionary of headers. 415 self.body: if define then used as request body. None by default. 416 self.timeout: global request timeout, 15 seconds by default. 417 :param url: url with REST request. 418 :param reqType: send "GET" or "POST" request. "GET" by default. 419 :param retry: how many times retry after first request if an 5xx server errors occurred. 420 :param pause: sleep time in seconds between retries. 421 :return: response JSON (dictionary) from broker. 422 """ 423 if reqType.upper() not in ("GET", "POST"): 424 uLogger.error("You can define request type: `GET` or `POST`!") 425 raise Exception("Incorrect value") 426 427 if self.moreDebug: 428 uLogger.debug("Request parameters:") 429 uLogger.debug(" - REST API URL: {}".format(url)) 430 uLogger.debug(" - request type: {}".format(reqType)) 431 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 432 uLogger.debug(" - body:\n{}".format(self.body)) 433 434 # fast hack to avoid all operations with some tickers/FIGI 435 responseJSON = {} 436 oK = True 437 for item in self.exclude: 438 if item in url: 439 if self.moreDebug: 440 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 441 442 oK = False 443 break 444 445 if oK: 446 counter = 0 447 response = None 448 errMsg = "" 449 450 while not response and counter <= retry: 451 if reqType == "GET": 452 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 453 454 if reqType == "POST": 455 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 456 457 if self.moreDebug: 458 uLogger.debug("Response:") 459 uLogger.debug(" - status code: {}".format(response.status_code)) 460 uLogger.debug(" - reason: {}".format(response.reason)) 461 uLogger.debug(" - body length: {}".format(len(response.text))) 462 uLogger.debug(" - headers:\n{}".format(response.headers)) 463 464 # Server returns some headers: 465 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 466 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 467 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 468 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 469 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 470 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 471 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 472 sleep(rateLimitWait) 473 474 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 475 if 400 <= response.status_code < 500: 476 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 477 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 478 479 if "code" in response.text and "message" in response.text: 480 msgDict = self._ParseJSON(rawData=response.text) 481 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 482 483 counter = retry + 1 # do not retry for 4xx errors 484 485 if 500 <= response.status_code < 600: 486 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 487 uLogger.debug(" - not oK, {}".format(errMsg)) 488 489 if "code" in response.text and "message" in response.text: 490 errMsgDict = self._ParseJSON(rawData=response.text) 491 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 492 493 counter += 1 494 495 if counter <= retry: 496 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 497 sleep(pause) 498 499 responseJSON = self._ParseJSON(rawData=response.text) 500 501 if errMsg: 502 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 503 uLogger.error(" - not oK, {}".format(errMsg)) 504 505 return responseJSON 506 507 def _IUpdater(self, iType: str) -> tuple: 508 """ 509 Request instrument by type from server. See available API methods for instruments: 510 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 511 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 512 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 513 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 514 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 515 516 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 517 :return: tuple with iType name and list of available instruments of current type for defined user token. 518 """ 519 result = [] 520 521 if iType in TKS_INSTRUMENTS: 522 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 523 524 # all instruments have the same body in API v2 requests: 525 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 526 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 527 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 528 529 return iType, result 530 531 def _IWrapper(self, kwargs): 532 """ 533 Wrapper runs instrument's update method `_IUpdater()`. 534 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 535 """ 536 return self._IUpdater(**kwargs) 537 538 def Listing(self) -> dict: 539 """ 540 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 541 542 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 543 """ 544 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 545 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 546 547 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 548 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 549 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 550 551 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 552 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 553 poolUpdater.close() 554 555 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 556 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 557 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 558 559 # calculate minimum price increment (step) for all instruments and set up instrument's type: 560 for iType in iList.keys(): 561 for ticker in iList[iType]: 562 iList[iType][ticker]["type"] = iType 563 564 if "minPriceIncrement" in iList[iType][ticker].keys(): 565 iList[iType][ticker]["step"] = NanoToFloat( 566 iList[iType][ticker]["minPriceIncrement"]["units"], 567 iList[iType][ticker]["minPriceIncrement"]["nano"], 568 ) 569 570 else: 571 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 572 573 return iList 574 575 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 576 """ 577 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 578 579 See also: `DumpInstruments()`, `Listing()`. 580 581 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 582 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 583 """ 584 if self.iListDumpFile is None or not self.iListDumpFile: 585 uLogger.error("Output name of dump file must be defined!") 586 raise Exception("Filename required") 587 588 if not self.iList or forceUpdate: 589 self.iList = self.Listing() 590 591 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 592 593 # Save as XLSX with separated sheets for every type of instruments: 594 with pd.ExcelWriter( 595 path=xlsxDumpFile, 596 date_format=TKS_DATE_FORMAT, 597 datetime_format=TKS_DATE_TIME_FORMAT, 598 mode="w", 599 ) as writer: 600 for iType in TKS_INSTRUMENTS: 601 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 602 df = df[sorted(df)] # sorted by column names 603 df = df.applymap( 604 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 605 na_action="ignore", 606 ) # converting numbers from nano-type to float in every cell 607 df.to_excel( 608 writer, 609 sheet_name=iType, 610 encoding="UTF-8", 611 freeze_panes=(1, 1), 612 ) # saving as XLSX-file with freeze first row and column as headers 613 614 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 615 616 def DumpInstruments(self, forceUpdate: bool = True) -> str: 617 """ 618 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 619 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 620 621 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 622 623 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 624 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 625 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 626 """ 627 if self.iListDumpFile is None or not self.iListDumpFile: 628 uLogger.error("Output name of dump file must be defined!") 629 raise Exception("Filename required") 630 631 if not self.iList or forceUpdate: 632 self.iList = self.Listing() 633 634 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 635 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 636 fH.write(jsonDump) 637 638 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 639 640 return jsonDump 641 642 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 643 """ 644 Show information about one instrument defined by json data and prints it in Markdown format. 645 646 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 647 648 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 649 :param show: if `True` then also printing information about instrument and its current price. 650 :return: multilines text in Markdown format with information about one instrument. 651 """ 652 splitLine = "| | |\n" 653 infoText = "" 654 655 if iJSON is not None and iJSON and isinstance(iJSON, dict): 656 info = [ 657 "# Main information\n\n", 658 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 659 "| Parameters | Values |\n", 660 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 661 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 662 "| Full name: | {:<54} |\n".format(iJSON["name"]), 663 ] 664 665 if "sector" in iJSON.keys() and iJSON["sector"]: 666 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 667 668 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 669 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 670 671 info.extend([ 672 splitLine, 673 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 674 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 675 ]) 676 677 if "isin" in iJSON.keys() and iJSON["isin"]: 678 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 679 680 if "classCode" in iJSON.keys(): 681 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 682 683 info.extend([ 684 splitLine, 685 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 686 splitLine, 687 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 688 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 689 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 690 ]) 691 692 if iJSON["figi"]: 693 self._figi = iJSON["figi"] 694 iJSON = iJSON | self.RequestTradingStatus() 695 696 info.extend([ 697 splitLine, 698 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 699 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 700 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 701 ]) 702 703 info.append(splitLine) 704 705 if "type" in iJSON.keys() and iJSON["type"]: 706 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 707 708 if "shareType" in iJSON.keys() and iJSON["shareType"]: 709 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 710 711 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 712 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 713 714 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 715 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 716 717 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 718 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 719 720 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 721 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 722 723 if "focusType" in iJSON.keys() and iJSON["focusType"]: 724 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 725 726 if "assetType" in iJSON.keys() and iJSON["assetType"]: 727 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 728 729 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 730 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 731 732 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 733 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 734 735 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 736 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 737 738 if "currency" in iJSON.keys(): 739 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 740 741 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 742 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 743 744 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 745 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 746 747 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 748 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 749 750 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 751 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 752 753 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 754 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 755 756 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 757 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 758 759 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 760 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 761 762 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 763 info.append("| Perpetual bond: | Yes |\n") 764 765 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 766 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 767 768 iExt = None 769 if iJSON["type"] == "Bonds": 770 info.extend([ 771 splitLine, 772 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 773 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 774 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 775 iJSON["nominal"]["currency"], 776 )), 777 ]) 778 779 if "floatingCouponFlag" in iJSON.keys(): 780 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 781 782 if "amortizationFlag" in iJSON.keys(): 783 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 784 785 info.append(splitLine) 786 787 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 788 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 789 790 if iJSON["figi"]: 791 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 792 793 info.extend([ 794 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 795 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 796 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 797 ]) 798 799 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 800 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 801 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 802 iJSON["aciValue"]["currency"] 803 ))) 804 805 if "currentPrice" in iJSON.keys(): 806 info.append(splitLine) 807 808 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 809 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 810 811 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 812 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 813 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 814 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 815 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 816 817 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 818 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 819 820 info.extend([ 821 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 822 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 823 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 824 )), 825 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 826 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 827 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 828 )), 829 "| Changes between last deal price and last close | {:<54} |\n".format( 830 "{:.2f}%{}".format( 831 iJSON["currentPrice"]["changes"], 832 " ({}{:.2f} {})".format( 833 "+" if bondChangesDelta > 0 else "", 834 bondChangesDelta, 835 aciCurrency 836 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 837 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 838 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 839 currency 840 ), 841 ) 842 ), 843 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 844 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 845 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 846 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 847 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 848 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 849 )), 850 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 851 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 852 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 853 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 854 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 855 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 856 )), 857 ]) 858 859 if "lot" in iJSON.keys(): 860 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 861 862 if "step" in iJSON.keys() and iJSON["step"] != 0: 863 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 864 865 # Add bond payment calendar: 866 if iJSON["type"] == "Bonds": 867 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 868 info.extend(["\n#", strCalendar]) 869 870 infoText += "".join(info) 871 872 if show: 873 uLogger.info("{}".format(infoText)) 874 875 else: 876 uLogger.debug("{}".format(infoText)) 877 878 if self.infoFile is not None: 879 with open(self.infoFile, "w", encoding="UTF-8") as fH: 880 fH.write(infoText) 881 882 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 883 884 if self.useHTMLReports: 885 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 886 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 887 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 888 889 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 890 891 return infoText 892 893 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 894 """ 895 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 896 897 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 898 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 899 :return: JSON formatted data with information about instrument. 900 """ 901 tickerJSON = {} 902 if self.moreDebug: 903 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 904 905 if not self._ticker: 906 uLogger.warning("self._ticker variable is not be empty!") 907 908 else: 909 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 910 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 911 raise Exception("Instrument not allowed") 912 913 if not self.iList: 914 self.iList = self.Listing() 915 916 if self._ticker in self.iList["Shares"].keys(): 917 tickerJSON = self.iList["Shares"][self._ticker] 918 if self.moreDebug: 919 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 920 921 elif self._ticker in self.iList["Currencies"].keys(): 922 tickerJSON = self.iList["Currencies"][self._ticker] 923 if self.moreDebug: 924 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 925 926 elif self._ticker in self.iList["Bonds"].keys(): 927 tickerJSON = self.iList["Bonds"][self._ticker] 928 if self.moreDebug: 929 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 930 931 elif self._ticker in self.iList["Etfs"].keys(): 932 tickerJSON = self.iList["Etfs"][self._ticker] 933 if self.moreDebug: 934 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 935 936 elif self._ticker in self.iList["Futures"].keys(): 937 tickerJSON = self.iList["Futures"][self._ticker] 938 if self.moreDebug: 939 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 940 941 if tickerJSON: 942 self._figi = tickerJSON["figi"] 943 944 if requestPrice: 945 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 946 947 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 948 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 949 950 else: 951 tickerJSON["currentPrice"]["changes"] = 0 952 953 if show: 954 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 955 956 else: 957 if show: 958 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 959 960 return tickerJSON 961 962 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 963 """ 964 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 965 966 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 967 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 968 :return: JSON formatted data with information about instrument. 969 """ 970 figiJSON = {} 971 if self.moreDebug: 972 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 973 974 if not self._figi: 975 uLogger.warning("self._figi variable is not be empty!") 976 977 else: 978 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 979 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 980 raise Exception("Instrument not allowed") 981 982 if not self.iList: 983 self.iList = self.Listing() 984 985 for item in self.iList["Shares"].keys(): 986 if self._figi == self.iList["Shares"][item]["figi"]: 987 figiJSON = self.iList["Shares"][item] 988 989 if self.moreDebug: 990 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 991 992 break 993 994 if not figiJSON: 995 for item in self.iList["Currencies"].keys(): 996 if self._figi == self.iList["Currencies"][item]["figi"]: 997 figiJSON = self.iList["Currencies"][item] 998 999 if self.moreDebug: 1000 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1001 1002 break 1003 1004 if not figiJSON: 1005 for item in self.iList["Bonds"].keys(): 1006 if self._figi == self.iList["Bonds"][item]["figi"]: 1007 figiJSON = self.iList["Bonds"][item] 1008 1009 if self.moreDebug: 1010 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1011 1012 break 1013 1014 if not figiJSON: 1015 for item in self.iList["Etfs"].keys(): 1016 if self._figi == self.iList["Etfs"][item]["figi"]: 1017 figiJSON = self.iList["Etfs"][item] 1018 1019 if self.moreDebug: 1020 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1021 1022 break 1023 1024 if not figiJSON: 1025 for item in self.iList["Futures"].keys(): 1026 if self._figi == self.iList["Futures"][item]["figi"]: 1027 figiJSON = self.iList["Futures"][item] 1028 1029 if self.moreDebug: 1030 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1031 1032 break 1033 1034 if figiJSON: 1035 self._figi = figiJSON["figi"] 1036 self._ticker = figiJSON["ticker"] 1037 1038 if requestPrice: 1039 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1040 1041 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1042 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1043 1044 else: 1045 figiJSON["currentPrice"]["changes"] = 0 1046 1047 if show: 1048 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1049 1050 else: 1051 if show: 1052 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1053 1054 return figiJSON 1055 1056 def GetCurrentPrices(self, show: bool = True) -> dict: 1057 """ 1058 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1059 `{"buy": [{"price": 1243.8, "quantity": 193}, 1060 {"price": 1244.0, "quantity": 168}, 1061 {"price": 1244.8, "quantity": 5}, 1062 {"price": 1245.0, "quantity": 61}, 1063 {"price": 1245.4, "quantity": 60}], 1064 "sell": [{"price": 1243.6, "quantity": 8}, 1065 {"price": 1242.6, "quantity": 10}, 1066 {"price": 1242.4, "quantity": 18}, 1067 {"price": 1242.2, "quantity": 50}, 1068 {"price": 1242.0, "quantity": 113}], 1069 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1070 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1071 - sell: list of dicts with Buyers prices, 1072 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1073 - quantity: volume value by current price in lots, 1074 - limitUp: current trade session limit price, maximum, 1075 - limitDown: current trade session limit price, minimum, 1076 - lastPrice: last deal price of the instrument, 1077 - closePrice: previous trade session close price of the instrument. 1078 1079 See also: `SearchByTicker()` and `SearchByFIGI()`. 1080 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1081 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1082 1083 :param show: if `True` then print DOM to log and console. 1084 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1085 If an error occurred then returns an empty record: 1086 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1087 """ 1088 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1089 1090 if self.depth < 1: 1091 uLogger.error("Depth of Market (DOM) must be >=1!") 1092 raise Exception("Incorrect value") 1093 1094 if not (self._ticker or self._figi): 1095 uLogger.error("self._ticker or self._figi variables must be defined!") 1096 raise Exception("Ticker or FIGI required") 1097 1098 if self._ticker and not self._figi: 1099 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1100 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1101 1102 if not self._ticker and self._figi: 1103 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1104 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1105 1106 if not self._figi: 1107 uLogger.error("FIGI is not defined!") 1108 raise Exception("Ticker or FIGI required") 1109 1110 else: 1111 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1112 1113 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1114 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1115 self.body = str({"figi": self._figi, "depth": self.depth}) 1116 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1117 1118 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1119 # list of dicts with sellers orders: 1120 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1121 1122 # list of dicts with buyers orders: 1123 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1124 1125 # max price of instrument at this time: 1126 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1127 1128 # min price of instrument at this time: 1129 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1130 1131 # last price of deal with instrument: 1132 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1133 1134 # last close price of instrument: 1135 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1136 1137 else: 1138 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1139 uLogger.debug("Server response: {}".format(pricesResponse)) 1140 1141 if show: 1142 if prices["buy"] or prices["sell"]: 1143 info = [ 1144 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1145 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1146 self._ticker, 1147 self._figi, 1148 self.depth, 1149 ), 1150 "-" * 60, "\n", 1151 " Orders of Buyers | Orders of Sellers\n", 1152 "-" * 60, "\n", 1153 " Sell prices (volumes) | Buy prices (volumes)\n", 1154 "-" * 60, "\n", 1155 ] 1156 1157 if not prices["buy"]: 1158 info.append(" | No orders!\n") 1159 sumBuy = 0 1160 1161 else: 1162 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1163 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1164 for item in maxMinSorted: 1165 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1166 1167 if not prices["sell"]: 1168 info.append("No orders! |\n") 1169 sumSell = 0 1170 1171 else: 1172 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1173 for item in prices["sell"]: 1174 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1175 1176 info.extend([ 1177 "-" * 60, "\n", 1178 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1179 "-" * 60, "\n", 1180 ]) 1181 1182 infoText = "".join(info) 1183 1184 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1185 1186 else: 1187 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1188 1189 return prices 1190 1191 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1192 """ 1193 This method get and show information about all available broker instruments for current user account. 1194 If `instrumentsFile` string is not empty then also save information to this file. 1195 1196 :param show: if `True` then print results to console, if `False` — print only to file. 1197 :return: multi-lines string with all available broker instruments 1198 """ 1199 if not self.iList: 1200 self.iList = self.Listing() 1201 1202 info = [ 1203 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1204 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1205 ] 1206 1207 # add instruments count by type: 1208 for iType in self.iList.keys(): 1209 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1210 1211 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1212 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1213 1214 # generating info tables with all instruments by type: 1215 for iType in self.iList.keys(): 1216 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1217 1218 for instrument in self.iList[iType].keys(): 1219 iName = self.iList[iType][instrument]["name"] # instrument's name 1220 if len(iName) > 57: 1221 iName = "{}...".format(iName[:54]) # right trim for a long string 1222 1223 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1224 self.iList[iType][instrument]["ticker"], 1225 iName, 1226 self.iList[iType][instrument]["figi"], 1227 self.iList[iType][instrument]["currency"], 1228 self.iList[iType][instrument]["lot"], 1229 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1230 )) 1231 1232 infoText = "".join(info) 1233 1234 if show: 1235 uLogger.info(infoText) 1236 1237 if self.instrumentsFile: 1238 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1239 fH.write(infoText) 1240 1241 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1242 1243 if self.useHTMLReports: 1244 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1245 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1246 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1247 1248 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1249 1250 return infoText 1251 1252 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1253 """ 1254 This method search and show information about instruments by part of its ticker, FIGI or name. 1255 If `searchResultsFile` string is not empty then also save information to this file. 1256 1257 :param pattern: string with part of ticker, FIGI or instrument's name. 1258 :param show: if `True` then print results to console, if `False` — return list of result only. 1259 :return: list of dictionaries with all found instruments. 1260 """ 1261 if not self.iList: 1262 self.iList = self.Listing() 1263 1264 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1265 compiledPattern = re.compile(pattern, re.IGNORECASE) 1266 1267 for iType in self.iList: 1268 for instrument in self.iList[iType].values(): 1269 searchResult = compiledPattern.search(" ".join( 1270 [instrument["ticker"], instrument["figi"], instrument["name"]] 1271 )) 1272 1273 if searchResult: 1274 searchResults[iType][instrument["ticker"]] = instrument 1275 1276 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1277 info = [ 1278 "# Search results\n\n", 1279 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1280 "* **Search pattern:** [{}]\n".format(pattern), 1281 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1282 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1283 ] 1284 infoShort = info[:] 1285 1286 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1287 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1288 skippedLine = "| ... | ... | ... | ... |\n" 1289 1290 if resultsLen == 0: 1291 info.append("\nNo results\n") 1292 infoShort.append("\nNo results\n") 1293 uLogger.warning("No results. Try changing your search pattern.") 1294 1295 else: 1296 for iType in searchResults: 1297 iTypeValuesCount = len(searchResults[iType].values()) 1298 if iTypeValuesCount > 0: 1299 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1300 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1301 1302 for instrument in searchResults[iType].values(): 1303 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1304 instrument["type"], 1305 instrument["ticker"], 1306 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1307 instrument["figi"], 1308 )) 1309 1310 if iTypeValuesCount <= 5: 1311 infoShort.extend(info[-iTypeValuesCount:]) 1312 1313 else: 1314 infoShort.extend(info[-5:]) 1315 infoShort.append(skippedLine) 1316 1317 infoText = "".join(info) 1318 infoTextShort = "".join(infoShort) 1319 1320 if show: 1321 uLogger.info(infoTextShort) 1322 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1323 1324 if self.searchResultsFile: 1325 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1326 fH.write(infoText) 1327 1328 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1329 1330 if self.useHTMLReports: 1331 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1332 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1333 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1334 1335 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1336 1337 return searchResults 1338 1339 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1340 """ 1341 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1342 1343 :param instruments: list of strings with tickers or FIGIs. 1344 :return: list with unique instrument FIGIs only. 1345 """ 1346 requestedInstruments = [] 1347 for iName in instruments: 1348 if iName not in self.aliases.keys(): 1349 if iName not in requestedInstruments: 1350 requestedInstruments.append(iName) 1351 1352 else: 1353 if iName not in requestedInstruments: 1354 if self.aliases[iName] not in requestedInstruments: 1355 requestedInstruments.append(self.aliases[iName]) 1356 1357 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1358 1359 onlyUniqueFIGIs = [] 1360 for iName in requestedInstruments: 1361 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1362 continue 1363 1364 self._ticker = iName 1365 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1366 1367 if not iData: 1368 self._ticker = "" 1369 self._figi = iName 1370 1371 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1372 1373 if not iData: 1374 self._figi = "" 1375 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1376 1377 if iData and iData["figi"] not in onlyUniqueFIGIs: 1378 onlyUniqueFIGIs.append(iData["figi"]) 1379 1380 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1381 1382 return onlyUniqueFIGIs 1383 1384 def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]: 1385 """ 1386 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1387 1388 See limits: https://tinkoff.github.io/investAPI/limits/ 1389 1390 If `pricesFile` string is not empty then also save information to this file. 1391 1392 :param instruments: list of strings with tickers or FIGIs. 1393 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1394 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1395 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1396 """ 1397 if instruments is None or not instruments: 1398 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1399 raise Exception("Ticker or FIGI required") 1400 1401 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1402 1403 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1404 1405 iList = [] # trying to get info and current prices about all unique instruments: 1406 for self._figi in onlyUniqueFIGIs: 1407 iData = self.SearchByFIGI(requestPrice=True) 1408 iList.append(iData) 1409 1410 self.ShowListOfPrices(iList, show) 1411 1412 return iList 1413 1414 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1415 """ 1416 Show table contains current prices of given instruments. 1417 1418 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1419 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1420 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1421 :return: multilines text in Markdown format as a table contains current prices. 1422 """ 1423 infoText = "" 1424 1425 if show or self.pricesFile: 1426 info = [ 1427 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1428 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1429 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1430 ] 1431 1432 for item in iList: 1433 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1434 item["ticker"], 1435 item["figi"], 1436 item["type"], 1437 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1438 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1439 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1440 "{} / {}".format( 1441 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1442 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1443 ), 1444 "{} / {}".format( 1445 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1446 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1447 ), 1448 item["currency"], 1449 )) 1450 1451 infoText = "".join(info) 1452 1453 if show: 1454 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1455 1456 if self.pricesFile: 1457 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1458 fH.write(infoText) 1459 1460 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1461 1462 if self.useHTMLReports: 1463 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1464 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1465 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1466 1467 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1468 1469 return infoText 1470 1471 def RequestTradingStatus(self) -> dict: 1472 """ 1473 Requesting trading status for the instrument defined by `figi` variable. 1474 1475 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1476 1477 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1478 1479 :return: dictionary with trading status attributes. Response example: 1480 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1481 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1482 """ 1483 if self._figi is None or not self._figi: 1484 uLogger.error("Variable `figi` must be defined for using this method!") 1485 raise Exception("FIGI required") 1486 1487 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1488 1489 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1490 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1491 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1492 1493 if self.moreDebug: 1494 uLogger.debug("Records about current trading status successfully received") 1495 1496 return tradingStatus 1497 1498 def RequestPortfolio(self) -> dict: 1499 """ 1500 Requesting actual user's portfolio for current `accountId`. 1501 1502 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1503 1504 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1505 1506 :return: dictionary with user's portfolio. 1507 """ 1508 if self.accountId is None or not self.accountId: 1509 uLogger.error("Variable `accountId` must be defined for using this method!") 1510 raise Exception("Account ID required") 1511 1512 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1513 1514 self.body = str({"accountId": self.accountId}) 1515 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1516 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1517 1518 if self.moreDebug: 1519 uLogger.debug("Records about user's portfolio successfully received") 1520 1521 return rawPortfolio 1522 1523 def RequestPositions(self) -> dict: 1524 """ 1525 Requesting open positions by currencies and instruments for current `accountId`. 1526 1527 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1528 1529 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1530 1531 :return: dictionary with open positions by instruments. 1532 """ 1533 if self.accountId is None or not self.accountId: 1534 uLogger.error("Variable `accountId` must be defined for using this method!") 1535 raise Exception("Account ID required") 1536 1537 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1538 1539 self.body = str({"accountId": self.accountId}) 1540 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1541 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1542 1543 if self.moreDebug: 1544 uLogger.debug("Records about current open positions successfully received") 1545 1546 return rawPositions 1547 1548 def RequestPendingOrders(self) -> list: 1549 """ 1550 Requesting current actual pending limit orders for current `accountId`. 1551 1552 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1553 1554 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1555 1556 :return: list of dictionaries with pending limit orders. 1557 """ 1558 if self.accountId is None or not self.accountId: 1559 uLogger.error("Variable `accountId` must be defined for using this method!") 1560 raise Exception("Account ID required") 1561 1562 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1563 1564 self.body = str({"accountId": self.accountId}) 1565 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1566 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1567 1568 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1569 1570 return rawOrders 1571 1572 def RequestStopOrders(self) -> list: 1573 """ 1574 Requesting current actual stop orders for current `accountId`. 1575 1576 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1577 1578 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1579 1580 :return: list of dictionaries with stop orders. 1581 """ 1582 if self.accountId is None or not self.accountId: 1583 uLogger.error("Variable `accountId` must be defined for using this method!") 1584 raise Exception("Account ID required") 1585 1586 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1587 1588 self.body = str({"accountId": self.accountId}) 1589 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1590 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1591 1592 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1593 1594 return rawStopOrders 1595 1596 def Overview(self, show: bool = False, details: str = "full") -> dict: 1597 """ 1598 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1599 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1600 and `overviewBondsCalendarFile` are defined then also save information to file. 1601 1602 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1603 many requests about the state of the portfolio, and then, based on the received data, a large number 1604 of calculation and statistics are collected. 1605 1606 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1607 :param details: how detailed should the information be? 1608 - `full` — shows full available information about portfolio status (by default), 1609 - `positions` — shows only open positions, 1610 - `orders` — shows only sections of open limits and stop orders. 1611 - `digest` — show a short digest of the portfolio status, 1612 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1613 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1614 :return: dictionary with client's raw portfolio and some statistics. 1615 """ 1616 if self.accountId is None or not self.accountId: 1617 uLogger.error("Variable `accountId` must be defined for using this method!") 1618 raise Exception("Account ID required") 1619 1620 view = { 1621 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1622 "headers": {}, # list of dictionaries, response headers without "positions" section 1623 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1624 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1625 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1626 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1627 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1628 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1629 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1630 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1631 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1632 }, 1633 "stat": { # --- some statistics calculated using "raw" sections: 1634 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1635 "availableRUB": 0., # available rubles (without other currencies) 1636 "blockedRUB": 0., # blocked sum in Russian Rouble 1637 "totalChangesRUB": 0., # changes for all open trades in RUB 1638 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1639 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1640 "sharesCostRUB": 0., # costs of all shares in RUB 1641 "bondsCostRUB": 0., # costs of all bonds in RUB 1642 "etfsCostRUB": 0., # costs of all etfs in RUB 1643 "futuresCostRUB": 0., # costs of all futures in RUB 1644 "Currencies": [], # list of dictionaries of all currencies statistics 1645 "Shares": [], # list of dictionaries of all shares statistics 1646 "Bonds": [], # list of dictionaries of all bonds statistics 1647 "Etfs": [], # list of dictionaries of all etfs statistics 1648 "Futures": [], # list of dictionaries of all futures statistics 1649 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1650 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1651 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1652 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1653 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1654 }, 1655 "analytics": { # --- some analytics of portfolio: 1656 "distrByAssets": {}, # portfolio distribution by assets 1657 "distrByCompanies": {}, # portfolio distribution by companies 1658 "distrBySectors": {}, # portfolio distribution by sectors 1659 "distrByCurrencies": {}, # portfolio distribution by currencies 1660 "distrByCountries": {}, # portfolio distribution by countries 1661 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1662 } 1663 } 1664 1665 details = details.lower() 1666 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1667 if details not in availableDetails: 1668 details = "full" 1669 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1670 1671 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1672 1673 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1674 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1675 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1676 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1677 1678 # save response headers without "positions" section: 1679 for key in portfolioResponse.keys(): 1680 if key != "positions": 1681 view["raw"]["headers"][key] = portfolioResponse[key] 1682 1683 else: 1684 continue 1685 1686 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1687 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1688 for item in portfolioResponse["positions"]: 1689 if item["instrumentType"] == "currency": 1690 self._figi = item["figi"] 1691 curr = self.SearchByFIGI(requestPrice=False) 1692 1693 # current price of currency in RUB: 1694 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1695 "name": curr["name"], 1696 "currentPrice": NanoToFloat( 1697 item["currentPrice"]["units"], 1698 item["currentPrice"]["nano"] 1699 ), 1700 } 1701 1702 view["raw"]["Currencies"].append(item) 1703 1704 elif item["instrumentType"] == "share": 1705 view["raw"]["Shares"].append(item) 1706 1707 elif item["instrumentType"] == "bond": 1708 view["raw"]["Bonds"].append(item) 1709 1710 elif item["instrumentType"] == "etf": 1711 view["raw"]["Etfs"].append(item) 1712 1713 elif item["instrumentType"] == "futures": 1714 view["raw"]["Futures"].append(item) 1715 1716 else: 1717 continue 1718 1719 # how many volume of currencies (by ISO currency name) are blocked: 1720 for item in view["raw"]["positions"]["blocked"]: 1721 blocked = NanoToFloat(item["units"], item["nano"]) 1722 if blocked > 0: 1723 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1724 1725 # how many volume of instruments (by FIGI) are blocked: 1726 for item in view["raw"]["positions"]["securities"]: 1727 blocked = int(item["blocked"]) 1728 if blocked > 0: 1729 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1730 1731 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1732 1733 if "rub" in allBlocked.keys(): 1734 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1735 1736 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1737 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1738 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1739 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1740 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1741 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1742 view["stat"]["portfolioCostRUB"] = sum([ 1743 view["stat"]["allCurrenciesCostRUB"], 1744 view["stat"]["sharesCostRUB"], 1745 view["stat"]["bondsCostRUB"], 1746 view["stat"]["etfsCostRUB"], 1747 view["stat"]["futuresCostRUB"], 1748 ]) 1749 1750 # --- calculating some portfolio statistics: 1751 byComp = {} # distribution by companies 1752 bySect = {} # distribution by sectors 1753 byCurr = {} # distribution by currencies (include RUB) 1754 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1755 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1756 1757 for item in portfolioResponse["positions"]: 1758 self._figi = item["figi"] 1759 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1760 1761 if instrument: 1762 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1763 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1764 1765 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1766 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1767 1768 else: 1769 blocked = 0 1770 1771 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1772 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1773 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1774 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1775 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1776 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1777 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1778 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1779 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1780 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1781 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1782 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1783 1784 statData = { 1785 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1786 "ticker": instrument["ticker"], # ticker by FIGI 1787 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1788 "volume": volume, # available volume of instrument 1789 "lots": lots, # volume in lots of instrument 1790 "direction": direction, # direction of an instrument's position: short or long 1791 "blocked": blocked, # blocked volume of currency or instrument 1792 "currentPrice": curPrice, # current instrument's price in basic asset 1793 "average": average, # current average position price 1794 "cost": cost, # current cost of all volume of instrument in basic asset 1795 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1796 "costRUB": costRUB, # cost of instrument in ruble 1797 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1798 "profit": profit, # expected profit at current moment 1799 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1800 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1801 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1802 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1803 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1804 "step": instrument["step"], # minimum price increment 1805 } 1806 1807 # adding distribution by unique countries: 1808 if statData["country"] not in byCountry.keys(): 1809 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1810 1811 else: 1812 byCountry[statData["country"]]["cost"] += costRUB 1813 byCountry[statData["country"]]["percent"] += percentCostRUB 1814 1815 if item["instrumentType"] != "currency": 1816 # adding distribution by unique companies: 1817 if statData["name"]: 1818 if statData["name"] not in byComp.keys(): 1819 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1820 1821 else: 1822 byComp[statData["name"]]["cost"] += costRUB 1823 byComp[statData["name"]]["percent"] += percentCostRUB 1824 1825 # adding distribution by unique sectors: 1826 if statData["sector"] not in bySect.keys(): 1827 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1828 1829 else: 1830 bySect[statData["sector"]]["cost"] += costRUB 1831 bySect[statData["sector"]]["percent"] += percentCostRUB 1832 1833 # adding distribution by unique currencies: 1834 if currency not in byCurr.keys(): 1835 byCurr[currency] = { 1836 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1837 "cost": costRUB, 1838 "percent": percentCostRUB 1839 } 1840 1841 else: 1842 byCurr[currency]["cost"] += costRUB 1843 byCurr[currency]["percent"] += percentCostRUB 1844 1845 # saving statistics for every instrument: 1846 if item["instrumentType"] == "currency": 1847 view["stat"]["Currencies"].append(statData) 1848 1849 # update dict with free funds for trading (total - blocked) by currencies 1850 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1851 view["stat"]["funds"][currency] = { 1852 "total": volume, 1853 "totalCostRUB": costRUB, # total volume cost in rubles 1854 "free": volume - blocked, 1855 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1856 } 1857 1858 elif item["instrumentType"] == "share": 1859 view["stat"]["Shares"].append(statData) 1860 1861 elif item["instrumentType"] == "bond": 1862 view["stat"]["Bonds"].append(statData) 1863 1864 elif item["instrumentType"] == "etf": 1865 view["stat"]["Etfs"].append(statData) 1866 1867 elif item["instrumentType"] == "Futures": 1868 view["stat"]["Futures"].append(statData) 1869 1870 else: 1871 continue 1872 1873 # total changes in Russian Ruble: 1874 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1875 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1876 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1877 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1878 view["stat"]["funds"]["rub"] = { 1879 "total": view["stat"]["availableRUB"], 1880 "totalCostRUB": view["stat"]["availableRUB"], 1881 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1882 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1883 } 1884 1885 # --- pending limit orders sector data: 1886 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1887 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1888 1889 for item in view["raw"]["orders"]: 1890 self._figi = item["figi"] 1891 1892 if item["figi"] not in uniquePendingOrdersFIGIs: 1893 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1894 1895 uniquePendingOrdersFIGIs.append(item["figi"]) 1896 uniquePendingOrders[item["figi"]] = instrument 1897 1898 else: 1899 instrument = uniquePendingOrders[item["figi"]] 1900 1901 if instrument: 1902 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1903 orderType = TKS_ORDER_TYPES[item["orderType"]] 1904 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1905 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1906 1907 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1908 if item["direction"] == "ORDER_DIRECTION_BUY": 1909 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1910 1911 else: 1912 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1913 1914 # requested price for order execution: 1915 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1916 1917 # necessary changes in percent to reach target from current price: 1918 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1919 1920 view["stat"]["orders"].append({ 1921 "orderID": item["orderId"], # orderId number parameter of current order 1922 "figi": item["figi"], # FIGI identification 1923 "ticker": instrument["ticker"], # ticker name by FIGI 1924 "lotsRequested": item["lotsRequested"], # requested lots value 1925 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1926 "currentPrice": lastPrice, # current instrument's price for defined action 1927 "targetPrice": target, # requested price for order execution in base currency 1928 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1929 "percentChanges": changes, # changes in percent to target from current price 1930 "currency": item["currency"], # instrument's currency name 1931 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1932 "type": orderType, # type of order from TKS_ORDER_TYPES 1933 "status": orderState, # order status from TKS_ORDER_STATES 1934 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1935 }) 1936 1937 # --- stop orders sector data: 1938 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1939 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1940 1941 for item in view["raw"]["stopOrders"]: 1942 self._figi = item["figi"] 1943 1944 if item["figi"] not in uniqueStopOrdersFIGIs: 1945 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1946 1947 uniqueStopOrdersFIGIs.append(item["figi"]) 1948 uniqueStopOrders[item["figi"]] = instrument 1949 1950 else: 1951 instrument = uniqueStopOrders[item["figi"]] 1952 1953 if instrument: 1954 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1955 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1956 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1957 1958 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1959 if "expirationTime" in item.keys(): 1960 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1961 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1962 1963 else: 1964 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1965 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1966 1967 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1968 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1969 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1970 1971 else: 1972 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1973 1974 # requested price when stop-order executed: 1975 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1976 1977 # price for limit-order, set up when stop-order executed: 1978 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1979 1980 # necessary changes in percent to reach target from current price: 1981 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1982 1983 view["stat"]["stopOrders"].append({ 1984 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1985 "figi": item["figi"], # FIGI identification 1986 "ticker": instrument["ticker"], # ticker name by FIGI 1987 "lotsRequested": item["lotsRequested"], # requested lots value 1988 "currentPrice": lastPrice, # current instrument's price for defined action 1989 "targetPrice": target, # requested price for stop-order execution in base currency 1990 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1991 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1992 "percentChanges": changes, # changes in percent to target from current price 1993 "currency": item["currency"], # instrument's currency name 1994 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1995 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1996 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1997 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1998 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1999 }) 2000 2001 # --- calculating data for analytics section: 2002 # portfolio distribution by assets: 2003 view["analytics"]["distrByAssets"] = { 2004 "Ruble": { 2005 "uniques": 1, 2006 "cost": view["stat"]["availableRUB"], 2007 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2008 }, 2009 "Currencies": { 2010 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2011 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2012 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2013 }, 2014 "Shares": { 2015 "uniques": len(view["stat"]["Shares"]), 2016 "cost": view["stat"]["sharesCostRUB"], 2017 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2018 }, 2019 "Bonds": { 2020 "uniques": len(view["stat"]["Bonds"]), 2021 "cost": view["stat"]["bondsCostRUB"], 2022 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2023 }, 2024 "Etfs": { 2025 "uniques": len(view["stat"]["Etfs"]), 2026 "cost": view["stat"]["etfsCostRUB"], 2027 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2028 }, 2029 "Futures": { 2030 "uniques": len(view["stat"]["Futures"]), 2031 "cost": view["stat"]["futuresCostRUB"], 2032 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2033 }, 2034 } 2035 2036 # portfolio distribution by companies: 2037 view["analytics"]["distrByCompanies"]["All money cash"] = { 2038 "ticker": "", 2039 "cost": view["stat"]["allCurrenciesCostRUB"], 2040 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2041 } 2042 view["analytics"]["distrByCompanies"].update(byComp) 2043 2044 # portfolio distribution by sectors: 2045 view["analytics"]["distrBySectors"]["All money cash"] = { 2046 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2047 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2048 } 2049 view["analytics"]["distrBySectors"].update(bySect) 2050 2051 # portfolio distribution by currencies: 2052 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2053 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2054 2055 if self.moreDebug: 2056 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2057 2058 view["analytics"]["distrByCurrencies"].update(byCurr) 2059 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2060 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2061 2062 # portfolio distribution by countries: 2063 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2064 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2065 2066 if self.moreDebug: 2067 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2068 2069 view["analytics"]["distrByCountries"].update(byCountry) 2070 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2071 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2072 2073 # --- Prepare text statistics overview in human-readable: 2074 if show: 2075 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2076 2077 # Whatever the value `details`, header not changes: 2078 info = [ 2079 "# Client's portfolio\n\n", 2080 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2081 "* **Account ID:** [{}]\n".format(self.accountId), 2082 ] 2083 2084 if details in ["full", "positions", "digest"]: 2085 info.extend([ 2086 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2087 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2088 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2089 view["stat"]["totalChangesRUB"], 2090 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2091 view["stat"]["totalChangesPercentRUB"], 2092 ), 2093 ]) 2094 2095 if details in ["full", "positions"]: 2096 info.extend([ 2097 "## Open positions\n\n", 2098 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2099 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2100 "| Ruble | {:>31} | | | | | |\n".format( 2101 "{:.2f} ({:.2f}) rub".format( 2102 view["stat"]["availableRUB"], 2103 view["stat"]["blockedRUB"], 2104 ) 2105 ) 2106 ]) 2107 2108 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2109 return [ 2110 "| | | | | | | |\n", 2111 "| {:<27} | | | | | {:>19} | |\n".format( 2112 noTradeStr if noTradeStr else typeStr, 2113 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2114 ), 2115 ] 2116 2117 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2118 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2119 "{} [{}]".format(data["ticker"], data["figi"]), 2120 "{:.2f} ({:.2f}) {}".format( 2121 data["volume"], 2122 data["blocked"], 2123 data["currency"], 2124 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2125 data["volume"], 2126 data["blocked"], 2127 ), 2128 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2129 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2130 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2131 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2132 "{}{:.2f} {} ({}{:.2f}%)".format( 2133 "+" if data["profit"] > 0 else "", 2134 data["profit"], data["baseCurrencyName"], 2135 "+" if data["percentProfit"] > 0 else "", 2136 data["percentProfit"], 2137 ), 2138 ) 2139 2140 # --- Show currencies section: 2141 if view["stat"]["Currencies"]: 2142 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2143 for item in view["stat"]["Currencies"]: 2144 info.append(_InfoStr(item, showCurrencyName=True)) 2145 2146 else: 2147 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2148 2149 # --- Show shares section: 2150 if view["stat"]["Shares"]: 2151 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2152 2153 for item in view["stat"]["Shares"]: 2154 info.append(_InfoStr(item)) 2155 2156 else: 2157 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2158 2159 # --- Show bonds section: 2160 if view["stat"]["Bonds"]: 2161 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2162 2163 for item in view["stat"]["Bonds"]: 2164 info.append(_InfoStr(item)) 2165 2166 else: 2167 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2168 2169 # --- Show etfs section: 2170 if view["stat"]["Etfs"]: 2171 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2172 2173 for item in view["stat"]["Etfs"]: 2174 info.append(_InfoStr(item)) 2175 2176 else: 2177 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2178 2179 # --- Show futures section: 2180 if view["stat"]["Futures"]: 2181 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2182 2183 for item in view["stat"]["Futures"]: 2184 info.append(_InfoStr(item)) 2185 2186 else: 2187 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2188 2189 if details in ["full", "orders"]: 2190 # --- Show pending limit orders section: 2191 if view["stat"]["orders"]: 2192 info.extend([ 2193 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2194 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2195 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2196 ]) 2197 2198 for item in view["stat"]["orders"]: 2199 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2200 "{} [{}]".format(item["ticker"], item["figi"]), 2201 item["orderID"], 2202 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2203 "{} {} ({}{:.2f}%)".format( 2204 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2205 item["baseCurrencyName"], 2206 "+" if item["percentChanges"] > 0 else "", 2207 float(item["percentChanges"]), 2208 ), 2209 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2210 item["action"], 2211 item["type"], 2212 item["date"], 2213 )) 2214 2215 else: 2216 info.append("\n## Total pending limit-orders: [0]\n") 2217 2218 # --- Show stop orders section: 2219 if view["stat"]["stopOrders"]: 2220 info.extend([ 2221 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2222 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2223 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2224 ]) 2225 2226 for item in view["stat"]["stopOrders"]: 2227 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2228 "{} [{}]".format(item["ticker"], item["figi"]), 2229 item["orderID"], 2230 item["lotsRequested"], 2231 "{} {} ({}{:.2f}%)".format( 2232 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2233 item["baseCurrencyName"], 2234 "+" if item["percentChanges"] > 0 else "", 2235 float(item["percentChanges"]), 2236 ), 2237 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2238 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2239 item["action"], 2240 item["type"], 2241 item["expType"], 2242 item["createDate"], 2243 item["expDate"], 2244 )) 2245 2246 else: 2247 info.append("\n## Total stop-orders: [0]\n") 2248 2249 if details in ["full", "analytics"]: 2250 # -- Show analytics section: 2251 if view["stat"]["portfolioCostRUB"] > 0: 2252 info.extend([ 2253 "\n# Analytics\n\n" 2254 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2255 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2256 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2257 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2258 view["stat"]["totalChangesRUB"], 2259 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2260 view["stat"]["totalChangesPercentRUB"], 2261 ), 2262 "\n## Portfolio distribution by assets\n" 2263 "\n| Type | Uniques | Percent | Current cost |\n", 2264 "|------------------------------------|---------|---------|--------------------|\n", 2265 ]) 2266 2267 for key in view["analytics"]["distrByAssets"].keys(): 2268 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2269 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2270 key, 2271 view["analytics"]["distrByAssets"][key]["uniques"], 2272 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2273 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2274 )) 2275 2276 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2277 2278 info.extend([ 2279 "\n## Portfolio distribution by companies\n" 2280 "\n| Company | Percent | Current cost |\n", 2281 aSepLine, 2282 ]) 2283 2284 for company in view["analytics"]["distrByCompanies"].keys(): 2285 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2286 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2287 "{}{}".format( 2288 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2289 company, 2290 ), 2291 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2292 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2293 )) 2294 2295 info.extend([ 2296 "\n## Portfolio distribution by sectors\n" 2297 "\n| Sector | Percent | Current cost |\n", 2298 aSepLine, 2299 ]) 2300 2301 for sector in view["analytics"]["distrBySectors"].keys(): 2302 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2303 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2304 sector, 2305 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2306 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2307 )) 2308 2309 info.extend([ 2310 "\n## Portfolio distribution by currencies\n" 2311 "\n| Instruments currencies | Percent | Current cost |\n", 2312 aSepLine, 2313 ]) 2314 2315 for curr in view["analytics"]["distrByCurrencies"].keys(): 2316 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2317 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2318 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2319 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2320 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2321 )) 2322 2323 info.extend([ 2324 "\n## Portfolio distribution by countries\n" 2325 "\n| Assets by country | Percent | Current cost |\n", 2326 aSepLine, 2327 ]) 2328 2329 for country in view["analytics"]["distrByCountries"].keys(): 2330 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2331 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2332 country, 2333 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2334 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2335 )) 2336 2337 if details in ["full", "calendar"]: 2338 # -- Show bonds payment calendar section: 2339 if view["stat"]["Bonds"]: 2340 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2341 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2342 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2343 2344 else: 2345 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2346 2347 infoText = "".join(info) 2348 2349 uLogger.info(infoText) 2350 2351 if details == "full" and self.overviewFile: 2352 filename = self.overviewFile 2353 2354 elif details == "digest" and self.overviewDigestFile: 2355 filename = self.overviewDigestFile 2356 2357 elif details == "positions" and self.overviewPositionsFile: 2358 filename = self.overviewPositionsFile 2359 2360 elif details == "orders" and self.overviewOrdersFile: 2361 filename = self.overviewOrdersFile 2362 2363 elif details == "analytics" and self.overviewAnalyticsFile: 2364 filename = self.overviewAnalyticsFile 2365 2366 elif details == "calendar" and self.overviewBondsCalendarFile: 2367 filename = self.overviewBondsCalendarFile 2368 2369 else: 2370 filename = "" 2371 2372 if filename: 2373 with open(filename, "w", encoding="UTF-8") as fH: 2374 fH.write(infoText) 2375 2376 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2377 2378 if self.useHTMLReports: 2379 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2380 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2381 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="", commonCSS=COMMON_CSS, markdown=infoText)) 2382 2383 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2384 2385 return view 2386 2387 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2388 """ 2389 Returns history operations between two given dates for current `accountId`. 2390 If `reportFile` string is not empty then also save human-readable report. 2391 Shows some statistical data of closed positions. 2392 2393 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2394 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2395 :param show: if `True` then also prints all records to the console. 2396 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2397 :return: original list of dictionaries with history of deals records from API ("operations" key): 2398 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2399 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2400 """ 2401 if self.accountId is None or not self.accountId: 2402 uLogger.error("Variable `accountId` must be defined for using this method!") 2403 raise Exception("Account ID required") 2404 2405 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2406 2407 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2408 2409 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2410 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2411 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2412 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2413 customStat = {} # custom statistics in additional to responseJSON 2414 2415 # --- output report in human-readable format: 2416 if show or self.reportFile: 2417 splitLine1 = "| | | | | |\n" # Summary section 2418 splitLine2 = "| | | | | | | | |\n" # Operations section 2419 nextDay = "" 2420 2421 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2422 2423 if len(ops) > 0: 2424 customStat = { 2425 "opsCount": 0, # total operations count 2426 "buyCount": 0, # buy operations 2427 "sellCount": 0, # sell operations 2428 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2429 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2430 "payIn": {"rub": 0.}, # Deposit brokerage account 2431 "payOut": {"rub": 0.}, # Withdrawals 2432 "divs": {"rub": 0.}, # Dividends income 2433 "coupons": {"rub": 0.}, # Coupon's income 2434 "brokerCom": {"rub": 0.}, # Service commissions 2435 "serviceCom": {"rub": 0.}, # Service commissions 2436 "marginCom": {"rub": 0.}, # Margin commissions 2437 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2438 } 2439 2440 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2441 for item in ops: 2442 if item["state"] == "OPERATION_STATE_EXECUTED": 2443 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2444 2445 # count buy operations: 2446 if "_BUY" in item["operationType"]: 2447 customStat["buyCount"] += 1 2448 2449 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2450 customStat["buyTotal"][item["payment"]["currency"]] += payment 2451 2452 else: 2453 customStat["buyTotal"][item["payment"]["currency"]] = payment 2454 2455 # count sell operations: 2456 elif "_SELL" in item["operationType"]: 2457 customStat["sellCount"] += 1 2458 2459 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2460 customStat["sellTotal"][item["payment"]["currency"]] += payment 2461 2462 else: 2463 customStat["sellTotal"][item["payment"]["currency"]] = payment 2464 2465 # count incoming operations: 2466 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2467 if item["payment"]["currency"] in customStat["payIn"].keys(): 2468 customStat["payIn"][item["payment"]["currency"]] += payment 2469 2470 else: 2471 customStat["payIn"][item["payment"]["currency"]] = payment 2472 2473 # count withdrawals operations: 2474 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2475 if item["payment"]["currency"] in customStat["payOut"].keys(): 2476 customStat["payOut"][item["payment"]["currency"]] += payment 2477 2478 else: 2479 customStat["payOut"][item["payment"]["currency"]] = payment 2480 2481 # count dividends income: 2482 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2483 if item["payment"]["currency"] in customStat["divs"].keys(): 2484 customStat["divs"][item["payment"]["currency"]] += payment 2485 2486 else: 2487 customStat["divs"][item["payment"]["currency"]] = payment 2488 2489 # count coupon's income: 2490 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2491 if item["payment"]["currency"] in customStat["coupons"].keys(): 2492 customStat["coupons"][item["payment"]["currency"]] += payment 2493 2494 else: 2495 customStat["coupons"][item["payment"]["currency"]] = payment 2496 2497 # count broker commissions: 2498 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2499 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2500 customStat["brokerCom"][item["payment"]["currency"]] += payment 2501 2502 else: 2503 customStat["brokerCom"][item["payment"]["currency"]] = payment 2504 2505 # count service commissions: 2506 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2507 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2508 customStat["serviceCom"][item["payment"]["currency"]] += payment 2509 2510 else: 2511 customStat["serviceCom"][item["payment"]["currency"]] = payment 2512 2513 # count margin commissions: 2514 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2515 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2516 customStat["marginCom"][item["payment"]["currency"]] += payment 2517 2518 else: 2519 customStat["marginCom"][item["payment"]["currency"]] = payment 2520 2521 # count withholding taxes: 2522 elif "_TAX" in item["operationType"]: 2523 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2524 customStat["allTaxes"][item["payment"]["currency"]] += payment 2525 2526 else: 2527 customStat["allTaxes"][item["payment"]["currency"]] = payment 2528 2529 else: 2530 continue 2531 2532 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2533 2534 # --- view "Actions" lines: 2535 info.extend([ 2536 "| Report sections | | | | |\n", 2537 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2538 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2539 "| | Buy: {:<22} | {:<28} | | |\n".format( 2540 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2541 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2542 ), 2543 "| | Sell: {:<21} | {:<28} | | |\n".format( 2544 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2545 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2546 ), 2547 ]) 2548 2549 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2550 for key in opsKeys: 2551 if key == "rub": 2552 continue 2553 2554 info.extend([ 2555 "| | | {:<28} | | |\n".format( 2556 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2557 ), 2558 "| | | {:<28} | | |\n".format( 2559 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2560 ), 2561 ]) 2562 2563 info.append(splitLine1) 2564 2565 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2566 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2567 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2568 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2569 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2570 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2571 ) 2572 2573 # --- view "Payments" lines: 2574 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2575 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2576 2577 for key in paymentsKeys: 2578 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2579 2580 info.append(splitLine1) 2581 2582 # --- view "Commissions and taxes" lines: 2583 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2584 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2585 2586 for key in comKeys: 2587 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2588 2589 info.extend([ 2590 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2591 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2592 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2593 ]) 2594 2595 else: 2596 info.append("Broker returned no operations during this period\n") 2597 2598 # --- view "Operations" section: 2599 for item in ops: 2600 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2601 continue 2602 2603 else: 2604 self._figi = item["figi"] if item["figi"] else "" 2605 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2606 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2607 2608 # group of deals during one day: 2609 if nextDay and item["date"].split("T")[0] != nextDay: 2610 info.append(splitLine2) 2611 nextDay = "" 2612 2613 else: 2614 nextDay = item["date"].split("T")[0] # saving current day for splitting 2615 2616 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2617 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2618 self._figi if self._figi else "—", 2619 instrument["ticker"] if instrument else "—", 2620 instrument["type"] if instrument else "—", 2621 item["quantity"] if int(item["quantity"]) > 0 else "—", 2622 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2623 TKS_OPERATION_STATES[item["state"]], 2624 TKS_OPERATION_TYPES[item["operationType"]], 2625 )) 2626 2627 infoText = "".join(info) 2628 2629 if show: 2630 if self.moreDebug: 2631 uLogger.debug("Records about history of a client's operations successfully received") 2632 2633 uLogger.info(infoText) 2634 2635 if self.reportFile: 2636 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2637 fH.write(infoText) 2638 2639 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2640 2641 if self.useHTMLReports: 2642 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2643 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2644 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2645 2646 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2647 2648 return ops, customStat 2649 2650 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2651 """ 2652 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2653 2654 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2655 Warning! Broker server used ISO UTC time by default. 2656 2657 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2658 Also, `historyFile` used to update history with `onlyMissing` parameter. 2659 2660 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2661 2662 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2663 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2664 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2665 `"hour"`, `"day"`. Default: `"hour"`. 2666 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2667 False by default. Warning! History appends only from last candle to current time 2668 with always update last candle! 2669 :param csvSep: separator if csv-file is used, `,` by default. 2670 :param show: if `True` then also prints Pandas DataFrame to the console. 2671 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2672 `["date", "time", "open", "high", "low", "close", "volume"]`. 2673 """ 2674 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2675 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2676 history = None # empty pandas object for history 2677 2678 if interval not in TKS_CANDLE_INTERVALS.keys(): 2679 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2680 raise Exception("Incorrect value") 2681 2682 if not (self._ticker or self._figi): 2683 uLogger.error("Ticker or FIGI must be defined!") 2684 raise Exception("Ticker or FIGI required") 2685 2686 if self._ticker and not self._figi: 2687 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2688 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2689 2690 if self._figi and not self._ticker: 2691 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2692 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2693 2694 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2695 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2696 if interval.lower() != "day": 2697 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2698 2699 delta = dtEnd - dtStart # current UTC time minus last time in file 2700 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2701 2702 # calculate history length in candles: 2703 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2704 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2705 length += 1 # to avoid fraction time 2706 2707 # calculate data blocks count: 2708 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2709 2710 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2711 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2712 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2713 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2714 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2715 2716 tempOld = None # pandas object for old history, if --only-missing key present 2717 lastTime = None # datetime object of last old candle in file 2718 2719 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2720 uLogger.debug("--only-missing key present, add only last missing candles...") 2721 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2722 2723 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2724 2725 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2726 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2727 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2728 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2729 2730 # get last datetime object from last string in file or minus 1 delta if file is empty: 2731 if len(tempOld) > 0: 2732 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2733 2734 else: 2735 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2736 2737 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2738 2739 responseJSONs = [] # raw history blocks of data 2740 2741 blockEnd = dtEnd 2742 for item in range(blocks): 2743 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2744 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2745 2746 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2747 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2748 )) 2749 2750 if blockStart == blockEnd: 2751 uLogger.debug("Skipped this zero-length block...") 2752 2753 else: 2754 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2755 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2756 self.body = str({ 2757 "figi": self._figi, 2758 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2759 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2760 "interval": TKS_CANDLE_INTERVALS[interval][0] 2761 }) 2762 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2763 2764 if "code" in responseJSON.keys(): 2765 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2766 2767 else: 2768 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2769 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2770 2771 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2772 2773 blockEnd = blockStart 2774 2775 printCount = len(responseJSONs) # candles to show in console 2776 if responseJSONs: 2777 tempHistory = pd.DataFrame( 2778 data={ 2779 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2780 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2781 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2782 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2783 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2784 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2785 "volume": [int(item["volume"]) for item in responseJSONs], 2786 }, 2787 index=range(len(responseJSONs)), 2788 columns=["date", "time", "open", "high", "low", "close", "volume"], 2789 ) 2790 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2791 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2792 2793 # append only newest candles to old history if --only-missing key present: 2794 if onlyMissing and tempOld is not None and lastTime is not None: 2795 index = 0 # find start index in tempHistory data: 2796 2797 for i, item in tempHistory.iterrows(): 2798 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2799 2800 if curTime == lastTime: 2801 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2802 index = i 2803 printCount = index + 1 2804 break 2805 2806 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2807 2808 else: 2809 history = tempHistory # if no `--only-missing` key then load full data from server 2810 2811 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2812 2813 if history is not None and not history.empty: 2814 if show: 2815 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2816 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2817 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2818 )) 2819 2820 else: 2821 uLogger.warning("Received an empty candles history!") 2822 2823 if self.historyFile is not None: 2824 if history is not None and not history.empty: 2825 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2826 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2827 2828 else: 2829 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2830 2831 else: 2832 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2833 2834 return history 2835 2836 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2837 """ 2838 Load candles history from csv-file and return Pandas DataFrame object. 2839 2840 See also: `History()` and `ShowHistoryChart()` methods. 2841 2842 :param filePath: path to csv-file to open. 2843 """ 2844 loadedHistory = None # init candles data object 2845 2846 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2847 2848 if os.path.exists(filePath): 2849 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2850 2851 tfStr = self.priceModel.FormattedDelta( 2852 self.priceModel.timeframe, 2853 "{days} days {hours}h {minutes}m {seconds}s", 2854 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2855 self.priceModel.timeframe, 2856 "{hours}h {minutes}m {seconds}s", 2857 ) 2858 2859 if loadedHistory is not None and not loadedHistory.empty: 2860 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2861 len(loadedHistory), 2862 tfStr, 2863 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2864 ) 2865 2866 else: 2867 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2868 2869 else: 2870 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2871 2872 return loadedHistory 2873 2874 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2875 """ 2876 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2877 2878 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2879 Default: `index.html` (both for interact and non-interact candlesticks chart). 2880 2881 See also: `History()` and `LoadHistory()` methods. 2882 2883 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2884 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2885 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2886 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2887 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2888 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2889 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2890 """ 2891 if isinstance(candles, str): 2892 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2893 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2894 2895 elif isinstance(candles, pd.DataFrame): 2896 self.priceModel.prices = candles # set candles chain from variable 2897 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2898 2899 if "datetime" not in candles.columns: 2900 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2901 2902 else: 2903 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2904 raise Exception("Incorrect value") 2905 2906 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2907 2908 if interact: 2909 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2910 2911 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2912 2913 else: 2914 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2915 2916 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2917 2918 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2919 2920 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2921 """ 2922 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2923 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2924 2925 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2926 2927 :param operation: string "Buy" or "Sell". 2928 :param lots: volume, integer count of lots >= 1. 2929 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2930 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2931 :param expDate: string "Undefined" by default or local date in future, 2932 it is a string with format `%Y-%m-%d %H:%M:%S`. 2933 :return: JSON with response from broker server. 2934 """ 2935 if self.accountId is None or not self.accountId: 2936 uLogger.error("Variable `accountId` must be defined for using this method!") 2937 raise Exception("Account ID required") 2938 2939 if operation is None or not operation or operation not in ("Buy", "Sell"): 2940 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2941 raise Exception("Incorrect value") 2942 2943 if lots is None or lots < 1: 2944 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2945 lots = 1 2946 2947 if tp is None or tp < 0: 2948 tp = 0 2949 2950 if sl is None or sl < 0: 2951 sl = 0 2952 2953 if expDate is None or not expDate: 2954 expDate = "Undefined" 2955 2956 if not (self._ticker or self._figi): 2957 uLogger.error("Ticker or FIGI must be defined!") 2958 raise Exception("Ticker or FIGI required") 2959 2960 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2961 self._ticker = instrument["ticker"] 2962 self._figi = instrument["figi"] 2963 2964 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 2965 2966 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2967 self.body = str({ 2968 "figi": self._figi, 2969 "quantity": str(lots), 2970 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2971 "accountId": str(self.accountId), 2972 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2973 }) 2974 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2975 2976 if "orderId" in response.keys(): 2977 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2978 operation, response["orderId"], 2979 self._ticker, self._figi, lots, 2980 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2981 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2982 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2983 )) 2984 2985 if tp > 0: 2986 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2987 2988 if sl > 0: 2989 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2990 2991 else: 2992 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 2993 2994 return response 2995 2996 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2997 """ 2998 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2999 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3000 3001 See also: `Order()` and `Trade()` docstrings. 3002 3003 :param lots: volume, integer count of lots >= 1. 3004 :param tp: float > 0, take profit price of stop-order. 3005 :param sl: float > 0, stop loss price of stop-order. 3006 :param expDate: it's a local date in future. 3007 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3008 :return: JSON with response from broker server. 3009 """ 3010 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3011 3012 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3013 """ 3014 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3015 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3016 3017 See also: `Order()` and `Trade()` docstrings. 3018 3019 :param lots: volume, integer count of lots >= 1. 3020 :param tp: float > 0, take profit price of stop-order. 3021 :param sl: float > 0, stop loss price of stop-order. 3022 :param expDate: it's a local date in the future. 3023 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3024 :return: JSON with response from broker server. 3025 """ 3026 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3027 3028 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3029 """ 3030 Close position of given instruments. 3031 3032 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3033 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3034 This avoids unnecessary downloading data from the server. 3035 """ 3036 if instruments is None or not instruments: 3037 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3038 raise Exception("Ticker or FIGI required") 3039 3040 if isinstance(instruments, str): 3041 instruments = [instruments] 3042 3043 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3044 if uniqueInstruments: 3045 if portfolio is None or not portfolio: 3046 portfolio = self.Overview(show=False) 3047 3048 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3049 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3050 3051 for self._figi in uniqueInstruments: 3052 if self._figi not in allOpened: 3053 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3054 continue 3055 3056 # search open trade info about instrument by ticker: 3057 instrument = {} 3058 for iType in TKS_INSTRUMENTS: 3059 if instrument: 3060 break 3061 3062 for item in portfolio["stat"][iType]: 3063 if item["figi"] == self._figi: 3064 instrument = item 3065 break 3066 3067 if instrument: 3068 self._ticker = instrument["ticker"] 3069 self._figi = instrument["figi"] 3070 3071 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3072 self._ticker, 3073 self._figi, 3074 int(instrument["volume"]), 3075 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3076 )) 3077 3078 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3079 3080 if tradeLots > 0: 3081 if instrument["blocked"] > 0: 3082 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3083 instrument["blocked"], 3084 self._ticker, 3085 tradeLots, 3086 )) 3087 3088 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3089 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3090 3091 else: 3092 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3093 3094 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3095 """ 3096 Close all positions of given instruments with defined type. 3097 3098 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3099 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3100 This avoids unnecessary downloading data from the server. 3101 """ 3102 if iType not in TKS_INSTRUMENTS: 3103 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3104 3105 else: 3106 if portfolio is None or not portfolio: 3107 portfolio = self.Overview(show=False) 3108 3109 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3110 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3111 3112 if tickers and portfolio: 3113 self.CloseTrades(tickers, portfolio) 3114 3115 else: 3116 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3117 3118 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3119 """ 3120 Universal method to create market or limit orders with all available parameters for current `accountId`. 3121 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3122 3123 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3124 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3125 3126 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3127 then broker immediately open market order as you can do simple --buy or --sell operations! 3128 3129 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3130 When current price will go up or down to target price value then broker opens a limit order. 3131 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3132 3133 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3134 3135 :param operation: string "Buy" or "Sell". 3136 :param orderType: string "Limit" or "Stop". 3137 :param lots: volume, integer count of lots >= 1. 3138 :param targetPrice: target price > 0. This is open trade price for limit order. 3139 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3140 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3141 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3142 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3143 Stop loss order always executed by market price. 3144 :param expDate: string "Undefined" by default or local date in future. 3145 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3146 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3147 A limit order has no expiration date, it lasts until the end of the trading day. 3148 :return: JSON with response from broker server. 3149 """ 3150 if self.accountId is None or not self.accountId: 3151 uLogger.error("Variable `accountId` must be defined for using this method!") 3152 raise Exception("Account ID required") 3153 3154 if operation is None or not operation or operation not in ("Buy", "Sell"): 3155 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3156 raise Exception("Incorrect value") 3157 3158 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3159 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3160 raise Exception("Incorrect value") 3161 3162 if lots is None or lots < 1: 3163 uLogger.error("You must define trade volume > 0: integer count of lots!") 3164 raise Exception("Incorrect value") 3165 3166 if targetPrice is None or targetPrice <= 0: 3167 uLogger.error("Target price for limit-order must be greater than 0!") 3168 raise Exception("Incorrect value") 3169 3170 if limitPrice is None or limitPrice <= 0: 3171 limitPrice = targetPrice 3172 3173 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3174 stopType = "Limit" 3175 3176 if expDate is None or not expDate: 3177 expDate = "Undefined" 3178 3179 if not (self._ticker or self._figi): 3180 uLogger.error("Tocker or FIGI must be defined!") 3181 raise Exception("Ticker or FIGI required") 3182 3183 response = {} 3184 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3185 self._ticker = instrument["ticker"] 3186 self._figi = instrument["figi"] 3187 3188 if orderType == "Limit": 3189 uLogger.debug( 3190 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3191 self._ticker, self._figi, 3192 operation, lots, targetPrice, instrument["currency"], 3193 )) 3194 3195 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3196 self.body = str({ 3197 "figi": self._figi, 3198 "quantity": str(lots), 3199 "price": FloatToNano(targetPrice), 3200 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3201 "accountId": str(self.accountId), 3202 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3203 }) 3204 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3205 3206 if "orderId" in response.keys(): 3207 uLogger.info( 3208 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3209 response["orderId"], 3210 self._ticker, self._figi, 3211 operation, lots, targetPrice, instrument["currency"], 3212 )) 3213 3214 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3215 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3216 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3217 targetPrice, instrument["currency"], 3218 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3219 )) 3220 3221 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3222 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3223 targetPrice, instrument["currency"], 3224 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3225 )) 3226 3227 else: 3228 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3229 3230 if orderType == "Stop": 3231 uLogger.debug( 3232 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3233 self._ticker, self._figi, 3234 operation, lots, 3235 targetPrice, instrument["currency"], 3236 limitPrice, instrument["currency"], 3237 stopType, expDate, 3238 )) 3239 3240 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3241 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3242 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3243 3244 body = { 3245 "figi": self._figi, 3246 "quantity": str(lots), 3247 "price": FloatToNano(limitPrice), 3248 "stopPrice": FloatToNano(targetPrice), 3249 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3250 "accountId": str(self.accountId), 3251 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3252 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3253 } 3254 3255 if expDateUTC: 3256 body["expireDate"] = expDateUTC 3257 3258 self.body = str(body) 3259 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3260 3261 if "stopOrderId" in response.keys(): 3262 uLogger.info( 3263 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3264 response["stopOrderId"], 3265 self._ticker, self._figi, 3266 operation, lots, 3267 targetPrice, instrument["currency"], 3268 limitPrice, instrument["currency"], 3269 TKS_STOP_ORDER_TYPES[stopOrderType], 3270 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3271 )) 3272 3273 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3274 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3275 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3276 targetPrice, instrument["currency"], 3277 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3278 )) 3279 3280 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3281 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3282 targetPrice, instrument["currency"], 3283 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3284 )) 3285 3286 else: 3287 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3288 3289 return response 3290 3291 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3292 """ 3293 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3294 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3295 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3296 See also: `Order()` docstring. 3297 3298 :param lots: volume, integer count of lots >= 1. 3299 :param targetPrice: target price > 0. This is open trade price for limit order. 3300 :return: JSON with response from broker server. 3301 """ 3302 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3303 3304 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3305 """ 3306 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3307 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3308 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3309 target price value then broker opens a limit order. See also: `Order()` docstring. 3310 3311 :param lots: volume, integer count of lots >= 1. 3312 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3313 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3314 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3315 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3316 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3317 :param expDate: string "Undefined" by default or local date in future. 3318 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3319 This date is converting to UTC format for server. 3320 :return: JSON with response from broker server. 3321 """ 3322 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3323 3324 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3325 """ 3326 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3327 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3328 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3329 See also: `Order()` docstring. 3330 3331 :param lots: volume, integer count of lots >= 1. 3332 :param targetPrice: target price > 0. This is open trade price for limit order. 3333 :return: JSON with response from broker server. 3334 """ 3335 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3336 3337 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3338 """ 3339 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3340 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3341 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3342 target price value then broker opens a limit order. See also: `Order()` docstring. 3343 3344 :param lots: volume, integer count of lots >= 1. 3345 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3346 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3347 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3348 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3349 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3350 :param expDate: string "Undefined" by default or local date in future. 3351 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3352 This date is converting to UTC format for server. 3353 :return: JSON with response from broker server. 3354 """ 3355 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3356 3357 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3358 """ 3359 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3360 3361 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3362 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3363 This avoids unnecessary downloading data from the server. 3364 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3365 """ 3366 if self.accountId is None or not self.accountId: 3367 uLogger.error("Variable `accountId` must be defined for using this method!") 3368 raise Exception("Account ID required") 3369 3370 if orderIDs: 3371 if allOrdersIDs is None: 3372 rawOrders = self.RequestPendingOrders() 3373 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3374 3375 if allStopOrdersIDs is None: 3376 rawStopOrders = self.RequestStopOrders() 3377 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3378 3379 for orderID in orderIDs: 3380 idInPendingOrders = orderID in allOrdersIDs 3381 idInStopOrders = orderID in allStopOrdersIDs 3382 3383 if not (idInPendingOrders or idInStopOrders): 3384 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3385 continue 3386 3387 else: 3388 if idInPendingOrders: 3389 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3390 3391 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3392 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3393 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3394 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3395 3396 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3397 if self.moreDebug: 3398 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3399 3400 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3401 3402 else: 3403 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3404 3405 elif idInStopOrders: 3406 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3407 3408 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3409 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3410 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3411 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3412 3413 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3414 if self.moreDebug: 3415 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3416 3417 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3418 3419 else: 3420 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3421 3422 else: 3423 continue 3424 3425 def CloseAllOrders(self) -> None: 3426 """ 3427 Gets a list of open pending and stop orders and cancel it all. 3428 """ 3429 rawOrders = self.RequestPendingOrders() 3430 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3431 lenOrders = len(allOrdersIDs) 3432 3433 rawStopOrders = self.RequestStopOrders() 3434 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3435 lenSOrders = len(allStopOrdersIDs) 3436 3437 if lenOrders > 0 or lenSOrders > 0: 3438 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3439 3440 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3441 3442 else: 3443 uLogger.info("Orders not found, nothing to cancel.") 3444 3445 def CloseAll(self, *args) -> None: 3446 """ 3447 Close all available (not blocked) opened trades and orders. 3448 3449 Also, you can select one or more keywords case-insensitive: 3450 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3451 3452 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3453 """ 3454 overview = self.Overview(show=False) # get all open trades info 3455 3456 if len(args) == 0: 3457 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3458 self.CloseAllOrders() # close all pending and stop orders 3459 3460 for iType in TKS_INSTRUMENTS: 3461 if iType != "Currencies": 3462 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3463 3464 else: 3465 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3466 lowerArgs = [x.lower() for x in args] 3467 3468 if "orders" in lowerArgs: 3469 self.CloseAllOrders() # close all pending and stop orders 3470 3471 for iType in TKS_INSTRUMENTS: 3472 if iType.lower() in lowerArgs and iType != "Currencies": 3473 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3474 3475 def CloseAllByTicker(self, instrument: str) -> None: 3476 """ 3477 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3478 3479 This method searches opened trade and orders of instrument throw all portfolio and then use 3480 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3481 3482 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3483 3484 :param instrument: string with ticker. 3485 """ 3486 if instrument is None or not instrument: 3487 uLogger.error("Ticker name must be defined for using this method!") 3488 raise Exception("Ticker required") 3489 3490 overview = self.Overview(show=False) # get user portfolio with all open trades info 3491 3492 self._ticker = instrument # try to set instrument as ticker 3493 self._figi = "" 3494 3495 if self.IsInPortfolio(portfolio=overview): 3496 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3497 self.CloseTrades(instruments=[instrument], portfolio=overview) 3498 3499 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3500 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3501 3502 if limitAll and self.IsInLimitOrders(portfolio=overview): 3503 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3504 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3505 3506 if stopAll and self.IsInStopOrders(portfolio=overview): 3507 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3508 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3509 3510 def CloseAllByFIGI(self, instrument: str) -> None: 3511 """ 3512 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3513 3514 This method searches opened trade and orders of instrument throw all portfolio and then use 3515 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3516 3517 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3518 3519 :param instrument: string with FIGI id. 3520 """ 3521 if instrument is None or not instrument: 3522 uLogger.error("FIGI id must be defined for using this method!") 3523 raise Exception("FIGI required") 3524 3525 overview = self.Overview(show=False) # get user portfolio with all open trades info 3526 3527 self._ticker = "" 3528 self._figi = instrument # try to set instrument as FIGI id 3529 3530 if self.IsInPortfolio(portfolio=overview): 3531 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3532 self.CloseTrades(instruments=[instrument], portfolio=overview) 3533 3534 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3535 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3536 3537 if limitAll and self.IsInLimitOrders(portfolio=overview): 3538 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3539 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3540 3541 if stopAll and self.IsInStopOrders(portfolio=overview): 3542 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3543 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3544 3545 @staticmethod 3546 def ParseOrderParameters(operation, **inputParameters): 3547 """ 3548 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3549 3550 :param operation: string "Buy" or "Sell". 3551 :param inputParameters: this is dict of strings that looks like this 3552 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3553 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3554 "prices" key: one or more prices to open limit-orders 3555 Counts of values in lots and prices lists must be equals! 3556 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3557 """ 3558 # TODO: update order grid work with api v2 3559 pass 3560 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3561 # 3562 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3563 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3564 # raise Exception("Incorrect value") 3565 # 3566 # if "l" in inputParameters.keys(): 3567 # inputParameters["lots"] = inputParameters.pop("l") 3568 # 3569 # if "p" in inputParameters.keys(): 3570 # inputParameters["prices"] = inputParameters.pop("p") 3571 # 3572 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3573 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3574 # raise Exception("Incorrect value") 3575 # 3576 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3577 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3578 # 3579 # if len(lots) != len(prices): 3580 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3581 # raise Exception("Incorrect value") 3582 # 3583 # uLogger.debug("Extracted parameters for orders:") 3584 # uLogger.debug("lots = {}".format(lots)) 3585 # uLogger.debug("prices = {}".format(prices)) 3586 # 3587 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3588 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3589 # uLogger.debug("Order parameters: {}".format(result)) 3590 # 3591 # return result 3592 3593 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3594 """ 3595 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3596 3597 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3598 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3599 """ 3600 result = False 3601 msg = "Instrument not defined!" 3602 3603 if portfolio is None or not portfolio: 3604 portfolio = self.Overview(show=False) 3605 3606 if self._ticker: 3607 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3608 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3609 3610 for iType in TKS_INSTRUMENTS: 3611 for instrument in portfolio["stat"][iType]: 3612 if instrument["ticker"] == self._ticker: 3613 result = True 3614 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3615 break 3616 3617 elif self._figi: 3618 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3619 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3620 3621 for iType in TKS_INSTRUMENTS: 3622 for instrument in portfolio["stat"][iType]: 3623 if instrument["figi"] == self._figi: 3624 result = True 3625 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3626 break 3627 3628 else: 3629 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3630 3631 uLogger.debug(msg) 3632 3633 return result 3634 3635 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3636 """ 3637 Returns instrument from the user's portfolio if it presents there. 3638 Instrument must be defined by `ticker` (highly priority) or `figi`. 3639 3640 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3641 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3642 """ 3643 result = None 3644 msg = "Instrument not defined!" 3645 3646 if portfolio is None or not portfolio: 3647 portfolio = self.Overview(show=False) 3648 3649 if self._ticker: 3650 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3651 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3652 3653 for iType in TKS_INSTRUMENTS: 3654 for instrument in portfolio["stat"][iType]: 3655 if instrument["ticker"] == self._ticker: 3656 result = instrument 3657 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3658 break 3659 3660 elif self._figi: 3661 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3662 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3663 3664 for iType in TKS_INSTRUMENTS: 3665 for instrument in portfolio["stat"][iType]: 3666 if instrument["figi"] == self._figi: 3667 result = instrument 3668 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3669 break 3670 3671 else: 3672 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3673 3674 uLogger.debug(msg) 3675 3676 return result 3677 3678 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3679 """ 3680 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3681 3682 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3683 3684 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3685 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3686 """ 3687 result = False 3688 msg = "Instrument not defined!" 3689 3690 if portfolio is None or not portfolio: 3691 portfolio = self.Overview(show=False) 3692 3693 if self._ticker: 3694 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3695 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3696 3697 for instrument in portfolio["stat"]["orders"]: 3698 if instrument["ticker"] == self._ticker: 3699 result = True 3700 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3701 break 3702 3703 elif self._figi: 3704 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3705 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3706 3707 for instrument in portfolio["stat"]["orders"]: 3708 if instrument["figi"] == self._figi: 3709 result = True 3710 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3711 break 3712 3713 else: 3714 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3715 3716 uLogger.debug(msg) 3717 3718 return result 3719 3720 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3721 """ 3722 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3723 Instrument must be defined by `ticker` (highly priority) or `figi`. 3724 3725 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3726 3727 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3728 :return: list with `orderID`s of limit orders. 3729 """ 3730 result = [] 3731 msg = "Instrument not defined!" 3732 3733 if portfolio is None or not portfolio: 3734 portfolio = self.Overview(show=False) 3735 3736 if self._ticker: 3737 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3738 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3739 3740 for instrument in portfolio["stat"]["orders"]: 3741 if instrument["ticker"] == self._ticker: 3742 result.append(instrument["orderID"]) 3743 3744 if result: 3745 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3746 3747 elif self._figi: 3748 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3749 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3750 3751 for instrument in portfolio["stat"]["orders"]: 3752 if instrument["figi"] == self._figi: 3753 result.append(instrument["orderID"]) 3754 3755 if result: 3756 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3757 3758 else: 3759 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3760 3761 uLogger.debug(msg) 3762 3763 return result 3764 3765 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3766 """ 3767 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3768 3769 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3770 3771 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3772 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3773 """ 3774 result = False 3775 msg = "Instrument not defined!" 3776 3777 if portfolio is None or not portfolio: 3778 portfolio = self.Overview(show=False) 3779 3780 if self._ticker: 3781 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3782 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3783 3784 for instrument in portfolio["stat"]["stopOrders"]: 3785 if instrument["ticker"] == self._ticker: 3786 result = True 3787 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3788 break 3789 3790 elif self._figi: 3791 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3792 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3793 3794 for instrument in portfolio["stat"]["stopOrders"]: 3795 if instrument["figi"] == self._figi: 3796 result = True 3797 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3798 break 3799 3800 else: 3801 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3802 3803 uLogger.debug(msg) 3804 3805 return result 3806 3807 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3808 """ 3809 Returns list with all `orderID`s of opened stop orders for the instrument. 3810 Instrument must be defined by `ticker` (highly priority) or `figi`. 3811 3812 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3813 3814 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3815 :return: list with `orderID`s of stop orders. 3816 """ 3817 result = [] 3818 msg = "Instrument not defined!" 3819 3820 if portfolio is None or not portfolio: 3821 portfolio = self.Overview(show=False) 3822 3823 if self._ticker: 3824 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3825 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3826 3827 for instrument in portfolio["stat"]["stopOrders"]: 3828 if instrument["ticker"] == self._ticker: 3829 result.append(instrument["orderID"]) 3830 3831 if result: 3832 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3833 3834 elif self._figi: 3835 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3836 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3837 3838 for instrument in portfolio["stat"]["stopOrders"]: 3839 if instrument["figi"] == self._figi: 3840 result.append(instrument["orderID"]) 3841 3842 if result: 3843 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3844 3845 else: 3846 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3847 3848 uLogger.debug(msg) 3849 3850 return result 3851 3852 def RequestLimits(self) -> dict: 3853 """ 3854 Method for obtaining the available funds for withdrawal for current `accountId`. 3855 3856 See also: 3857 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3858 - `OverviewLimits()` method 3859 3860 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3861 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3862 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3863 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3864 """ 3865 if self.accountId is None or not self.accountId: 3866 uLogger.error("Variable `accountId` must be defined for using this method!") 3867 raise Exception("Account ID required") 3868 3869 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3870 3871 self.body = str({"accountId": self.accountId}) 3872 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3873 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3874 3875 if self.moreDebug: 3876 uLogger.debug("Records about available funds for withdrawal successfully received") 3877 3878 return rawLimits 3879 3880 def OverviewLimits(self, show: bool = False) -> dict: 3881 """ 3882 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3883 3884 See also: `RequestLimits()`. 3885 3886 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3887 :return: dict with raw parsed data from server and some calculated statistics about it. 3888 """ 3889 if self.accountId is None or not self.accountId: 3890 uLogger.error("Variable `accountId` must be defined for using this method!") 3891 raise Exception("Account ID required") 3892 3893 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3894 3895 view = { 3896 "rawLimits": rawLimits, 3897 "limits": { # parsed data for every currency: 3898 "money": { # this is an array of portfolio currency positions 3899 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3900 }, 3901 "blocked": { # this is an array of blocked currency 3902 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3903 }, 3904 "blockedGuarantee": { # this is locked money under collateral for futures 3905 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3906 }, 3907 }, 3908 } 3909 3910 # --- Prepare text table with limits in human-readable format: 3911 if show: 3912 info = [ 3913 "# Withdrawal limits\n\n", 3914 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3915 "* **Account ID:** [{}]\n".format(self.accountId), 3916 ] 3917 3918 if view["limits"]["money"]: 3919 info.extend([ 3920 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3921 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3922 ]) 3923 3924 else: 3925 info.append("\nNo withdrawal limits\n") 3926 3927 for curr in view["limits"]["money"].keys(): 3928 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3929 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3930 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3931 3932 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3933 "[{}]".format(curr), 3934 "{:.2f}".format(view["limits"]["money"][curr]), 3935 "{:.2f}".format(availableMoney), 3936 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3937 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3938 ) 3939 3940 if curr == "rub": 3941 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3942 3943 else: 3944 info.append(infoStr) 3945 3946 infoText = "".join(info) 3947 3948 uLogger.info(infoText) 3949 3950 if self.withdrawalLimitsFile: 3951 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3952 fH.write(infoText) 3953 3954 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3955 3956 if self.useHTMLReports: 3957 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3958 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3959 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 3960 3961 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 3962 3963 return view 3964 3965 def RequestAccounts(self) -> dict: 3966 """ 3967 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3968 3969 See also: 3970 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3971 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3972 - `OverviewUserInfo()` method 3973 3974 :return: dict with raw data from server that contains accounts info. Example of dict: 3975 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3976 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3977 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3978 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3979 """ 3980 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3981 3982 self.body = str({}) 3983 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3984 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3985 3986 if self.moreDebug: 3987 uLogger.debug("Records about available accounts successfully received") 3988 3989 return rawAccounts 3990 3991 def RequestUserInfo(self) -> dict: 3992 """ 3993 Method for requesting common user's information. 3994 3995 See also: 3996 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3997 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3998 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3999 - `OverviewUserInfo()` method 4000 4001 :return: dict with raw data from server that contains user's information. Example of dict: 4002 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4003 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4004 """ 4005 uLogger.debug("Requesting common user's information. Wait, please...") 4006 4007 self.body = str({}) 4008 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4009 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4010 4011 if self.moreDebug: 4012 uLogger.debug("Records about current user successfully received") 4013 4014 return rawUserInfo 4015 4016 def RequestMarginStatus(self, accountId: str = None) -> dict: 4017 """ 4018 Method for requesting margin calculation for defined account ID. 4019 4020 See also: 4021 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4022 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4023 - `OverviewUserInfo()` method 4024 4025 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4026 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4027 Example of responses: 4028 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4029 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4030 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4031 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4032 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4033 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4034 """ 4035 if accountId is None or not accountId: 4036 if self.accountId is None or not self.accountId: 4037 uLogger.error("Variable `accountId` must be defined for using this method!") 4038 raise Exception("Account ID required") 4039 4040 else: 4041 accountId = self.accountId # use `self.accountId` (main ID) by default 4042 4043 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4044 4045 self.body = str({"accountId": accountId}) 4046 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4047 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4048 4049 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4050 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4051 rawMargin = {} 4052 4053 else: 4054 if self.moreDebug: 4055 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4056 4057 return rawMargin 4058 4059 def RequestTariffLimits(self) -> dict: 4060 """ 4061 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4062 4063 See also: 4064 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4065 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4066 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4067 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4068 - `OverviewUserInfo()` method 4069 4070 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4071 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4072 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4073 """ 4074 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4075 4076 self.body = str({}) 4077 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4078 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4079 4080 if self.moreDebug: 4081 uLogger.debug("Records with limits of current tariff successfully received") 4082 4083 return rawTariffLimits 4084 4085 def RequestBondCoupons(self, iJSON: dict) -> dict: 4086 """ 4087 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4088 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4089 All dates are in UTC timezone. 4090 4091 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4092 Documentation: 4093 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4094 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4095 4096 See also: `ExtendBondsData()`. 4097 4098 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4099 If raw iJSON is not data of bond then server returns an error [400] with message: 4100 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4101 :return: dictionary with bond payment calendar. Response example 4102 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4103 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4104 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4105 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4106 """ 4107 if iJSON["figi"] is None or not iJSON["figi"]: 4108 uLogger.error("FIGI must be defined for using this method!") 4109 raise Exception("FIGI required") 4110 4111 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4112 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4113 4114 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4115 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4116 self._figi, 4117 startDate, 4118 endDate, 4119 )) 4120 4121 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4122 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4123 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4124 4125 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4126 uLogger.warning("Instrument type is not bond!") 4127 4128 else: 4129 if self.moreDebug: 4130 uLogger.debug("Records about bond payment calendar successfully received") 4131 4132 return calendar 4133 4134 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4135 """ 4136 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4137 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4138 coupon yields, current yields and some statistics etc. 4139 4140 WARNING! This is too long operation if a lot of bonds requested from broker server. 4141 4142 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4143 4144 :param instruments: list of strings with tickers or FIGIs. 4145 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4146 for further used by data scientists or stock analytics. 4147 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4148 In XLSX-file and Pandas DataFrame fields mean: 4149 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4150 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4151 """ 4152 if instruments is None or not instruments: 4153 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4154 raise Exception("Ticker or FIGI required") 4155 4156 if isinstance(instruments, str): 4157 instruments = [instruments] 4158 4159 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4160 4161 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4162 4163 iCount = len(uniqueInstruments) 4164 tooLong = iCount >= 20 4165 if tooLong: 4166 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4167 4168 bonds = None 4169 for i, self._figi in enumerate(uniqueInstruments): 4170 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4171 4172 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4173 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4174 rawBond = self.SearchByFIGI(requestPrice=True) 4175 4176 # Widen raw data with UTC current time (iData["actualDateTime"]): 4177 actualDate = datetime.now(tzutc()) 4178 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4179 4180 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4181 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4182 4183 # Replace some values with human-readable: 4184 iData["nominalCurrency"] = iData["nominal"]["currency"] 4185 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4186 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4187 iData["aciCurrency"] = iData["aciValue"]["currency"] 4188 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4189 iData["issueSize"] = int(iData["issueSize"]) 4190 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4191 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4192 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4193 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4194 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4195 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4196 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4197 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4198 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4199 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4200 4201 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4202 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4203 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4204 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4205 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4206 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4207 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4208 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4209 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4210 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4211 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4212 4213 # Widen raw data with calendar data from `rawCalendar` values: 4214 calendarData = [] 4215 if "events" in iData["rawCalendar"].keys(): 4216 for item in iData["rawCalendar"]["events"]: 4217 calendarData.append({ 4218 "couponDate": item["couponDate"], 4219 "couponNumber": int(item["couponNumber"]), 4220 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4221 "payCurrency": item["payOneBond"]["currency"], 4222 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4223 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4224 "couponStartDate": item["couponStartDate"], 4225 "couponEndDate": item["couponEndDate"], 4226 "couponPeriod": item["couponPeriod"], 4227 }) 4228 4229 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4230 if "maturityDate" not in iData.keys(): 4231 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4232 4233 # Widen raw data with Coupon Rate. 4234 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4235 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4236 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4237 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4238 4239 # Widen raw data with Yield to Maturity (YTM) on current date. 4240 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4241 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4242 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4243 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4244 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4245 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4246 4247 iData["calendar"] = calendarData # adds calendar at the end 4248 4249 # Remove not used data: 4250 iData.pop("uid") 4251 iData.pop("positionUid") 4252 iData.pop("currentPrice") 4253 iData.pop("rawCalendar") 4254 4255 colNames = list(iData.keys()) 4256 if bonds is None: 4257 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4258 4259 else: 4260 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4261 4262 else: 4263 uLogger.warning("Instrument is not a bond!") 4264 4265 processed = round(100 * (i + 1) / iCount, 1) 4266 if tooLong and processed % 5 == 0: 4267 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4268 4269 else: 4270 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4271 4272 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4273 4274 # Saving bonds from Pandas DataFrame to XLSX sheet: 4275 if xlsx and self.bondsXLSXFile: 4276 with pd.ExcelWriter( 4277 path=self.bondsXLSXFile, 4278 date_format=TKS_DATE_FORMAT, 4279 datetime_format=TKS_DATE_TIME_FORMAT, 4280 mode="w", 4281 ) as writer: 4282 bonds.to_excel( 4283 writer, 4284 sheet_name="Extended bonds data", 4285 index=True, 4286 encoding="UTF-8", 4287 freeze_panes=(1, 1), 4288 ) # saving as XLSX-file with freeze first row and column as headers 4289 4290 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4291 4292 return bonds 4293 4294 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4295 """ 4296 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4297 4298 WARNING! This is too long operation if a lot of bonds requested from broker server. 4299 4300 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4301 4302 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4303 extended information about bonds: main info, current prices, bond payment calendar, 4304 coupon yields, current yields and some statistics etc. 4305 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4306 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4307 for further used by data scientists or stock analytics. 4308 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4309 """ 4310 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4311 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4312 4313 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4314 4315 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4316 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4317 calendar = None 4318 for bond in extBonds.iterrows(): 4319 for item in bond[1]["calendar"]: 4320 cData = { 4321 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4322 "couponDate": item["couponDate"], 4323 "figi": bond[1]["figi"], 4324 "ticker": bond[1]["ticker"], 4325 "name": bond[1]["name"], 4326 "couponNumber": item["couponNumber"], 4327 "payOneBond": item["payOneBond"], 4328 "payCurrency": item["payCurrency"], 4329 "couponType": item["couponType"], 4330 "couponPeriod": item["couponPeriod"], 4331 "fixDate": item["fixDate"], 4332 "couponStartDate": item["couponStartDate"], 4333 "couponEndDate": item["couponEndDate"], 4334 } 4335 4336 if calendar is None: 4337 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4338 4339 else: 4340 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4341 4342 if calendar is not None: 4343 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4344 4345 # Saving calendar from Pandas DataFrame to XLSX sheet: 4346 if xlsx: 4347 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4348 4349 with pd.ExcelWriter( 4350 path=xlsxCalendarFile, 4351 date_format=TKS_DATE_FORMAT, 4352 datetime_format=TKS_DATE_TIME_FORMAT, 4353 mode="w", 4354 ) as writer: 4355 humanReadable = calendar.copy(deep=True) 4356 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4357 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4358 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4359 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4360 humanReadable.columns = colNames # human-readable column names 4361 4362 humanReadable.to_excel( 4363 writer, 4364 sheet_name="Bond payments calendar", 4365 index=False, 4366 encoding="UTF-8", 4367 freeze_panes=(1, 2), 4368 ) # saving as XLSX-file with freeze first row and column as headers 4369 4370 del humanReadable # release df in memory 4371 4372 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4373 4374 return calendar 4375 4376 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4377 """ 4378 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4379 Also, creates Markdown file with calendar data, `calendar.md` by default. 4380 4381 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4382 4383 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4384 extended information about bonds: main info, current prices, bond payment calendar, 4385 coupon yields, current yields and some statistics etc. 4386 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4387 :param show: if `True` then also printing bonds payment calendar to the console, 4388 otherwise save to file `calendarFile` only. `False` by default. 4389 :return: multilines text in Markdown format with bonds payment calendar as a table. 4390 """ 4391 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4392 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4393 4394 infoText = "# Bond payments calendar\n\n" 4395 4396 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4397 4398 if not (calendar is None or calendar.empty): 4399 splitLine = "| | | | | | | | | |\n" 4400 4401 info = [ 4402 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4403 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4404 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4405 ] 4406 4407 newMonth = False 4408 notOneBond = calendar["figi"].nunique() > 1 4409 for i, bond in enumerate(calendar.iterrows()): 4410 if newMonth and notOneBond: 4411 info.append(splitLine) 4412 4413 info.append( 4414 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4415 " √" if bond[1]["paid"] else " —", 4416 bond[1]["couponDate"].split("T")[0], 4417 bond[1]["figi"], 4418 bond[1]["ticker"], 4419 bond[1]["couponNumber"], 4420 "{} {}".format( 4421 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4422 bond[1]["payCurrency"], 4423 ), 4424 bond[1]["couponType"], 4425 bond[1]["couponPeriod"], 4426 bond[1]["fixDate"].split("T")[0], 4427 ) 4428 ) 4429 4430 if i < len(calendar.values) - 1: 4431 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4432 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4433 newMonth = False if curDate.month == nextDate.month else True 4434 4435 else: 4436 newMonth = False 4437 4438 infoText += "".join(info) 4439 4440 if show: 4441 uLogger.info("{}".format(infoText)) 4442 4443 if self.calendarFile is not None: 4444 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4445 fH.write(infoText) 4446 4447 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4448 4449 if self.useHTMLReports: 4450 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4451 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4452 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4453 4454 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4455 4456 else: 4457 infoText += "No data\n" 4458 4459 return infoText 4460 4461 def OverviewAccounts(self, show: bool = False) -> dict: 4462 """ 4463 Method for parsing and show simple table with all available user accounts. 4464 4465 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4466 4467 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4468 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4469 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4470 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4471 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4472 "closed": "—", "access": "Full access" }, ...}}` 4473 """ 4474 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4475 4476 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4477 accounts = { 4478 item["id"]: { 4479 "type": TKS_ACCOUNT_TYPES[item["type"]], 4480 "name": item["name"], 4481 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4482 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4483 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4484 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4485 } for item in rawAccounts["accounts"] 4486 } 4487 4488 # Raw and parsed data with some fields replaced in "stat" section: 4489 view = { 4490 "rawAccounts": rawAccounts, 4491 "stat": accounts, 4492 } 4493 4494 # --- Prepare simple text table with only accounts data in human-readable format: 4495 if show: 4496 info = [ 4497 "# User accounts\n\n", 4498 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4499 "| Account ID | Type | Status | Name |\n", 4500 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4501 ] 4502 4503 for account in view["stat"].keys(): 4504 info.extend([ 4505 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4506 account, 4507 view["stat"][account]["type"], 4508 view["stat"][account]["status"], 4509 view["stat"][account]["name"], 4510 ) 4511 ]) 4512 4513 infoText = "".join(info) 4514 4515 uLogger.info(infoText) 4516 4517 if self.userAccountsFile: 4518 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4519 fH.write(infoText) 4520 4521 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4522 4523 if self.useHTMLReports: 4524 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4525 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4526 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4527 4528 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4529 4530 return view 4531 4532 def OverviewUserInfo(self, show: bool = False) -> dict: 4533 """ 4534 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4535 4536 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4537 4538 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4539 :return: dict with raw parsed data from server and some calculated statistics about it. 4540 """ 4541 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4542 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4543 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4544 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4545 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4546 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4547 4548 # This is dict with parsed common user data: 4549 userInfo = { 4550 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4551 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4552 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4553 "tariff": rawUserInfo["tariff"], 4554 } 4555 4556 # This is an array of dict with parsed margin statuses for every account IDs: 4557 margins = {} 4558 for accountId in accounts.keys(): 4559 if rawMargins[accountId]: 4560 margins[accountId] = { 4561 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4562 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4563 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4564 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4565 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4566 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4567 } 4568 4569 else: 4570 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4571 4572 unary = {} # unary-connection limits 4573 for item in rawTariffLimits["unaryLimits"]: 4574 if item["limitPerMinute"] in unary.keys(): 4575 unary[item["limitPerMinute"]].extend(item["methods"]) 4576 4577 else: 4578 unary[item["limitPerMinute"]] = item["methods"] 4579 4580 stream = {} # stream-connection limits 4581 for item in rawTariffLimits["streamLimits"]: 4582 if item["limit"] in stream.keys(): 4583 stream[item["limit"]].extend(item["streams"]) 4584 4585 else: 4586 stream[item["limit"]] = item["streams"] 4587 4588 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4589 limits = { 4590 "unary": unary, 4591 "stream": stream, 4592 } 4593 4594 # Raw and parsed data as an output result: 4595 view = { 4596 "rawUserInfo": rawUserInfo, 4597 "rawAccounts": rawAccounts, 4598 "rawMargins": rawMargins, 4599 "rawTariffLimits": rawTariffLimits, 4600 "stat": { 4601 "userInfo": userInfo, 4602 "accounts": accounts, 4603 "margins": margins, 4604 "limits": limits, 4605 }, 4606 } 4607 4608 # --- Prepare text table with user information in human-readable format: 4609 if show: 4610 info = [ 4611 "# Full user information\n\n", 4612 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4613 "## Common information\n\n", 4614 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4615 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4616 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4617 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4618 "\n## User accounts\n\n", 4619 ] 4620 4621 for account in view["stat"]["accounts"].keys(): 4622 info.extend([ 4623 "### ID: [{}]\n\n".format(account), 4624 "| Parameters | Values |\n", 4625 "|----------------------|--------------------------------------------------------------|\n", 4626 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4627 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4628 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4629 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4630 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4631 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4632 ]) 4633 4634 if margins[account]: 4635 info.extend([ 4636 "| Margin status: | Enabled |\n", 4637 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4638 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4639 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4640 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4641 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4642 ]) 4643 4644 else: 4645 info.append("| Margin status: | Disabled |\n\n") 4646 4647 info.extend([ 4648 "\n## Current user tariff limits\n", 4649 "\n### See also\n", 4650 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4651 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4652 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4653 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4654 "\n### Unary limits\n", 4655 ]) 4656 4657 if unary: 4658 for key, values in sorted(unary.items()): 4659 info.append("\n* Max requests per minute: {}\n".format(key)) 4660 4661 for value in values: 4662 info.append(" - {}\n".format(value)) 4663 4664 else: 4665 info.append("\nNot available\n") 4666 4667 info.append("\n### Stream limits\n") 4668 4669 if stream: 4670 for key, values in sorted(stream.items()): 4671 info.append("\n* Max stream connections: {}\n".format(key)) 4672 4673 for value in values: 4674 info.append(" - {}\n".format(value)) 4675 4676 else: 4677 info.append("\nNot available\n") 4678 4679 infoText = "".join(info) 4680 4681 uLogger.info(infoText) 4682 4683 if self.userInfoFile: 4684 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4685 fH.write(infoText) 4686 4687 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4688 4689 if self.useHTMLReports: 4690 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4691 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4692 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4693 4694 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4695 4696 return view 4697 4698 4699class Args: 4700 """ 4701 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4702 """ 4703 def __init__(self, **kwargs): 4704 self.__dict__.update(kwargs) 4705 4706 def __getattr__(self, item): 4707 return None 4708 4709 4710def ParseArgs(): 4711 """This function get and parse command line keys.""" 4712 parser = ArgumentParser() # command-line string parser 4713 4714 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4715 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4716 4717 # --- options: 4718 4719 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4720 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4721 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4722 4723 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4724 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4725 4726 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4727 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4728 4729 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4730 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4731 4732 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4733 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4734 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4735 4736 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4737 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4738 4739 # --- commands: 4740 4741 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4742 4743 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4744 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4745 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4746 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4747 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4748 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4749 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4750 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4751 4752 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4753 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4754 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4755 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4756 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4757 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4758 4759 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4760 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4761 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4762 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4763 4764 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4765 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4766 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4767 4768 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4769 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4770 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4771 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4772 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4773 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4774 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4775 4776 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4777 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4778 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4779 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4780 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4781 4782 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4783 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4784 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4785 4786 cmdArgs = parser.parse_args() 4787 return cmdArgs 4788 4789 4790def Main(**kwargs): 4791 """ 4792 Main function for work with TKSBrokerAPI in the console. 4793 4794 See examples: 4795 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4796 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4797 """ 4798 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4799 4800 if args.debug_level: 4801 uLogger.level = 10 # always debug level by default 4802 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4803 4804 exitCode = 0 4805 start = datetime.now(tzutc()) 4806 uLogger.debug("=-" * 50) 4807 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4808 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4809 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4810 )) 4811 4812 # trying to calculate full current version: 4813 buildVersion = __version__ 4814 try: 4815 v = version("tksbrokerapi") 4816 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4817 4818 except Exception: 4819 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4820 4821 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4822 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4823 4824 try: 4825 if args.version: 4826 print("TKSBrokerAPI {}".format(buildVersion)) 4827 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4828 4829 else: 4830 # Init class for trading with Tinkoff Broker: 4831 trader = TinkoffBrokerServer( 4832 token=args.token, 4833 accountId=args.account_id, 4834 useCache=not args.no_cache, 4835 ) 4836 4837 # --- set some options: 4838 4839 if args.more: 4840 trader.moreDebug = True 4841 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4842 4843 if args.html: 4844 trader.useHTMLReports = True 4845 4846 if args.ticker: 4847 ticker = str(args.ticker).upper() # Tickers may be upper case only 4848 4849 if ticker in trader.aliasesKeys: 4850 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4851 4852 else: 4853 trader.ticker = ticker 4854 4855 if args.figi: 4856 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4857 4858 if args.depth is not None: 4859 trader.depth = args.depth 4860 4861 # --- do one command: 4862 4863 if args.list: 4864 if args.output is not None: 4865 trader.instrumentsFile = args.output 4866 4867 trader.ShowInstrumentsInfo(show=True) 4868 4869 elif args.list_xlsx: 4870 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4871 4872 elif args.bonds_xlsx is not None: 4873 if args.output is not None: 4874 trader.bondsXLSXFile = args.output 4875 4876 if len(args.bonds_xlsx) == 0: 4877 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4878 4879 else: 4880 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4881 4882 elif args.search: 4883 if args.output is not None: 4884 trader.searchResultsFile = args.output 4885 4886 trader.SearchInstruments(pattern=args.search[0], show=True) 4887 4888 elif args.info: 4889 if not (args.ticker or args.figi): 4890 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4891 raise Exception("Ticker or FIGI required") 4892 4893 if args.output is not None: 4894 trader.infoFile = args.output 4895 4896 if args.ticker: 4897 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4898 4899 else: 4900 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4901 4902 elif args.calendar is not None: 4903 if args.output is not None: 4904 trader.calendarFile = args.output 4905 4906 if len(args.calendar) == 0: 4907 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4908 4909 else: 4910 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4911 4912 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4913 4914 elif args.price: 4915 if not (args.ticker or args.figi): 4916 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4917 raise Exception("Ticker or FIGI required") 4918 4919 trader.GetCurrentPrices(show=True) 4920 4921 elif args.prices is not None: 4922 if args.output is not None: 4923 trader.pricesFile = args.output 4924 4925 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4926 4927 elif args.overview: 4928 if args.output is not None: 4929 trader.overviewFile = args.output 4930 4931 trader.Overview(show=True, details="full") 4932 4933 elif args.overview_digest: 4934 if args.output is not None: 4935 trader.overviewDigestFile = args.output 4936 4937 trader.Overview(show=True, details="digest") 4938 4939 elif args.overview_positions: 4940 if args.output is not None: 4941 trader.overviewPositionsFile = args.output 4942 4943 trader.Overview(show=True, details="positions") 4944 4945 elif args.overview_orders: 4946 if args.output is not None: 4947 trader.overviewOrdersFile = args.output 4948 4949 trader.Overview(show=True, details="orders") 4950 4951 elif args.overview_analytics: 4952 if args.output is not None: 4953 trader.overviewAnalyticsFile = args.output 4954 4955 trader.Overview(show=True, details="analytics") 4956 4957 elif args.overview_calendar: 4958 if args.output is not None: 4959 trader.overviewAnalyticsFile = args.output 4960 4961 trader.Overview(show=True, details="calendar") 4962 4963 elif args.deals is not None: 4964 if args.output is not None: 4965 trader.reportFile = args.output 4966 4967 if 0 <= len(args.deals) < 3: 4968 trader.Deals( 4969 start=args.deals[0] if len(args.deals) >= 1 else None, 4970 end=args.deals[1] if len(args.deals) == 2 else None, 4971 show=True, # Always show deals report in console 4972 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4973 ) 4974 4975 else: 4976 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4977 raise Exception("Incorrect value") 4978 4979 elif args.history is not None: 4980 if args.output is not None: 4981 trader.historyFile = args.output 4982 4983 if 0 <= len(args.history) < 3: 4984 dataReceived = trader.History( 4985 start=args.history[0] if len(args.history) >= 1 else None, 4986 end=args.history[1] if len(args.history) == 2 else None, 4987 interval="hour" if args.interval is None or not args.interval else args.interval, 4988 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4989 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4990 show=True, # shows all downloaded candles in console 4991 ) 4992 4993 if args.render_chart is not None and dataReceived is not None: 4994 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4995 4996 trader.ShowHistoryChart( 4997 candles=dataReceived, 4998 interact=iChart, 4999 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5000 ) 5001 5002 else: 5003 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5004 raise Exception("Incorrect value") 5005 5006 elif args.load_history is not None: 5007 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5008 5009 if args.render_chart is not None and histData is not None: 5010 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5011 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5012 5013 trader.ShowHistoryChart( 5014 candles=histData, 5015 interact=iChart, 5016 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5017 ) 5018 5019 elif args.trade is not None: 5020 if 1 <= len(args.trade) <= 5: 5021 trader.Trade( 5022 operation=args.trade[0], 5023 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5024 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5025 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5026 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5027 ) 5028 5029 else: 5030 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5031 5032 elif args.buy is not None: 5033 if 0 <= len(args.buy) <= 4: 5034 trader.Buy( 5035 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5036 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5037 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5038 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5039 ) 5040 5041 else: 5042 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5043 5044 elif args.sell is not None: 5045 if 0 <= len(args.sell) <= 4: 5046 trader.Sell( 5047 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5048 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5049 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5050 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5051 ) 5052 5053 else: 5054 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5055 5056 elif args.order: 5057 if 4 <= len(args.order) <= 7: 5058 trader.Order( 5059 operation=args.order[0], 5060 orderType=args.order[1], 5061 lots=int(args.order[2]), 5062 targetPrice=float(args.order[3]), 5063 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5064 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5065 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5066 ) 5067 5068 else: 5069 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5070 5071 elif args.buy_limit: 5072 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5073 5074 elif args.sell_limit: 5075 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5076 5077 elif args.buy_stop: 5078 if 2 <= len(args.buy_stop) <= 7: 5079 trader.BuyStop( 5080 lots=int(args.buy_stop[0]), 5081 targetPrice=float(args.buy_stop[1]), 5082 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5083 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5084 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5085 ) 5086 5087 else: 5088 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5089 5090 elif args.sell_stop: 5091 if 2 <= len(args.sell_stop) <= 7: 5092 trader.SellStop( 5093 lots=int(args.sell_stop[0]), 5094 targetPrice=float(args.sell_stop[1]), 5095 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5096 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5097 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5098 ) 5099 5100 else: 5101 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5102 5103 # elif args.buy_order_grid is not None: 5104 # # update order grid work with api v2 5105 # if len(args.buy_order_grid) == 2: 5106 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5107 # 5108 # for order in orderParams: 5109 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5110 # 5111 # else: 5112 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5113 # 5114 # elif args.sell_order_grid is not None: 5115 # # update order grid work with api v2 5116 # if len(args.sell_order_grid) >= 2: 5117 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5118 # 5119 # for order in orderParams: 5120 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5121 # 5122 # else: 5123 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5124 5125 elif args.close_order is not None: 5126 trader.CloseOrders(args.close_order) # close only one order 5127 5128 elif args.close_orders is not None: 5129 trader.CloseOrders(args.close_orders) # close list of orders 5130 5131 elif args.close_trade: 5132 if not (args.ticker or args.figi): 5133 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5134 raise Exception("Ticker or FIGI required") 5135 5136 if args.ticker: 5137 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5138 5139 else: 5140 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5141 5142 elif args.close_trades is not None: 5143 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5144 5145 elif args.close_all is not None: 5146 if args.ticker: 5147 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5148 5149 elif args.figi: 5150 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5151 5152 else: 5153 trader.CloseAll(*args.close_all) 5154 5155 elif args.limits: 5156 if args.output is not None: 5157 trader.withdrawalLimitsFile = args.output 5158 5159 trader.OverviewLimits(show=True) 5160 5161 elif args.user_info: 5162 if args.output is not None: 5163 trader.userInfoFile = args.output 5164 5165 trader.OverviewUserInfo(show=True) 5166 5167 elif args.account: 5168 if args.output is not None: 5169 trader.userAccountsFile = args.output 5170 5171 trader.OverviewAccounts(show=True) 5172 5173 else: 5174 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5175 raise Exception("There is no command to execute") 5176 5177 except Exception: 5178 trace = tb.format_exc() 5179 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5180 if e in trace: 5181 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5182 break 5183 5184 uLogger.debug(trace) 5185 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5186 exitCode = 255 # an error occurred, must be open a ticket for this issue 5187 5188 finally: 5189 finish = datetime.now(tzutc()) 5190 5191 if exitCode == 0: 5192 if args.more: 5193 uLogger.debug("All operations were finished success (summary code is 0).") 5194 5195 else: 5196 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5197 os.path.abspath(uLog.defaultLogFile), exitCode, 5198 )) 5199 5200 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5201 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5202 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5203 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5204 )) 5205 uLogger.debug("=-" * 50) 5206 5207 if not kwargs: 5208 sys.exit(exitCode) 5209 5210 else: 5211 return exitCode 5212 5213 5214if __name__ == "__main__": 5215 Main()
78class TinkoffBrokerServer: 79 """ 80 This class implements methods to work with Tinkoff broker server. 81 82 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 83 84 About `token`: https://tinkoff.github.io/investAPI/token/ 85 """ 86 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 87 """ 88 Main class init. 89 90 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 91 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 92 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 93 :param useCache: use default cache file with raw data to use instead of `iList`. 94 True by default. Cache is auto-update if new day has come. 95 If you don't want to use cache and always updates raw data then set `useCache=False`. 96 :param defaultCache: path to default cache file. `dump.json` by default. 97 """ 98 if token is None or not token: 99 try: 100 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 101 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 102 103 except KeyError: 104 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 105 raise Exception("Token required") 106 107 else: 108 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 109 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 110 111 if accountId is None or not accountId: 112 try: 113 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 114 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 115 116 except KeyError: 117 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 118 119 else: 120 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 121 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 122 123 self.version = __version__ # duplicate here used TKSBrokerAPI main version 124 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 125 126 Latest version: https://pypi.org/project/tksbrokerapi/ 127 """ 128 129 self.aliases = TKS_TICKER_ALIASES 130 """Some aliases instead official tickers. 131 132 See also: `TKSEnums.TKS_TICKER_ALIASES` 133 """ 134 135 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 136 137 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 138 139 self._ticker = "" 140 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 141 142 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 143 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 144 145 See also: `SearchByTicker()`, `SearchInstruments()`. 146 """ 147 148 self._figi = "" 149 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 150 151 See also: `SearchByFIGI()`, `SearchInstruments()`. 152 """ 153 154 self.depth = 1 155 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 156 157 See also: `GetCurrentPrices()`. 158 """ 159 160 self.server = r"https://invest-public-api.tinkoff.ru/rest" 161 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 162 163 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 164 """ 165 166 uLogger.debug("Broker API server: {}".format(self.server)) 167 168 self.timeout = 15 169 """Server operations timeout in seconds. Default: `15`. 170 171 See also: `SendAPIRequest()`. 172 """ 173 174 self.headers = { 175 "Content-Type": "application/json", 176 "accept": "application/json", 177 "Authorization": "Bearer {}".format(self.token), 178 "x-app-name": "Tim55667757.TKSBrokerAPI", 179 } 180 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 181 182 See also: `SendAPIRequest()`. 183 """ 184 185 self.body = None 186 """Request body which send to broker server. Default: `None`. 187 188 See also: `SendAPIRequest()`. 189 """ 190 191 self.moreDebug = False 192 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 193 194 self.useHTMLReports = False 195 """ 196 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 197 198 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 199 """ 200 201 self.historyFile = None 202 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 203 204 See also: `History()`. 205 """ 206 207 self.htmlHistoryFile = "index.html" 208 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 209 210 See also: `ShowHistoryChart()`. 211 """ 212 213 self.instrumentsFile = "instruments.md" 214 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 215 216 See also: `ShowInstrumentsInfo()`. 217 """ 218 219 self.searchResultsFile = "search-results.md" 220 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 221 222 See also: `SearchInstruments()`. 223 """ 224 225 self.pricesFile = "prices.md" 226 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 227 228 See also: `GetListOfPrices()`. 229 """ 230 231 self.infoFile = "info.md" 232 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 233 234 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 235 """ 236 237 self.bondsXLSXFile = "ext-bonds.xlsx" 238 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 239 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 240 241 See also: `ExtendBondsData()`. 242 """ 243 244 self.calendarFile = "calendar.md" 245 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 246 247 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 248 249 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 250 """ 251 252 self.overviewFile = "overview.md" 253 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 254 255 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 256 """ 257 258 self.overviewDigestFile = "overview-digest.md" 259 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 260 261 See also: `Overview()` with parameter `details="digest"`. 262 """ 263 264 self.overviewPositionsFile = "overview-positions.md" 265 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 266 267 See also: `Overview()` with parameter `details="positions"`. 268 """ 269 270 self.overviewOrdersFile = "overview-orders.md" 271 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 272 273 See also: `Overview()` with parameter `details="orders"`. 274 """ 275 276 self.overviewAnalyticsFile = "overview-analytics.md" 277 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 278 279 See also: `Overview()` with parameter `details="analytics"`. 280 """ 281 282 self.overviewBondsCalendarFile = "overview-calendar.md" 283 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 284 285 See also: `Overview()` with parameter `details="calendar"`. 286 """ 287 288 self.reportFile = "deals.md" 289 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 290 291 See also: `Deals()`. 292 """ 293 294 self.withdrawalLimitsFile = "limits.md" 295 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 296 297 See also: `OverviewLimits()` and `RequestLimits()`. 298 """ 299 300 self.userInfoFile = "user-info.md" 301 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 302 303 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 304 """ 305 306 self.userAccountsFile = "accounts.md" 307 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 308 309 See also: `OverviewAccounts()`, `RequestAccounts()`. 310 """ 311 312 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 313 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 314 315 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 316 317 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 318 """ 319 320 self.iList = None # init iList for raw instruments data 321 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 322 323 See also: `Listing()`, `DumpInstruments()`. 324 """ 325 326 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 327 if useCache: 328 if os.path.exists(self.iListDumpFile): 329 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 330 curTime = datetime.now(tzutc()) 331 332 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 333 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 334 335 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 336 337 else: 338 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 339 340 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 341 os.path.abspath(self.iListDumpFile), 342 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 343 )) 344 345 else: 346 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 347 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 348 349 else: 350 self.iList = self.Listing() # request new raw instruments data from broker server 351 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 352 353 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 354 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 355 356 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 357 """ 358 359 @property 360 def ticker(self) -> str: 361 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 362 363 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 364 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 365 366 See also: `SearchByTicker()`, `SearchInstruments()`. 367 """ 368 return self._ticker 369 370 @ticker.setter 371 def ticker(self, value): 372 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 373 374 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 375 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 376 377 See also: `SearchByTicker()`, `SearchInstruments()`. 378 """ 379 self._ticker = str(value).upper() # Tickers may be upper case only 380 381 @property 382 def figi(self) -> str: 383 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 384 385 See also: `SearchByFIGI()`, `SearchInstruments()`. 386 """ 387 return self._figi 388 389 @figi.setter 390 def figi(self, value): 391 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 392 393 See also: `SearchByFIGI()`, `SearchInstruments()`. 394 """ 395 self._figi = str(value).upper() # FIGI may be upper case only 396 397 def _ParseJSON(self, rawData="{}") -> dict: 398 """ 399 Parse JSON from response string. 400 401 :param rawData: this is a string with JSON-formatted text. 402 :return: JSON (dictionary), parsed from server response string. 403 """ 404 responseJSON = json.loads(rawData) if rawData else {} 405 406 if self.moreDebug: 407 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 408 409 return responseJSON 410 411 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 412 """ 413 Send GET or POST request to broker server and receive JSON object. 414 415 self.header: must be defining with dictionary of headers. 416 self.body: if define then used as request body. None by default. 417 self.timeout: global request timeout, 15 seconds by default. 418 :param url: url with REST request. 419 :param reqType: send "GET" or "POST" request. "GET" by default. 420 :param retry: how many times retry after first request if an 5xx server errors occurred. 421 :param pause: sleep time in seconds between retries. 422 :return: response JSON (dictionary) from broker. 423 """ 424 if reqType.upper() not in ("GET", "POST"): 425 uLogger.error("You can define request type: `GET` or `POST`!") 426 raise Exception("Incorrect value") 427 428 if self.moreDebug: 429 uLogger.debug("Request parameters:") 430 uLogger.debug(" - REST API URL: {}".format(url)) 431 uLogger.debug(" - request type: {}".format(reqType)) 432 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 433 uLogger.debug(" - body:\n{}".format(self.body)) 434 435 # fast hack to avoid all operations with some tickers/FIGI 436 responseJSON = {} 437 oK = True 438 for item in self.exclude: 439 if item in url: 440 if self.moreDebug: 441 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 442 443 oK = False 444 break 445 446 if oK: 447 counter = 0 448 response = None 449 errMsg = "" 450 451 while not response and counter <= retry: 452 if reqType == "GET": 453 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 454 455 if reqType == "POST": 456 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 457 458 if self.moreDebug: 459 uLogger.debug("Response:") 460 uLogger.debug(" - status code: {}".format(response.status_code)) 461 uLogger.debug(" - reason: {}".format(response.reason)) 462 uLogger.debug(" - body length: {}".format(len(response.text))) 463 uLogger.debug(" - headers:\n{}".format(response.headers)) 464 465 # Server returns some headers: 466 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 467 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 468 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 469 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 470 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 471 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 472 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 473 sleep(rateLimitWait) 474 475 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 476 if 400 <= response.status_code < 500: 477 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 478 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 479 480 if "code" in response.text and "message" in response.text: 481 msgDict = self._ParseJSON(rawData=response.text) 482 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 483 484 counter = retry + 1 # do not retry for 4xx errors 485 486 if 500 <= response.status_code < 600: 487 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 488 uLogger.debug(" - not oK, {}".format(errMsg)) 489 490 if "code" in response.text and "message" in response.text: 491 errMsgDict = self._ParseJSON(rawData=response.text) 492 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 493 494 counter += 1 495 496 if counter <= retry: 497 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 498 sleep(pause) 499 500 responseJSON = self._ParseJSON(rawData=response.text) 501 502 if errMsg: 503 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 504 uLogger.error(" - not oK, {}".format(errMsg)) 505 506 return responseJSON 507 508 def _IUpdater(self, iType: str) -> tuple: 509 """ 510 Request instrument by type from server. See available API methods for instruments: 511 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 512 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 513 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 514 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 515 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 516 517 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 518 :return: tuple with iType name and list of available instruments of current type for defined user token. 519 """ 520 result = [] 521 522 if iType in TKS_INSTRUMENTS: 523 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 524 525 # all instruments have the same body in API v2 requests: 526 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 527 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 528 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 529 530 return iType, result 531 532 def _IWrapper(self, kwargs): 533 """ 534 Wrapper runs instrument's update method `_IUpdater()`. 535 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 536 """ 537 return self._IUpdater(**kwargs) 538 539 def Listing(self) -> dict: 540 """ 541 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 542 543 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 544 """ 545 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 546 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 547 548 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 549 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 550 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 551 552 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 553 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 554 poolUpdater.close() 555 556 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 557 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 558 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 559 560 # calculate minimum price increment (step) for all instruments and set up instrument's type: 561 for iType in iList.keys(): 562 for ticker in iList[iType]: 563 iList[iType][ticker]["type"] = iType 564 565 if "minPriceIncrement" in iList[iType][ticker].keys(): 566 iList[iType][ticker]["step"] = NanoToFloat( 567 iList[iType][ticker]["minPriceIncrement"]["units"], 568 iList[iType][ticker]["minPriceIncrement"]["nano"], 569 ) 570 571 else: 572 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 573 574 return iList 575 576 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 577 """ 578 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 579 580 See also: `DumpInstruments()`, `Listing()`. 581 582 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 583 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 584 """ 585 if self.iListDumpFile is None or not self.iListDumpFile: 586 uLogger.error("Output name of dump file must be defined!") 587 raise Exception("Filename required") 588 589 if not self.iList or forceUpdate: 590 self.iList = self.Listing() 591 592 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 593 594 # Save as XLSX with separated sheets for every type of instruments: 595 with pd.ExcelWriter( 596 path=xlsxDumpFile, 597 date_format=TKS_DATE_FORMAT, 598 datetime_format=TKS_DATE_TIME_FORMAT, 599 mode="w", 600 ) as writer: 601 for iType in TKS_INSTRUMENTS: 602 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 603 df = df[sorted(df)] # sorted by column names 604 df = df.applymap( 605 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 606 na_action="ignore", 607 ) # converting numbers from nano-type to float in every cell 608 df.to_excel( 609 writer, 610 sheet_name=iType, 611 encoding="UTF-8", 612 freeze_panes=(1, 1), 613 ) # saving as XLSX-file with freeze first row and column as headers 614 615 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 616 617 def DumpInstruments(self, forceUpdate: bool = True) -> str: 618 """ 619 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 620 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 621 622 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 623 624 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 625 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 626 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 627 """ 628 if self.iListDumpFile is None or not self.iListDumpFile: 629 uLogger.error("Output name of dump file must be defined!") 630 raise Exception("Filename required") 631 632 if not self.iList or forceUpdate: 633 self.iList = self.Listing() 634 635 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 636 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 637 fH.write(jsonDump) 638 639 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 640 641 return jsonDump 642 643 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 644 """ 645 Show information about one instrument defined by json data and prints it in Markdown format. 646 647 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 648 649 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 650 :param show: if `True` then also printing information about instrument and its current price. 651 :return: multilines text in Markdown format with information about one instrument. 652 """ 653 splitLine = "| | |\n" 654 infoText = "" 655 656 if iJSON is not None and iJSON and isinstance(iJSON, dict): 657 info = [ 658 "# Main information\n\n", 659 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 660 "| Parameters | Values |\n", 661 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 662 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 663 "| Full name: | {:<54} |\n".format(iJSON["name"]), 664 ] 665 666 if "sector" in iJSON.keys() and iJSON["sector"]: 667 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 668 669 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 670 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 671 672 info.extend([ 673 splitLine, 674 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 675 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 676 ]) 677 678 if "isin" in iJSON.keys() and iJSON["isin"]: 679 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 680 681 if "classCode" in iJSON.keys(): 682 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 683 684 info.extend([ 685 splitLine, 686 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 687 splitLine, 688 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 689 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 690 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 691 ]) 692 693 if iJSON["figi"]: 694 self._figi = iJSON["figi"] 695 iJSON = iJSON | self.RequestTradingStatus() 696 697 info.extend([ 698 splitLine, 699 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 700 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 701 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 702 ]) 703 704 info.append(splitLine) 705 706 if "type" in iJSON.keys() and iJSON["type"]: 707 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 708 709 if "shareType" in iJSON.keys() and iJSON["shareType"]: 710 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 711 712 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 713 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 714 715 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 716 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 717 718 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 719 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 720 721 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 722 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 723 724 if "focusType" in iJSON.keys() and iJSON["focusType"]: 725 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 726 727 if "assetType" in iJSON.keys() and iJSON["assetType"]: 728 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 729 730 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 731 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 732 733 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 734 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 735 736 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 737 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 738 739 if "currency" in iJSON.keys(): 740 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 741 742 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 743 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 744 745 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 746 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 747 748 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 749 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 750 751 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 752 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 753 754 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 755 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 756 757 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 758 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 759 760 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 761 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 762 763 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 764 info.append("| Perpetual bond: | Yes |\n") 765 766 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 767 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 768 769 iExt = None 770 if iJSON["type"] == "Bonds": 771 info.extend([ 772 splitLine, 773 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 774 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 775 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 776 iJSON["nominal"]["currency"], 777 )), 778 ]) 779 780 if "floatingCouponFlag" in iJSON.keys(): 781 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 782 783 if "amortizationFlag" in iJSON.keys(): 784 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 785 786 info.append(splitLine) 787 788 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 789 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 790 791 if iJSON["figi"]: 792 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 793 794 info.extend([ 795 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 796 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 797 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 798 ]) 799 800 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 801 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 802 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 803 iJSON["aciValue"]["currency"] 804 ))) 805 806 if "currentPrice" in iJSON.keys(): 807 info.append(splitLine) 808 809 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 810 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 811 812 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 813 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 814 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 815 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 816 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 817 818 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 819 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 820 821 info.extend([ 822 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 823 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 824 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 825 )), 826 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 827 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 828 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 829 )), 830 "| Changes between last deal price and last close | {:<54} |\n".format( 831 "{:.2f}%{}".format( 832 iJSON["currentPrice"]["changes"], 833 " ({}{:.2f} {})".format( 834 "+" if bondChangesDelta > 0 else "", 835 bondChangesDelta, 836 aciCurrency 837 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 838 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 839 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 840 currency 841 ), 842 ) 843 ), 844 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 845 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 846 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 847 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 848 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 849 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 850 )), 851 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 852 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 853 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 854 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 855 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 856 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 857 )), 858 ]) 859 860 if "lot" in iJSON.keys(): 861 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 862 863 if "step" in iJSON.keys() and iJSON["step"] != 0: 864 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 865 866 # Add bond payment calendar: 867 if iJSON["type"] == "Bonds": 868 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 869 info.extend(["\n#", strCalendar]) 870 871 infoText += "".join(info) 872 873 if show: 874 uLogger.info("{}".format(infoText)) 875 876 else: 877 uLogger.debug("{}".format(infoText)) 878 879 if self.infoFile is not None: 880 with open(self.infoFile, "w", encoding="UTF-8") as fH: 881 fH.write(infoText) 882 883 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 884 885 if self.useHTMLReports: 886 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 887 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 888 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 889 890 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 891 892 return infoText 893 894 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 895 """ 896 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 897 898 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 899 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 900 :return: JSON formatted data with information about instrument. 901 """ 902 tickerJSON = {} 903 if self.moreDebug: 904 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 905 906 if not self._ticker: 907 uLogger.warning("self._ticker variable is not be empty!") 908 909 else: 910 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 911 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 912 raise Exception("Instrument not allowed") 913 914 if not self.iList: 915 self.iList = self.Listing() 916 917 if self._ticker in self.iList["Shares"].keys(): 918 tickerJSON = self.iList["Shares"][self._ticker] 919 if self.moreDebug: 920 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 921 922 elif self._ticker in self.iList["Currencies"].keys(): 923 tickerJSON = self.iList["Currencies"][self._ticker] 924 if self.moreDebug: 925 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 926 927 elif self._ticker in self.iList["Bonds"].keys(): 928 tickerJSON = self.iList["Bonds"][self._ticker] 929 if self.moreDebug: 930 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 931 932 elif self._ticker in self.iList["Etfs"].keys(): 933 tickerJSON = self.iList["Etfs"][self._ticker] 934 if self.moreDebug: 935 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 936 937 elif self._ticker in self.iList["Futures"].keys(): 938 tickerJSON = self.iList["Futures"][self._ticker] 939 if self.moreDebug: 940 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 941 942 if tickerJSON: 943 self._figi = tickerJSON["figi"] 944 945 if requestPrice: 946 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 947 948 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 949 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 950 951 else: 952 tickerJSON["currentPrice"]["changes"] = 0 953 954 if show: 955 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 956 957 else: 958 if show: 959 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 960 961 return tickerJSON 962 963 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 964 """ 965 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 966 967 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 968 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 969 :return: JSON formatted data with information about instrument. 970 """ 971 figiJSON = {} 972 if self.moreDebug: 973 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 974 975 if not self._figi: 976 uLogger.warning("self._figi variable is not be empty!") 977 978 else: 979 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 980 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 981 raise Exception("Instrument not allowed") 982 983 if not self.iList: 984 self.iList = self.Listing() 985 986 for item in self.iList["Shares"].keys(): 987 if self._figi == self.iList["Shares"][item]["figi"]: 988 figiJSON = self.iList["Shares"][item] 989 990 if self.moreDebug: 991 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 992 993 break 994 995 if not figiJSON: 996 for item in self.iList["Currencies"].keys(): 997 if self._figi == self.iList["Currencies"][item]["figi"]: 998 figiJSON = self.iList["Currencies"][item] 999 1000 if self.moreDebug: 1001 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1002 1003 break 1004 1005 if not figiJSON: 1006 for item in self.iList["Bonds"].keys(): 1007 if self._figi == self.iList["Bonds"][item]["figi"]: 1008 figiJSON = self.iList["Bonds"][item] 1009 1010 if self.moreDebug: 1011 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1012 1013 break 1014 1015 if not figiJSON: 1016 for item in self.iList["Etfs"].keys(): 1017 if self._figi == self.iList["Etfs"][item]["figi"]: 1018 figiJSON = self.iList["Etfs"][item] 1019 1020 if self.moreDebug: 1021 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1022 1023 break 1024 1025 if not figiJSON: 1026 for item in self.iList["Futures"].keys(): 1027 if self._figi == self.iList["Futures"][item]["figi"]: 1028 figiJSON = self.iList["Futures"][item] 1029 1030 if self.moreDebug: 1031 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1032 1033 break 1034 1035 if figiJSON: 1036 self._figi = figiJSON["figi"] 1037 self._ticker = figiJSON["ticker"] 1038 1039 if requestPrice: 1040 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1041 1042 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1043 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1044 1045 else: 1046 figiJSON["currentPrice"]["changes"] = 0 1047 1048 if show: 1049 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1050 1051 else: 1052 if show: 1053 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1054 1055 return figiJSON 1056 1057 def GetCurrentPrices(self, show: bool = True) -> dict: 1058 """ 1059 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1060 `{"buy": [{"price": 1243.8, "quantity": 193}, 1061 {"price": 1244.0, "quantity": 168}, 1062 {"price": 1244.8, "quantity": 5}, 1063 {"price": 1245.0, "quantity": 61}, 1064 {"price": 1245.4, "quantity": 60}], 1065 "sell": [{"price": 1243.6, "quantity": 8}, 1066 {"price": 1242.6, "quantity": 10}, 1067 {"price": 1242.4, "quantity": 18}, 1068 {"price": 1242.2, "quantity": 50}, 1069 {"price": 1242.0, "quantity": 113}], 1070 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1071 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1072 - sell: list of dicts with Buyers prices, 1073 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1074 - quantity: volume value by current price in lots, 1075 - limitUp: current trade session limit price, maximum, 1076 - limitDown: current trade session limit price, minimum, 1077 - lastPrice: last deal price of the instrument, 1078 - closePrice: previous trade session close price of the instrument. 1079 1080 See also: `SearchByTicker()` and `SearchByFIGI()`. 1081 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1082 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1083 1084 :param show: if `True` then print DOM to log and console. 1085 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1086 If an error occurred then returns an empty record: 1087 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1088 """ 1089 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1090 1091 if self.depth < 1: 1092 uLogger.error("Depth of Market (DOM) must be >=1!") 1093 raise Exception("Incorrect value") 1094 1095 if not (self._ticker or self._figi): 1096 uLogger.error("self._ticker or self._figi variables must be defined!") 1097 raise Exception("Ticker or FIGI required") 1098 1099 if self._ticker and not self._figi: 1100 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1101 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1102 1103 if not self._ticker and self._figi: 1104 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1105 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1106 1107 if not self._figi: 1108 uLogger.error("FIGI is not defined!") 1109 raise Exception("Ticker or FIGI required") 1110 1111 else: 1112 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1113 1114 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1115 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1116 self.body = str({"figi": self._figi, "depth": self.depth}) 1117 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1118 1119 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1120 # list of dicts with sellers orders: 1121 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1122 1123 # list of dicts with buyers orders: 1124 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1125 1126 # max price of instrument at this time: 1127 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1128 1129 # min price of instrument at this time: 1130 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1131 1132 # last price of deal with instrument: 1133 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1134 1135 # last close price of instrument: 1136 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1137 1138 else: 1139 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1140 uLogger.debug("Server response: {}".format(pricesResponse)) 1141 1142 if show: 1143 if prices["buy"] or prices["sell"]: 1144 info = [ 1145 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1146 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1147 self._ticker, 1148 self._figi, 1149 self.depth, 1150 ), 1151 "-" * 60, "\n", 1152 " Orders of Buyers | Orders of Sellers\n", 1153 "-" * 60, "\n", 1154 " Sell prices (volumes) | Buy prices (volumes)\n", 1155 "-" * 60, "\n", 1156 ] 1157 1158 if not prices["buy"]: 1159 info.append(" | No orders!\n") 1160 sumBuy = 0 1161 1162 else: 1163 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1164 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1165 for item in maxMinSorted: 1166 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1167 1168 if not prices["sell"]: 1169 info.append("No orders! |\n") 1170 sumSell = 0 1171 1172 else: 1173 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1174 for item in prices["sell"]: 1175 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1176 1177 info.extend([ 1178 "-" * 60, "\n", 1179 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1180 "-" * 60, "\n", 1181 ]) 1182 1183 infoText = "".join(info) 1184 1185 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1186 1187 else: 1188 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1189 1190 return prices 1191 1192 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1193 """ 1194 This method get and show information about all available broker instruments for current user account. 1195 If `instrumentsFile` string is not empty then also save information to this file. 1196 1197 :param show: if `True` then print results to console, if `False` — print only to file. 1198 :return: multi-lines string with all available broker instruments 1199 """ 1200 if not self.iList: 1201 self.iList = self.Listing() 1202 1203 info = [ 1204 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1205 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1206 ] 1207 1208 # add instruments count by type: 1209 for iType in self.iList.keys(): 1210 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1211 1212 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1213 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1214 1215 # generating info tables with all instruments by type: 1216 for iType in self.iList.keys(): 1217 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1218 1219 for instrument in self.iList[iType].keys(): 1220 iName = self.iList[iType][instrument]["name"] # instrument's name 1221 if len(iName) > 57: 1222 iName = "{}...".format(iName[:54]) # right trim for a long string 1223 1224 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1225 self.iList[iType][instrument]["ticker"], 1226 iName, 1227 self.iList[iType][instrument]["figi"], 1228 self.iList[iType][instrument]["currency"], 1229 self.iList[iType][instrument]["lot"], 1230 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1231 )) 1232 1233 infoText = "".join(info) 1234 1235 if show: 1236 uLogger.info(infoText) 1237 1238 if self.instrumentsFile: 1239 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1240 fH.write(infoText) 1241 1242 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1243 1244 if self.useHTMLReports: 1245 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1246 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1247 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1248 1249 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1250 1251 return infoText 1252 1253 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1254 """ 1255 This method search and show information about instruments by part of its ticker, FIGI or name. 1256 If `searchResultsFile` string is not empty then also save information to this file. 1257 1258 :param pattern: string with part of ticker, FIGI or instrument's name. 1259 :param show: if `True` then print results to console, if `False` — return list of result only. 1260 :return: list of dictionaries with all found instruments. 1261 """ 1262 if not self.iList: 1263 self.iList = self.Listing() 1264 1265 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1266 compiledPattern = re.compile(pattern, re.IGNORECASE) 1267 1268 for iType in self.iList: 1269 for instrument in self.iList[iType].values(): 1270 searchResult = compiledPattern.search(" ".join( 1271 [instrument["ticker"], instrument["figi"], instrument["name"]] 1272 )) 1273 1274 if searchResult: 1275 searchResults[iType][instrument["ticker"]] = instrument 1276 1277 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1278 info = [ 1279 "# Search results\n\n", 1280 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1281 "* **Search pattern:** [{}]\n".format(pattern), 1282 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1283 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1284 ] 1285 infoShort = info[:] 1286 1287 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1288 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1289 skippedLine = "| ... | ... | ... | ... |\n" 1290 1291 if resultsLen == 0: 1292 info.append("\nNo results\n") 1293 infoShort.append("\nNo results\n") 1294 uLogger.warning("No results. Try changing your search pattern.") 1295 1296 else: 1297 for iType in searchResults: 1298 iTypeValuesCount = len(searchResults[iType].values()) 1299 if iTypeValuesCount > 0: 1300 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1301 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1302 1303 for instrument in searchResults[iType].values(): 1304 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1305 instrument["type"], 1306 instrument["ticker"], 1307 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1308 instrument["figi"], 1309 )) 1310 1311 if iTypeValuesCount <= 5: 1312 infoShort.extend(info[-iTypeValuesCount:]) 1313 1314 else: 1315 infoShort.extend(info[-5:]) 1316 infoShort.append(skippedLine) 1317 1318 infoText = "".join(info) 1319 infoTextShort = "".join(infoShort) 1320 1321 if show: 1322 uLogger.info(infoTextShort) 1323 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1324 1325 if self.searchResultsFile: 1326 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1327 fH.write(infoText) 1328 1329 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1330 1331 if self.useHTMLReports: 1332 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1333 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1334 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1335 1336 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1337 1338 return searchResults 1339 1340 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1341 """ 1342 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1343 1344 :param instruments: list of strings with tickers or FIGIs. 1345 :return: list with unique instrument FIGIs only. 1346 """ 1347 requestedInstruments = [] 1348 for iName in instruments: 1349 if iName not in self.aliases.keys(): 1350 if iName not in requestedInstruments: 1351 requestedInstruments.append(iName) 1352 1353 else: 1354 if iName not in requestedInstruments: 1355 if self.aliases[iName] not in requestedInstruments: 1356 requestedInstruments.append(self.aliases[iName]) 1357 1358 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1359 1360 onlyUniqueFIGIs = [] 1361 for iName in requestedInstruments: 1362 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1363 continue 1364 1365 self._ticker = iName 1366 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1367 1368 if not iData: 1369 self._ticker = "" 1370 self._figi = iName 1371 1372 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1373 1374 if not iData: 1375 self._figi = "" 1376 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1377 1378 if iData and iData["figi"] not in onlyUniqueFIGIs: 1379 onlyUniqueFIGIs.append(iData["figi"]) 1380 1381 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1382 1383 return onlyUniqueFIGIs 1384 1385 def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]: 1386 """ 1387 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1388 1389 See limits: https://tinkoff.github.io/investAPI/limits/ 1390 1391 If `pricesFile` string is not empty then also save information to this file. 1392 1393 :param instruments: list of strings with tickers or FIGIs. 1394 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1395 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1396 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1397 """ 1398 if instruments is None or not instruments: 1399 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1400 raise Exception("Ticker or FIGI required") 1401 1402 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1403 1404 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1405 1406 iList = [] # trying to get info and current prices about all unique instruments: 1407 for self._figi in onlyUniqueFIGIs: 1408 iData = self.SearchByFIGI(requestPrice=True) 1409 iList.append(iData) 1410 1411 self.ShowListOfPrices(iList, show) 1412 1413 return iList 1414 1415 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1416 """ 1417 Show table contains current prices of given instruments. 1418 1419 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1420 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1421 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1422 :return: multilines text in Markdown format as a table contains current prices. 1423 """ 1424 infoText = "" 1425 1426 if show or self.pricesFile: 1427 info = [ 1428 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1429 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1430 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1431 ] 1432 1433 for item in iList: 1434 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1435 item["ticker"], 1436 item["figi"], 1437 item["type"], 1438 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1439 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1440 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1441 "{} / {}".format( 1442 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1443 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1444 ), 1445 "{} / {}".format( 1446 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1447 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1448 ), 1449 item["currency"], 1450 )) 1451 1452 infoText = "".join(info) 1453 1454 if show: 1455 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1456 1457 if self.pricesFile: 1458 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1459 fH.write(infoText) 1460 1461 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1462 1463 if self.useHTMLReports: 1464 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1465 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1466 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1467 1468 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1469 1470 return infoText 1471 1472 def RequestTradingStatus(self) -> dict: 1473 """ 1474 Requesting trading status for the instrument defined by `figi` variable. 1475 1476 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1477 1478 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1479 1480 :return: dictionary with trading status attributes. Response example: 1481 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1482 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1483 """ 1484 if self._figi is None or not self._figi: 1485 uLogger.error("Variable `figi` must be defined for using this method!") 1486 raise Exception("FIGI required") 1487 1488 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1489 1490 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1491 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1492 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1493 1494 if self.moreDebug: 1495 uLogger.debug("Records about current trading status successfully received") 1496 1497 return tradingStatus 1498 1499 def RequestPortfolio(self) -> dict: 1500 """ 1501 Requesting actual user's portfolio for current `accountId`. 1502 1503 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1504 1505 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1506 1507 :return: dictionary with user's portfolio. 1508 """ 1509 if self.accountId is None or not self.accountId: 1510 uLogger.error("Variable `accountId` must be defined for using this method!") 1511 raise Exception("Account ID required") 1512 1513 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1514 1515 self.body = str({"accountId": self.accountId}) 1516 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1517 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1518 1519 if self.moreDebug: 1520 uLogger.debug("Records about user's portfolio successfully received") 1521 1522 return rawPortfolio 1523 1524 def RequestPositions(self) -> dict: 1525 """ 1526 Requesting open positions by currencies and instruments for current `accountId`. 1527 1528 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1529 1530 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1531 1532 :return: dictionary with open positions by instruments. 1533 """ 1534 if self.accountId is None or not self.accountId: 1535 uLogger.error("Variable `accountId` must be defined for using this method!") 1536 raise Exception("Account ID required") 1537 1538 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1539 1540 self.body = str({"accountId": self.accountId}) 1541 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1542 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1543 1544 if self.moreDebug: 1545 uLogger.debug("Records about current open positions successfully received") 1546 1547 return rawPositions 1548 1549 def RequestPendingOrders(self) -> list: 1550 """ 1551 Requesting current actual pending limit orders for current `accountId`. 1552 1553 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1554 1555 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1556 1557 :return: list of dictionaries with pending limit orders. 1558 """ 1559 if self.accountId is None or not self.accountId: 1560 uLogger.error("Variable `accountId` must be defined for using this method!") 1561 raise Exception("Account ID required") 1562 1563 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1564 1565 self.body = str({"accountId": self.accountId}) 1566 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1567 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1568 1569 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1570 1571 return rawOrders 1572 1573 def RequestStopOrders(self) -> list: 1574 """ 1575 Requesting current actual stop orders for current `accountId`. 1576 1577 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1578 1579 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1580 1581 :return: list of dictionaries with stop orders. 1582 """ 1583 if self.accountId is None or not self.accountId: 1584 uLogger.error("Variable `accountId` must be defined for using this method!") 1585 raise Exception("Account ID required") 1586 1587 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1588 1589 self.body = str({"accountId": self.accountId}) 1590 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1591 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1592 1593 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1594 1595 return rawStopOrders 1596 1597 def Overview(self, show: bool = False, details: str = "full") -> dict: 1598 """ 1599 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1600 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1601 and `overviewBondsCalendarFile` are defined then also save information to file. 1602 1603 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1604 many requests about the state of the portfolio, and then, based on the received data, a large number 1605 of calculation and statistics are collected. 1606 1607 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1608 :param details: how detailed should the information be? 1609 - `full` — shows full available information about portfolio status (by default), 1610 - `positions` — shows only open positions, 1611 - `orders` — shows only sections of open limits and stop orders. 1612 - `digest` — show a short digest of the portfolio status, 1613 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1614 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1615 :return: dictionary with client's raw portfolio and some statistics. 1616 """ 1617 if self.accountId is None or not self.accountId: 1618 uLogger.error("Variable `accountId` must be defined for using this method!") 1619 raise Exception("Account ID required") 1620 1621 view = { 1622 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1623 "headers": {}, # list of dictionaries, response headers without "positions" section 1624 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1625 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1626 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1627 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1628 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1629 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1630 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1631 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1632 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1633 }, 1634 "stat": { # --- some statistics calculated using "raw" sections: 1635 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1636 "availableRUB": 0., # available rubles (without other currencies) 1637 "blockedRUB": 0., # blocked sum in Russian Rouble 1638 "totalChangesRUB": 0., # changes for all open trades in RUB 1639 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1640 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1641 "sharesCostRUB": 0., # costs of all shares in RUB 1642 "bondsCostRUB": 0., # costs of all bonds in RUB 1643 "etfsCostRUB": 0., # costs of all etfs in RUB 1644 "futuresCostRUB": 0., # costs of all futures in RUB 1645 "Currencies": [], # list of dictionaries of all currencies statistics 1646 "Shares": [], # list of dictionaries of all shares statistics 1647 "Bonds": [], # list of dictionaries of all bonds statistics 1648 "Etfs": [], # list of dictionaries of all etfs statistics 1649 "Futures": [], # list of dictionaries of all futures statistics 1650 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1651 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1652 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1653 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1654 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1655 }, 1656 "analytics": { # --- some analytics of portfolio: 1657 "distrByAssets": {}, # portfolio distribution by assets 1658 "distrByCompanies": {}, # portfolio distribution by companies 1659 "distrBySectors": {}, # portfolio distribution by sectors 1660 "distrByCurrencies": {}, # portfolio distribution by currencies 1661 "distrByCountries": {}, # portfolio distribution by countries 1662 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1663 } 1664 } 1665 1666 details = details.lower() 1667 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1668 if details not in availableDetails: 1669 details = "full" 1670 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1671 1672 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1673 1674 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1675 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1676 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1677 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1678 1679 # save response headers without "positions" section: 1680 for key in portfolioResponse.keys(): 1681 if key != "positions": 1682 view["raw"]["headers"][key] = portfolioResponse[key] 1683 1684 else: 1685 continue 1686 1687 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1688 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1689 for item in portfolioResponse["positions"]: 1690 if item["instrumentType"] == "currency": 1691 self._figi = item["figi"] 1692 curr = self.SearchByFIGI(requestPrice=False) 1693 1694 # current price of currency in RUB: 1695 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1696 "name": curr["name"], 1697 "currentPrice": NanoToFloat( 1698 item["currentPrice"]["units"], 1699 item["currentPrice"]["nano"] 1700 ), 1701 } 1702 1703 view["raw"]["Currencies"].append(item) 1704 1705 elif item["instrumentType"] == "share": 1706 view["raw"]["Shares"].append(item) 1707 1708 elif item["instrumentType"] == "bond": 1709 view["raw"]["Bonds"].append(item) 1710 1711 elif item["instrumentType"] == "etf": 1712 view["raw"]["Etfs"].append(item) 1713 1714 elif item["instrumentType"] == "futures": 1715 view["raw"]["Futures"].append(item) 1716 1717 else: 1718 continue 1719 1720 # how many volume of currencies (by ISO currency name) are blocked: 1721 for item in view["raw"]["positions"]["blocked"]: 1722 blocked = NanoToFloat(item["units"], item["nano"]) 1723 if blocked > 0: 1724 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1725 1726 # how many volume of instruments (by FIGI) are blocked: 1727 for item in view["raw"]["positions"]["securities"]: 1728 blocked = int(item["blocked"]) 1729 if blocked > 0: 1730 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1731 1732 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1733 1734 if "rub" in allBlocked.keys(): 1735 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1736 1737 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1738 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1739 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1740 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1741 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1742 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1743 view["stat"]["portfolioCostRUB"] = sum([ 1744 view["stat"]["allCurrenciesCostRUB"], 1745 view["stat"]["sharesCostRUB"], 1746 view["stat"]["bondsCostRUB"], 1747 view["stat"]["etfsCostRUB"], 1748 view["stat"]["futuresCostRUB"], 1749 ]) 1750 1751 # --- calculating some portfolio statistics: 1752 byComp = {} # distribution by companies 1753 bySect = {} # distribution by sectors 1754 byCurr = {} # distribution by currencies (include RUB) 1755 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1756 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1757 1758 for item in portfolioResponse["positions"]: 1759 self._figi = item["figi"] 1760 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1761 1762 if instrument: 1763 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1764 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1765 1766 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1767 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1768 1769 else: 1770 blocked = 0 1771 1772 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1773 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1774 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1775 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1776 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1777 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1778 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1779 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1780 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1781 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1782 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1783 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1784 1785 statData = { 1786 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1787 "ticker": instrument["ticker"], # ticker by FIGI 1788 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1789 "volume": volume, # available volume of instrument 1790 "lots": lots, # volume in lots of instrument 1791 "direction": direction, # direction of an instrument's position: short or long 1792 "blocked": blocked, # blocked volume of currency or instrument 1793 "currentPrice": curPrice, # current instrument's price in basic asset 1794 "average": average, # current average position price 1795 "cost": cost, # current cost of all volume of instrument in basic asset 1796 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1797 "costRUB": costRUB, # cost of instrument in ruble 1798 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1799 "profit": profit, # expected profit at current moment 1800 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1801 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1802 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1803 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1804 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1805 "step": instrument["step"], # minimum price increment 1806 } 1807 1808 # adding distribution by unique countries: 1809 if statData["country"] not in byCountry.keys(): 1810 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1811 1812 else: 1813 byCountry[statData["country"]]["cost"] += costRUB 1814 byCountry[statData["country"]]["percent"] += percentCostRUB 1815 1816 if item["instrumentType"] != "currency": 1817 # adding distribution by unique companies: 1818 if statData["name"]: 1819 if statData["name"] not in byComp.keys(): 1820 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1821 1822 else: 1823 byComp[statData["name"]]["cost"] += costRUB 1824 byComp[statData["name"]]["percent"] += percentCostRUB 1825 1826 # adding distribution by unique sectors: 1827 if statData["sector"] not in bySect.keys(): 1828 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1829 1830 else: 1831 bySect[statData["sector"]]["cost"] += costRUB 1832 bySect[statData["sector"]]["percent"] += percentCostRUB 1833 1834 # adding distribution by unique currencies: 1835 if currency not in byCurr.keys(): 1836 byCurr[currency] = { 1837 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1838 "cost": costRUB, 1839 "percent": percentCostRUB 1840 } 1841 1842 else: 1843 byCurr[currency]["cost"] += costRUB 1844 byCurr[currency]["percent"] += percentCostRUB 1845 1846 # saving statistics for every instrument: 1847 if item["instrumentType"] == "currency": 1848 view["stat"]["Currencies"].append(statData) 1849 1850 # update dict with free funds for trading (total - blocked) by currencies 1851 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1852 view["stat"]["funds"][currency] = { 1853 "total": volume, 1854 "totalCostRUB": costRUB, # total volume cost in rubles 1855 "free": volume - blocked, 1856 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1857 } 1858 1859 elif item["instrumentType"] == "share": 1860 view["stat"]["Shares"].append(statData) 1861 1862 elif item["instrumentType"] == "bond": 1863 view["stat"]["Bonds"].append(statData) 1864 1865 elif item["instrumentType"] == "etf": 1866 view["stat"]["Etfs"].append(statData) 1867 1868 elif item["instrumentType"] == "Futures": 1869 view["stat"]["Futures"].append(statData) 1870 1871 else: 1872 continue 1873 1874 # total changes in Russian Ruble: 1875 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1876 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1877 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1878 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1879 view["stat"]["funds"]["rub"] = { 1880 "total": view["stat"]["availableRUB"], 1881 "totalCostRUB": view["stat"]["availableRUB"], 1882 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1883 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1884 } 1885 1886 # --- pending limit orders sector data: 1887 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1888 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1889 1890 for item in view["raw"]["orders"]: 1891 self._figi = item["figi"] 1892 1893 if item["figi"] not in uniquePendingOrdersFIGIs: 1894 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1895 1896 uniquePendingOrdersFIGIs.append(item["figi"]) 1897 uniquePendingOrders[item["figi"]] = instrument 1898 1899 else: 1900 instrument = uniquePendingOrders[item["figi"]] 1901 1902 if instrument: 1903 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1904 orderType = TKS_ORDER_TYPES[item["orderType"]] 1905 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1906 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1907 1908 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1909 if item["direction"] == "ORDER_DIRECTION_BUY": 1910 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1911 1912 else: 1913 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1914 1915 # requested price for order execution: 1916 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1917 1918 # necessary changes in percent to reach target from current price: 1919 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1920 1921 view["stat"]["orders"].append({ 1922 "orderID": item["orderId"], # orderId number parameter of current order 1923 "figi": item["figi"], # FIGI identification 1924 "ticker": instrument["ticker"], # ticker name by FIGI 1925 "lotsRequested": item["lotsRequested"], # requested lots value 1926 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1927 "currentPrice": lastPrice, # current instrument's price for defined action 1928 "targetPrice": target, # requested price for order execution in base currency 1929 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1930 "percentChanges": changes, # changes in percent to target from current price 1931 "currency": item["currency"], # instrument's currency name 1932 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1933 "type": orderType, # type of order from TKS_ORDER_TYPES 1934 "status": orderState, # order status from TKS_ORDER_STATES 1935 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1936 }) 1937 1938 # --- stop orders sector data: 1939 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1940 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1941 1942 for item in view["raw"]["stopOrders"]: 1943 self._figi = item["figi"] 1944 1945 if item["figi"] not in uniqueStopOrdersFIGIs: 1946 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1947 1948 uniqueStopOrdersFIGIs.append(item["figi"]) 1949 uniqueStopOrders[item["figi"]] = instrument 1950 1951 else: 1952 instrument = uniqueStopOrders[item["figi"]] 1953 1954 if instrument: 1955 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1956 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1957 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1958 1959 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1960 if "expirationTime" in item.keys(): 1961 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1962 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1963 1964 else: 1965 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1966 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1967 1968 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1969 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1970 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1971 1972 else: 1973 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1974 1975 # requested price when stop-order executed: 1976 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1977 1978 # price for limit-order, set up when stop-order executed: 1979 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1980 1981 # necessary changes in percent to reach target from current price: 1982 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1983 1984 view["stat"]["stopOrders"].append({ 1985 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1986 "figi": item["figi"], # FIGI identification 1987 "ticker": instrument["ticker"], # ticker name by FIGI 1988 "lotsRequested": item["lotsRequested"], # requested lots value 1989 "currentPrice": lastPrice, # current instrument's price for defined action 1990 "targetPrice": target, # requested price for stop-order execution in base currency 1991 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1992 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1993 "percentChanges": changes, # changes in percent to target from current price 1994 "currency": item["currency"], # instrument's currency name 1995 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1996 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1997 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1998 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1999 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2000 }) 2001 2002 # --- calculating data for analytics section: 2003 # portfolio distribution by assets: 2004 view["analytics"]["distrByAssets"] = { 2005 "Ruble": { 2006 "uniques": 1, 2007 "cost": view["stat"]["availableRUB"], 2008 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2009 }, 2010 "Currencies": { 2011 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2012 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2013 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2014 }, 2015 "Shares": { 2016 "uniques": len(view["stat"]["Shares"]), 2017 "cost": view["stat"]["sharesCostRUB"], 2018 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2019 }, 2020 "Bonds": { 2021 "uniques": len(view["stat"]["Bonds"]), 2022 "cost": view["stat"]["bondsCostRUB"], 2023 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2024 }, 2025 "Etfs": { 2026 "uniques": len(view["stat"]["Etfs"]), 2027 "cost": view["stat"]["etfsCostRUB"], 2028 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2029 }, 2030 "Futures": { 2031 "uniques": len(view["stat"]["Futures"]), 2032 "cost": view["stat"]["futuresCostRUB"], 2033 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2034 }, 2035 } 2036 2037 # portfolio distribution by companies: 2038 view["analytics"]["distrByCompanies"]["All money cash"] = { 2039 "ticker": "", 2040 "cost": view["stat"]["allCurrenciesCostRUB"], 2041 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2042 } 2043 view["analytics"]["distrByCompanies"].update(byComp) 2044 2045 # portfolio distribution by sectors: 2046 view["analytics"]["distrBySectors"]["All money cash"] = { 2047 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2048 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2049 } 2050 view["analytics"]["distrBySectors"].update(bySect) 2051 2052 # portfolio distribution by currencies: 2053 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2054 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2055 2056 if self.moreDebug: 2057 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2058 2059 view["analytics"]["distrByCurrencies"].update(byCurr) 2060 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2061 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2062 2063 # portfolio distribution by countries: 2064 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2065 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2066 2067 if self.moreDebug: 2068 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2069 2070 view["analytics"]["distrByCountries"].update(byCountry) 2071 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2072 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2073 2074 # --- Prepare text statistics overview in human-readable: 2075 if show: 2076 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2077 2078 # Whatever the value `details`, header not changes: 2079 info = [ 2080 "# Client's portfolio\n\n", 2081 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2082 "* **Account ID:** [{}]\n".format(self.accountId), 2083 ] 2084 2085 if details in ["full", "positions", "digest"]: 2086 info.extend([ 2087 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2088 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2089 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2090 view["stat"]["totalChangesRUB"], 2091 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2092 view["stat"]["totalChangesPercentRUB"], 2093 ), 2094 ]) 2095 2096 if details in ["full", "positions"]: 2097 info.extend([ 2098 "## Open positions\n\n", 2099 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2100 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2101 "| Ruble | {:>31} | | | | | |\n".format( 2102 "{:.2f} ({:.2f}) rub".format( 2103 view["stat"]["availableRUB"], 2104 view["stat"]["blockedRUB"], 2105 ) 2106 ) 2107 ]) 2108 2109 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2110 return [ 2111 "| | | | | | | |\n", 2112 "| {:<27} | | | | | {:>19} | |\n".format( 2113 noTradeStr if noTradeStr else typeStr, 2114 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2115 ), 2116 ] 2117 2118 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2119 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2120 "{} [{}]".format(data["ticker"], data["figi"]), 2121 "{:.2f} ({:.2f}) {}".format( 2122 data["volume"], 2123 data["blocked"], 2124 data["currency"], 2125 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2126 data["volume"], 2127 data["blocked"], 2128 ), 2129 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2130 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2131 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2132 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2133 "{}{:.2f} {} ({}{:.2f}%)".format( 2134 "+" if data["profit"] > 0 else "", 2135 data["profit"], data["baseCurrencyName"], 2136 "+" if data["percentProfit"] > 0 else "", 2137 data["percentProfit"], 2138 ), 2139 ) 2140 2141 # --- Show currencies section: 2142 if view["stat"]["Currencies"]: 2143 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2144 for item in view["stat"]["Currencies"]: 2145 info.append(_InfoStr(item, showCurrencyName=True)) 2146 2147 else: 2148 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2149 2150 # --- Show shares section: 2151 if view["stat"]["Shares"]: 2152 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2153 2154 for item in view["stat"]["Shares"]: 2155 info.append(_InfoStr(item)) 2156 2157 else: 2158 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2159 2160 # --- Show bonds section: 2161 if view["stat"]["Bonds"]: 2162 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2163 2164 for item in view["stat"]["Bonds"]: 2165 info.append(_InfoStr(item)) 2166 2167 else: 2168 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2169 2170 # --- Show etfs section: 2171 if view["stat"]["Etfs"]: 2172 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2173 2174 for item in view["stat"]["Etfs"]: 2175 info.append(_InfoStr(item)) 2176 2177 else: 2178 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2179 2180 # --- Show futures section: 2181 if view["stat"]["Futures"]: 2182 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2183 2184 for item in view["stat"]["Futures"]: 2185 info.append(_InfoStr(item)) 2186 2187 else: 2188 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2189 2190 if details in ["full", "orders"]: 2191 # --- Show pending limit orders section: 2192 if view["stat"]["orders"]: 2193 info.extend([ 2194 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2195 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2196 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2197 ]) 2198 2199 for item in view["stat"]["orders"]: 2200 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2201 "{} [{}]".format(item["ticker"], item["figi"]), 2202 item["orderID"], 2203 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2204 "{} {} ({}{:.2f}%)".format( 2205 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2206 item["baseCurrencyName"], 2207 "+" if item["percentChanges"] > 0 else "", 2208 float(item["percentChanges"]), 2209 ), 2210 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2211 item["action"], 2212 item["type"], 2213 item["date"], 2214 )) 2215 2216 else: 2217 info.append("\n## Total pending limit-orders: [0]\n") 2218 2219 # --- Show stop orders section: 2220 if view["stat"]["stopOrders"]: 2221 info.extend([ 2222 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2223 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2224 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2225 ]) 2226 2227 for item in view["stat"]["stopOrders"]: 2228 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2229 "{} [{}]".format(item["ticker"], item["figi"]), 2230 item["orderID"], 2231 item["lotsRequested"], 2232 "{} {} ({}{:.2f}%)".format( 2233 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2234 item["baseCurrencyName"], 2235 "+" if item["percentChanges"] > 0 else "", 2236 float(item["percentChanges"]), 2237 ), 2238 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2239 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2240 item["action"], 2241 item["type"], 2242 item["expType"], 2243 item["createDate"], 2244 item["expDate"], 2245 )) 2246 2247 else: 2248 info.append("\n## Total stop-orders: [0]\n") 2249 2250 if details in ["full", "analytics"]: 2251 # -- Show analytics section: 2252 if view["stat"]["portfolioCostRUB"] > 0: 2253 info.extend([ 2254 "\n# Analytics\n\n" 2255 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2256 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2257 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2258 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2259 view["stat"]["totalChangesRUB"], 2260 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2261 view["stat"]["totalChangesPercentRUB"], 2262 ), 2263 "\n## Portfolio distribution by assets\n" 2264 "\n| Type | Uniques | Percent | Current cost |\n", 2265 "|------------------------------------|---------|---------|--------------------|\n", 2266 ]) 2267 2268 for key in view["analytics"]["distrByAssets"].keys(): 2269 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2270 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2271 key, 2272 view["analytics"]["distrByAssets"][key]["uniques"], 2273 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2274 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2275 )) 2276 2277 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2278 2279 info.extend([ 2280 "\n## Portfolio distribution by companies\n" 2281 "\n| Company | Percent | Current cost |\n", 2282 aSepLine, 2283 ]) 2284 2285 for company in view["analytics"]["distrByCompanies"].keys(): 2286 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2287 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2288 "{}{}".format( 2289 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2290 company, 2291 ), 2292 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2293 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2294 )) 2295 2296 info.extend([ 2297 "\n## Portfolio distribution by sectors\n" 2298 "\n| Sector | Percent | Current cost |\n", 2299 aSepLine, 2300 ]) 2301 2302 for sector in view["analytics"]["distrBySectors"].keys(): 2303 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2304 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2305 sector, 2306 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2307 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2308 )) 2309 2310 info.extend([ 2311 "\n## Portfolio distribution by currencies\n" 2312 "\n| Instruments currencies | Percent | Current cost |\n", 2313 aSepLine, 2314 ]) 2315 2316 for curr in view["analytics"]["distrByCurrencies"].keys(): 2317 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2318 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2319 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2320 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2321 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2322 )) 2323 2324 info.extend([ 2325 "\n## Portfolio distribution by countries\n" 2326 "\n| Assets by country | Percent | Current cost |\n", 2327 aSepLine, 2328 ]) 2329 2330 for country in view["analytics"]["distrByCountries"].keys(): 2331 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2332 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2333 country, 2334 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2335 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2336 )) 2337 2338 if details in ["full", "calendar"]: 2339 # -- Show bonds payment calendar section: 2340 if view["stat"]["Bonds"]: 2341 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2342 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2343 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2344 2345 else: 2346 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2347 2348 infoText = "".join(info) 2349 2350 uLogger.info(infoText) 2351 2352 if details == "full" and self.overviewFile: 2353 filename = self.overviewFile 2354 2355 elif details == "digest" and self.overviewDigestFile: 2356 filename = self.overviewDigestFile 2357 2358 elif details == "positions" and self.overviewPositionsFile: 2359 filename = self.overviewPositionsFile 2360 2361 elif details == "orders" and self.overviewOrdersFile: 2362 filename = self.overviewOrdersFile 2363 2364 elif details == "analytics" and self.overviewAnalyticsFile: 2365 filename = self.overviewAnalyticsFile 2366 2367 elif details == "calendar" and self.overviewBondsCalendarFile: 2368 filename = self.overviewBondsCalendarFile 2369 2370 else: 2371 filename = "" 2372 2373 if filename: 2374 with open(filename, "w", encoding="UTF-8") as fH: 2375 fH.write(infoText) 2376 2377 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2378 2379 if self.useHTMLReports: 2380 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2381 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2382 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="", commonCSS=COMMON_CSS, markdown=infoText)) 2383 2384 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2385 2386 return view 2387 2388 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2389 """ 2390 Returns history operations between two given dates for current `accountId`. 2391 If `reportFile` string is not empty then also save human-readable report. 2392 Shows some statistical data of closed positions. 2393 2394 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2395 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2396 :param show: if `True` then also prints all records to the console. 2397 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2398 :return: original list of dictionaries with history of deals records from API ("operations" key): 2399 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2400 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2401 """ 2402 if self.accountId is None or not self.accountId: 2403 uLogger.error("Variable `accountId` must be defined for using this method!") 2404 raise Exception("Account ID required") 2405 2406 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2407 2408 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2409 2410 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2411 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2412 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2413 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2414 customStat = {} # custom statistics in additional to responseJSON 2415 2416 # --- output report in human-readable format: 2417 if show or self.reportFile: 2418 splitLine1 = "| | | | | |\n" # Summary section 2419 splitLine2 = "| | | | | | | | |\n" # Operations section 2420 nextDay = "" 2421 2422 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2423 2424 if len(ops) > 0: 2425 customStat = { 2426 "opsCount": 0, # total operations count 2427 "buyCount": 0, # buy operations 2428 "sellCount": 0, # sell operations 2429 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2430 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2431 "payIn": {"rub": 0.}, # Deposit brokerage account 2432 "payOut": {"rub": 0.}, # Withdrawals 2433 "divs": {"rub": 0.}, # Dividends income 2434 "coupons": {"rub": 0.}, # Coupon's income 2435 "brokerCom": {"rub": 0.}, # Service commissions 2436 "serviceCom": {"rub": 0.}, # Service commissions 2437 "marginCom": {"rub": 0.}, # Margin commissions 2438 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2439 } 2440 2441 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2442 for item in ops: 2443 if item["state"] == "OPERATION_STATE_EXECUTED": 2444 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2445 2446 # count buy operations: 2447 if "_BUY" in item["operationType"]: 2448 customStat["buyCount"] += 1 2449 2450 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2451 customStat["buyTotal"][item["payment"]["currency"]] += payment 2452 2453 else: 2454 customStat["buyTotal"][item["payment"]["currency"]] = payment 2455 2456 # count sell operations: 2457 elif "_SELL" in item["operationType"]: 2458 customStat["sellCount"] += 1 2459 2460 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2461 customStat["sellTotal"][item["payment"]["currency"]] += payment 2462 2463 else: 2464 customStat["sellTotal"][item["payment"]["currency"]] = payment 2465 2466 # count incoming operations: 2467 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2468 if item["payment"]["currency"] in customStat["payIn"].keys(): 2469 customStat["payIn"][item["payment"]["currency"]] += payment 2470 2471 else: 2472 customStat["payIn"][item["payment"]["currency"]] = payment 2473 2474 # count withdrawals operations: 2475 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2476 if item["payment"]["currency"] in customStat["payOut"].keys(): 2477 customStat["payOut"][item["payment"]["currency"]] += payment 2478 2479 else: 2480 customStat["payOut"][item["payment"]["currency"]] = payment 2481 2482 # count dividends income: 2483 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2484 if item["payment"]["currency"] in customStat["divs"].keys(): 2485 customStat["divs"][item["payment"]["currency"]] += payment 2486 2487 else: 2488 customStat["divs"][item["payment"]["currency"]] = payment 2489 2490 # count coupon's income: 2491 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2492 if item["payment"]["currency"] in customStat["coupons"].keys(): 2493 customStat["coupons"][item["payment"]["currency"]] += payment 2494 2495 else: 2496 customStat["coupons"][item["payment"]["currency"]] = payment 2497 2498 # count broker commissions: 2499 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2500 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2501 customStat["brokerCom"][item["payment"]["currency"]] += payment 2502 2503 else: 2504 customStat["brokerCom"][item["payment"]["currency"]] = payment 2505 2506 # count service commissions: 2507 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2508 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2509 customStat["serviceCom"][item["payment"]["currency"]] += payment 2510 2511 else: 2512 customStat["serviceCom"][item["payment"]["currency"]] = payment 2513 2514 # count margin commissions: 2515 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2516 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2517 customStat["marginCom"][item["payment"]["currency"]] += payment 2518 2519 else: 2520 customStat["marginCom"][item["payment"]["currency"]] = payment 2521 2522 # count withholding taxes: 2523 elif "_TAX" in item["operationType"]: 2524 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2525 customStat["allTaxes"][item["payment"]["currency"]] += payment 2526 2527 else: 2528 customStat["allTaxes"][item["payment"]["currency"]] = payment 2529 2530 else: 2531 continue 2532 2533 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2534 2535 # --- view "Actions" lines: 2536 info.extend([ 2537 "| Report sections | | | | |\n", 2538 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2539 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2540 "| | Buy: {:<22} | {:<28} | | |\n".format( 2541 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2542 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2543 ), 2544 "| | Sell: {:<21} | {:<28} | | |\n".format( 2545 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2546 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2547 ), 2548 ]) 2549 2550 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2551 for key in opsKeys: 2552 if key == "rub": 2553 continue 2554 2555 info.extend([ 2556 "| | | {:<28} | | |\n".format( 2557 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2558 ), 2559 "| | | {:<28} | | |\n".format( 2560 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2561 ), 2562 ]) 2563 2564 info.append(splitLine1) 2565 2566 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2567 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2568 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2569 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2570 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2571 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2572 ) 2573 2574 # --- view "Payments" lines: 2575 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2576 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2577 2578 for key in paymentsKeys: 2579 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2580 2581 info.append(splitLine1) 2582 2583 # --- view "Commissions and taxes" lines: 2584 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2585 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2586 2587 for key in comKeys: 2588 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2589 2590 info.extend([ 2591 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2592 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2593 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2594 ]) 2595 2596 else: 2597 info.append("Broker returned no operations during this period\n") 2598 2599 # --- view "Operations" section: 2600 for item in ops: 2601 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2602 continue 2603 2604 else: 2605 self._figi = item["figi"] if item["figi"] else "" 2606 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2607 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2608 2609 # group of deals during one day: 2610 if nextDay and item["date"].split("T")[0] != nextDay: 2611 info.append(splitLine2) 2612 nextDay = "" 2613 2614 else: 2615 nextDay = item["date"].split("T")[0] # saving current day for splitting 2616 2617 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2618 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2619 self._figi if self._figi else "—", 2620 instrument["ticker"] if instrument else "—", 2621 instrument["type"] if instrument else "—", 2622 item["quantity"] if int(item["quantity"]) > 0 else "—", 2623 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2624 TKS_OPERATION_STATES[item["state"]], 2625 TKS_OPERATION_TYPES[item["operationType"]], 2626 )) 2627 2628 infoText = "".join(info) 2629 2630 if show: 2631 if self.moreDebug: 2632 uLogger.debug("Records about history of a client's operations successfully received") 2633 2634 uLogger.info(infoText) 2635 2636 if self.reportFile: 2637 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2638 fH.write(infoText) 2639 2640 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2641 2642 if self.useHTMLReports: 2643 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2644 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2645 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2646 2647 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2648 2649 return ops, customStat 2650 2651 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2652 """ 2653 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2654 2655 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2656 Warning! Broker server used ISO UTC time by default. 2657 2658 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2659 Also, `historyFile` used to update history with `onlyMissing` parameter. 2660 2661 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2662 2663 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2664 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2665 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2666 `"hour"`, `"day"`. Default: `"hour"`. 2667 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2668 False by default. Warning! History appends only from last candle to current time 2669 with always update last candle! 2670 :param csvSep: separator if csv-file is used, `,` by default. 2671 :param show: if `True` then also prints Pandas DataFrame to the console. 2672 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2673 `["date", "time", "open", "high", "low", "close", "volume"]`. 2674 """ 2675 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2676 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2677 history = None # empty pandas object for history 2678 2679 if interval not in TKS_CANDLE_INTERVALS.keys(): 2680 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2681 raise Exception("Incorrect value") 2682 2683 if not (self._ticker or self._figi): 2684 uLogger.error("Ticker or FIGI must be defined!") 2685 raise Exception("Ticker or FIGI required") 2686 2687 if self._ticker and not self._figi: 2688 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2689 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2690 2691 if self._figi and not self._ticker: 2692 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2693 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2694 2695 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2696 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2697 if interval.lower() != "day": 2698 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2699 2700 delta = dtEnd - dtStart # current UTC time minus last time in file 2701 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2702 2703 # calculate history length in candles: 2704 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2705 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2706 length += 1 # to avoid fraction time 2707 2708 # calculate data blocks count: 2709 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2710 2711 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2712 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2713 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2714 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2715 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2716 2717 tempOld = None # pandas object for old history, if --only-missing key present 2718 lastTime = None # datetime object of last old candle in file 2719 2720 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2721 uLogger.debug("--only-missing key present, add only last missing candles...") 2722 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2723 2724 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2725 2726 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2727 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2728 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2729 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2730 2731 # get last datetime object from last string in file or minus 1 delta if file is empty: 2732 if len(tempOld) > 0: 2733 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2734 2735 else: 2736 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2737 2738 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2739 2740 responseJSONs = [] # raw history blocks of data 2741 2742 blockEnd = dtEnd 2743 for item in range(blocks): 2744 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2745 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2746 2747 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2748 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2749 )) 2750 2751 if blockStart == blockEnd: 2752 uLogger.debug("Skipped this zero-length block...") 2753 2754 else: 2755 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2756 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2757 self.body = str({ 2758 "figi": self._figi, 2759 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2760 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2761 "interval": TKS_CANDLE_INTERVALS[interval][0] 2762 }) 2763 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2764 2765 if "code" in responseJSON.keys(): 2766 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2767 2768 else: 2769 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2770 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2771 2772 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2773 2774 blockEnd = blockStart 2775 2776 printCount = len(responseJSONs) # candles to show in console 2777 if responseJSONs: 2778 tempHistory = pd.DataFrame( 2779 data={ 2780 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2781 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2782 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2783 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2784 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2785 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2786 "volume": [int(item["volume"]) for item in responseJSONs], 2787 }, 2788 index=range(len(responseJSONs)), 2789 columns=["date", "time", "open", "high", "low", "close", "volume"], 2790 ) 2791 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2792 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2793 2794 # append only newest candles to old history if --only-missing key present: 2795 if onlyMissing and tempOld is not None and lastTime is not None: 2796 index = 0 # find start index in tempHistory data: 2797 2798 for i, item in tempHistory.iterrows(): 2799 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2800 2801 if curTime == lastTime: 2802 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2803 index = i 2804 printCount = index + 1 2805 break 2806 2807 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2808 2809 else: 2810 history = tempHistory # if no `--only-missing` key then load full data from server 2811 2812 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2813 2814 if history is not None and not history.empty: 2815 if show: 2816 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2817 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2818 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2819 )) 2820 2821 else: 2822 uLogger.warning("Received an empty candles history!") 2823 2824 if self.historyFile is not None: 2825 if history is not None and not history.empty: 2826 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2827 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2828 2829 else: 2830 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2831 2832 else: 2833 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2834 2835 return history 2836 2837 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2838 """ 2839 Load candles history from csv-file and return Pandas DataFrame object. 2840 2841 See also: `History()` and `ShowHistoryChart()` methods. 2842 2843 :param filePath: path to csv-file to open. 2844 """ 2845 loadedHistory = None # init candles data object 2846 2847 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2848 2849 if os.path.exists(filePath): 2850 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2851 2852 tfStr = self.priceModel.FormattedDelta( 2853 self.priceModel.timeframe, 2854 "{days} days {hours}h {minutes}m {seconds}s", 2855 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2856 self.priceModel.timeframe, 2857 "{hours}h {minutes}m {seconds}s", 2858 ) 2859 2860 if loadedHistory is not None and not loadedHistory.empty: 2861 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2862 len(loadedHistory), 2863 tfStr, 2864 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2865 ) 2866 2867 else: 2868 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2869 2870 else: 2871 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2872 2873 return loadedHistory 2874 2875 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2876 """ 2877 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2878 2879 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2880 Default: `index.html` (both for interact and non-interact candlesticks chart). 2881 2882 See also: `History()` and `LoadHistory()` methods. 2883 2884 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2885 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2886 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2887 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2888 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2889 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2890 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2891 """ 2892 if isinstance(candles, str): 2893 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2894 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2895 2896 elif isinstance(candles, pd.DataFrame): 2897 self.priceModel.prices = candles # set candles chain from variable 2898 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2899 2900 if "datetime" not in candles.columns: 2901 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2902 2903 else: 2904 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2905 raise Exception("Incorrect value") 2906 2907 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2908 2909 if interact: 2910 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2911 2912 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2913 2914 else: 2915 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2916 2917 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2918 2919 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2920 2921 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2922 """ 2923 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2924 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2925 2926 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2927 2928 :param operation: string "Buy" or "Sell". 2929 :param lots: volume, integer count of lots >= 1. 2930 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2931 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2932 :param expDate: string "Undefined" by default or local date in future, 2933 it is a string with format `%Y-%m-%d %H:%M:%S`. 2934 :return: JSON with response from broker server. 2935 """ 2936 if self.accountId is None or not self.accountId: 2937 uLogger.error("Variable `accountId` must be defined for using this method!") 2938 raise Exception("Account ID required") 2939 2940 if operation is None or not operation or operation not in ("Buy", "Sell"): 2941 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2942 raise Exception("Incorrect value") 2943 2944 if lots is None or lots < 1: 2945 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2946 lots = 1 2947 2948 if tp is None or tp < 0: 2949 tp = 0 2950 2951 if sl is None or sl < 0: 2952 sl = 0 2953 2954 if expDate is None or not expDate: 2955 expDate = "Undefined" 2956 2957 if not (self._ticker or self._figi): 2958 uLogger.error("Ticker or FIGI must be defined!") 2959 raise Exception("Ticker or FIGI required") 2960 2961 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2962 self._ticker = instrument["ticker"] 2963 self._figi = instrument["figi"] 2964 2965 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 2966 2967 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2968 self.body = str({ 2969 "figi": self._figi, 2970 "quantity": str(lots), 2971 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2972 "accountId": str(self.accountId), 2973 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2974 }) 2975 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2976 2977 if "orderId" in response.keys(): 2978 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2979 operation, response["orderId"], 2980 self._ticker, self._figi, lots, 2981 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2982 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2983 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2984 )) 2985 2986 if tp > 0: 2987 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2988 2989 if sl > 0: 2990 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2991 2992 else: 2993 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 2994 2995 return response 2996 2997 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2998 """ 2999 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3000 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3001 3002 See also: `Order()` and `Trade()` docstrings. 3003 3004 :param lots: volume, integer count of lots >= 1. 3005 :param tp: float > 0, take profit price of stop-order. 3006 :param sl: float > 0, stop loss price of stop-order. 3007 :param expDate: it's a local date in future. 3008 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3009 :return: JSON with response from broker server. 3010 """ 3011 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3012 3013 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3014 """ 3015 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3016 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3017 3018 See also: `Order()` and `Trade()` docstrings. 3019 3020 :param lots: volume, integer count of lots >= 1. 3021 :param tp: float > 0, take profit price of stop-order. 3022 :param sl: float > 0, stop loss price of stop-order. 3023 :param expDate: it's a local date in the future. 3024 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3025 :return: JSON with response from broker server. 3026 """ 3027 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3028 3029 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3030 """ 3031 Close position of given instruments. 3032 3033 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3034 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3035 This avoids unnecessary downloading data from the server. 3036 """ 3037 if instruments is None or not instruments: 3038 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3039 raise Exception("Ticker or FIGI required") 3040 3041 if isinstance(instruments, str): 3042 instruments = [instruments] 3043 3044 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3045 if uniqueInstruments: 3046 if portfolio is None or not portfolio: 3047 portfolio = self.Overview(show=False) 3048 3049 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3050 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3051 3052 for self._figi in uniqueInstruments: 3053 if self._figi not in allOpened: 3054 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3055 continue 3056 3057 # search open trade info about instrument by ticker: 3058 instrument = {} 3059 for iType in TKS_INSTRUMENTS: 3060 if instrument: 3061 break 3062 3063 for item in portfolio["stat"][iType]: 3064 if item["figi"] == self._figi: 3065 instrument = item 3066 break 3067 3068 if instrument: 3069 self._ticker = instrument["ticker"] 3070 self._figi = instrument["figi"] 3071 3072 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3073 self._ticker, 3074 self._figi, 3075 int(instrument["volume"]), 3076 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3077 )) 3078 3079 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3080 3081 if tradeLots > 0: 3082 if instrument["blocked"] > 0: 3083 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3084 instrument["blocked"], 3085 self._ticker, 3086 tradeLots, 3087 )) 3088 3089 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3090 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3091 3092 else: 3093 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3094 3095 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3096 """ 3097 Close all positions of given instruments with defined type. 3098 3099 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3100 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3101 This avoids unnecessary downloading data from the server. 3102 """ 3103 if iType not in TKS_INSTRUMENTS: 3104 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3105 3106 else: 3107 if portfolio is None or not portfolio: 3108 portfolio = self.Overview(show=False) 3109 3110 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3111 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3112 3113 if tickers and portfolio: 3114 self.CloseTrades(tickers, portfolio) 3115 3116 else: 3117 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3118 3119 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3120 """ 3121 Universal method to create market or limit orders with all available parameters for current `accountId`. 3122 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3123 3124 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3125 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3126 3127 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3128 then broker immediately open market order as you can do simple --buy or --sell operations! 3129 3130 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3131 When current price will go up or down to target price value then broker opens a limit order. 3132 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3133 3134 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3135 3136 :param operation: string "Buy" or "Sell". 3137 :param orderType: string "Limit" or "Stop". 3138 :param lots: volume, integer count of lots >= 1. 3139 :param targetPrice: target price > 0. This is open trade price for limit order. 3140 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3141 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3142 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3143 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3144 Stop loss order always executed by market price. 3145 :param expDate: string "Undefined" by default or local date in future. 3146 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3147 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3148 A limit order has no expiration date, it lasts until the end of the trading day. 3149 :return: JSON with response from broker server. 3150 """ 3151 if self.accountId is None or not self.accountId: 3152 uLogger.error("Variable `accountId` must be defined for using this method!") 3153 raise Exception("Account ID required") 3154 3155 if operation is None or not operation or operation not in ("Buy", "Sell"): 3156 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3157 raise Exception("Incorrect value") 3158 3159 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3160 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3161 raise Exception("Incorrect value") 3162 3163 if lots is None or lots < 1: 3164 uLogger.error("You must define trade volume > 0: integer count of lots!") 3165 raise Exception("Incorrect value") 3166 3167 if targetPrice is None or targetPrice <= 0: 3168 uLogger.error("Target price for limit-order must be greater than 0!") 3169 raise Exception("Incorrect value") 3170 3171 if limitPrice is None or limitPrice <= 0: 3172 limitPrice = targetPrice 3173 3174 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3175 stopType = "Limit" 3176 3177 if expDate is None or not expDate: 3178 expDate = "Undefined" 3179 3180 if not (self._ticker or self._figi): 3181 uLogger.error("Tocker or FIGI must be defined!") 3182 raise Exception("Ticker or FIGI required") 3183 3184 response = {} 3185 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3186 self._ticker = instrument["ticker"] 3187 self._figi = instrument["figi"] 3188 3189 if orderType == "Limit": 3190 uLogger.debug( 3191 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3192 self._ticker, self._figi, 3193 operation, lots, targetPrice, instrument["currency"], 3194 )) 3195 3196 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3197 self.body = str({ 3198 "figi": self._figi, 3199 "quantity": str(lots), 3200 "price": FloatToNano(targetPrice), 3201 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3202 "accountId": str(self.accountId), 3203 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3204 }) 3205 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3206 3207 if "orderId" in response.keys(): 3208 uLogger.info( 3209 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3210 response["orderId"], 3211 self._ticker, self._figi, 3212 operation, lots, targetPrice, instrument["currency"], 3213 )) 3214 3215 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3216 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3217 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3218 targetPrice, instrument["currency"], 3219 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3220 )) 3221 3222 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3223 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3224 targetPrice, instrument["currency"], 3225 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3226 )) 3227 3228 else: 3229 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3230 3231 if orderType == "Stop": 3232 uLogger.debug( 3233 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3234 self._ticker, self._figi, 3235 operation, lots, 3236 targetPrice, instrument["currency"], 3237 limitPrice, instrument["currency"], 3238 stopType, expDate, 3239 )) 3240 3241 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3242 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3243 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3244 3245 body = { 3246 "figi": self._figi, 3247 "quantity": str(lots), 3248 "price": FloatToNano(limitPrice), 3249 "stopPrice": FloatToNano(targetPrice), 3250 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3251 "accountId": str(self.accountId), 3252 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3253 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3254 } 3255 3256 if expDateUTC: 3257 body["expireDate"] = expDateUTC 3258 3259 self.body = str(body) 3260 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3261 3262 if "stopOrderId" in response.keys(): 3263 uLogger.info( 3264 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3265 response["stopOrderId"], 3266 self._ticker, self._figi, 3267 operation, lots, 3268 targetPrice, instrument["currency"], 3269 limitPrice, instrument["currency"], 3270 TKS_STOP_ORDER_TYPES[stopOrderType], 3271 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3272 )) 3273 3274 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3275 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3276 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3277 targetPrice, instrument["currency"], 3278 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3279 )) 3280 3281 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3282 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3283 targetPrice, instrument["currency"], 3284 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3285 )) 3286 3287 else: 3288 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3289 3290 return response 3291 3292 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3293 """ 3294 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3295 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3296 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3297 See also: `Order()` docstring. 3298 3299 :param lots: volume, integer count of lots >= 1. 3300 :param targetPrice: target price > 0. This is open trade price for limit order. 3301 :return: JSON with response from broker server. 3302 """ 3303 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3304 3305 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3306 """ 3307 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3308 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3309 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3310 target price value then broker opens a limit order. See also: `Order()` docstring. 3311 3312 :param lots: volume, integer count of lots >= 1. 3313 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3314 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3315 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3316 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3317 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3318 :param expDate: string "Undefined" by default or local date in future. 3319 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3320 This date is converting to UTC format for server. 3321 :return: JSON with response from broker server. 3322 """ 3323 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3324 3325 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3326 """ 3327 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3328 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3329 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3330 See also: `Order()` docstring. 3331 3332 :param lots: volume, integer count of lots >= 1. 3333 :param targetPrice: target price > 0. This is open trade price for limit order. 3334 :return: JSON with response from broker server. 3335 """ 3336 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3337 3338 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3339 """ 3340 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3341 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3342 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3343 target price value then broker opens a limit order. See also: `Order()` docstring. 3344 3345 :param lots: volume, integer count of lots >= 1. 3346 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3347 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3348 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3349 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3350 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3351 :param expDate: string "Undefined" by default or local date in future. 3352 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3353 This date is converting to UTC format for server. 3354 :return: JSON with response from broker server. 3355 """ 3356 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3357 3358 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3359 """ 3360 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3361 3362 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3363 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3364 This avoids unnecessary downloading data from the server. 3365 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3366 """ 3367 if self.accountId is None or not self.accountId: 3368 uLogger.error("Variable `accountId` must be defined for using this method!") 3369 raise Exception("Account ID required") 3370 3371 if orderIDs: 3372 if allOrdersIDs is None: 3373 rawOrders = self.RequestPendingOrders() 3374 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3375 3376 if allStopOrdersIDs is None: 3377 rawStopOrders = self.RequestStopOrders() 3378 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3379 3380 for orderID in orderIDs: 3381 idInPendingOrders = orderID in allOrdersIDs 3382 idInStopOrders = orderID in allStopOrdersIDs 3383 3384 if not (idInPendingOrders or idInStopOrders): 3385 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3386 continue 3387 3388 else: 3389 if idInPendingOrders: 3390 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3391 3392 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3393 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3394 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3395 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3396 3397 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3398 if self.moreDebug: 3399 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3400 3401 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3402 3403 else: 3404 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3405 3406 elif idInStopOrders: 3407 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3408 3409 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3410 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3411 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3412 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3413 3414 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3415 if self.moreDebug: 3416 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3417 3418 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3419 3420 else: 3421 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3422 3423 else: 3424 continue 3425 3426 def CloseAllOrders(self) -> None: 3427 """ 3428 Gets a list of open pending and stop orders and cancel it all. 3429 """ 3430 rawOrders = self.RequestPendingOrders() 3431 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3432 lenOrders = len(allOrdersIDs) 3433 3434 rawStopOrders = self.RequestStopOrders() 3435 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3436 lenSOrders = len(allStopOrdersIDs) 3437 3438 if lenOrders > 0 or lenSOrders > 0: 3439 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3440 3441 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3442 3443 else: 3444 uLogger.info("Orders not found, nothing to cancel.") 3445 3446 def CloseAll(self, *args) -> None: 3447 """ 3448 Close all available (not blocked) opened trades and orders. 3449 3450 Also, you can select one or more keywords case-insensitive: 3451 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3452 3453 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3454 """ 3455 overview = self.Overview(show=False) # get all open trades info 3456 3457 if len(args) == 0: 3458 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3459 self.CloseAllOrders() # close all pending and stop orders 3460 3461 for iType in TKS_INSTRUMENTS: 3462 if iType != "Currencies": 3463 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3464 3465 else: 3466 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3467 lowerArgs = [x.lower() for x in args] 3468 3469 if "orders" in lowerArgs: 3470 self.CloseAllOrders() # close all pending and stop orders 3471 3472 for iType in TKS_INSTRUMENTS: 3473 if iType.lower() in lowerArgs and iType != "Currencies": 3474 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3475 3476 def CloseAllByTicker(self, instrument: str) -> None: 3477 """ 3478 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3479 3480 This method searches opened trade and orders of instrument throw all portfolio and then use 3481 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3482 3483 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3484 3485 :param instrument: string with ticker. 3486 """ 3487 if instrument is None or not instrument: 3488 uLogger.error("Ticker name must be defined for using this method!") 3489 raise Exception("Ticker required") 3490 3491 overview = self.Overview(show=False) # get user portfolio with all open trades info 3492 3493 self._ticker = instrument # try to set instrument as ticker 3494 self._figi = "" 3495 3496 if self.IsInPortfolio(portfolio=overview): 3497 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3498 self.CloseTrades(instruments=[instrument], portfolio=overview) 3499 3500 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3501 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3502 3503 if limitAll and self.IsInLimitOrders(portfolio=overview): 3504 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3505 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3506 3507 if stopAll and self.IsInStopOrders(portfolio=overview): 3508 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3509 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3510 3511 def CloseAllByFIGI(self, instrument: str) -> None: 3512 """ 3513 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3514 3515 This method searches opened trade and orders of instrument throw all portfolio and then use 3516 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3517 3518 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3519 3520 :param instrument: string with FIGI id. 3521 """ 3522 if instrument is None or not instrument: 3523 uLogger.error("FIGI id must be defined for using this method!") 3524 raise Exception("FIGI required") 3525 3526 overview = self.Overview(show=False) # get user portfolio with all open trades info 3527 3528 self._ticker = "" 3529 self._figi = instrument # try to set instrument as FIGI id 3530 3531 if self.IsInPortfolio(portfolio=overview): 3532 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3533 self.CloseTrades(instruments=[instrument], portfolio=overview) 3534 3535 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3536 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3537 3538 if limitAll and self.IsInLimitOrders(portfolio=overview): 3539 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3540 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3541 3542 if stopAll and self.IsInStopOrders(portfolio=overview): 3543 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3544 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3545 3546 @staticmethod 3547 def ParseOrderParameters(operation, **inputParameters): 3548 """ 3549 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3550 3551 :param operation: string "Buy" or "Sell". 3552 :param inputParameters: this is dict of strings that looks like this 3553 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3554 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3555 "prices" key: one or more prices to open limit-orders 3556 Counts of values in lots and prices lists must be equals! 3557 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3558 """ 3559 # TODO: update order grid work with api v2 3560 pass 3561 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3562 # 3563 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3564 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3565 # raise Exception("Incorrect value") 3566 # 3567 # if "l" in inputParameters.keys(): 3568 # inputParameters["lots"] = inputParameters.pop("l") 3569 # 3570 # if "p" in inputParameters.keys(): 3571 # inputParameters["prices"] = inputParameters.pop("p") 3572 # 3573 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3574 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3575 # raise Exception("Incorrect value") 3576 # 3577 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3578 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3579 # 3580 # if len(lots) != len(prices): 3581 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3582 # raise Exception("Incorrect value") 3583 # 3584 # uLogger.debug("Extracted parameters for orders:") 3585 # uLogger.debug("lots = {}".format(lots)) 3586 # uLogger.debug("prices = {}".format(prices)) 3587 # 3588 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3589 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3590 # uLogger.debug("Order parameters: {}".format(result)) 3591 # 3592 # return result 3593 3594 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3595 """ 3596 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3597 3598 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3599 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3600 """ 3601 result = False 3602 msg = "Instrument not defined!" 3603 3604 if portfolio is None or not portfolio: 3605 portfolio = self.Overview(show=False) 3606 3607 if self._ticker: 3608 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3609 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3610 3611 for iType in TKS_INSTRUMENTS: 3612 for instrument in portfolio["stat"][iType]: 3613 if instrument["ticker"] == self._ticker: 3614 result = True 3615 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3616 break 3617 3618 elif self._figi: 3619 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3620 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3621 3622 for iType in TKS_INSTRUMENTS: 3623 for instrument in portfolio["stat"][iType]: 3624 if instrument["figi"] == self._figi: 3625 result = True 3626 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3627 break 3628 3629 else: 3630 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3631 3632 uLogger.debug(msg) 3633 3634 return result 3635 3636 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3637 """ 3638 Returns instrument from the user's portfolio if it presents there. 3639 Instrument must be defined by `ticker` (highly priority) or `figi`. 3640 3641 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3642 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3643 """ 3644 result = None 3645 msg = "Instrument not defined!" 3646 3647 if portfolio is None or not portfolio: 3648 portfolio = self.Overview(show=False) 3649 3650 if self._ticker: 3651 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3652 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3653 3654 for iType in TKS_INSTRUMENTS: 3655 for instrument in portfolio["stat"][iType]: 3656 if instrument["ticker"] == self._ticker: 3657 result = instrument 3658 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3659 break 3660 3661 elif self._figi: 3662 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3663 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3664 3665 for iType in TKS_INSTRUMENTS: 3666 for instrument in portfolio["stat"][iType]: 3667 if instrument["figi"] == self._figi: 3668 result = instrument 3669 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3670 break 3671 3672 else: 3673 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3674 3675 uLogger.debug(msg) 3676 3677 return result 3678 3679 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3680 """ 3681 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3682 3683 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3684 3685 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3686 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3687 """ 3688 result = False 3689 msg = "Instrument not defined!" 3690 3691 if portfolio is None or not portfolio: 3692 portfolio = self.Overview(show=False) 3693 3694 if self._ticker: 3695 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3696 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3697 3698 for instrument in portfolio["stat"]["orders"]: 3699 if instrument["ticker"] == self._ticker: 3700 result = True 3701 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3702 break 3703 3704 elif self._figi: 3705 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3706 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3707 3708 for instrument in portfolio["stat"]["orders"]: 3709 if instrument["figi"] == self._figi: 3710 result = True 3711 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3712 break 3713 3714 else: 3715 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3716 3717 uLogger.debug(msg) 3718 3719 return result 3720 3721 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3722 """ 3723 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3724 Instrument must be defined by `ticker` (highly priority) or `figi`. 3725 3726 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3727 3728 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3729 :return: list with `orderID`s of limit orders. 3730 """ 3731 result = [] 3732 msg = "Instrument not defined!" 3733 3734 if portfolio is None or not portfolio: 3735 portfolio = self.Overview(show=False) 3736 3737 if self._ticker: 3738 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3739 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3740 3741 for instrument in portfolio["stat"]["orders"]: 3742 if instrument["ticker"] == self._ticker: 3743 result.append(instrument["orderID"]) 3744 3745 if result: 3746 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3747 3748 elif self._figi: 3749 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3750 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3751 3752 for instrument in portfolio["stat"]["orders"]: 3753 if instrument["figi"] == self._figi: 3754 result.append(instrument["orderID"]) 3755 3756 if result: 3757 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3758 3759 else: 3760 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3761 3762 uLogger.debug(msg) 3763 3764 return result 3765 3766 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3767 """ 3768 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3769 3770 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3771 3772 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3773 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3774 """ 3775 result = False 3776 msg = "Instrument not defined!" 3777 3778 if portfolio is None or not portfolio: 3779 portfolio = self.Overview(show=False) 3780 3781 if self._ticker: 3782 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3783 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3784 3785 for instrument in portfolio["stat"]["stopOrders"]: 3786 if instrument["ticker"] == self._ticker: 3787 result = True 3788 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3789 break 3790 3791 elif self._figi: 3792 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3793 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3794 3795 for instrument in portfolio["stat"]["stopOrders"]: 3796 if instrument["figi"] == self._figi: 3797 result = True 3798 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3799 break 3800 3801 else: 3802 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3803 3804 uLogger.debug(msg) 3805 3806 return result 3807 3808 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3809 """ 3810 Returns list with all `orderID`s of opened stop orders for the instrument. 3811 Instrument must be defined by `ticker` (highly priority) or `figi`. 3812 3813 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3814 3815 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3816 :return: list with `orderID`s of stop orders. 3817 """ 3818 result = [] 3819 msg = "Instrument not defined!" 3820 3821 if portfolio is None or not portfolio: 3822 portfolio = self.Overview(show=False) 3823 3824 if self._ticker: 3825 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3826 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3827 3828 for instrument in portfolio["stat"]["stopOrders"]: 3829 if instrument["ticker"] == self._ticker: 3830 result.append(instrument["orderID"]) 3831 3832 if result: 3833 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3834 3835 elif self._figi: 3836 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3837 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3838 3839 for instrument in portfolio["stat"]["stopOrders"]: 3840 if instrument["figi"] == self._figi: 3841 result.append(instrument["orderID"]) 3842 3843 if result: 3844 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3845 3846 else: 3847 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3848 3849 uLogger.debug(msg) 3850 3851 return result 3852 3853 def RequestLimits(self) -> dict: 3854 """ 3855 Method for obtaining the available funds for withdrawal for current `accountId`. 3856 3857 See also: 3858 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3859 - `OverviewLimits()` method 3860 3861 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3862 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3863 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3864 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3865 """ 3866 if self.accountId is None or not self.accountId: 3867 uLogger.error("Variable `accountId` must be defined for using this method!") 3868 raise Exception("Account ID required") 3869 3870 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3871 3872 self.body = str({"accountId": self.accountId}) 3873 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3874 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3875 3876 if self.moreDebug: 3877 uLogger.debug("Records about available funds for withdrawal successfully received") 3878 3879 return rawLimits 3880 3881 def OverviewLimits(self, show: bool = False) -> dict: 3882 """ 3883 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3884 3885 See also: `RequestLimits()`. 3886 3887 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3888 :return: dict with raw parsed data from server and some calculated statistics about it. 3889 """ 3890 if self.accountId is None or not self.accountId: 3891 uLogger.error("Variable `accountId` must be defined for using this method!") 3892 raise Exception("Account ID required") 3893 3894 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3895 3896 view = { 3897 "rawLimits": rawLimits, 3898 "limits": { # parsed data for every currency: 3899 "money": { # this is an array of portfolio currency positions 3900 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3901 }, 3902 "blocked": { # this is an array of blocked currency 3903 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3904 }, 3905 "blockedGuarantee": { # this is locked money under collateral for futures 3906 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3907 }, 3908 }, 3909 } 3910 3911 # --- Prepare text table with limits in human-readable format: 3912 if show: 3913 info = [ 3914 "# Withdrawal limits\n\n", 3915 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3916 "* **Account ID:** [{}]\n".format(self.accountId), 3917 ] 3918 3919 if view["limits"]["money"]: 3920 info.extend([ 3921 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3922 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3923 ]) 3924 3925 else: 3926 info.append("\nNo withdrawal limits\n") 3927 3928 for curr in view["limits"]["money"].keys(): 3929 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3930 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3931 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3932 3933 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3934 "[{}]".format(curr), 3935 "{:.2f}".format(view["limits"]["money"][curr]), 3936 "{:.2f}".format(availableMoney), 3937 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3938 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3939 ) 3940 3941 if curr == "rub": 3942 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3943 3944 else: 3945 info.append(infoStr) 3946 3947 infoText = "".join(info) 3948 3949 uLogger.info(infoText) 3950 3951 if self.withdrawalLimitsFile: 3952 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3953 fH.write(infoText) 3954 3955 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3956 3957 if self.useHTMLReports: 3958 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3959 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3960 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 3961 3962 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 3963 3964 return view 3965 3966 def RequestAccounts(self) -> dict: 3967 """ 3968 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3969 3970 See also: 3971 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3972 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3973 - `OverviewUserInfo()` method 3974 3975 :return: dict with raw data from server that contains accounts info. Example of dict: 3976 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3977 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3978 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3979 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3980 """ 3981 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3982 3983 self.body = str({}) 3984 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3985 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3986 3987 if self.moreDebug: 3988 uLogger.debug("Records about available accounts successfully received") 3989 3990 return rawAccounts 3991 3992 def RequestUserInfo(self) -> dict: 3993 """ 3994 Method for requesting common user's information. 3995 3996 See also: 3997 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3998 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3999 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4000 - `OverviewUserInfo()` method 4001 4002 :return: dict with raw data from server that contains user's information. Example of dict: 4003 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4004 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4005 """ 4006 uLogger.debug("Requesting common user's information. Wait, please...") 4007 4008 self.body = str({}) 4009 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4010 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4011 4012 if self.moreDebug: 4013 uLogger.debug("Records about current user successfully received") 4014 4015 return rawUserInfo 4016 4017 def RequestMarginStatus(self, accountId: str = None) -> dict: 4018 """ 4019 Method for requesting margin calculation for defined account ID. 4020 4021 See also: 4022 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4023 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4024 - `OverviewUserInfo()` method 4025 4026 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4027 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4028 Example of responses: 4029 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4030 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4031 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4032 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4033 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4034 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4035 """ 4036 if accountId is None or not accountId: 4037 if self.accountId is None or not self.accountId: 4038 uLogger.error("Variable `accountId` must be defined for using this method!") 4039 raise Exception("Account ID required") 4040 4041 else: 4042 accountId = self.accountId # use `self.accountId` (main ID) by default 4043 4044 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4045 4046 self.body = str({"accountId": accountId}) 4047 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4048 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4049 4050 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4051 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4052 rawMargin = {} 4053 4054 else: 4055 if self.moreDebug: 4056 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4057 4058 return rawMargin 4059 4060 def RequestTariffLimits(self) -> dict: 4061 """ 4062 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4063 4064 See also: 4065 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4066 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4067 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4068 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4069 - `OverviewUserInfo()` method 4070 4071 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4072 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4073 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4074 """ 4075 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4076 4077 self.body = str({}) 4078 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4079 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4080 4081 if self.moreDebug: 4082 uLogger.debug("Records with limits of current tariff successfully received") 4083 4084 return rawTariffLimits 4085 4086 def RequestBondCoupons(self, iJSON: dict) -> dict: 4087 """ 4088 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4089 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4090 All dates are in UTC timezone. 4091 4092 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4093 Documentation: 4094 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4095 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4096 4097 See also: `ExtendBondsData()`. 4098 4099 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4100 If raw iJSON is not data of bond then server returns an error [400] with message: 4101 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4102 :return: dictionary with bond payment calendar. Response example 4103 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4104 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4105 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4106 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4107 """ 4108 if iJSON["figi"] is None or not iJSON["figi"]: 4109 uLogger.error("FIGI must be defined for using this method!") 4110 raise Exception("FIGI required") 4111 4112 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4113 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4114 4115 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4116 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4117 self._figi, 4118 startDate, 4119 endDate, 4120 )) 4121 4122 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4123 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4124 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4125 4126 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4127 uLogger.warning("Instrument type is not bond!") 4128 4129 else: 4130 if self.moreDebug: 4131 uLogger.debug("Records about bond payment calendar successfully received") 4132 4133 return calendar 4134 4135 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4136 """ 4137 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4138 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4139 coupon yields, current yields and some statistics etc. 4140 4141 WARNING! This is too long operation if a lot of bonds requested from broker server. 4142 4143 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4144 4145 :param instruments: list of strings with tickers or FIGIs. 4146 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4147 for further used by data scientists or stock analytics. 4148 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4149 In XLSX-file and Pandas DataFrame fields mean: 4150 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4151 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4152 """ 4153 if instruments is None or not instruments: 4154 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4155 raise Exception("Ticker or FIGI required") 4156 4157 if isinstance(instruments, str): 4158 instruments = [instruments] 4159 4160 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4161 4162 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4163 4164 iCount = len(uniqueInstruments) 4165 tooLong = iCount >= 20 4166 if tooLong: 4167 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4168 4169 bonds = None 4170 for i, self._figi in enumerate(uniqueInstruments): 4171 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4172 4173 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4174 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4175 rawBond = self.SearchByFIGI(requestPrice=True) 4176 4177 # Widen raw data with UTC current time (iData["actualDateTime"]): 4178 actualDate = datetime.now(tzutc()) 4179 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4180 4181 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4182 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4183 4184 # Replace some values with human-readable: 4185 iData["nominalCurrency"] = iData["nominal"]["currency"] 4186 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4187 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4188 iData["aciCurrency"] = iData["aciValue"]["currency"] 4189 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4190 iData["issueSize"] = int(iData["issueSize"]) 4191 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4192 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4193 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4194 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4195 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4196 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4197 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4198 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4199 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4200 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4201 4202 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4203 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4204 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4205 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4206 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4207 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4208 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4209 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4210 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4211 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4212 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4213 4214 # Widen raw data with calendar data from `rawCalendar` values: 4215 calendarData = [] 4216 if "events" in iData["rawCalendar"].keys(): 4217 for item in iData["rawCalendar"]["events"]: 4218 calendarData.append({ 4219 "couponDate": item["couponDate"], 4220 "couponNumber": int(item["couponNumber"]), 4221 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4222 "payCurrency": item["payOneBond"]["currency"], 4223 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4224 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4225 "couponStartDate": item["couponStartDate"], 4226 "couponEndDate": item["couponEndDate"], 4227 "couponPeriod": item["couponPeriod"], 4228 }) 4229 4230 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4231 if "maturityDate" not in iData.keys(): 4232 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4233 4234 # Widen raw data with Coupon Rate. 4235 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4236 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4237 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4238 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4239 4240 # Widen raw data with Yield to Maturity (YTM) on current date. 4241 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4242 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4243 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4244 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4245 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4246 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4247 4248 iData["calendar"] = calendarData # adds calendar at the end 4249 4250 # Remove not used data: 4251 iData.pop("uid") 4252 iData.pop("positionUid") 4253 iData.pop("currentPrice") 4254 iData.pop("rawCalendar") 4255 4256 colNames = list(iData.keys()) 4257 if bonds is None: 4258 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4259 4260 else: 4261 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4262 4263 else: 4264 uLogger.warning("Instrument is not a bond!") 4265 4266 processed = round(100 * (i + 1) / iCount, 1) 4267 if tooLong and processed % 5 == 0: 4268 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4269 4270 else: 4271 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4272 4273 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4274 4275 # Saving bonds from Pandas DataFrame to XLSX sheet: 4276 if xlsx and self.bondsXLSXFile: 4277 with pd.ExcelWriter( 4278 path=self.bondsXLSXFile, 4279 date_format=TKS_DATE_FORMAT, 4280 datetime_format=TKS_DATE_TIME_FORMAT, 4281 mode="w", 4282 ) as writer: 4283 bonds.to_excel( 4284 writer, 4285 sheet_name="Extended bonds data", 4286 index=True, 4287 encoding="UTF-8", 4288 freeze_panes=(1, 1), 4289 ) # saving as XLSX-file with freeze first row and column as headers 4290 4291 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4292 4293 return bonds 4294 4295 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4296 """ 4297 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4298 4299 WARNING! This is too long operation if a lot of bonds requested from broker server. 4300 4301 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4302 4303 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4304 extended information about bonds: main info, current prices, bond payment calendar, 4305 coupon yields, current yields and some statistics etc. 4306 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4307 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4308 for further used by data scientists or stock analytics. 4309 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4310 """ 4311 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4312 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4313 4314 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4315 4316 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4317 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4318 calendar = None 4319 for bond in extBonds.iterrows(): 4320 for item in bond[1]["calendar"]: 4321 cData = { 4322 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4323 "couponDate": item["couponDate"], 4324 "figi": bond[1]["figi"], 4325 "ticker": bond[1]["ticker"], 4326 "name": bond[1]["name"], 4327 "couponNumber": item["couponNumber"], 4328 "payOneBond": item["payOneBond"], 4329 "payCurrency": item["payCurrency"], 4330 "couponType": item["couponType"], 4331 "couponPeriod": item["couponPeriod"], 4332 "fixDate": item["fixDate"], 4333 "couponStartDate": item["couponStartDate"], 4334 "couponEndDate": item["couponEndDate"], 4335 } 4336 4337 if calendar is None: 4338 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4339 4340 else: 4341 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4342 4343 if calendar is not None: 4344 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4345 4346 # Saving calendar from Pandas DataFrame to XLSX sheet: 4347 if xlsx: 4348 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4349 4350 with pd.ExcelWriter( 4351 path=xlsxCalendarFile, 4352 date_format=TKS_DATE_FORMAT, 4353 datetime_format=TKS_DATE_TIME_FORMAT, 4354 mode="w", 4355 ) as writer: 4356 humanReadable = calendar.copy(deep=True) 4357 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4358 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4359 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4360 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4361 humanReadable.columns = colNames # human-readable column names 4362 4363 humanReadable.to_excel( 4364 writer, 4365 sheet_name="Bond payments calendar", 4366 index=False, 4367 encoding="UTF-8", 4368 freeze_panes=(1, 2), 4369 ) # saving as XLSX-file with freeze first row and column as headers 4370 4371 del humanReadable # release df in memory 4372 4373 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4374 4375 return calendar 4376 4377 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4378 """ 4379 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4380 Also, creates Markdown file with calendar data, `calendar.md` by default. 4381 4382 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4383 4384 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4385 extended information about bonds: main info, current prices, bond payment calendar, 4386 coupon yields, current yields and some statistics etc. 4387 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4388 :param show: if `True` then also printing bonds payment calendar to the console, 4389 otherwise save to file `calendarFile` only. `False` by default. 4390 :return: multilines text in Markdown format with bonds payment calendar as a table. 4391 """ 4392 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4393 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4394 4395 infoText = "# Bond payments calendar\n\n" 4396 4397 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4398 4399 if not (calendar is None or calendar.empty): 4400 splitLine = "| | | | | | | | | |\n" 4401 4402 info = [ 4403 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4404 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4405 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4406 ] 4407 4408 newMonth = False 4409 notOneBond = calendar["figi"].nunique() > 1 4410 for i, bond in enumerate(calendar.iterrows()): 4411 if newMonth and notOneBond: 4412 info.append(splitLine) 4413 4414 info.append( 4415 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4416 " √" if bond[1]["paid"] else " —", 4417 bond[1]["couponDate"].split("T")[0], 4418 bond[1]["figi"], 4419 bond[1]["ticker"], 4420 bond[1]["couponNumber"], 4421 "{} {}".format( 4422 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4423 bond[1]["payCurrency"], 4424 ), 4425 bond[1]["couponType"], 4426 bond[1]["couponPeriod"], 4427 bond[1]["fixDate"].split("T")[0], 4428 ) 4429 ) 4430 4431 if i < len(calendar.values) - 1: 4432 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4433 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4434 newMonth = False if curDate.month == nextDate.month else True 4435 4436 else: 4437 newMonth = False 4438 4439 infoText += "".join(info) 4440 4441 if show: 4442 uLogger.info("{}".format(infoText)) 4443 4444 if self.calendarFile is not None: 4445 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4446 fH.write(infoText) 4447 4448 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4449 4450 if self.useHTMLReports: 4451 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4452 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4453 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4454 4455 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4456 4457 else: 4458 infoText += "No data\n" 4459 4460 return infoText 4461 4462 def OverviewAccounts(self, show: bool = False) -> dict: 4463 """ 4464 Method for parsing and show simple table with all available user accounts. 4465 4466 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4467 4468 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4469 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4470 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4471 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4472 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4473 "closed": "—", "access": "Full access" }, ...}}` 4474 """ 4475 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4476 4477 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4478 accounts = { 4479 item["id"]: { 4480 "type": TKS_ACCOUNT_TYPES[item["type"]], 4481 "name": item["name"], 4482 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4483 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4484 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4485 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4486 } for item in rawAccounts["accounts"] 4487 } 4488 4489 # Raw and parsed data with some fields replaced in "stat" section: 4490 view = { 4491 "rawAccounts": rawAccounts, 4492 "stat": accounts, 4493 } 4494 4495 # --- Prepare simple text table with only accounts data in human-readable format: 4496 if show: 4497 info = [ 4498 "# User accounts\n\n", 4499 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4500 "| Account ID | Type | Status | Name |\n", 4501 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4502 ] 4503 4504 for account in view["stat"].keys(): 4505 info.extend([ 4506 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4507 account, 4508 view["stat"][account]["type"], 4509 view["stat"][account]["status"], 4510 view["stat"][account]["name"], 4511 ) 4512 ]) 4513 4514 infoText = "".join(info) 4515 4516 uLogger.info(infoText) 4517 4518 if self.userAccountsFile: 4519 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4520 fH.write(infoText) 4521 4522 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4523 4524 if self.useHTMLReports: 4525 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4526 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4527 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4528 4529 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4530 4531 return view 4532 4533 def OverviewUserInfo(self, show: bool = False) -> dict: 4534 """ 4535 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4536 4537 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4538 4539 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4540 :return: dict with raw parsed data from server and some calculated statistics about it. 4541 """ 4542 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4543 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4544 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4545 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4546 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4547 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4548 4549 # This is dict with parsed common user data: 4550 userInfo = { 4551 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4552 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4553 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4554 "tariff": rawUserInfo["tariff"], 4555 } 4556 4557 # This is an array of dict with parsed margin statuses for every account IDs: 4558 margins = {} 4559 for accountId in accounts.keys(): 4560 if rawMargins[accountId]: 4561 margins[accountId] = { 4562 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4563 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4564 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4565 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4566 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4567 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4568 } 4569 4570 else: 4571 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4572 4573 unary = {} # unary-connection limits 4574 for item in rawTariffLimits["unaryLimits"]: 4575 if item["limitPerMinute"] in unary.keys(): 4576 unary[item["limitPerMinute"]].extend(item["methods"]) 4577 4578 else: 4579 unary[item["limitPerMinute"]] = item["methods"] 4580 4581 stream = {} # stream-connection limits 4582 for item in rawTariffLimits["streamLimits"]: 4583 if item["limit"] in stream.keys(): 4584 stream[item["limit"]].extend(item["streams"]) 4585 4586 else: 4587 stream[item["limit"]] = item["streams"] 4588 4589 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4590 limits = { 4591 "unary": unary, 4592 "stream": stream, 4593 } 4594 4595 # Raw and parsed data as an output result: 4596 view = { 4597 "rawUserInfo": rawUserInfo, 4598 "rawAccounts": rawAccounts, 4599 "rawMargins": rawMargins, 4600 "rawTariffLimits": rawTariffLimits, 4601 "stat": { 4602 "userInfo": userInfo, 4603 "accounts": accounts, 4604 "margins": margins, 4605 "limits": limits, 4606 }, 4607 } 4608 4609 # --- Prepare text table with user information in human-readable format: 4610 if show: 4611 info = [ 4612 "# Full user information\n\n", 4613 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4614 "## Common information\n\n", 4615 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4616 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4617 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4618 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4619 "\n## User accounts\n\n", 4620 ] 4621 4622 for account in view["stat"]["accounts"].keys(): 4623 info.extend([ 4624 "### ID: [{}]\n\n".format(account), 4625 "| Parameters | Values |\n", 4626 "|----------------------|--------------------------------------------------------------|\n", 4627 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4628 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4629 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4630 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4631 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4632 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4633 ]) 4634 4635 if margins[account]: 4636 info.extend([ 4637 "| Margin status: | Enabled |\n", 4638 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4639 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4640 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4641 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4642 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4643 ]) 4644 4645 else: 4646 info.append("| Margin status: | Disabled |\n\n") 4647 4648 info.extend([ 4649 "\n## Current user tariff limits\n", 4650 "\n### See also\n", 4651 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4652 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4653 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4654 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4655 "\n### Unary limits\n", 4656 ]) 4657 4658 if unary: 4659 for key, values in sorted(unary.items()): 4660 info.append("\n* Max requests per minute: {}\n".format(key)) 4661 4662 for value in values: 4663 info.append(" - {}\n".format(value)) 4664 4665 else: 4666 info.append("\nNot available\n") 4667 4668 info.append("\n### Stream limits\n") 4669 4670 if stream: 4671 for key, values in sorted(stream.items()): 4672 info.append("\n* Max stream connections: {}\n".format(key)) 4673 4674 for value in values: 4675 info.append(" - {}\n".format(value)) 4676 4677 else: 4678 info.append("\nNot available\n") 4679 4680 infoText = "".join(info) 4681 4682 uLogger.info(infoText) 4683 4684 if self.userInfoFile: 4685 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4686 fH.write(infoText) 4687 4688 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4689 4690 if self.useHTMLReports: 4691 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4692 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4693 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4694 4695 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4696 4697 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token
: https://tinkoff.github.io/investAPI/token/
86 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 87 """ 88 Main class init. 89 90 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 91 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 92 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 93 :param useCache: use default cache file with raw data to use instead of `iList`. 94 True by default. Cache is auto-update if new day has come. 95 If you don't want to use cache and always updates raw data then set `useCache=False`. 96 :param defaultCache: path to default cache file. `dump.json` by default. 97 """ 98 if token is None or not token: 99 try: 100 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 101 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 102 103 except KeyError: 104 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 105 raise Exception("Token required") 106 107 else: 108 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 109 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 110 111 if accountId is None or not accountId: 112 try: 113 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 114 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 115 116 except KeyError: 117 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 118 119 else: 120 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 121 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 122 123 self.version = __version__ # duplicate here used TKSBrokerAPI main version 124 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 125 126 Latest version: https://pypi.org/project/tksbrokerapi/ 127 """ 128 129 self.aliases = TKS_TICKER_ALIASES 130 """Some aliases instead official tickers. 131 132 See also: `TKSEnums.TKS_TICKER_ALIASES` 133 """ 134 135 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 136 137 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 138 139 self._ticker = "" 140 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 141 142 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 143 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 144 145 See also: `SearchByTicker()`, `SearchInstruments()`. 146 """ 147 148 self._figi = "" 149 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 150 151 See also: `SearchByFIGI()`, `SearchInstruments()`. 152 """ 153 154 self.depth = 1 155 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 156 157 See also: `GetCurrentPrices()`. 158 """ 159 160 self.server = r"https://invest-public-api.tinkoff.ru/rest" 161 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 162 163 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 164 """ 165 166 uLogger.debug("Broker API server: {}".format(self.server)) 167 168 self.timeout = 15 169 """Server operations timeout in seconds. Default: `15`. 170 171 See also: `SendAPIRequest()`. 172 """ 173 174 self.headers = { 175 "Content-Type": "application/json", 176 "accept": "application/json", 177 "Authorization": "Bearer {}".format(self.token), 178 "x-app-name": "Tim55667757.TKSBrokerAPI", 179 } 180 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 181 182 See also: `SendAPIRequest()`. 183 """ 184 185 self.body = None 186 """Request body which send to broker server. Default: `None`. 187 188 See also: `SendAPIRequest()`. 189 """ 190 191 self.moreDebug = False 192 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 193 194 self.useHTMLReports = False 195 """ 196 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 197 198 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 199 """ 200 201 self.historyFile = None 202 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 203 204 See also: `History()`. 205 """ 206 207 self.htmlHistoryFile = "index.html" 208 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 209 210 See also: `ShowHistoryChart()`. 211 """ 212 213 self.instrumentsFile = "instruments.md" 214 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 215 216 See also: `ShowInstrumentsInfo()`. 217 """ 218 219 self.searchResultsFile = "search-results.md" 220 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 221 222 See also: `SearchInstruments()`. 223 """ 224 225 self.pricesFile = "prices.md" 226 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 227 228 See also: `GetListOfPrices()`. 229 """ 230 231 self.infoFile = "info.md" 232 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 233 234 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 235 """ 236 237 self.bondsXLSXFile = "ext-bonds.xlsx" 238 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 239 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 240 241 See also: `ExtendBondsData()`. 242 """ 243 244 self.calendarFile = "calendar.md" 245 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 246 247 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 248 249 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 250 """ 251 252 self.overviewFile = "overview.md" 253 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 254 255 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 256 """ 257 258 self.overviewDigestFile = "overview-digest.md" 259 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 260 261 See also: `Overview()` with parameter `details="digest"`. 262 """ 263 264 self.overviewPositionsFile = "overview-positions.md" 265 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 266 267 See also: `Overview()` with parameter `details="positions"`. 268 """ 269 270 self.overviewOrdersFile = "overview-orders.md" 271 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 272 273 See also: `Overview()` with parameter `details="orders"`. 274 """ 275 276 self.overviewAnalyticsFile = "overview-analytics.md" 277 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 278 279 See also: `Overview()` with parameter `details="analytics"`. 280 """ 281 282 self.overviewBondsCalendarFile = "overview-calendar.md" 283 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 284 285 See also: `Overview()` with parameter `details="calendar"`. 286 """ 287 288 self.reportFile = "deals.md" 289 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 290 291 See also: `Deals()`. 292 """ 293 294 self.withdrawalLimitsFile = "limits.md" 295 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 296 297 See also: `OverviewLimits()` and `RequestLimits()`. 298 """ 299 300 self.userInfoFile = "user-info.md" 301 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 302 303 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 304 """ 305 306 self.userAccountsFile = "accounts.md" 307 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 308 309 See also: `OverviewAccounts()`, `RequestAccounts()`. 310 """ 311 312 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 313 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 314 315 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 316 317 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 318 """ 319 320 self.iList = None # init iList for raw instruments data 321 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 322 323 See also: `Listing()`, `DumpInstruments()`. 324 """ 325 326 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 327 if useCache: 328 if os.path.exists(self.iListDumpFile): 329 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 330 curTime = datetime.now(tzutc()) 331 332 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 333 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 334 335 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 336 337 else: 338 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 339 340 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 341 os.path.abspath(self.iListDumpFile), 342 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 343 )) 344 345 else: 346 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 347 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 348 349 else: 350 self.iList = self.Listing() # request new raw instruments data from broker server 351 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 352 353 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 354 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 355 356 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 357 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN
. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID
. - useCache: use default cache file with raw data to use instead of
iList
. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False
. - defaultCache: path to default cache file.
dump.json
by default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price
key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices()
.
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest()
.
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}
.
See also: SendAPIRequest()
.
Enables more debug information in this class, such as net request and response headers in all methods. False
by default.
If True
then TKSBrokerAPI generate also HTML reports from Markdown. False
by default.
See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
Full path to the output file where history candles will be saved or updated. Default: None
, it mean that returns only Pandas DataFrame.
See also: History()
.
Full path to the html file where rendered candles chart stored. Default: index.html
.
See also: ShowHistoryChart()
.
Filename where full available to user instruments list will be saved. Default: instruments.md
.
See also: ShowInstrumentsInfo()
.
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md
.
See also: SearchInstruments()
.
Filename where prices of selected instruments will be saved. Default: prices.md
.
See also: GetListOfPrices()
.
Filename where prices of selected instruments will be saved. Default: prices.md
.
See also: ShowInstrumentsInfo()
, RequestBondCoupons()
and RequestTradingStatus()
.
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx
.
See also: ExtendBondsData()
.
Filename where bonds payment calendar will be saved. Default: calendar.md
.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx
.
See also: CreateBondsCalendar()
, ShowBondsCalendar()
, ShowInstrumentInfo()
, RequestBondCoupons()
and ExtendBondsData()
.
Filename where current portfolio, open trades and orders will be saved. Default: overview.md
.
See also: Overview()
, RequestPortfolio()
, RequestPositions()
, RequestPendingOrders()
and RequestStopOrders()
.
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md
.
See also: Overview()
with parameter details="digest"
.
Filename where only open positions, without everything else will be saved. Default: overview-positions.md
.
See also: Overview()
with parameter details="positions"
.
Filename where open limits and stop orders will be saved. Default: overview-orders.md
.
See also: Overview()
with parameter details="orders"
.
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md
.
See also: Overview()
with parameter details="analytics"
.
Filename where only the bonds calendar section will be saved. Default: overview-calendar.md
.
See also: Overview()
with parameter details="calendar"
.
Filename where history of deals and trade statistics will be saved. Default: deals.md
.
See also: Deals()
.
Filename where table of funds available for withdrawal will be saved. Default: limits.md
.
See also: OverviewLimits()
and RequestLimits()
.
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
.
See also: OverviewUserInfo()
, RequestAccounts()
, RequestUserInfo()
, RequestMarginStatus()
and RequestTariffLimits()
.
Filename where simple table with all available user accounts (accountId
s) will be saved. Default: accounts.md
.
See also: OverviewAccounts()
, RequestAccounts()
.
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json
.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx
.
See also: DumpInstruments()
and DumpInstrumentsAsXLSX()
.
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile
.
See also: Listing()
, DumpInstruments()
.
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory()
, ShowHistoryChart()
and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
Setter for string with ticker, e.g. GOOGL
. Tickers may be upper case only.
Use alias for USD000UTSTOM
simple as USD
, EUR_RUB__TOM
as EUR
etc.
More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES
.
See also: SearchByTicker()
, SearchInstruments()
.
Setter for string with FIGI, e.g. ticker GOOGL
has FIGI BBG009S39JX6
. FIGIs may be upper case only.
See also: SearchByFIGI()
, SearchInstruments()
.
411 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 412 """ 413 Send GET or POST request to broker server and receive JSON object. 414 415 self.header: must be defining with dictionary of headers. 416 self.body: if define then used as request body. None by default. 417 self.timeout: global request timeout, 15 seconds by default. 418 :param url: url with REST request. 419 :param reqType: send "GET" or "POST" request. "GET" by default. 420 :param retry: how many times retry after first request if an 5xx server errors occurred. 421 :param pause: sleep time in seconds between retries. 422 :return: response JSON (dictionary) from broker. 423 """ 424 if reqType.upper() not in ("GET", "POST"): 425 uLogger.error("You can define request type: `GET` or `POST`!") 426 raise Exception("Incorrect value") 427 428 if self.moreDebug: 429 uLogger.debug("Request parameters:") 430 uLogger.debug(" - REST API URL: {}".format(url)) 431 uLogger.debug(" - request type: {}".format(reqType)) 432 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 433 uLogger.debug(" - body:\n{}".format(self.body)) 434 435 # fast hack to avoid all operations with some tickers/FIGI 436 responseJSON = {} 437 oK = True 438 for item in self.exclude: 439 if item in url: 440 if self.moreDebug: 441 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 442 443 oK = False 444 break 445 446 if oK: 447 counter = 0 448 response = None 449 errMsg = "" 450 451 while not response and counter <= retry: 452 if reqType == "GET": 453 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 454 455 if reqType == "POST": 456 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 457 458 if self.moreDebug: 459 uLogger.debug("Response:") 460 uLogger.debug(" - status code: {}".format(response.status_code)) 461 uLogger.debug(" - reason: {}".format(response.reason)) 462 uLogger.debug(" - body length: {}".format(len(response.text))) 463 uLogger.debug(" - headers:\n{}".format(response.headers)) 464 465 # Server returns some headers: 466 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 467 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 468 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 469 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 470 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 471 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 472 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 473 sleep(rateLimitWait) 474 475 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 476 if 400 <= response.status_code < 500: 477 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 478 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 479 480 if "code" in response.text and "message" in response.text: 481 msgDict = self._ParseJSON(rawData=response.text) 482 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 483 484 counter = retry + 1 # do not retry for 4xx errors 485 486 if 500 <= response.status_code < 600: 487 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 488 uLogger.debug(" - not oK, {}".format(errMsg)) 489 490 if "code" in response.text and "message" in response.text: 491 errMsgDict = self._ParseJSON(rawData=response.text) 492 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 493 494 counter += 1 495 496 if counter <= retry: 497 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 498 sleep(pause) 499 500 responseJSON = self._ParseJSON(rawData=response.text) 501 502 if errMsg: 503 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 504 uLogger.error(" - not oK, {}".format(errMsg)) 505 506 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
539 def Listing(self) -> dict: 540 """ 541 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 542 543 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 544 """ 545 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 546 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 547 548 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 549 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 550 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 551 552 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 553 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 554 poolUpdater.close() 555 556 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 557 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 558 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 559 560 # calculate minimum price increment (step) for all instruments and set up instrument's type: 561 for iType in iList.keys(): 562 for ticker in iList[iType]: 563 iList[iType][ticker]["type"] = iType 564 565 if "minPriceIncrement" in iList[iType][ticker].keys(): 566 iList[iType][ticker]["step"] = NanoToFloat( 567 iList[iType][ticker]["minPriceIncrement"]["units"], 568 iList[iType][ticker]["minPriceIncrement"]["nano"], 569 ) 570 571 else: 572 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 573 574 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
576 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 577 """ 578 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 579 580 See also: `DumpInstruments()`, `Listing()`. 581 582 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 583 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 584 """ 585 if self.iListDumpFile is None or not self.iListDumpFile: 586 uLogger.error("Output name of dump file must be defined!") 587 raise Exception("Filename required") 588 589 if not self.iList or forceUpdate: 590 self.iList = self.Listing() 591 592 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 593 594 # Save as XLSX with separated sheets for every type of instruments: 595 with pd.ExcelWriter( 596 path=xlsxDumpFile, 597 date_format=TKS_DATE_FORMAT, 598 datetime_format=TKS_DATE_TIME_FORMAT, 599 mode="w", 600 ) as writer: 601 for iType in TKS_INSTRUMENTS: 602 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 603 df = df[sorted(df)] # sorted by column names 604 df = df.applymap( 605 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 606 na_action="ignore", 607 ) # converting numbers from nano-type to float in every cell 608 df.to_excel( 609 writer, 610 sheet_name=iType, 611 encoding="UTF-8", 612 freeze_panes=(1, 1), 613 ) # saving as XLSX-file with freeze first row and column as headers 614 615 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments()
, Listing()
.
Parameters
617 def DumpInstruments(self, forceUpdate: bool = True) -> str: 618 """ 619 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 620 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 621 622 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 623 624 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 625 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 626 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 627 """ 628 if self.iListDumpFile is None or not self.iListDumpFile: 629 uLogger.error("Output name of dump file must be defined!") 630 raise Exception("Filename required") 631 632 if not self.iList or forceUpdate: 633 self.iList = self.Listing() 634 635 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 636 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 637 fH.write(jsonDump) 638 639 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 640 641 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing()
method. If iListDumpFile
string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX()
, Listing()
.
Parameters
- forceUpdate: if
True
then at first updates data withListing()
method, otherwise just saves existiList
as JSON-file (default:dump.json
).
Returns
serialized JSON formatted
str
with full data of instruments, also saved to the--output
JSON-file.
643 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 644 """ 645 Show information about one instrument defined by json data and prints it in Markdown format. 646 647 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 648 649 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 650 :param show: if `True` then also printing information about instrument and its current price. 651 :return: multilines text in Markdown format with information about one instrument. 652 """ 653 splitLine = "| | |\n" 654 infoText = "" 655 656 if iJSON is not None and iJSON and isinstance(iJSON, dict): 657 info = [ 658 "# Main information\n\n", 659 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 660 "| Parameters | Values |\n", 661 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 662 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 663 "| Full name: | {:<54} |\n".format(iJSON["name"]), 664 ] 665 666 if "sector" in iJSON.keys() and iJSON["sector"]: 667 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 668 669 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 670 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 671 672 info.extend([ 673 splitLine, 674 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 675 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 676 ]) 677 678 if "isin" in iJSON.keys() and iJSON["isin"]: 679 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 680 681 if "classCode" in iJSON.keys(): 682 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 683 684 info.extend([ 685 splitLine, 686 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 687 splitLine, 688 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 689 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 690 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 691 ]) 692 693 if iJSON["figi"]: 694 self._figi = iJSON["figi"] 695 iJSON = iJSON | self.RequestTradingStatus() 696 697 info.extend([ 698 splitLine, 699 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 700 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 701 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 702 ]) 703 704 info.append(splitLine) 705 706 if "type" in iJSON.keys() and iJSON["type"]: 707 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 708 709 if "shareType" in iJSON.keys() and iJSON["shareType"]: 710 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 711 712 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 713 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 714 715 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 716 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 717 718 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 719 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 720 721 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 722 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 723 724 if "focusType" in iJSON.keys() and iJSON["focusType"]: 725 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 726 727 if "assetType" in iJSON.keys() and iJSON["assetType"]: 728 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 729 730 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 731 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 732 733 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 734 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 735 736 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 737 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 738 739 if "currency" in iJSON.keys(): 740 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 741 742 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 743 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 744 745 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 746 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 747 748 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 749 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 750 751 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 752 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 753 754 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 755 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 756 757 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 758 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 759 760 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 761 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 762 763 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 764 info.append("| Perpetual bond: | Yes |\n") 765 766 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 767 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 768 769 iExt = None 770 if iJSON["type"] == "Bonds": 771 info.extend([ 772 splitLine, 773 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 774 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 775 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 776 iJSON["nominal"]["currency"], 777 )), 778 ]) 779 780 if "floatingCouponFlag" in iJSON.keys(): 781 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 782 783 if "amortizationFlag" in iJSON.keys(): 784 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 785 786 info.append(splitLine) 787 788 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 789 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 790 791 if iJSON["figi"]: 792 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 793 794 info.extend([ 795 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 796 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 797 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 798 ]) 799 800 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 801 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 802 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 803 iJSON["aciValue"]["currency"] 804 ))) 805 806 if "currentPrice" in iJSON.keys(): 807 info.append(splitLine) 808 809 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 810 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 811 812 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 813 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 814 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 815 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 816 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 817 818 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 819 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 820 821 info.extend([ 822 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 823 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 824 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 825 )), 826 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 827 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 828 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 829 )), 830 "| Changes between last deal price and last close | {:<54} |\n".format( 831 "{:.2f}%{}".format( 832 iJSON["currentPrice"]["changes"], 833 " ({}{:.2f} {})".format( 834 "+" if bondChangesDelta > 0 else "", 835 bondChangesDelta, 836 aciCurrency 837 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 838 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 839 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 840 currency 841 ), 842 ) 843 ), 844 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 845 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 846 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 847 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 848 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 849 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 850 )), 851 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 852 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 853 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 854 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 855 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 856 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 857 )), 858 ]) 859 860 if "lot" in iJSON.keys(): 861 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 862 863 if "step" in iJSON.keys() and iJSON["step"] != 0: 864 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 865 866 # Add bond payment calendar: 867 if iJSON["type"] == "Bonds": 868 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 869 info.extend(["\n#", strCalendar]) 870 871 infoText += "".join(info) 872 873 if show: 874 uLogger.info("{}".format(infoText)) 875 876 else: 877 uLogger.debug("{}".format(infoText)) 878 879 if self.infoFile is not None: 880 with open(self.infoFile, "w", encoding="UTF-8") as fH: 881 fH.write(infoText) 882 883 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 884 885 if self.useHTMLReports: 886 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 887 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 888 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 889 890 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 891 892 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker()
, SearchByFIGI()
, RequestBondCoupons()
, ExtendBondsData()
, ShowBondsCalendar()
and RequestTradingStatus()
.
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self._ticker]
- show: if
True
then also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
894 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 895 """ 896 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 897 898 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 899 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 900 :return: JSON formatted data with information about instrument. 901 """ 902 tickerJSON = {} 903 if self.moreDebug: 904 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 905 906 if not self._ticker: 907 uLogger.warning("self._ticker variable is not be empty!") 908 909 else: 910 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 911 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 912 raise Exception("Instrument not allowed") 913 914 if not self.iList: 915 self.iList = self.Listing() 916 917 if self._ticker in self.iList["Shares"].keys(): 918 tickerJSON = self.iList["Shares"][self._ticker] 919 if self.moreDebug: 920 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 921 922 elif self._ticker in self.iList["Currencies"].keys(): 923 tickerJSON = self.iList["Currencies"][self._ticker] 924 if self.moreDebug: 925 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 926 927 elif self._ticker in self.iList["Bonds"].keys(): 928 tickerJSON = self.iList["Bonds"][self._ticker] 929 if self.moreDebug: 930 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 931 932 elif self._ticker in self.iList["Etfs"].keys(): 933 tickerJSON = self.iList["Etfs"][self._ticker] 934 if self.moreDebug: 935 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 936 937 elif self._ticker in self.iList["Futures"].keys(): 938 tickerJSON = self.iList["Futures"][self._ticker] 939 if self.moreDebug: 940 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 941 942 if tickerJSON: 943 self._figi = tickerJSON["figi"] 944 945 if requestPrice: 946 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 947 948 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 949 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 950 951 else: 952 tickerJSON["currentPrice"]["changes"] = 0 953 954 if show: 955 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 956 957 else: 958 if show: 959 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 960 961 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker
must be defined!
Parameters
- requestPrice: if
False
then do not request current price of instrument (because this is long operation). - show: if
False
then do not runShowInstrumentInfo()
method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
963 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 964 """ 965 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 966 967 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 968 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 969 :return: JSON formatted data with information about instrument. 970 """ 971 figiJSON = {} 972 if self.moreDebug: 973 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 974 975 if not self._figi: 976 uLogger.warning("self._figi variable is not be empty!") 977 978 else: 979 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 980 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 981 raise Exception("Instrument not allowed") 982 983 if not self.iList: 984 self.iList = self.Listing() 985 986 for item in self.iList["Shares"].keys(): 987 if self._figi == self.iList["Shares"][item]["figi"]: 988 figiJSON = self.iList["Shares"][item] 989 990 if self.moreDebug: 991 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 992 993 break 994 995 if not figiJSON: 996 for item in self.iList["Currencies"].keys(): 997 if self._figi == self.iList["Currencies"][item]["figi"]: 998 figiJSON = self.iList["Currencies"][item] 999 1000 if self.moreDebug: 1001 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1002 1003 break 1004 1005 if not figiJSON: 1006 for item in self.iList["Bonds"].keys(): 1007 if self._figi == self.iList["Bonds"][item]["figi"]: 1008 figiJSON = self.iList["Bonds"][item] 1009 1010 if self.moreDebug: 1011 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1012 1013 break 1014 1015 if not figiJSON: 1016 for item in self.iList["Etfs"].keys(): 1017 if self._figi == self.iList["Etfs"][item]["figi"]: 1018 figiJSON = self.iList["Etfs"][item] 1019 1020 if self.moreDebug: 1021 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1022 1023 break 1024 1025 if not figiJSON: 1026 for item in self.iList["Futures"].keys(): 1027 if self._figi == self.iList["Futures"][item]["figi"]: 1028 figiJSON = self.iList["Futures"][item] 1029 1030 if self.moreDebug: 1031 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1032 1033 break 1034 1035 if figiJSON: 1036 self._figi = figiJSON["figi"] 1037 self._ticker = figiJSON["ticker"] 1038 1039 if requestPrice: 1040 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1041 1042 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1043 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1044 1045 else: 1046 figiJSON["currentPrice"]["changes"] = 0 1047 1048 if show: 1049 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1050 1051 else: 1052 if show: 1053 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1054 1055 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi
must be defined!
Parameters
- requestPrice: if
False
then do not request current price of instrument (it's long operation). - show: if
False
then do not runShowInstrumentInfo()
method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1057 def GetCurrentPrices(self, show: bool = True) -> dict: 1058 """ 1059 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1060 `{"buy": [{"price": 1243.8, "quantity": 193}, 1061 {"price": 1244.0, "quantity": 168}, 1062 {"price": 1244.8, "quantity": 5}, 1063 {"price": 1245.0, "quantity": 61}, 1064 {"price": 1245.4, "quantity": 60}], 1065 "sell": [{"price": 1243.6, "quantity": 8}, 1066 {"price": 1242.6, "quantity": 10}, 1067 {"price": 1242.4, "quantity": 18}, 1068 {"price": 1242.2, "quantity": 50}, 1069 {"price": 1242.0, "quantity": 113}], 1070 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1071 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1072 - sell: list of dicts with Buyers prices, 1073 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1074 - quantity: volume value by current price in lots, 1075 - limitUp: current trade session limit price, maximum, 1076 - limitDown: current trade session limit price, minimum, 1077 - lastPrice: last deal price of the instrument, 1078 - closePrice: previous trade session close price of the instrument. 1079 1080 See also: `SearchByTicker()` and `SearchByFIGI()`. 1081 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1082 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1083 1084 :param show: if `True` then print DOM to log and console. 1085 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1086 If an error occurred then returns an empty record: 1087 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1088 """ 1089 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1090 1091 if self.depth < 1: 1092 uLogger.error("Depth of Market (DOM) must be >=1!") 1093 raise Exception("Incorrect value") 1094 1095 if not (self._ticker or self._figi): 1096 uLogger.error("self._ticker or self._figi variables must be defined!") 1097 raise Exception("Ticker or FIGI required") 1098 1099 if self._ticker and not self._figi: 1100 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1101 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1102 1103 if not self._ticker and self._figi: 1104 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1105 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1106 1107 if not self._figi: 1108 uLogger.error("FIGI is not defined!") 1109 raise Exception("Ticker or FIGI required") 1110 1111 else: 1112 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1113 1114 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1115 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1116 self.body = str({"figi": self._figi, "depth": self.depth}) 1117 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1118 1119 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1120 # list of dicts with sellers orders: 1121 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1122 1123 # list of dicts with buyers orders: 1124 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1125 1126 # max price of instrument at this time: 1127 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1128 1129 # min price of instrument at this time: 1130 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1131 1132 # last price of deal with instrument: 1133 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1134 1135 # last close price of instrument: 1136 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1137 1138 else: 1139 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1140 uLogger.debug("Server response: {}".format(pricesResponse)) 1141 1142 if show: 1143 if prices["buy"] or prices["sell"]: 1144 info = [ 1145 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1146 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1147 self._ticker, 1148 self._figi, 1149 self.depth, 1150 ), 1151 "-" * 60, "\n", 1152 " Orders of Buyers | Orders of Sellers\n", 1153 "-" * 60, "\n", 1154 " Sell prices (volumes) | Buy prices (volumes)\n", 1155 "-" * 60, "\n", 1156 ] 1157 1158 if not prices["buy"]: 1159 info.append(" | No orders!\n") 1160 sumBuy = 0 1161 1162 else: 1163 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1164 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1165 for item in maxMinSorted: 1166 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1167 1168 if not prices["sell"]: 1169 info.append("No orders! |\n") 1170 sumSell = 0 1171 1172 else: 1173 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1174 for item in prices["sell"]: 1175 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1176 1177 info.extend([ 1178 "-" * 60, "\n", 1179 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1180 "-" * 60, "\n", 1181 ]) 1182 1183 infoText = "".join(info) 1184 1185 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1186 1187 else: 1188 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1189 1190 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth
5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}
, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker()
and SearchByFIGI()
.
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
True
then print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}
. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}
.
1192 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1193 """ 1194 This method get and show information about all available broker instruments for current user account. 1195 If `instrumentsFile` string is not empty then also save information to this file. 1196 1197 :param show: if `True` then print results to console, if `False` — print only to file. 1198 :return: multi-lines string with all available broker instruments 1199 """ 1200 if not self.iList: 1201 self.iList = self.Listing() 1202 1203 info = [ 1204 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1205 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1206 ] 1207 1208 # add instruments count by type: 1209 for iType in self.iList.keys(): 1210 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1211 1212 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1213 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1214 1215 # generating info tables with all instruments by type: 1216 for iType in self.iList.keys(): 1217 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1218 1219 for instrument in self.iList[iType].keys(): 1220 iName = self.iList[iType][instrument]["name"] # instrument's name 1221 if len(iName) > 57: 1222 iName = "{}...".format(iName[:54]) # right trim for a long string 1223 1224 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1225 self.iList[iType][instrument]["ticker"], 1226 iName, 1227 self.iList[iType][instrument]["figi"], 1228 self.iList[iType][instrument]["currency"], 1229 self.iList[iType][instrument]["lot"], 1230 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1231 )) 1232 1233 infoText = "".join(info) 1234 1235 if show: 1236 uLogger.info(infoText) 1237 1238 if self.instrumentsFile: 1239 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1240 fH.write(infoText) 1241 1242 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1243 1244 if self.useHTMLReports: 1245 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1246 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1247 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1248 1249 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1250 1251 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile
string is not empty then also save information to this file.
Parameters
- show: if
True
then print results to console, ifFalse
— print only to file.
Returns
multi-lines string with all available broker instruments
1253 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1254 """ 1255 This method search and show information about instruments by part of its ticker, FIGI or name. 1256 If `searchResultsFile` string is not empty then also save information to this file. 1257 1258 :param pattern: string with part of ticker, FIGI or instrument's name. 1259 :param show: if `True` then print results to console, if `False` — return list of result only. 1260 :return: list of dictionaries with all found instruments. 1261 """ 1262 if not self.iList: 1263 self.iList = self.Listing() 1264 1265 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1266 compiledPattern = re.compile(pattern, re.IGNORECASE) 1267 1268 for iType in self.iList: 1269 for instrument in self.iList[iType].values(): 1270 searchResult = compiledPattern.search(" ".join( 1271 [instrument["ticker"], instrument["figi"], instrument["name"]] 1272 )) 1273 1274 if searchResult: 1275 searchResults[iType][instrument["ticker"]] = instrument 1276 1277 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1278 info = [ 1279 "# Search results\n\n", 1280 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1281 "* **Search pattern:** [{}]\n".format(pattern), 1282 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1283 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1284 ] 1285 infoShort = info[:] 1286 1287 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1288 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1289 skippedLine = "| ... | ... | ... | ... |\n" 1290 1291 if resultsLen == 0: 1292 info.append("\nNo results\n") 1293 infoShort.append("\nNo results\n") 1294 uLogger.warning("No results. Try changing your search pattern.") 1295 1296 else: 1297 for iType in searchResults: 1298 iTypeValuesCount = len(searchResults[iType].values()) 1299 if iTypeValuesCount > 0: 1300 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1301 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1302 1303 for instrument in searchResults[iType].values(): 1304 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1305 instrument["type"], 1306 instrument["ticker"], 1307 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1308 instrument["figi"], 1309 )) 1310 1311 if iTypeValuesCount <= 5: 1312 infoShort.extend(info[-iTypeValuesCount:]) 1313 1314 else: 1315 infoShort.extend(info[-5:]) 1316 infoShort.append(skippedLine) 1317 1318 infoText = "".join(info) 1319 infoTextShort = "".join(infoShort) 1320 1321 if show: 1322 uLogger.info(infoTextShort) 1323 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1324 1325 if self.searchResultsFile: 1326 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1327 fH.write(infoText) 1328 1329 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1330 1331 if self.useHTMLReports: 1332 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1333 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1334 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1335 1336 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1337 1338 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile
string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
True
then print results to console, ifFalse
— return list of result only.
Returns
list of dictionaries with all found instruments.
1340 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1341 """ 1342 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1343 1344 :param instruments: list of strings with tickers or FIGIs. 1345 :return: list with unique instrument FIGIs only. 1346 """ 1347 requestedInstruments = [] 1348 for iName in instruments: 1349 if iName not in self.aliases.keys(): 1350 if iName not in requestedInstruments: 1351 requestedInstruments.append(iName) 1352 1353 else: 1354 if iName not in requestedInstruments: 1355 if self.aliases[iName] not in requestedInstruments: 1356 requestedInstruments.append(self.aliases[iName]) 1357 1358 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1359 1360 onlyUniqueFIGIs = [] 1361 for iName in requestedInstruments: 1362 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1363 continue 1364 1365 self._ticker = iName 1366 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1367 1368 if not iData: 1369 self._ticker = "" 1370 self._figi = iName 1371 1372 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1373 1374 if not iData: 1375 self._figi = "" 1376 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1377 1378 if iData and iData["figi"] not in onlyUniqueFIGIs: 1379 onlyUniqueFIGIs.append(iData["figi"]) 1380 1381 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1382 1383 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1385 def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]: 1386 """ 1387 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1388 1389 See limits: https://tinkoff.github.io/investAPI/limits/ 1390 1391 If `pricesFile` string is not empty then also save information to this file. 1392 1393 :param instruments: list of strings with tickers or FIGIs. 1394 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1395 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1396 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1397 """ 1398 if instruments is None or not instruments: 1399 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1400 raise Exception("Ticker or FIGI required") 1401 1402 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1403 1404 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1405 1406 iList = [] # trying to get info and current prices about all unique instruments: 1407 for self._figi in onlyUniqueFIGIs: 1408 iData = self.SearchByFIGI(requestPrice=True) 1409 iList.append(iData) 1410 1411 self.ShowListOfPrices(iList, show) 1412 1413 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile
string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
True
then prints prices to console, ifFalse
— prints only to filepricesFile
.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]
. One item is dict returned bySearchByTicker()
orSearchByFIGI()
methods.
1415 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1416 """ 1417 Show table contains current prices of given instruments. 1418 1419 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1420 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1421 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1422 :return: multilines text in Markdown format as a table contains current prices. 1423 """ 1424 infoText = "" 1425 1426 if show or self.pricesFile: 1427 info = [ 1428 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1429 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1430 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1431 ] 1432 1433 for item in iList: 1434 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1435 item["ticker"], 1436 item["figi"], 1437 item["type"], 1438 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1439 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1440 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1441 "{} / {}".format( 1442 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1443 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1444 ), 1445 "{} / {}".format( 1446 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1447 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1448 ), 1449 item["currency"], 1450 )) 1451 1452 infoText = "".join(info) 1453 1454 if show: 1455 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1456 1457 if self.pricesFile: 1458 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1459 fH.write(infoText) 1460 1461 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1462 1463 if self.useHTMLReports: 1464 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1465 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1466 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1467 1468 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1469 1470 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]
. One item is dict returned bySearchByTicker(requestPrice=True)
or bySearchByFIGI(requestPrice=True)
methods. - show: if
True
then prints prices to console, ifFalse
— prints only to filepricesFile
.
Returns
multilines text in Markdown format as a table contains current prices.
1472 def RequestTradingStatus(self) -> dict: 1473 """ 1474 Requesting trading status for the instrument defined by `figi` variable. 1475 1476 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1477 1478 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1479 1480 :return: dictionary with trading status attributes. Response example: 1481 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1482 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1483 """ 1484 if self._figi is None or not self._figi: 1485 uLogger.error("Variable `figi` must be defined for using this method!") 1486 raise Exception("FIGI required") 1487 1488 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1489 1490 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1491 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1492 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1493 1494 if self.moreDebug: 1495 uLogger.debug("Records about current trading status successfully received") 1496 1497 return tradingStatus
Requesting trading status for the instrument defined by figi
variable.
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1499 def RequestPortfolio(self) -> dict: 1500 """ 1501 Requesting actual user's portfolio for current `accountId`. 1502 1503 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1504 1505 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1506 1507 :return: dictionary with user's portfolio. 1508 """ 1509 if self.accountId is None or not self.accountId: 1510 uLogger.error("Variable `accountId` must be defined for using this method!") 1511 raise Exception("Account ID required") 1512 1513 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1514 1515 self.body = str({"accountId": self.accountId}) 1516 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1517 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1518 1519 if self.moreDebug: 1520 uLogger.debug("Records about user's portfolio successfully received") 1521 1522 return rawPortfolio
Requesting actual user's portfolio for current accountId
.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1524 def RequestPositions(self) -> dict: 1525 """ 1526 Requesting open positions by currencies and instruments for current `accountId`. 1527 1528 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1529 1530 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1531 1532 :return: dictionary with open positions by instruments. 1533 """ 1534 if self.accountId is None or not self.accountId: 1535 uLogger.error("Variable `accountId` must be defined for using this method!") 1536 raise Exception("Account ID required") 1537 1538 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1539 1540 self.body = str({"accountId": self.accountId}) 1541 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1542 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1543 1544 if self.moreDebug: 1545 uLogger.debug("Records about current open positions successfully received") 1546 1547 return rawPositions
Requesting open positions by currencies and instruments for current accountId
.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1549 def RequestPendingOrders(self) -> list: 1550 """ 1551 Requesting current actual pending limit orders for current `accountId`. 1552 1553 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1554 1555 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1556 1557 :return: list of dictionaries with pending limit orders. 1558 """ 1559 if self.accountId is None or not self.accountId: 1560 uLogger.error("Variable `accountId` must be defined for using this method!") 1561 raise Exception("Account ID required") 1562 1563 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1564 1565 self.body = str({"accountId": self.accountId}) 1566 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1567 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1568 1569 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1570 1571 return rawOrders
Requesting current actual pending limit orders for current accountId
.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending limit orders.
1573 def RequestStopOrders(self) -> list: 1574 """ 1575 Requesting current actual stop orders for current `accountId`. 1576 1577 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1578 1579 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1580 1581 :return: list of dictionaries with stop orders. 1582 """ 1583 if self.accountId is None or not self.accountId: 1584 uLogger.error("Variable `accountId` must be defined for using this method!") 1585 raise Exception("Account ID required") 1586 1587 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1588 1589 self.body = str({"accountId": self.accountId}) 1590 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1591 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1592 1593 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1594 1595 return rawStopOrders
Requesting current actual stop orders for current accountId
.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1597 def Overview(self, show: bool = False, details: str = "full") -> dict: 1598 """ 1599 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1600 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1601 and `overviewBondsCalendarFile` are defined then also save information to file. 1602 1603 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1604 many requests about the state of the portfolio, and then, based on the received data, a large number 1605 of calculation and statistics are collected. 1606 1607 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1608 :param details: how detailed should the information be? 1609 - `full` — shows full available information about portfolio status (by default), 1610 - `positions` — shows only open positions, 1611 - `orders` — shows only sections of open limits and stop orders. 1612 - `digest` — show a short digest of the portfolio status, 1613 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1614 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1615 :return: dictionary with client's raw portfolio and some statistics. 1616 """ 1617 if self.accountId is None or not self.accountId: 1618 uLogger.error("Variable `accountId` must be defined for using this method!") 1619 raise Exception("Account ID required") 1620 1621 view = { 1622 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1623 "headers": {}, # list of dictionaries, response headers without "positions" section 1624 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1625 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1626 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1627 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1628 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1629 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1630 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1631 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1632 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1633 }, 1634 "stat": { # --- some statistics calculated using "raw" sections: 1635 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1636 "availableRUB": 0., # available rubles (without other currencies) 1637 "blockedRUB": 0., # blocked sum in Russian Rouble 1638 "totalChangesRUB": 0., # changes for all open trades in RUB 1639 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1640 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1641 "sharesCostRUB": 0., # costs of all shares in RUB 1642 "bondsCostRUB": 0., # costs of all bonds in RUB 1643 "etfsCostRUB": 0., # costs of all etfs in RUB 1644 "futuresCostRUB": 0., # costs of all futures in RUB 1645 "Currencies": [], # list of dictionaries of all currencies statistics 1646 "Shares": [], # list of dictionaries of all shares statistics 1647 "Bonds": [], # list of dictionaries of all bonds statistics 1648 "Etfs": [], # list of dictionaries of all etfs statistics 1649 "Futures": [], # list of dictionaries of all futures statistics 1650 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1651 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1652 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1653 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1654 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1655 }, 1656 "analytics": { # --- some analytics of portfolio: 1657 "distrByAssets": {}, # portfolio distribution by assets 1658 "distrByCompanies": {}, # portfolio distribution by companies 1659 "distrBySectors": {}, # portfolio distribution by sectors 1660 "distrByCurrencies": {}, # portfolio distribution by currencies 1661 "distrByCountries": {}, # portfolio distribution by countries 1662 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1663 } 1664 } 1665 1666 details = details.lower() 1667 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1668 if details not in availableDetails: 1669 details = "full" 1670 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1671 1672 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1673 1674 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1675 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1676 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1677 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1678 1679 # save response headers without "positions" section: 1680 for key in portfolioResponse.keys(): 1681 if key != "positions": 1682 view["raw"]["headers"][key] = portfolioResponse[key] 1683 1684 else: 1685 continue 1686 1687 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1688 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1689 for item in portfolioResponse["positions"]: 1690 if item["instrumentType"] == "currency": 1691 self._figi = item["figi"] 1692 curr = self.SearchByFIGI(requestPrice=False) 1693 1694 # current price of currency in RUB: 1695 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1696 "name": curr["name"], 1697 "currentPrice": NanoToFloat( 1698 item["currentPrice"]["units"], 1699 item["currentPrice"]["nano"] 1700 ), 1701 } 1702 1703 view["raw"]["Currencies"].append(item) 1704 1705 elif item["instrumentType"] == "share": 1706 view["raw"]["Shares"].append(item) 1707 1708 elif item["instrumentType"] == "bond": 1709 view["raw"]["Bonds"].append(item) 1710 1711 elif item["instrumentType"] == "etf": 1712 view["raw"]["Etfs"].append(item) 1713 1714 elif item["instrumentType"] == "futures": 1715 view["raw"]["Futures"].append(item) 1716 1717 else: 1718 continue 1719 1720 # how many volume of currencies (by ISO currency name) are blocked: 1721 for item in view["raw"]["positions"]["blocked"]: 1722 blocked = NanoToFloat(item["units"], item["nano"]) 1723 if blocked > 0: 1724 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1725 1726 # how many volume of instruments (by FIGI) are blocked: 1727 for item in view["raw"]["positions"]["securities"]: 1728 blocked = int(item["blocked"]) 1729 if blocked > 0: 1730 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1731 1732 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1733 1734 if "rub" in allBlocked.keys(): 1735 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1736 1737 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1738 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1739 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1740 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1741 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1742 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1743 view["stat"]["portfolioCostRUB"] = sum([ 1744 view["stat"]["allCurrenciesCostRUB"], 1745 view["stat"]["sharesCostRUB"], 1746 view["stat"]["bondsCostRUB"], 1747 view["stat"]["etfsCostRUB"], 1748 view["stat"]["futuresCostRUB"], 1749 ]) 1750 1751 # --- calculating some portfolio statistics: 1752 byComp = {} # distribution by companies 1753 bySect = {} # distribution by sectors 1754 byCurr = {} # distribution by currencies (include RUB) 1755 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1756 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1757 1758 for item in portfolioResponse["positions"]: 1759 self._figi = item["figi"] 1760 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1761 1762 if instrument: 1763 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1764 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1765 1766 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1767 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1768 1769 else: 1770 blocked = 0 1771 1772 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1773 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1774 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1775 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1776 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1777 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1778 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1779 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1780 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1781 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1782 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1783 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1784 1785 statData = { 1786 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1787 "ticker": instrument["ticker"], # ticker by FIGI 1788 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1789 "volume": volume, # available volume of instrument 1790 "lots": lots, # volume in lots of instrument 1791 "direction": direction, # direction of an instrument's position: short or long 1792 "blocked": blocked, # blocked volume of currency or instrument 1793 "currentPrice": curPrice, # current instrument's price in basic asset 1794 "average": average, # current average position price 1795 "cost": cost, # current cost of all volume of instrument in basic asset 1796 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1797 "costRUB": costRUB, # cost of instrument in ruble 1798 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1799 "profit": profit, # expected profit at current moment 1800 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1801 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1802 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1803 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1804 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1805 "step": instrument["step"], # minimum price increment 1806 } 1807 1808 # adding distribution by unique countries: 1809 if statData["country"] not in byCountry.keys(): 1810 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1811 1812 else: 1813 byCountry[statData["country"]]["cost"] += costRUB 1814 byCountry[statData["country"]]["percent"] += percentCostRUB 1815 1816 if item["instrumentType"] != "currency": 1817 # adding distribution by unique companies: 1818 if statData["name"]: 1819 if statData["name"] not in byComp.keys(): 1820 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1821 1822 else: 1823 byComp[statData["name"]]["cost"] += costRUB 1824 byComp[statData["name"]]["percent"] += percentCostRUB 1825 1826 # adding distribution by unique sectors: 1827 if statData["sector"] not in bySect.keys(): 1828 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1829 1830 else: 1831 bySect[statData["sector"]]["cost"] += costRUB 1832 bySect[statData["sector"]]["percent"] += percentCostRUB 1833 1834 # adding distribution by unique currencies: 1835 if currency not in byCurr.keys(): 1836 byCurr[currency] = { 1837 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1838 "cost": costRUB, 1839 "percent": percentCostRUB 1840 } 1841 1842 else: 1843 byCurr[currency]["cost"] += costRUB 1844 byCurr[currency]["percent"] += percentCostRUB 1845 1846 # saving statistics for every instrument: 1847 if item["instrumentType"] == "currency": 1848 view["stat"]["Currencies"].append(statData) 1849 1850 # update dict with free funds for trading (total - blocked) by currencies 1851 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1852 view["stat"]["funds"][currency] = { 1853 "total": volume, 1854 "totalCostRUB": costRUB, # total volume cost in rubles 1855 "free": volume - blocked, 1856 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1857 } 1858 1859 elif item["instrumentType"] == "share": 1860 view["stat"]["Shares"].append(statData) 1861 1862 elif item["instrumentType"] == "bond": 1863 view["stat"]["Bonds"].append(statData) 1864 1865 elif item["instrumentType"] == "etf": 1866 view["stat"]["Etfs"].append(statData) 1867 1868 elif item["instrumentType"] == "Futures": 1869 view["stat"]["Futures"].append(statData) 1870 1871 else: 1872 continue 1873 1874 # total changes in Russian Ruble: 1875 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1876 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1877 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1878 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1879 view["stat"]["funds"]["rub"] = { 1880 "total": view["stat"]["availableRUB"], 1881 "totalCostRUB": view["stat"]["availableRUB"], 1882 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1883 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1884 } 1885 1886 # --- pending limit orders sector data: 1887 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1888 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1889 1890 for item in view["raw"]["orders"]: 1891 self._figi = item["figi"] 1892 1893 if item["figi"] not in uniquePendingOrdersFIGIs: 1894 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1895 1896 uniquePendingOrdersFIGIs.append(item["figi"]) 1897 uniquePendingOrders[item["figi"]] = instrument 1898 1899 else: 1900 instrument = uniquePendingOrders[item["figi"]] 1901 1902 if instrument: 1903 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1904 orderType = TKS_ORDER_TYPES[item["orderType"]] 1905 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1906 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1907 1908 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1909 if item["direction"] == "ORDER_DIRECTION_BUY": 1910 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1911 1912 else: 1913 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1914 1915 # requested price for order execution: 1916 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1917 1918 # necessary changes in percent to reach target from current price: 1919 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1920 1921 view["stat"]["orders"].append({ 1922 "orderID": item["orderId"], # orderId number parameter of current order 1923 "figi": item["figi"], # FIGI identification 1924 "ticker": instrument["ticker"], # ticker name by FIGI 1925 "lotsRequested": item["lotsRequested"], # requested lots value 1926 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1927 "currentPrice": lastPrice, # current instrument's price for defined action 1928 "targetPrice": target, # requested price for order execution in base currency 1929 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1930 "percentChanges": changes, # changes in percent to target from current price 1931 "currency": item["currency"], # instrument's currency name 1932 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1933 "type": orderType, # type of order from TKS_ORDER_TYPES 1934 "status": orderState, # order status from TKS_ORDER_STATES 1935 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1936 }) 1937 1938 # --- stop orders sector data: 1939 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1940 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1941 1942 for item in view["raw"]["stopOrders"]: 1943 self._figi = item["figi"] 1944 1945 if item["figi"] not in uniqueStopOrdersFIGIs: 1946 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1947 1948 uniqueStopOrdersFIGIs.append(item["figi"]) 1949 uniqueStopOrders[item["figi"]] = instrument 1950 1951 else: 1952 instrument = uniqueStopOrders[item["figi"]] 1953 1954 if instrument: 1955 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1956 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1957 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1958 1959 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1960 if "expirationTime" in item.keys(): 1961 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1962 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1963 1964 else: 1965 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1966 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1967 1968 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1969 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1970 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1971 1972 else: 1973 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1974 1975 # requested price when stop-order executed: 1976 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1977 1978 # price for limit-order, set up when stop-order executed: 1979 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1980 1981 # necessary changes in percent to reach target from current price: 1982 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1983 1984 view["stat"]["stopOrders"].append({ 1985 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1986 "figi": item["figi"], # FIGI identification 1987 "ticker": instrument["ticker"], # ticker name by FIGI 1988 "lotsRequested": item["lotsRequested"], # requested lots value 1989 "currentPrice": lastPrice, # current instrument's price for defined action 1990 "targetPrice": target, # requested price for stop-order execution in base currency 1991 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1992 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1993 "percentChanges": changes, # changes in percent to target from current price 1994 "currency": item["currency"], # instrument's currency name 1995 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1996 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1997 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1998 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1999 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2000 }) 2001 2002 # --- calculating data for analytics section: 2003 # portfolio distribution by assets: 2004 view["analytics"]["distrByAssets"] = { 2005 "Ruble": { 2006 "uniques": 1, 2007 "cost": view["stat"]["availableRUB"], 2008 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2009 }, 2010 "Currencies": { 2011 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2012 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2013 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2014 }, 2015 "Shares": { 2016 "uniques": len(view["stat"]["Shares"]), 2017 "cost": view["stat"]["sharesCostRUB"], 2018 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2019 }, 2020 "Bonds": { 2021 "uniques": len(view["stat"]["Bonds"]), 2022 "cost": view["stat"]["bondsCostRUB"], 2023 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2024 }, 2025 "Etfs": { 2026 "uniques": len(view["stat"]["Etfs"]), 2027 "cost": view["stat"]["etfsCostRUB"], 2028 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2029 }, 2030 "Futures": { 2031 "uniques": len(view["stat"]["Futures"]), 2032 "cost": view["stat"]["futuresCostRUB"], 2033 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2034 }, 2035 } 2036 2037 # portfolio distribution by companies: 2038 view["analytics"]["distrByCompanies"]["All money cash"] = { 2039 "ticker": "", 2040 "cost": view["stat"]["allCurrenciesCostRUB"], 2041 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2042 } 2043 view["analytics"]["distrByCompanies"].update(byComp) 2044 2045 # portfolio distribution by sectors: 2046 view["analytics"]["distrBySectors"]["All money cash"] = { 2047 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2048 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2049 } 2050 view["analytics"]["distrBySectors"].update(bySect) 2051 2052 # portfolio distribution by currencies: 2053 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2054 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2055 2056 if self.moreDebug: 2057 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2058 2059 view["analytics"]["distrByCurrencies"].update(byCurr) 2060 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2061 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2062 2063 # portfolio distribution by countries: 2064 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2065 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2066 2067 if self.moreDebug: 2068 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2069 2070 view["analytics"]["distrByCountries"].update(byCountry) 2071 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2072 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2073 2074 # --- Prepare text statistics overview in human-readable: 2075 if show: 2076 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2077 2078 # Whatever the value `details`, header not changes: 2079 info = [ 2080 "# Client's portfolio\n\n", 2081 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2082 "* **Account ID:** [{}]\n".format(self.accountId), 2083 ] 2084 2085 if details in ["full", "positions", "digest"]: 2086 info.extend([ 2087 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2088 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2089 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2090 view["stat"]["totalChangesRUB"], 2091 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2092 view["stat"]["totalChangesPercentRUB"], 2093 ), 2094 ]) 2095 2096 if details in ["full", "positions"]: 2097 info.extend([ 2098 "## Open positions\n\n", 2099 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2100 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2101 "| Ruble | {:>31} | | | | | |\n".format( 2102 "{:.2f} ({:.2f}) rub".format( 2103 view["stat"]["availableRUB"], 2104 view["stat"]["blockedRUB"], 2105 ) 2106 ) 2107 ]) 2108 2109 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2110 return [ 2111 "| | | | | | | |\n", 2112 "| {:<27} | | | | | {:>19} | |\n".format( 2113 noTradeStr if noTradeStr else typeStr, 2114 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2115 ), 2116 ] 2117 2118 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2119 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2120 "{} [{}]".format(data["ticker"], data["figi"]), 2121 "{:.2f} ({:.2f}) {}".format( 2122 data["volume"], 2123 data["blocked"], 2124 data["currency"], 2125 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2126 data["volume"], 2127 data["blocked"], 2128 ), 2129 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2130 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2131 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2132 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2133 "{}{:.2f} {} ({}{:.2f}%)".format( 2134 "+" if data["profit"] > 0 else "", 2135 data["profit"], data["baseCurrencyName"], 2136 "+" if data["percentProfit"] > 0 else "", 2137 data["percentProfit"], 2138 ), 2139 ) 2140 2141 # --- Show currencies section: 2142 if view["stat"]["Currencies"]: 2143 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2144 for item in view["stat"]["Currencies"]: 2145 info.append(_InfoStr(item, showCurrencyName=True)) 2146 2147 else: 2148 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2149 2150 # --- Show shares section: 2151 if view["stat"]["Shares"]: 2152 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2153 2154 for item in view["stat"]["Shares"]: 2155 info.append(_InfoStr(item)) 2156 2157 else: 2158 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2159 2160 # --- Show bonds section: 2161 if view["stat"]["Bonds"]: 2162 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2163 2164 for item in view["stat"]["Bonds"]: 2165 info.append(_InfoStr(item)) 2166 2167 else: 2168 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2169 2170 # --- Show etfs section: 2171 if view["stat"]["Etfs"]: 2172 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2173 2174 for item in view["stat"]["Etfs"]: 2175 info.append(_InfoStr(item)) 2176 2177 else: 2178 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2179 2180 # --- Show futures section: 2181 if view["stat"]["Futures"]: 2182 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2183 2184 for item in view["stat"]["Futures"]: 2185 info.append(_InfoStr(item)) 2186 2187 else: 2188 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2189 2190 if details in ["full", "orders"]: 2191 # --- Show pending limit orders section: 2192 if view["stat"]["orders"]: 2193 info.extend([ 2194 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2195 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2196 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2197 ]) 2198 2199 for item in view["stat"]["orders"]: 2200 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2201 "{} [{}]".format(item["ticker"], item["figi"]), 2202 item["orderID"], 2203 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2204 "{} {} ({}{:.2f}%)".format( 2205 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2206 item["baseCurrencyName"], 2207 "+" if item["percentChanges"] > 0 else "", 2208 float(item["percentChanges"]), 2209 ), 2210 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2211 item["action"], 2212 item["type"], 2213 item["date"], 2214 )) 2215 2216 else: 2217 info.append("\n## Total pending limit-orders: [0]\n") 2218 2219 # --- Show stop orders section: 2220 if view["stat"]["stopOrders"]: 2221 info.extend([ 2222 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2223 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2224 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2225 ]) 2226 2227 for item in view["stat"]["stopOrders"]: 2228 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2229 "{} [{}]".format(item["ticker"], item["figi"]), 2230 item["orderID"], 2231 item["lotsRequested"], 2232 "{} {} ({}{:.2f}%)".format( 2233 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2234 item["baseCurrencyName"], 2235 "+" if item["percentChanges"] > 0 else "", 2236 float(item["percentChanges"]), 2237 ), 2238 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2239 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2240 item["action"], 2241 item["type"], 2242 item["expType"], 2243 item["createDate"], 2244 item["expDate"], 2245 )) 2246 2247 else: 2248 info.append("\n## Total stop-orders: [0]\n") 2249 2250 if details in ["full", "analytics"]: 2251 # -- Show analytics section: 2252 if view["stat"]["portfolioCostRUB"] > 0: 2253 info.extend([ 2254 "\n# Analytics\n\n" 2255 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2256 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2257 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2258 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2259 view["stat"]["totalChangesRUB"], 2260 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2261 view["stat"]["totalChangesPercentRUB"], 2262 ), 2263 "\n## Portfolio distribution by assets\n" 2264 "\n| Type | Uniques | Percent | Current cost |\n", 2265 "|------------------------------------|---------|---------|--------------------|\n", 2266 ]) 2267 2268 for key in view["analytics"]["distrByAssets"].keys(): 2269 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2270 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2271 key, 2272 view["analytics"]["distrByAssets"][key]["uniques"], 2273 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2274 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2275 )) 2276 2277 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2278 2279 info.extend([ 2280 "\n## Portfolio distribution by companies\n" 2281 "\n| Company | Percent | Current cost |\n", 2282 aSepLine, 2283 ]) 2284 2285 for company in view["analytics"]["distrByCompanies"].keys(): 2286 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2287 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2288 "{}{}".format( 2289 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2290 company, 2291 ), 2292 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2293 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2294 )) 2295 2296 info.extend([ 2297 "\n## Portfolio distribution by sectors\n" 2298 "\n| Sector | Percent | Current cost |\n", 2299 aSepLine, 2300 ]) 2301 2302 for sector in view["analytics"]["distrBySectors"].keys(): 2303 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2304 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2305 sector, 2306 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2307 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2308 )) 2309 2310 info.extend([ 2311 "\n## Portfolio distribution by currencies\n" 2312 "\n| Instruments currencies | Percent | Current cost |\n", 2313 aSepLine, 2314 ]) 2315 2316 for curr in view["analytics"]["distrByCurrencies"].keys(): 2317 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2318 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2319 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2320 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2321 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2322 )) 2323 2324 info.extend([ 2325 "\n## Portfolio distribution by countries\n" 2326 "\n| Assets by country | Percent | Current cost |\n", 2327 aSepLine, 2328 ]) 2329 2330 for country in view["analytics"]["distrByCountries"].keys(): 2331 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2332 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2333 country, 2334 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2335 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2336 )) 2337 2338 if details in ["full", "calendar"]: 2339 # -- Show bonds payment calendar section: 2340 if view["stat"]["Bonds"]: 2341 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2342 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2343 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2344 2345 else: 2346 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2347 2348 infoText = "".join(info) 2349 2350 uLogger.info(infoText) 2351 2352 if details == "full" and self.overviewFile: 2353 filename = self.overviewFile 2354 2355 elif details == "digest" and self.overviewDigestFile: 2356 filename = self.overviewDigestFile 2357 2358 elif details == "positions" and self.overviewPositionsFile: 2359 filename = self.overviewPositionsFile 2360 2361 elif details == "orders" and self.overviewOrdersFile: 2362 filename = self.overviewOrdersFile 2363 2364 elif details == "analytics" and self.overviewAnalyticsFile: 2365 filename = self.overviewAnalyticsFile 2366 2367 elif details == "calendar" and self.overviewBondsCalendarFile: 2368 filename = self.overviewBondsCalendarFile 2369 2370 else: 2371 filename = "" 2372 2373 if filename: 2374 with open(filename, "w", encoding="UTF-8") as fH: 2375 fH.write(infoText) 2376 2377 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2378 2379 if self.useHTMLReports: 2380 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2381 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2382 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="", commonCSS=COMMON_CSS, markdown=infoText)) 2383 2384 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2385 2386 return view
Get portfolio: all open positions, orders and some statistics for current accountId
.
If overviewFile
, overviewDigestFile
, overviewPositionsFile
, overviewOrdersFile
, overviewAnalyticsFile
and overviewBondsCalendarFile
are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
False
then only dictionary returns, ifTrue
then show more debug information. - details: how detailed should the information be?
full
— shows full available information about portfolio status (by default),positions
— shows only open positions,orders
— shows only sections of open limits and stop orders.digest
— show a short digest of the portfolio status,analytics
— shows only the analytics section and the distribution of the portfolio by various categories,calendar
— shows only the bonds calendar section (if these present in portfolio),
Returns
dictionary with client's raw portfolio and some statistics.
2388 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2389 """ 2390 Returns history operations between two given dates for current `accountId`. 2391 If `reportFile` string is not empty then also save human-readable report. 2392 Shows some statistical data of closed positions. 2393 2394 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2395 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2396 :param show: if `True` then also prints all records to the console. 2397 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2398 :return: original list of dictionaries with history of deals records from API ("operations" key): 2399 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2400 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2401 """ 2402 if self.accountId is None or not self.accountId: 2403 uLogger.error("Variable `accountId` must be defined for using this method!") 2404 raise Exception("Account ID required") 2405 2406 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2407 2408 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2409 2410 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2411 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2412 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2413 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2414 customStat = {} # custom statistics in additional to responseJSON 2415 2416 # --- output report in human-readable format: 2417 if show or self.reportFile: 2418 splitLine1 = "| | | | | |\n" # Summary section 2419 splitLine2 = "| | | | | | | | |\n" # Operations section 2420 nextDay = "" 2421 2422 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2423 2424 if len(ops) > 0: 2425 customStat = { 2426 "opsCount": 0, # total operations count 2427 "buyCount": 0, # buy operations 2428 "sellCount": 0, # sell operations 2429 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2430 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2431 "payIn": {"rub": 0.}, # Deposit brokerage account 2432 "payOut": {"rub": 0.}, # Withdrawals 2433 "divs": {"rub": 0.}, # Dividends income 2434 "coupons": {"rub": 0.}, # Coupon's income 2435 "brokerCom": {"rub": 0.}, # Service commissions 2436 "serviceCom": {"rub": 0.}, # Service commissions 2437 "marginCom": {"rub": 0.}, # Margin commissions 2438 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2439 } 2440 2441 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2442 for item in ops: 2443 if item["state"] == "OPERATION_STATE_EXECUTED": 2444 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2445 2446 # count buy operations: 2447 if "_BUY" in item["operationType"]: 2448 customStat["buyCount"] += 1 2449 2450 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2451 customStat["buyTotal"][item["payment"]["currency"]] += payment 2452 2453 else: 2454 customStat["buyTotal"][item["payment"]["currency"]] = payment 2455 2456 # count sell operations: 2457 elif "_SELL" in item["operationType"]: 2458 customStat["sellCount"] += 1 2459 2460 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2461 customStat["sellTotal"][item["payment"]["currency"]] += payment 2462 2463 else: 2464 customStat["sellTotal"][item["payment"]["currency"]] = payment 2465 2466 # count incoming operations: 2467 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2468 if item["payment"]["currency"] in customStat["payIn"].keys(): 2469 customStat["payIn"][item["payment"]["currency"]] += payment 2470 2471 else: 2472 customStat["payIn"][item["payment"]["currency"]] = payment 2473 2474 # count withdrawals operations: 2475 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2476 if item["payment"]["currency"] in customStat["payOut"].keys(): 2477 customStat["payOut"][item["payment"]["currency"]] += payment 2478 2479 else: 2480 customStat["payOut"][item["payment"]["currency"]] = payment 2481 2482 # count dividends income: 2483 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2484 if item["payment"]["currency"] in customStat["divs"].keys(): 2485 customStat["divs"][item["payment"]["currency"]] += payment 2486 2487 else: 2488 customStat["divs"][item["payment"]["currency"]] = payment 2489 2490 # count coupon's income: 2491 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2492 if item["payment"]["currency"] in customStat["coupons"].keys(): 2493 customStat["coupons"][item["payment"]["currency"]] += payment 2494 2495 else: 2496 customStat["coupons"][item["payment"]["currency"]] = payment 2497 2498 # count broker commissions: 2499 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2500 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2501 customStat["brokerCom"][item["payment"]["currency"]] += payment 2502 2503 else: 2504 customStat["brokerCom"][item["payment"]["currency"]] = payment 2505 2506 # count service commissions: 2507 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2508 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2509 customStat["serviceCom"][item["payment"]["currency"]] += payment 2510 2511 else: 2512 customStat["serviceCom"][item["payment"]["currency"]] = payment 2513 2514 # count margin commissions: 2515 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2516 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2517 customStat["marginCom"][item["payment"]["currency"]] += payment 2518 2519 else: 2520 customStat["marginCom"][item["payment"]["currency"]] = payment 2521 2522 # count withholding taxes: 2523 elif "_TAX" in item["operationType"]: 2524 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2525 customStat["allTaxes"][item["payment"]["currency"]] += payment 2526 2527 else: 2528 customStat["allTaxes"][item["payment"]["currency"]] = payment 2529 2530 else: 2531 continue 2532 2533 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2534 2535 # --- view "Actions" lines: 2536 info.extend([ 2537 "| Report sections | | | | |\n", 2538 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2539 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2540 "| | Buy: {:<22} | {:<28} | | |\n".format( 2541 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2542 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2543 ), 2544 "| | Sell: {:<21} | {:<28} | | |\n".format( 2545 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2546 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2547 ), 2548 ]) 2549 2550 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2551 for key in opsKeys: 2552 if key == "rub": 2553 continue 2554 2555 info.extend([ 2556 "| | | {:<28} | | |\n".format( 2557 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2558 ), 2559 "| | | {:<28} | | |\n".format( 2560 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2561 ), 2562 ]) 2563 2564 info.append(splitLine1) 2565 2566 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2567 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2568 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2569 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2570 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2571 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2572 ) 2573 2574 # --- view "Payments" lines: 2575 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2576 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2577 2578 for key in paymentsKeys: 2579 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2580 2581 info.append(splitLine1) 2582 2583 # --- view "Commissions and taxes" lines: 2584 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2585 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2586 2587 for key in comKeys: 2588 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2589 2590 info.extend([ 2591 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2592 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2593 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2594 ]) 2595 2596 else: 2597 info.append("Broker returned no operations during this period\n") 2598 2599 # --- view "Operations" section: 2600 for item in ops: 2601 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2602 continue 2603 2604 else: 2605 self._figi = item["figi"] if item["figi"] else "" 2606 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2607 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2608 2609 # group of deals during one day: 2610 if nextDay and item["date"].split("T")[0] != nextDay: 2611 info.append(splitLine2) 2612 nextDay = "" 2613 2614 else: 2615 nextDay = item["date"].split("T")[0] # saving current day for splitting 2616 2617 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2618 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2619 self._figi if self._figi else "—", 2620 instrument["ticker"] if instrument else "—", 2621 instrument["type"] if instrument else "—", 2622 item["quantity"] if int(item["quantity"]) > 0 else "—", 2623 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2624 TKS_OPERATION_STATES[item["state"]], 2625 TKS_OPERATION_TYPES[item["operationType"]], 2626 )) 2627 2628 infoText = "".join(info) 2629 2630 if show: 2631 if self.moreDebug: 2632 uLogger.debug("Records about history of a client's operations successfully received") 2633 2634 uLogger.info(infoText) 2635 2636 if self.reportFile: 2637 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2638 fH.write(infoText) 2639 2640 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2641 2642 if self.useHTMLReports: 2643 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2644 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2645 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2646 2647 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2648 2649 return ops, customStat
Returns history operations between two given dates for current accountId
.
If reportFile
string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()
method. - end: see docstring in
TradeRoutines.GetDatesAsString()
method. - show: if
True
then also prints all records to the console. - showCancelled: if
False
then remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2651 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2652 """ 2653 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2654 2655 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2656 Warning! Broker server used ISO UTC time by default. 2657 2658 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2659 Also, `historyFile` used to update history with `onlyMissing` parameter. 2660 2661 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2662 2663 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2664 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2665 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2666 `"hour"`, `"day"`. Default: `"hour"`. 2667 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2668 False by default. Warning! History appends only from last candle to current time 2669 with always update last candle! 2670 :param csvSep: separator if csv-file is used, `,` by default. 2671 :param show: if `True` then also prints Pandas DataFrame to the console. 2672 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2673 `["date", "time", "open", "high", "low", "close", "volume"]`. 2674 """ 2675 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2676 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2677 history = None # empty pandas object for history 2678 2679 if interval not in TKS_CANDLE_INTERVALS.keys(): 2680 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2681 raise Exception("Incorrect value") 2682 2683 if not (self._ticker or self._figi): 2684 uLogger.error("Ticker or FIGI must be defined!") 2685 raise Exception("Ticker or FIGI required") 2686 2687 if self._ticker and not self._figi: 2688 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2689 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2690 2691 if self._figi and not self._ticker: 2692 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2693 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2694 2695 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2696 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2697 if interval.lower() != "day": 2698 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2699 2700 delta = dtEnd - dtStart # current UTC time minus last time in file 2701 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2702 2703 # calculate history length in candles: 2704 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2705 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2706 length += 1 # to avoid fraction time 2707 2708 # calculate data blocks count: 2709 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2710 2711 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2712 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2713 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2714 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2715 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2716 2717 tempOld = None # pandas object for old history, if --only-missing key present 2718 lastTime = None # datetime object of last old candle in file 2719 2720 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2721 uLogger.debug("--only-missing key present, add only last missing candles...") 2722 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2723 2724 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2725 2726 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2727 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2728 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2729 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2730 2731 # get last datetime object from last string in file or minus 1 delta if file is empty: 2732 if len(tempOld) > 0: 2733 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2734 2735 else: 2736 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2737 2738 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2739 2740 responseJSONs = [] # raw history blocks of data 2741 2742 blockEnd = dtEnd 2743 for item in range(blocks): 2744 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2745 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2746 2747 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2748 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2749 )) 2750 2751 if blockStart == blockEnd: 2752 uLogger.debug("Skipped this zero-length block...") 2753 2754 else: 2755 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2756 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2757 self.body = str({ 2758 "figi": self._figi, 2759 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2760 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2761 "interval": TKS_CANDLE_INTERVALS[interval][0] 2762 }) 2763 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2764 2765 if "code" in responseJSON.keys(): 2766 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2767 2768 else: 2769 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2770 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2771 2772 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2773 2774 blockEnd = blockStart 2775 2776 printCount = len(responseJSONs) # candles to show in console 2777 if responseJSONs: 2778 tempHistory = pd.DataFrame( 2779 data={ 2780 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2781 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2782 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2783 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2784 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2785 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2786 "volume": [int(item["volume"]) for item in responseJSONs], 2787 }, 2788 index=range(len(responseJSONs)), 2789 columns=["date", "time", "open", "high", "low", "close", "volume"], 2790 ) 2791 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2792 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2793 2794 # append only newest candles to old history if --only-missing key present: 2795 if onlyMissing and tempOld is not None and lastTime is not None: 2796 index = 0 # find start index in tempHistory data: 2797 2798 for i, item in tempHistory.iterrows(): 2799 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2800 2801 if curTime == lastTime: 2802 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2803 index = i 2804 printCount = index + 1 2805 break 2806 2807 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2808 2809 else: 2810 history = tempHistory # if no `--only-missing` key then load full data from server 2811 2812 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2813 2814 if history is not None and not history.empty: 2815 if show: 2816 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2817 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2818 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2819 )) 2820 2821 else: 2822 uLogger.warning("Received an empty candles history!") 2823 2824 if self.historyFile is not None: 2825 if history is not None and not history.empty: 2826 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2827 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2828 2829 else: 2830 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2831 2832 else: 2833 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2834 2835 return history
This method returns last history candles of the current instrument defined by ticker
or figi
(FIGI id).
History returned between two given dates: start
and end
. Minimum requested date in the past is 1970-01-01
.
Warning! Broker server used ISO UTC time by default.
If historyFile
is not None
then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile
used to update history with onlyMissing
parameter.
See also: LoadHistory()
and ShowHistoryChart()
methods.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()
method. - end: see docstring in
TradeRoutines.GetDatesAsString()
method. - interval: this is a candle interval. Current available values are
"1min"
,"5min"
,"15min"
,"hour"
,"day"
. Default:"hour"
. - onlyMissing: if
True
then add only last missing candles, do not request all history length fromstart
. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,
by default. - show: if
True
then also prints Pandas DataFrame to the console.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"]
.
2837 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2838 """ 2839 Load candles history from csv-file and return Pandas DataFrame object. 2840 2841 See also: `History()` and `ShowHistoryChart()` methods. 2842 2843 :param filePath: path to csv-file to open. 2844 """ 2845 loadedHistory = None # init candles data object 2846 2847 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2848 2849 if os.path.exists(filePath): 2850 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2851 2852 tfStr = self.priceModel.FormattedDelta( 2853 self.priceModel.timeframe, 2854 "{days} days {hours}h {minutes}m {seconds}s", 2855 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2856 self.priceModel.timeframe, 2857 "{hours}h {minutes}m {seconds}s", 2858 ) 2859 2860 if loadedHistory is not None and not loadedHistory.empty: 2861 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2862 len(loadedHistory), 2863 tfStr, 2864 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2865 ) 2866 2867 else: 2868 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2869 2870 else: 2871 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2872 2873 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History()
and ShowHistoryChart()
methods.
Parameters
- filePath: path to csv-file to open.
2875 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2876 """ 2877 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2878 2879 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2880 Default: `index.html` (both for interact and non-interact candlesticks chart). 2881 2882 See also: `History()` and `LoadHistory()` methods. 2883 2884 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2885 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2886 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2887 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2888 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2889 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2890 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2891 """ 2892 if isinstance(candles, str): 2893 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2894 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2895 2896 elif isinstance(candles, pd.DataFrame): 2897 self.priceModel.prices = candles # set candles chain from variable 2898 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2899 2900 if "datetime" not in candles.columns: 2901 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2902 2903 else: 2904 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2905 raise Exception("Incorrect value") 2906 2907 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2908 2909 if interact: 2910 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2911 2912 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2913 2914 else: 2915 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2916 2917 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2918 2919 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile
can be use as html-file name to save interaction or non-interaction chart.
Default: index.html
(both for interact and non-interact candlesticks chart).
See also: History()
and LoadHistory()
methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions denied
to html-file.
2921 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2922 """ 2923 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2924 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2925 2926 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2927 2928 :param operation: string "Buy" or "Sell". 2929 :param lots: volume, integer count of lots >= 1. 2930 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2931 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2932 :param expDate: string "Undefined" by default or local date in future, 2933 it is a string with format `%Y-%m-%d %H:%M:%S`. 2934 :return: JSON with response from broker server. 2935 """ 2936 if self.accountId is None or not self.accountId: 2937 uLogger.error("Variable `accountId` must be defined for using this method!") 2938 raise Exception("Account ID required") 2939 2940 if operation is None or not operation or operation not in ("Buy", "Sell"): 2941 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2942 raise Exception("Incorrect value") 2943 2944 if lots is None or lots < 1: 2945 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2946 lots = 1 2947 2948 if tp is None or tp < 0: 2949 tp = 0 2950 2951 if sl is None or sl < 0: 2952 sl = 0 2953 2954 if expDate is None or not expDate: 2955 expDate = "Undefined" 2956 2957 if not (self._ticker or self._figi): 2958 uLogger.error("Ticker or FIGI must be defined!") 2959 raise Exception("Ticker or FIGI required") 2960 2961 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2962 self._ticker = instrument["ticker"] 2963 self._figi = instrument["figi"] 2964 2965 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 2966 2967 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2968 self.body = str({ 2969 "figi": self._figi, 2970 "quantity": str(lots), 2971 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2972 "accountId": str(self.accountId), 2973 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2974 }) 2975 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2976 2977 if "orderId" in response.keys(): 2978 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2979 operation, response["orderId"], 2980 self._ticker, self._figi, lots, 2981 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2982 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2983 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2984 )) 2985 2986 if tp > 0: 2987 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2988 2989 if sl > 0: 2990 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2991 2992 else: 2993 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 2994 2995 return response
Universal method to create market order and make deal at the current price for current accountId
. Returns JSON data with response.
If tp
or sl
> 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType
parameter.
See also: Order()
docstring. More simple methods than Trade()
are Buy()
and Sell()
.
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPrice
inself.Order()
. - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPrice
inself.Order()
. - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S
.
Returns
JSON with response from broker server.
2997 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2998 """ 2999 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3000 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3001 3002 See also: `Order()` and `Trade()` docstrings. 3003 3004 :param lots: volume, integer count of lots >= 1. 3005 :param tp: float > 0, take profit price of stop-order. 3006 :param sl: float > 0, stop loss price of stop-order. 3007 :param expDate: it's a local date in future. 3008 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3009 :return: JSON with response from broker server. 3010 """ 3011 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade()
. Create Buy
market order and make deal at the current price. Returns JSON data with response.
If tp
or sl
> 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType
parameter.
See also: Order()
and Trade()
docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S
.
Returns
JSON with response from broker server.
3013 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3014 """ 3015 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3016 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3017 3018 See also: `Order()` and `Trade()` docstrings. 3019 3020 :param lots: volume, integer count of lots >= 1. 3021 :param tp: float > 0, take profit price of stop-order. 3022 :param sl: float > 0, stop loss price of stop-order. 3023 :param expDate: it's a local date in the future. 3024 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3025 :return: JSON with response from broker server. 3026 """ 3027 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade()
. Create Sell
market order and make deal at the current price. Returns JSON data with response.
If tp
or sl
> 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType
parameter.
See also: Order()
and Trade()
docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S
.
Returns
JSON with response from broker server.
3029 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3030 """ 3031 Close position of given instruments. 3032 3033 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3034 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3035 This avoids unnecessary downloading data from the server. 3036 """ 3037 if instruments is None or not instruments: 3038 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3039 raise Exception("Ticker or FIGI required") 3040 3041 if isinstance(instruments, str): 3042 instruments = [instruments] 3043 3044 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3045 if uniqueInstruments: 3046 if portfolio is None or not portfolio: 3047 portfolio = self.Overview(show=False) 3048 3049 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3050 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3051 3052 for self._figi in uniqueInstruments: 3053 if self._figi not in allOpened: 3054 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3055 continue 3056 3057 # search open trade info about instrument by ticker: 3058 instrument = {} 3059 for iType in TKS_INSTRUMENTS: 3060 if instrument: 3061 break 3062 3063 for item in portfolio["stat"][iType]: 3064 if item["figi"] == self._figi: 3065 instrument = item 3066 break 3067 3068 if instrument: 3069 self._ticker = instrument["ticker"] 3070 self._figi = instrument["figi"] 3071 3072 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3073 self._ticker, 3074 self._figi, 3075 int(instrument["volume"]), 3076 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3077 )) 3078 3079 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3080 3081 if tradeLots > 0: 3082 if instrument["blocked"] > 0: 3083 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3084 instrument["blocked"], 3085 self._ticker, 3086 tradeLots, 3087 )) 3088 3089 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3090 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3091 3092 else: 3093 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()
method. This avoids unnecessary downloading data from the server.
3095 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3096 """ 3097 Close all positions of given instruments with defined type. 3098 3099 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3100 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3101 This avoids unnecessary downloading data from the server. 3102 """ 3103 if iType not in TKS_INSTRUMENTS: 3104 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3105 3106 else: 3107 if portfolio is None or not portfolio: 3108 portfolio = self.Overview(show=False) 3109 3110 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3111 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3112 3113 if tickers and portfolio: 3114 self.CloseTrades(tickers, portfolio) 3115 3116 else: 3117 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()
method. This avoids unnecessary downloading data from the server.
3119 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3120 """ 3121 Universal method to create market or limit orders with all available parameters for current `accountId`. 3122 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3123 3124 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3125 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3126 3127 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3128 then broker immediately open market order as you can do simple --buy or --sell operations! 3129 3130 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3131 When current price will go up or down to target price value then broker opens a limit order. 3132 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3133 3134 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3135 3136 :param operation: string "Buy" or "Sell". 3137 :param orderType: string "Limit" or "Stop". 3138 :param lots: volume, integer count of lots >= 1. 3139 :param targetPrice: target price > 0. This is open trade price for limit order. 3140 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3141 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3142 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3143 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3144 Stop loss order always executed by market price. 3145 :param expDate: string "Undefined" by default or local date in future. 3146 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3147 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3148 A limit order has no expiration date, it lasts until the end of the trading day. 3149 :return: JSON with response from broker server. 3150 """ 3151 if self.accountId is None or not self.accountId: 3152 uLogger.error("Variable `accountId` must be defined for using this method!") 3153 raise Exception("Account ID required") 3154 3155 if operation is None or not operation or operation not in ("Buy", "Sell"): 3156 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3157 raise Exception("Incorrect value") 3158 3159 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3160 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3161 raise Exception("Incorrect value") 3162 3163 if lots is None or lots < 1: 3164 uLogger.error("You must define trade volume > 0: integer count of lots!") 3165 raise Exception("Incorrect value") 3166 3167 if targetPrice is None or targetPrice <= 0: 3168 uLogger.error("Target price for limit-order must be greater than 0!") 3169 raise Exception("Incorrect value") 3170 3171 if limitPrice is None or limitPrice <= 0: 3172 limitPrice = targetPrice 3173 3174 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3175 stopType = "Limit" 3176 3177 if expDate is None or not expDate: 3178 expDate = "Undefined" 3179 3180 if not (self._ticker or self._figi): 3181 uLogger.error("Tocker or FIGI must be defined!") 3182 raise Exception("Ticker or FIGI required") 3183 3184 response = {} 3185 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3186 self._ticker = instrument["ticker"] 3187 self._figi = instrument["figi"] 3188 3189 if orderType == "Limit": 3190 uLogger.debug( 3191 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3192 self._ticker, self._figi, 3193 operation, lots, targetPrice, instrument["currency"], 3194 )) 3195 3196 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3197 self.body = str({ 3198 "figi": self._figi, 3199 "quantity": str(lots), 3200 "price": FloatToNano(targetPrice), 3201 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3202 "accountId": str(self.accountId), 3203 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3204 }) 3205 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3206 3207 if "orderId" in response.keys(): 3208 uLogger.info( 3209 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3210 response["orderId"], 3211 self._ticker, self._figi, 3212 operation, lots, targetPrice, instrument["currency"], 3213 )) 3214 3215 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3216 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3217 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3218 targetPrice, instrument["currency"], 3219 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3220 )) 3221 3222 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3223 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3224 targetPrice, instrument["currency"], 3225 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3226 )) 3227 3228 else: 3229 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3230 3231 if orderType == "Stop": 3232 uLogger.debug( 3233 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3234 self._ticker, self._figi, 3235 operation, lots, 3236 targetPrice, instrument["currency"], 3237 limitPrice, instrument["currency"], 3238 stopType, expDate, 3239 )) 3240 3241 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3242 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3243 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3244 3245 body = { 3246 "figi": self._figi, 3247 "quantity": str(lots), 3248 "price": FloatToNano(limitPrice), 3249 "stopPrice": FloatToNano(targetPrice), 3250 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3251 "accountId": str(self.accountId), 3252 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3253 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3254 } 3255 3256 if expDateUTC: 3257 body["expireDate"] = expDateUTC 3258 3259 self.body = str(body) 3260 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3261 3262 if "stopOrderId" in response.keys(): 3263 uLogger.info( 3264 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3265 response["stopOrderId"], 3266 self._ticker, self._figi, 3267 operation, lots, 3268 targetPrice, instrument["currency"], 3269 limitPrice, instrument["currency"], 3270 TKS_STOP_ORDER_TYPES[stopOrderType], 3271 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3272 )) 3273 3274 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3275 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3276 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3277 targetPrice, instrument["currency"], 3278 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3279 )) 3280 3281 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3282 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3283 targetPrice, instrument["currency"], 3284 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3285 )) 3286 3287 else: 3288 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3289 3290 return response
Universal method to create market or limit orders with all available parameters for current accountId
.
See more simple methods: BuyLimit()
, BuyStop()
, SellLimit()
, SellStop()
.
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S
. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3292 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3293 """ 3294 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3295 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3296 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3297 See also: `Order()` docstring. 3298 3299 :param lots: volume, integer count of lots >= 1. 3300 :param targetPrice: target price > 0. This is open trade price for limit order. 3301 :return: JSON with response from broker server. 3302 """ 3303 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy
limit-order (below current price). You must specify only 2 parameters:
lots
and target price
to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy
market order, such as if you do simple --buy
operation!
See also: Order()
docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3305 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3306 """ 3307 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3308 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3309 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3310 target price value then broker opens a limit order. See also: `Order()` docstring. 3311 3312 :param lots: volume, integer count of lots >= 1. 3313 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3314 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3315 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3316 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3317 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3318 :param expDate: string "Undefined" by default or local date in future. 3319 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3320 This date is converting to UTC format for server. 3321 :return: JSON with response from broker server. 3322 """ 3323 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy
stop-order. You must specify at least 2 parameters: lots
target price
to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price
>=0, stop type
= Limit|SL|TP,
expiration date
= Undefined|%%Y-%%m-%%d %%H:%%M:%%S
. When current price will go up or down to
target price value then broker opens a limit order. See also: Order()
docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S
. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3325 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3326 """ 3327 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3328 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3329 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3330 See also: `Order()` docstring. 3331 3332 :param lots: volume, integer count of lots >= 1. 3333 :param targetPrice: target price > 0. This is open trade price for limit order. 3334 :return: JSON with response from broker server. 3335 """ 3336 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell
limit-order (above current price). You must specify only 2 parameters:
lots
and target price
to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell
market order, such as if you do simple --sell
operation!
See also: Order()
docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3338 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3339 """ 3340 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3341 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3342 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3343 target price value then broker opens a limit order. See also: `Order()` docstring. 3344 3345 :param lots: volume, integer count of lots >= 1. 3346 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3347 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3348 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3349 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3350 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3351 :param expDate: string "Undefined" by default or local date in future. 3352 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3353 This date is converting to UTC format for server. 3354 :return: JSON with response from broker server. 3355 """ 3356 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell
stop-order. You must specify at least 2 parameters: lots
target price
to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price
>=0, stop type
= Limit|SL|TP,
expiration date
= Undefined|%%Y-%%m-%%d %%H:%%M:%%S
. When current price will go up or down to
target price value then broker opens a limit order. See also: Order()
docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S
. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3358 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3359 """ 3360 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3361 3362 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3363 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3364 This avoids unnecessary downloading data from the server. 3365 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3366 """ 3367 if self.accountId is None or not self.accountId: 3368 uLogger.error("Variable `accountId` must be defined for using this method!") 3369 raise Exception("Account ID required") 3370 3371 if orderIDs: 3372 if allOrdersIDs is None: 3373 rawOrders = self.RequestPendingOrders() 3374 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3375 3376 if allStopOrdersIDs is None: 3377 rawStopOrders = self.RequestStopOrders() 3378 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3379 3380 for orderID in orderIDs: 3381 idInPendingOrders = orderID in allOrdersIDs 3382 idInStopOrders = orderID in allStopOrdersIDs 3383 3384 if not (idInPendingOrders or idInStopOrders): 3385 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3386 continue 3387 3388 else: 3389 if idInPendingOrders: 3390 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3391 3392 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3393 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3394 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3395 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3396 3397 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3398 if self.moreDebug: 3399 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3400 3401 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3402 3403 else: 3404 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3405 3406 elif idInStopOrders: 3407 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3408 3409 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3410 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3411 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3412 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3413 3414 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3415 if self.moreDebug: 3416 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3417 3418 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3419 3420 else: 3421 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3422 3423 else: 3424 continue
Cancel order or list of orders by its orderId
or stopOrderId
for current accountId
.
Parameters
- orderIDs: list of integers with
orderId
orstopOrderId
. - allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3426 def CloseAllOrders(self) -> None: 3427 """ 3428 Gets a list of open pending and stop orders and cancel it all. 3429 """ 3430 rawOrders = self.RequestPendingOrders() 3431 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3432 lenOrders = len(allOrdersIDs) 3433 3434 rawStopOrders = self.RequestStopOrders() 3435 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3436 lenSOrders = len(allStopOrdersIDs) 3437 3438 if lenOrders > 0 or lenSOrders > 0: 3439 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3440 3441 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3442 3443 else: 3444 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3446 def CloseAll(self, *args) -> None: 3447 """ 3448 Close all available (not blocked) opened trades and orders. 3449 3450 Also, you can select one or more keywords case-insensitive: 3451 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3452 3453 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3454 """ 3455 overview = self.Overview(show=False) # get all open trades info 3456 3457 if len(args) == 0: 3458 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3459 self.CloseAllOrders() # close all pending and stop orders 3460 3461 for iType in TKS_INSTRUMENTS: 3462 if iType != "Currencies": 3463 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3464 3465 else: 3466 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3467 lowerArgs = [x.lower() for x in args] 3468 3469 if "orders" in lowerArgs: 3470 self.CloseAllOrders() # close all pending and stop orders 3471 3472 for iType in TKS_INSTRUMENTS: 3473 if iType.lower() in lowerArgs and iType != "Currencies": 3474 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders
, shares
, bonds
, etfs
and futures
from TKS_INSTRUMENTS
enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades()
or CloseAllTrades()
methods.
3476 def CloseAllByTicker(self, instrument: str) -> None: 3477 """ 3478 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3479 3480 This method searches opened trade and orders of instrument throw all portfolio and then use 3481 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3482 3483 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3484 3485 :param instrument: string with ticker. 3486 """ 3487 if instrument is None or not instrument: 3488 uLogger.error("Ticker name must be defined for using this method!") 3489 raise Exception("Ticker required") 3490 3491 overview = self.Overview(show=False) # get user portfolio with all open trades info 3492 3493 self._ticker = instrument # try to set instrument as ticker 3494 self._figi = "" 3495 3496 if self.IsInPortfolio(portfolio=overview): 3497 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3498 self.CloseTrades(instruments=[instrument], portfolio=overview) 3499 3500 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3501 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3502 3503 if limitAll and self.IsInLimitOrders(portfolio=overview): 3504 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3505 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3506 3507 if stopAll and self.IsInStopOrders(portfolio=overview): 3508 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3509 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades()
and CloseOrders()
methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders()
, GetLimitOrderIDs()
, IsInStopOrders()
, GetStopOrderIDs()
, CloseTrades()
and CloseOrders()
.
Parameters
- instrument: string with ticker.
3511 def CloseAllByFIGI(self, instrument: str) -> None: 3512 """ 3513 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3514 3515 This method searches opened trade and orders of instrument throw all portfolio and then use 3516 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3517 3518 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3519 3520 :param instrument: string with FIGI id. 3521 """ 3522 if instrument is None or not instrument: 3523 uLogger.error("FIGI id must be defined for using this method!") 3524 raise Exception("FIGI required") 3525 3526 overview = self.Overview(show=False) # get user portfolio with all open trades info 3527 3528 self._ticker = "" 3529 self._figi = instrument # try to set instrument as FIGI id 3530 3531 if self.IsInPortfolio(portfolio=overview): 3532 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3533 self.CloseTrades(instruments=[instrument], portfolio=overview) 3534 3535 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3536 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3537 3538 if limitAll and self.IsInLimitOrders(portfolio=overview): 3539 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3540 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3541 3542 if stopAll and self.IsInStopOrders(portfolio=overview): 3543 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3544 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades()
and CloseOrders()
methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders()
, GetLimitOrderIDs()
, IsInStopOrders()
, GetStopOrderIDs()
, CloseTrades()
and CloseOrders()
.
Parameters
- instrument: string with FIGI id.
3546 @staticmethod 3547 def ParseOrderParameters(operation, **inputParameters): 3548 """ 3549 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3550 3551 :param operation: string "Buy" or "Sell". 3552 :param inputParameters: this is dict of strings that looks like this 3553 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3554 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3555 "prices" key: one or more prices to open limit-orders 3556 Counts of values in lots and prices lists must be equals! 3557 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3558 """ 3559 # TODO: update order grid work with api v2 3560 pass 3561 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3562 # 3563 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3564 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3565 # raise Exception("Incorrect value") 3566 # 3567 # if "l" in inputParameters.keys(): 3568 # inputParameters["lots"] = inputParameters.pop("l") 3569 # 3570 # if "p" in inputParameters.keys(): 3571 # inputParameters["prices"] = inputParameters.pop("p") 3572 # 3573 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3574 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3575 # raise Exception("Incorrect value") 3576 # 3577 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3578 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3579 # 3580 # if len(lots) != len(prices): 3581 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3582 # raise Exception("Incorrect value") 3583 # 3584 # uLogger.debug("Extracted parameters for orders:") 3585 # uLogger.debug("lots = {}".format(lots)) 3586 # uLogger.debug("prices = {}".format(prices)) 3587 # 3588 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3589 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3590 # uLogger.debug("Order parameters: {}".format(result)) 3591 # 3592 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}
where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3594 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3595 """ 3596 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3597 3598 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3599 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3600 """ 3601 result = False 3602 msg = "Instrument not defined!" 3603 3604 if portfolio is None or not portfolio: 3605 portfolio = self.Overview(show=False) 3606 3607 if self._ticker: 3608 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3609 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3610 3611 for iType in TKS_INSTRUMENTS: 3612 for instrument in portfolio["stat"][iType]: 3613 if instrument["ticker"] == self._ticker: 3614 result = True 3615 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3616 break 3617 3618 elif self._figi: 3619 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3620 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3621 3622 for iType in TKS_INSTRUMENTS: 3623 for instrument in portfolio["stat"][iType]: 3624 if instrument["figi"] == self._figi: 3625 result = True 3626 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3627 break 3628 3629 else: 3630 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3631 3632 uLogger.debug(msg) 3633 3634 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker
(highly priority) or figi
.
Parameters
- portfolio: dict with user's portfolio data. If
None
, then requests portfolio fromOverview()
method.
Returns
True
if portfolio contains open position with given instrument,False
otherwise.
3636 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3637 """ 3638 Returns instrument from the user's portfolio if it presents there. 3639 Instrument must be defined by `ticker` (highly priority) or `figi`. 3640 3641 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3642 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3643 """ 3644 result = None 3645 msg = "Instrument not defined!" 3646 3647 if portfolio is None or not portfolio: 3648 portfolio = self.Overview(show=False) 3649 3650 if self._ticker: 3651 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3652 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3653 3654 for iType in TKS_INSTRUMENTS: 3655 for instrument in portfolio["stat"][iType]: 3656 if instrument["ticker"] == self._ticker: 3657 result = instrument 3658 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3659 break 3660 3661 elif self._figi: 3662 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3663 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3664 3665 for iType in TKS_INSTRUMENTS: 3666 for instrument in portfolio["stat"][iType]: 3667 if instrument["figi"] == self._figi: 3668 result = instrument 3669 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3670 break 3671 3672 else: 3673 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3674 3675 uLogger.debug(msg) 3676 3677 return result
Returns instrument from the user's portfolio if it presents there.
Instrument must be defined by ticker
(highly priority) or figi
.
Parameters
- portfolio: dict with user's portfolio data. If
None
, then requests portfolio fromOverview()
method.
Returns
dict with instrument if portfolio contains open position with this instrument,
None
otherwise.
3679 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3680 """ 3681 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3682 3683 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3684 3685 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3686 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3687 """ 3688 result = False 3689 msg = "Instrument not defined!" 3690 3691 if portfolio is None or not portfolio: 3692 portfolio = self.Overview(show=False) 3693 3694 if self._ticker: 3695 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3696 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3697 3698 for instrument in portfolio["stat"]["orders"]: 3699 if instrument["ticker"] == self._ticker: 3700 result = True 3701 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3702 break 3703 3704 elif self._figi: 3705 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3706 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3707 3708 for instrument in portfolio["stat"]["orders"]: 3709 if instrument["figi"] == self._figi: 3710 result = True 3711 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3712 break 3713 3714 else: 3715 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3716 3717 uLogger.debug(msg) 3718 3719 return result
Checks if instrument is in the limit orders list. Instrument must be defined by ticker
(highly priority) or figi
.
See also: CloseAllByTicker()
and CloseAllByFIGI()
.
Parameters
- portfolio: dict with user's portfolio data. If
None
, then requests portfolio fromOverview()
method.
Returns
True
if limit orders list contains some limit orders for the instrument,False
otherwise.
3721 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3722 """ 3723 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3724 Instrument must be defined by `ticker` (highly priority) or `figi`. 3725 3726 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3727 3728 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3729 :return: list with `orderID`s of limit orders. 3730 """ 3731 result = [] 3732 msg = "Instrument not defined!" 3733 3734 if portfolio is None or not portfolio: 3735 portfolio = self.Overview(show=False) 3736 3737 if self._ticker: 3738 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3739 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3740 3741 for instrument in portfolio["stat"]["orders"]: 3742 if instrument["ticker"] == self._ticker: 3743 result.append(instrument["orderID"]) 3744 3745 if result: 3746 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3747 3748 elif self._figi: 3749 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3750 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3751 3752 for instrument in portfolio["stat"]["orders"]: 3753 if instrument["figi"] == self._figi: 3754 result.append(instrument["orderID"]) 3755 3756 if result: 3757 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3758 3759 else: 3760 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3761 3762 uLogger.debug(msg) 3763 3764 return result
Returns list with all orderID
s of opened pending limit orders for the instrument.
Instrument must be defined by ticker
(highly priority) or figi
.
See also: CloseAllByTicker()
and CloseAllByFIGI()
.
Parameters
- portfolio: dict with user's portfolio data. If
None
, then requests portfolio fromOverview()
method.
Returns
list with
orderID
s of limit orders.
3766 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3767 """ 3768 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3769 3770 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3771 3772 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3773 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3774 """ 3775 result = False 3776 msg = "Instrument not defined!" 3777 3778 if portfolio is None or not portfolio: 3779 portfolio = self.Overview(show=False) 3780 3781 if self._ticker: 3782 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3783 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3784 3785 for instrument in portfolio["stat"]["stopOrders"]: 3786 if instrument["ticker"] == self._ticker: 3787 result = True 3788 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3789 break 3790 3791 elif self._figi: 3792 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3793 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3794 3795 for instrument in portfolio["stat"]["stopOrders"]: 3796 if instrument["figi"] == self._figi: 3797 result = True 3798 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3799 break 3800 3801 else: 3802 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3803 3804 uLogger.debug(msg) 3805 3806 return result
Checks if instrument is in the stop orders list. Instrument must be defined by ticker
(highly priority) or figi
.
See also: CloseAllByTicker()
and CloseAllByFIGI()
.
Parameters
- portfolio: dict with user's portfolio data. If
None
, then requests portfolio fromOverview()
method.
Returns
True
if stop orders list contains some stop orders for the instrument,False
otherwise.
3808 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3809 """ 3810 Returns list with all `orderID`s of opened stop orders for the instrument. 3811 Instrument must be defined by `ticker` (highly priority) or `figi`. 3812 3813 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3814 3815 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3816 :return: list with `orderID`s of stop orders. 3817 """ 3818 result = [] 3819 msg = "Instrument not defined!" 3820 3821 if portfolio is None or not portfolio: 3822 portfolio = self.Overview(show=False) 3823 3824 if self._ticker: 3825 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3826 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3827 3828 for instrument in portfolio["stat"]["stopOrders"]: 3829 if instrument["ticker"] == self._ticker: 3830 result.append(instrument["orderID"]) 3831 3832 if result: 3833 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3834 3835 elif self._figi: 3836 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3837 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3838 3839 for instrument in portfolio["stat"]["stopOrders"]: 3840 if instrument["figi"] == self._figi: 3841 result.append(instrument["orderID"]) 3842 3843 if result: 3844 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3845 3846 else: 3847 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3848 3849 uLogger.debug(msg) 3850 3851 return result
Returns list with all orderID
s of opened stop orders for the instrument.
Instrument must be defined by ticker
(highly priority) or figi
.
See also: CloseAllByTicker()
and CloseAllByFIGI()
.
Parameters
- portfolio: dict with user's portfolio data. If
None
, then requests portfolio fromOverview()
method.
Returns
list with
orderID
s of stop orders.
3853 def RequestLimits(self) -> dict: 3854 """ 3855 Method for obtaining the available funds for withdrawal for current `accountId`. 3856 3857 See also: 3858 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3859 - `OverviewLimits()` method 3860 3861 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3862 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3863 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3864 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3865 """ 3866 if self.accountId is None or not self.accountId: 3867 uLogger.error("Variable `accountId` must be defined for using this method!") 3868 raise Exception("Account ID required") 3869 3870 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3871 3872 self.body = str({"accountId": self.accountId}) 3873 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3874 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3875 3876 if self.moreDebug: 3877 uLogger.debug("Records about available funds for withdrawal successfully received") 3878 3879 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId
.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()
method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}
. Heremoney
is an array of portfolio currency positions,blocked
is an array of blocked currency positions of the portfolio andblockedGuarantee
is locked money under collateral for futures.
3881 def OverviewLimits(self, show: bool = False) -> dict: 3882 """ 3883 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3884 3885 See also: `RequestLimits()`. 3886 3887 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3888 :return: dict with raw parsed data from server and some calculated statistics about it. 3889 """ 3890 if self.accountId is None or not self.accountId: 3891 uLogger.error("Variable `accountId` must be defined for using this method!") 3892 raise Exception("Account ID required") 3893 3894 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3895 3896 view = { 3897 "rawLimits": rawLimits, 3898 "limits": { # parsed data for every currency: 3899 "money": { # this is an array of portfolio currency positions 3900 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3901 }, 3902 "blocked": { # this is an array of blocked currency 3903 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3904 }, 3905 "blockedGuarantee": { # this is locked money under collateral for futures 3906 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3907 }, 3908 }, 3909 } 3910 3911 # --- Prepare text table with limits in human-readable format: 3912 if show: 3913 info = [ 3914 "# Withdrawal limits\n\n", 3915 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3916 "* **Account ID:** [{}]\n".format(self.accountId), 3917 ] 3918 3919 if view["limits"]["money"]: 3920 info.extend([ 3921 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3922 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3923 ]) 3924 3925 else: 3926 info.append("\nNo withdrawal limits\n") 3927 3928 for curr in view["limits"]["money"].keys(): 3929 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3930 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3931 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3932 3933 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3934 "[{}]".format(curr), 3935 "{:.2f}".format(view["limits"]["money"][curr]), 3936 "{:.2f}".format(availableMoney), 3937 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3938 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3939 ) 3940 3941 if curr == "rub": 3942 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3943 3944 else: 3945 info.append(infoStr) 3946 3947 infoText = "".join(info) 3948 3949 uLogger.info(infoText) 3950 3951 if self.withdrawalLimitsFile: 3952 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3953 fH.write(infoText) 3954 3955 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3956 3957 if self.useHTMLReports: 3958 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3959 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3960 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 3961 3962 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 3963 3964 return view
Method for parsing and show table with available funds for withdrawal for current accountId
.
See also: RequestLimits()
.
Parameters
- show: if
False
then only dictionary returns, ifTrue
then also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3966 def RequestAccounts(self) -> dict: 3967 """ 3968 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3969 3970 See also: 3971 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3972 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3973 - `OverviewUserInfo()` method 3974 3975 :return: dict with raw data from server that contains accounts info. Example of dict: 3976 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3977 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3978 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3979 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3980 """ 3981 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3982 3983 self.body = str({}) 3984 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3985 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3986 3987 if self.moreDebug: 3988 uLogger.debug("Records about available accounts successfully received") 3989 3990 return rawAccounts
Method for requesting all brokerage accounts (accountId
s) of current user detected by token
.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()
method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}
. IfclosedDate="1970-01-01T00:00:00Z"
it means that account is active now.
3992 def RequestUserInfo(self) -> dict: 3993 """ 3994 Method for requesting common user's information. 3995 3996 See also: 3997 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3998 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3999 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4000 - `OverviewUserInfo()` method 4001 4002 :return: dict with raw data from server that contains user's information. Example of dict: 4003 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4004 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4005 """ 4006 uLogger.debug("Requesting common user's information. Wait, please...") 4007 4008 self.body = str({}) 4009 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4010 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4011 4012 if self.moreDebug: 4013 uLogger.debug("Records about current user successfully received") 4014 4015 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_with
field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()
method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}
.
4017 def RequestMarginStatus(self, accountId: str = None) -> dict: 4018 """ 4019 Method for requesting margin calculation for defined account ID. 4020 4021 See also: 4022 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4023 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4024 - `OverviewUserInfo()` method 4025 4026 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4027 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4028 Example of responses: 4029 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4030 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4031 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4032 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4033 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4034 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4035 """ 4036 if accountId is None or not accountId: 4037 if self.accountId is None or not self.accountId: 4038 uLogger.error("Variable `accountId` must be defined for using this method!") 4039 raise Exception("Account ID required") 4040 4041 else: 4042 accountId = self.accountId # use `self.accountId` (main ID) by default 4043 4044 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4045 4046 self.body = str({"accountId": accountId}) 4047 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4048 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4049 4050 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4051 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4052 rawMargin = {} 4053 4054 else: 4055 if self.moreDebug: 4056 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4057 4058 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()
method
Parameters
- accountId: string with numeric account ID. If
None
, then used class fieldaccountId
.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }
, returns:{}
. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}
.
4060 def RequestTariffLimits(self) -> dict: 4061 """ 4062 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4063 4064 See also: 4065 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4066 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4067 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4068 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4069 - `OverviewUserInfo()` method 4070 4071 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4072 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4073 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4074 """ 4075 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4076 4077 self.body = str({}) 4078 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4079 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4080 4081 if self.moreDebug: 4082 uLogger.debug("Records with limits of current tariff successfully received") 4083 4084 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token
.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()
method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}
.
4086 def RequestBondCoupons(self, iJSON: dict) -> dict: 4087 """ 4088 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4089 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4090 All dates are in UTC timezone. 4091 4092 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4093 Documentation: 4094 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4095 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4096 4097 See also: `ExtendBondsData()`. 4098 4099 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4100 If raw iJSON is not data of bond then server returns an error [400] with message: 4101 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4102 :return: dictionary with bond payment calendar. Response example 4103 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4104 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4105 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4106 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4107 """ 4108 if iJSON["figi"] is None or not iJSON["figi"]: 4109 uLogger.error("FIGI must be defined for using this method!") 4110 raise Exception("FIGI required") 4111 4112 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4113 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4114 4115 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4116 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4117 self._figi, 4118 startDate, 4119 endDate, 4120 )) 4121 4122 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4123 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4124 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4125 4126 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4127 uLogger.warning("Instrument type is not bond!") 4128 4129 else: 4130 if self.moreDebug: 4131 uLogger.debug("Records about bond payment calendar successfully received") 4132 4133 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z"
and "to": "2099-12-31T23:59:59.000Z"
.
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData()
.
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self._ticker]
If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}
.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
4135 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4136 """ 4137 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4138 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4139 coupon yields, current yields and some statistics etc. 4140 4141 WARNING! This is too long operation if a lot of bonds requested from broker server. 4142 4143 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4144 4145 :param instruments: list of strings with tickers or FIGIs. 4146 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4147 for further used by data scientists or stock analytics. 4148 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4149 In XLSX-file and Pandas DataFrame fields mean: 4150 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4151 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4152 """ 4153 if instruments is None or not instruments: 4154 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4155 raise Exception("Ticker or FIGI required") 4156 4157 if isinstance(instruments, str): 4158 instruments = [instruments] 4159 4160 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4161 4162 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4163 4164 iCount = len(uniqueInstruments) 4165 tooLong = iCount >= 20 4166 if tooLong: 4167 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4168 4169 bonds = None 4170 for i, self._figi in enumerate(uniqueInstruments): 4171 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4172 4173 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4174 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4175 rawBond = self.SearchByFIGI(requestPrice=True) 4176 4177 # Widen raw data with UTC current time (iData["actualDateTime"]): 4178 actualDate = datetime.now(tzutc()) 4179 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4180 4181 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4182 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4183 4184 # Replace some values with human-readable: 4185 iData["nominalCurrency"] = iData["nominal"]["currency"] 4186 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4187 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4188 iData["aciCurrency"] = iData["aciValue"]["currency"] 4189 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4190 iData["issueSize"] = int(iData["issueSize"]) 4191 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4192 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4193 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4194 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4195 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4196 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4197 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4198 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4199 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4200 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4201 4202 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4203 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4204 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4205 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4206 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4207 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4208 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4209 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4210 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4211 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4212 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4213 4214 # Widen raw data with calendar data from `rawCalendar` values: 4215 calendarData = [] 4216 if "events" in iData["rawCalendar"].keys(): 4217 for item in iData["rawCalendar"]["events"]: 4218 calendarData.append({ 4219 "couponDate": item["couponDate"], 4220 "couponNumber": int(item["couponNumber"]), 4221 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4222 "payCurrency": item["payOneBond"]["currency"], 4223 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4224 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4225 "couponStartDate": item["couponStartDate"], 4226 "couponEndDate": item["couponEndDate"], 4227 "couponPeriod": item["couponPeriod"], 4228 }) 4229 4230 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4231 if "maturityDate" not in iData.keys(): 4232 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4233 4234 # Widen raw data with Coupon Rate. 4235 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4236 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4237 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4238 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4239 4240 # Widen raw data with Yield to Maturity (YTM) on current date. 4241 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4242 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4243 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4244 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4245 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4246 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4247 4248 iData["calendar"] = calendarData # adds calendar at the end 4249 4250 # Remove not used data: 4251 iData.pop("uid") 4252 iData.pop("positionUid") 4253 iData.pop("currentPrice") 4254 iData.pop("rawCalendar") 4255 4256 colNames = list(iData.keys()) 4257 if bonds is None: 4258 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4259 4260 else: 4261 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4262 4263 else: 4264 uLogger.warning("Instrument is not a bond!") 4265 4266 processed = round(100 * (i + 1) / iCount, 1) 4267 if tooLong and processed % 5 == 0: 4268 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4269 4270 else: 4271 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4272 4273 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4274 4275 # Saving bonds from Pandas DataFrame to XLSX sheet: 4276 if xlsx and self.bondsXLSXFile: 4277 with pd.ExcelWriter( 4278 path=self.bondsXLSXFile, 4279 date_format=TKS_DATE_FORMAT, 4280 datetime_format=TKS_DATE_TIME_FORMAT, 4281 mode="w", 4282 ) as writer: 4283 bonds.to_excel( 4284 writer, 4285 sheet_name="Extended bonds data", 4286 index=True, 4287 encoding="UTF-8", 4288 freeze_panes=(1, 1), 4289 ) # saving as XLSX-file with freeze first row and column as headers 4290 4291 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4292 4293 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo()
, CreateBondsCalendar()
, ShowBondsCalendar()
, RequestBondCoupons()
.
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile
, defaultext-bonds.xlsx
, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4295 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4296 """ 4297 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4298 4299 WARNING! This is too long operation if a lot of bonds requested from broker server. 4300 4301 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4302 4303 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4304 extended information about bonds: main info, current prices, bond payment calendar, 4305 coupon yields, current yields and some statistics etc. 4306 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4307 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4308 for further used by data scientists or stock analytics. 4309 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4310 """ 4311 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4312 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4313 4314 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4315 4316 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4317 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4318 calendar = None 4319 for bond in extBonds.iterrows(): 4320 for item in bond[1]["calendar"]: 4321 cData = { 4322 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4323 "couponDate": item["couponDate"], 4324 "figi": bond[1]["figi"], 4325 "ticker": bond[1]["ticker"], 4326 "name": bond[1]["name"], 4327 "couponNumber": item["couponNumber"], 4328 "payOneBond": item["payOneBond"], 4329 "payCurrency": item["payCurrency"], 4330 "couponType": item["couponType"], 4331 "couponPeriod": item["couponPeriod"], 4332 "fixDate": item["fixDate"], 4333 "couponStartDate": item["couponStartDate"], 4334 "couponEndDate": item["couponEndDate"], 4335 } 4336 4337 if calendar is None: 4338 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4339 4340 else: 4341 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4342 4343 if calendar is not None: 4344 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4345 4346 # Saving calendar from Pandas DataFrame to XLSX sheet: 4347 if xlsx: 4348 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4349 4350 with pd.ExcelWriter( 4351 path=xlsxCalendarFile, 4352 date_format=TKS_DATE_FORMAT, 4353 datetime_format=TKS_DATE_TIME_FORMAT, 4354 mode="w", 4355 ) as writer: 4356 humanReadable = calendar.copy(deep=True) 4357 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4358 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4359 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4360 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4361 humanReadable.columns = colNames # human-readable column names 4362 4363 humanReadable.to_excel( 4364 writer, 4365 sheet_name="Bond payments calendar", 4366 index=False, 4367 encoding="UTF-8", 4368 freeze_panes=(1, 2), 4369 ) # saving as XLSX-file with freeze first row and column as headers 4370 4371 del humanReadable # release df in memory 4372 4373 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4374 4375 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx
by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar()
, ExtendBondsData()
.
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()
method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNone
then usedfigi
orticker
as bond name and then calculateExtendBondsData()
. - xlsx: if True then also exports Pandas DataFrame to file
calendarFile
+".xlsx"
,calendar.xlsx
by default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4377 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4378 """ 4379 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4380 Also, creates Markdown file with calendar data, `calendar.md` by default. 4381 4382 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4383 4384 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4385 extended information about bonds: main info, current prices, bond payment calendar, 4386 coupon yields, current yields and some statistics etc. 4387 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4388 :param show: if `True` then also printing bonds payment calendar to the console, 4389 otherwise save to file `calendarFile` only. `False` by default. 4390 :return: multilines text in Markdown format with bonds payment calendar as a table. 4391 """ 4392 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4393 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4394 4395 infoText = "# Bond payments calendar\n\n" 4396 4397 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4398 4399 if not (calendar is None or calendar.empty): 4400 splitLine = "| | | | | | | | | |\n" 4401 4402 info = [ 4403 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4404 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4405 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4406 ] 4407 4408 newMonth = False 4409 notOneBond = calendar["figi"].nunique() > 1 4410 for i, bond in enumerate(calendar.iterrows()): 4411 if newMonth and notOneBond: 4412 info.append(splitLine) 4413 4414 info.append( 4415 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4416 " √" if bond[1]["paid"] else " —", 4417 bond[1]["couponDate"].split("T")[0], 4418 bond[1]["figi"], 4419 bond[1]["ticker"], 4420 bond[1]["couponNumber"], 4421 "{} {}".format( 4422 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4423 bond[1]["payCurrency"], 4424 ), 4425 bond[1]["couponType"], 4426 bond[1]["couponPeriod"], 4427 bond[1]["fixDate"].split("T")[0], 4428 ) 4429 ) 4430 4431 if i < len(calendar.values) - 1: 4432 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4433 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4434 newMonth = False if curDate.month == nextDate.month else True 4435 4436 else: 4437 newMonth = False 4438 4439 infoText += "".join(info) 4440 4441 if show: 4442 uLogger.info("{}".format(infoText)) 4443 4444 if self.calendarFile is not None: 4445 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4446 fH.write(infoText) 4447 4448 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4449 4450 if self.useHTMLReports: 4451 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4452 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4453 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4454 4455 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4456 4457 else: 4458 infoText += "No data\n" 4459 4460 return infoText
Show bond payments calendar as a table. One row in input bonds
dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md
by default.
See also: ShowInstrumentInfo()
, RequestBondCoupons()
, CreateBondsCalendar()
and ExtendBondsData()
.
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()
method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNone
then usedfigi
orticker
as bond name and then calculateExtendBondsData()
. - show: if
True
then also printing bonds payment calendar to the console, otherwise save to filecalendarFile
only.False
by default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4462 def OverviewAccounts(self, show: bool = False) -> dict: 4463 """ 4464 Method for parsing and show simple table with all available user accounts. 4465 4466 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4467 4468 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4469 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4470 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4471 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4472 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4473 "closed": "—", "access": "Full access" }, ...}}` 4474 """ 4475 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4476 4477 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4478 accounts = { 4479 item["id"]: { 4480 "type": TKS_ACCOUNT_TYPES[item["type"]], 4481 "name": item["name"], 4482 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4483 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4484 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4485 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4486 } for item in rawAccounts["accounts"] 4487 } 4488 4489 # Raw and parsed data with some fields replaced in "stat" section: 4490 view = { 4491 "rawAccounts": rawAccounts, 4492 "stat": accounts, 4493 } 4494 4495 # --- Prepare simple text table with only accounts data in human-readable format: 4496 if show: 4497 info = [ 4498 "# User accounts\n\n", 4499 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4500 "| Account ID | Type | Status | Name |\n", 4501 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4502 ] 4503 4504 for account in view["stat"].keys(): 4505 info.extend([ 4506 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4507 account, 4508 view["stat"][account]["type"], 4509 view["stat"][account]["status"], 4510 view["stat"][account]["name"], 4511 ) 4512 ]) 4513 4514 infoText = "".join(info) 4515 4516 uLogger.info(infoText) 4517 4518 if self.userAccountsFile: 4519 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4520 fH.write(infoText) 4521 4522 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4523 4524 if self.useHTMLReports: 4525 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4526 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4527 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4528 4529 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4530 4531 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts()
and OverviewUserInfo()
methods.
Parameters
- show: if
False
then only dictionary with accounts data returns, ifTrue
then also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()
method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4533 def OverviewUserInfo(self, show: bool = False) -> dict: 4534 """ 4535 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4536 4537 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4538 4539 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4540 :return: dict with raw parsed data from server and some calculated statistics about it. 4541 """ 4542 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4543 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4544 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4545 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4546 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4547 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4548 4549 # This is dict with parsed common user data: 4550 userInfo = { 4551 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4552 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4553 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4554 "tariff": rawUserInfo["tariff"], 4555 } 4556 4557 # This is an array of dict with parsed margin statuses for every account IDs: 4558 margins = {} 4559 for accountId in accounts.keys(): 4560 if rawMargins[accountId]: 4561 margins[accountId] = { 4562 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4563 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4564 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4565 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4566 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4567 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4568 } 4569 4570 else: 4571 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4572 4573 unary = {} # unary-connection limits 4574 for item in rawTariffLimits["unaryLimits"]: 4575 if item["limitPerMinute"] in unary.keys(): 4576 unary[item["limitPerMinute"]].extend(item["methods"]) 4577 4578 else: 4579 unary[item["limitPerMinute"]] = item["methods"] 4580 4581 stream = {} # stream-connection limits 4582 for item in rawTariffLimits["streamLimits"]: 4583 if item["limit"] in stream.keys(): 4584 stream[item["limit"]].extend(item["streams"]) 4585 4586 else: 4587 stream[item["limit"]] = item["streams"] 4588 4589 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4590 limits = { 4591 "unary": unary, 4592 "stream": stream, 4593 } 4594 4595 # Raw and parsed data as an output result: 4596 view = { 4597 "rawUserInfo": rawUserInfo, 4598 "rawAccounts": rawAccounts, 4599 "rawMargins": rawMargins, 4600 "rawTariffLimits": rawTariffLimits, 4601 "stat": { 4602 "userInfo": userInfo, 4603 "accounts": accounts, 4604 "margins": margins, 4605 "limits": limits, 4606 }, 4607 } 4608 4609 # --- Prepare text table with user information in human-readable format: 4610 if show: 4611 info = [ 4612 "# Full user information\n\n", 4613 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4614 "## Common information\n\n", 4615 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4616 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4617 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4618 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4619 "\n## User accounts\n\n", 4620 ] 4621 4622 for account in view["stat"]["accounts"].keys(): 4623 info.extend([ 4624 "### ID: [{}]\n\n".format(account), 4625 "| Parameters | Values |\n", 4626 "|----------------------|--------------------------------------------------------------|\n", 4627 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4628 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4629 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4630 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4631 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4632 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4633 ]) 4634 4635 if margins[account]: 4636 info.extend([ 4637 "| Margin status: | Enabled |\n", 4638 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4639 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4640 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4641 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4642 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4643 ]) 4644 4645 else: 4646 info.append("| Margin status: | Disabled |\n\n") 4647 4648 info.extend([ 4649 "\n## Current user tariff limits\n", 4650 "\n### See also\n", 4651 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4652 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4653 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4654 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4655 "\n### Unary limits\n", 4656 ]) 4657 4658 if unary: 4659 for key, values in sorted(unary.items()): 4660 info.append("\n* Max requests per minute: {}\n".format(key)) 4661 4662 for value in values: 4663 info.append(" - {}\n".format(value)) 4664 4665 else: 4666 info.append("\nNot available\n") 4667 4668 info.append("\n### Stream limits\n") 4669 4670 if stream: 4671 for key, values in sorted(stream.items()): 4672 info.append("\n* Max stream connections: {}\n".format(key)) 4673 4674 for value in values: 4675 info.append(" - {}\n".format(value)) 4676 4677 else: 4678 info.append("\nNot available\n") 4679 4680 infoText = "".join(info) 4681 4682 uLogger.info(infoText) 4683 4684 if self.userInfoFile: 4685 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4686 fH.write(infoText) 4687 4688 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4689 4690 if self.useHTMLReports: 4691 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4692 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4693 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4694 4695 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4696 4697 return view
Method for parsing and show all available user's data (accountId
s, common user information, margin status and tariff connections limit).
See also: OverviewAccounts()
, RequestAccounts()
, RequestUserInfo()
, RequestMarginStatus()
and RequestTariffLimits()
methods.
Parameters
- show: if
False
then only dictionary returns, ifTrue
then also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4700class Args: 4701 """ 4702 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4703 """ 4704 def __init__(self, **kwargs): 4705 self.__dict__.update(kwargs) 4706 4707 def __getattr__(self, item): 4708 return None
If Main()
function is imported as module, then this class used to convert arguments from **kwargs as object.
4711def ParseArgs(): 4712 """This function get and parse command line keys.""" 4713 parser = ArgumentParser() # command-line string parser 4714 4715 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4716 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4717 4718 # --- options: 4719 4720 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4721 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4722 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4723 4724 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4725 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4726 4727 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4728 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4729 4730 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4731 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4732 4733 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4734 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4735 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4736 4737 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4738 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4739 4740 # --- commands: 4741 4742 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4743 4744 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4745 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4746 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4747 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4748 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4749 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4750 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4751 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4752 4753 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4754 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4755 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4756 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4757 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4758 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4759 4760 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4761 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4762 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4763 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4764 4765 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4766 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4767 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4768 4769 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4770 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4771 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4772 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4773 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4774 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4775 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4776 4777 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4778 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4779 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4780 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4781 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4782 4783 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4784 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4785 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4786 4787 cmdArgs = parser.parse_args() 4788 return cmdArgs
This function get and parse command line keys.
4791def Main(**kwargs): 4792 """ 4793 Main function for work with TKSBrokerAPI in the console. 4794 4795 See examples: 4796 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4797 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4798 """ 4799 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4800 4801 if args.debug_level: 4802 uLogger.level = 10 # always debug level by default 4803 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4804 4805 exitCode = 0 4806 start = datetime.now(tzutc()) 4807 uLogger.debug("=-" * 50) 4808 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4809 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4810 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4811 )) 4812 4813 # trying to calculate full current version: 4814 buildVersion = __version__ 4815 try: 4816 v = version("tksbrokerapi") 4817 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4818 4819 except Exception: 4820 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4821 4822 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4823 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4824 4825 try: 4826 if args.version: 4827 print("TKSBrokerAPI {}".format(buildVersion)) 4828 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4829 4830 else: 4831 # Init class for trading with Tinkoff Broker: 4832 trader = TinkoffBrokerServer( 4833 token=args.token, 4834 accountId=args.account_id, 4835 useCache=not args.no_cache, 4836 ) 4837 4838 # --- set some options: 4839 4840 if args.more: 4841 trader.moreDebug = True 4842 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4843 4844 if args.html: 4845 trader.useHTMLReports = True 4846 4847 if args.ticker: 4848 ticker = str(args.ticker).upper() # Tickers may be upper case only 4849 4850 if ticker in trader.aliasesKeys: 4851 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4852 4853 else: 4854 trader.ticker = ticker 4855 4856 if args.figi: 4857 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4858 4859 if args.depth is not None: 4860 trader.depth = args.depth 4861 4862 # --- do one command: 4863 4864 if args.list: 4865 if args.output is not None: 4866 trader.instrumentsFile = args.output 4867 4868 trader.ShowInstrumentsInfo(show=True) 4869 4870 elif args.list_xlsx: 4871 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4872 4873 elif args.bonds_xlsx is not None: 4874 if args.output is not None: 4875 trader.bondsXLSXFile = args.output 4876 4877 if len(args.bonds_xlsx) == 0: 4878 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4879 4880 else: 4881 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4882 4883 elif args.search: 4884 if args.output is not None: 4885 trader.searchResultsFile = args.output 4886 4887 trader.SearchInstruments(pattern=args.search[0], show=True) 4888 4889 elif args.info: 4890 if not (args.ticker or args.figi): 4891 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4892 raise Exception("Ticker or FIGI required") 4893 4894 if args.output is not None: 4895 trader.infoFile = args.output 4896 4897 if args.ticker: 4898 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4899 4900 else: 4901 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4902 4903 elif args.calendar is not None: 4904 if args.output is not None: 4905 trader.calendarFile = args.output 4906 4907 if len(args.calendar) == 0: 4908 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4909 4910 else: 4911 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4912 4913 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4914 4915 elif args.price: 4916 if not (args.ticker or args.figi): 4917 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4918 raise Exception("Ticker or FIGI required") 4919 4920 trader.GetCurrentPrices(show=True) 4921 4922 elif args.prices is not None: 4923 if args.output is not None: 4924 trader.pricesFile = args.output 4925 4926 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4927 4928 elif args.overview: 4929 if args.output is not None: 4930 trader.overviewFile = args.output 4931 4932 trader.Overview(show=True, details="full") 4933 4934 elif args.overview_digest: 4935 if args.output is not None: 4936 trader.overviewDigestFile = args.output 4937 4938 trader.Overview(show=True, details="digest") 4939 4940 elif args.overview_positions: 4941 if args.output is not None: 4942 trader.overviewPositionsFile = args.output 4943 4944 trader.Overview(show=True, details="positions") 4945 4946 elif args.overview_orders: 4947 if args.output is not None: 4948 trader.overviewOrdersFile = args.output 4949 4950 trader.Overview(show=True, details="orders") 4951 4952 elif args.overview_analytics: 4953 if args.output is not None: 4954 trader.overviewAnalyticsFile = args.output 4955 4956 trader.Overview(show=True, details="analytics") 4957 4958 elif args.overview_calendar: 4959 if args.output is not None: 4960 trader.overviewAnalyticsFile = args.output 4961 4962 trader.Overview(show=True, details="calendar") 4963 4964 elif args.deals is not None: 4965 if args.output is not None: 4966 trader.reportFile = args.output 4967 4968 if 0 <= len(args.deals) < 3: 4969 trader.Deals( 4970 start=args.deals[0] if len(args.deals) >= 1 else None, 4971 end=args.deals[1] if len(args.deals) == 2 else None, 4972 show=True, # Always show deals report in console 4973 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4974 ) 4975 4976 else: 4977 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4978 raise Exception("Incorrect value") 4979 4980 elif args.history is not None: 4981 if args.output is not None: 4982 trader.historyFile = args.output 4983 4984 if 0 <= len(args.history) < 3: 4985 dataReceived = trader.History( 4986 start=args.history[0] if len(args.history) >= 1 else None, 4987 end=args.history[1] if len(args.history) == 2 else None, 4988 interval="hour" if args.interval is None or not args.interval else args.interval, 4989 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4990 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4991 show=True, # shows all downloaded candles in console 4992 ) 4993 4994 if args.render_chart is not None and dataReceived is not None: 4995 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4996 4997 trader.ShowHistoryChart( 4998 candles=dataReceived, 4999 interact=iChart, 5000 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5001 ) 5002 5003 else: 5004 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5005 raise Exception("Incorrect value") 5006 5007 elif args.load_history is not None: 5008 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5009 5010 if args.render_chart is not None and histData is not None: 5011 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5012 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5013 5014 trader.ShowHistoryChart( 5015 candles=histData, 5016 interact=iChart, 5017 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5018 ) 5019 5020 elif args.trade is not None: 5021 if 1 <= len(args.trade) <= 5: 5022 trader.Trade( 5023 operation=args.trade[0], 5024 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5025 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5026 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5027 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5028 ) 5029 5030 else: 5031 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5032 5033 elif args.buy is not None: 5034 if 0 <= len(args.buy) <= 4: 5035 trader.Buy( 5036 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5037 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5038 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5039 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5040 ) 5041 5042 else: 5043 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5044 5045 elif args.sell is not None: 5046 if 0 <= len(args.sell) <= 4: 5047 trader.Sell( 5048 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5049 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5050 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5051 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5052 ) 5053 5054 else: 5055 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5056 5057 elif args.order: 5058 if 4 <= len(args.order) <= 7: 5059 trader.Order( 5060 operation=args.order[0], 5061 orderType=args.order[1], 5062 lots=int(args.order[2]), 5063 targetPrice=float(args.order[3]), 5064 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5065 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5066 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5067 ) 5068 5069 else: 5070 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5071 5072 elif args.buy_limit: 5073 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5074 5075 elif args.sell_limit: 5076 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5077 5078 elif args.buy_stop: 5079 if 2 <= len(args.buy_stop) <= 7: 5080 trader.BuyStop( 5081 lots=int(args.buy_stop[0]), 5082 targetPrice=float(args.buy_stop[1]), 5083 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5084 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5085 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5086 ) 5087 5088 else: 5089 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5090 5091 elif args.sell_stop: 5092 if 2 <= len(args.sell_stop) <= 7: 5093 trader.SellStop( 5094 lots=int(args.sell_stop[0]), 5095 targetPrice=float(args.sell_stop[1]), 5096 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5097 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5098 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5099 ) 5100 5101 else: 5102 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5103 5104 # elif args.buy_order_grid is not None: 5105 # # update order grid work with api v2 5106 # if len(args.buy_order_grid) == 2: 5107 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5108 # 5109 # for order in orderParams: 5110 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5111 # 5112 # else: 5113 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5114 # 5115 # elif args.sell_order_grid is not None: 5116 # # update order grid work with api v2 5117 # if len(args.sell_order_grid) >= 2: 5118 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5119 # 5120 # for order in orderParams: 5121 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5122 # 5123 # else: 5124 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5125 5126 elif args.close_order is not None: 5127 trader.CloseOrders(args.close_order) # close only one order 5128 5129 elif args.close_orders is not None: 5130 trader.CloseOrders(args.close_orders) # close list of orders 5131 5132 elif args.close_trade: 5133 if not (args.ticker or args.figi): 5134 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5135 raise Exception("Ticker or FIGI required") 5136 5137 if args.ticker: 5138 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5139 5140 else: 5141 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5142 5143 elif args.close_trades is not None: 5144 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5145 5146 elif args.close_all is not None: 5147 if args.ticker: 5148 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5149 5150 elif args.figi: 5151 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5152 5153 else: 5154 trader.CloseAll(*args.close_all) 5155 5156 elif args.limits: 5157 if args.output is not None: 5158 trader.withdrawalLimitsFile = args.output 5159 5160 trader.OverviewLimits(show=True) 5161 5162 elif args.user_info: 5163 if args.output is not None: 5164 trader.userInfoFile = args.output 5165 5166 trader.OverviewUserInfo(show=True) 5167 5168 elif args.account: 5169 if args.output is not None: 5170 trader.userAccountsFile = args.output 5171 5172 trader.OverviewAccounts(show=True) 5173 5174 else: 5175 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5176 raise Exception("There is no command to execute") 5177 5178 except Exception: 5179 trace = tb.format_exc() 5180 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5181 if e in trace: 5182 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5183 break 5184 5185 uLogger.debug(trace) 5186 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5187 exitCode = 255 # an error occurred, must be open a ticket for this issue 5188 5189 finally: 5190 finish = datetime.now(tzutc()) 5191 5192 if exitCode == 0: 5193 if args.more: 5194 uLogger.debug("All operations were finished success (summary code is 0).") 5195 5196 else: 5197 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5198 os.path.abspath(uLog.defaultLogFile), exitCode, 5199 )) 5200 5201 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5202 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5203 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5204 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5205 )) 5206 uLogger.debug("=-" * 50) 5207 5208 if not kwargs: 5209 sys.exit(exitCode) 5210 5211 else: 5212 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: