TKSBrokerAPI
5215 строк · 294.6 Кб
1# -*- coding: utf-8 -*-
2# Author: Timur Gilmullin
3
4"""
5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
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"""
79This class implements methods to work with Tinkoff broker server.
80
81Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
82
83About `token`: https://tinkoff.github.io/investAPI/token/
84"""
85def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
86"""
87Main 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.
91Also, 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`.
93True by default. Cache is auto-update if new day has come.
94If 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"""
97if token is None or not token:
98try:
99self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
100uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
101
102except KeyError:
103uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
104raise Exception("Token required")
105
106else:
107self.token = token # highly priority than environment variable 'TKS_API_TOKEN'
108uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
109
110if accountId is None or not accountId:
111try:
112self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
113uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
114
115except KeyError:
116uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
117
118else:
119self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID'
120uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
121
122self.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
125Latest version: https://pypi.org/project/tksbrokerapi/
126"""
127
128self.aliases = TKS_TICKER_ALIASES
129"""Some aliases instead official tickers.
130
131See also: `TKSEnums.TKS_TICKER_ALIASES`
132"""
133
134self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init
135
136self.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
138self._ticker = ""
139"""String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
140
141Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
142More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
143
144See also: `SearchByTicker()`, `SearchInstruments()`.
145"""
146
147self._figi = ""
148"""String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
149
150See also: `SearchByFIGI()`, `SearchInstruments()`.
151"""
152
153self.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
156See also: `GetCurrentPrices()`.
157"""
158
159self.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
162See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
163"""
164
165uLogger.debug("Broker API server: {}".format(self.server))
166
167self.timeout = 15
168"""Server operations timeout in seconds. Default: `15`.
169
170See also: `SendAPIRequest()`.
171"""
172
173self.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
181See also: `SendAPIRequest()`.
182"""
183
184self.body = None
185"""Request body which send to broker server. Default: `None`.
186
187See also: `SendAPIRequest()`.
188"""
189
190self.moreDebug = False
191"""Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
192
193self.useHTMLReports = False
194"""
195If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
196
197See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
198"""
199
200self.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
203See also: `History()`.
204"""
205
206self.htmlHistoryFile = "index.html"
207"""Full path to the html file where rendered candles chart stored. Default: `index.html`.
208
209See also: `ShowHistoryChart()`.
210"""
211
212self.instrumentsFile = "instruments.md"
213"""Filename where full available to user instruments list will be saved. Default: `instruments.md`.
214
215See also: `ShowInstrumentsInfo()`.
216"""
217
218self.searchResultsFile = "search-results.md"
219"""Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
220
221See also: `SearchInstruments()`.
222"""
223
224self.pricesFile = "prices.md"
225"""Filename where prices of selected instruments will be saved. Default: `prices.md`.
226
227See also: `GetListOfPrices()`.
228"""
229
230self.infoFile = "info.md"
231"""Filename where prices of selected instruments will be saved. Default: `prices.md`.
232
233See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
234"""
235
236self.bondsXLSXFile = "ext-bonds.xlsx"
237"""Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
238bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
239
240See also: `ExtendBondsData()`.
241"""
242
243self.calendarFile = "calendar.md"
244"""Filename where bonds payment calendar will be saved. Default: `calendar.md`.
245
246Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
247
248See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
249"""
250
251self.overviewFile = "overview.md"
252"""Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
253
254See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
255"""
256
257self.overviewDigestFile = "overview-digest.md"
258"""Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
259
260See also: `Overview()` with parameter `details="digest"`.
261"""
262
263self.overviewPositionsFile = "overview-positions.md"
264"""Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
265
266See also: `Overview()` with parameter `details="positions"`.
267"""
268
269self.overviewOrdersFile = "overview-orders.md"
270"""Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
271
272See also: `Overview()` with parameter `details="orders"`.
273"""
274
275self.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
278See also: `Overview()` with parameter `details="analytics"`.
279"""
280
281self.overviewBondsCalendarFile = "overview-calendar.md"
282"""Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
283
284See also: `Overview()` with parameter `details="calendar"`.
285"""
286
287self.reportFile = "deals.md"
288"""Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
289
290See also: `Deals()`.
291"""
292
293self.withdrawalLimitsFile = "limits.md"
294"""Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
295
296See also: `OverviewLimits()` and `RequestLimits()`.
297"""
298
299self.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
302See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
303"""
304
305self.userAccountsFile = "accounts.md"
306"""Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
307
308See also: `OverviewAccounts()`, `RequestAccounts()`.
309"""
310
311self.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
314Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
315
316See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
317"""
318
319self.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
322See also: `Listing()`, `DumpInstruments()`.
323"""
324
325# trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
326if useCache:
327if os.path.exists(self.iListDumpFile):
328dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time
329curTime = datetime.now(tzutc())
330
331if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
332uLogger.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
334self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file
335
336else:
337self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump
338
339uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
340os.path.abspath(self.iListDumpFile),
341dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
342))
343
344else:
345uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
346self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file
347
348else:
349self.iList = self.Listing() # request new raw instruments data from broker server
350self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile`
351
352self.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
355See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
356"""
357
358@property
359def ticker(self) -> str:
360"""String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
361
362Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
363More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
364
365See also: `SearchByTicker()`, `SearchInstruments()`.
366"""
367return self._ticker
368
369@ticker.setter
370def ticker(self, value):
371"""Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
372
373Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
374More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
375
376See also: `SearchByTicker()`, `SearchInstruments()`.
377"""
378self._ticker = str(value).upper() # Tickers may be upper case only
379
380@property
381def figi(self) -> str:
382"""String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
383
384See also: `SearchByFIGI()`, `SearchInstruments()`.
385"""
386return self._figi
387
388@figi.setter
389def figi(self, value):
390"""Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
391
392See also: `SearchByFIGI()`, `SearchInstruments()`.
393"""
394self._figi = str(value).upper() # FIGI may be upper case only
395
396def _ParseJSON(self, rawData="{}") -> dict:
397"""
398Parse 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"""
403responseJSON = json.loads(rawData) if rawData else {}
404
405if self.moreDebug:
406uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
407
408return responseJSON
409
410def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
411"""
412Send GET or POST request to broker server and receive JSON object.
413
414self.header: must be defining with dictionary of headers.
415self.body: if define then used as request body. None by default.
416self.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"""
423if reqType.upper() not in ("GET", "POST"):
424uLogger.error("You can define request type: `GET` or `POST`!")
425raise Exception("Incorrect value")
426
427if self.moreDebug:
428uLogger.debug("Request parameters:")
429uLogger.debug(" - REST API URL: {}".format(url))
430uLogger.debug(" - request type: {}".format(reqType))
431uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
432uLogger.debug(" - body:\n{}".format(self.body))
433
434# fast hack to avoid all operations with some tickers/FIGI
435responseJSON = {}
436oK = True
437for item in self.exclude:
438if item in url:
439if self.moreDebug:
440uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
441
442oK = False
443break
444
445if oK:
446counter = 0
447response = None
448errMsg = ""
449
450while not response and counter <= retry:
451if reqType == "GET":
452response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
453
454if reqType == "POST":
455response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
456
457if self.moreDebug:
458uLogger.debug("Response:")
459uLogger.debug(" - status code: {}".format(response.status_code))
460uLogger.debug(" - reason: {}".format(response.reason))
461uLogger.debug(" - body length: {}".format(len(response.text)))
462uLogger.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
469if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
470rateLimitWait = int(response.headers["x-ratelimit-reset"])
471uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
472sleep(rateLimitWait)
473
474# Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
475if 400 <= response.status_code < 500:
476msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
477uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg))
478
479if "code" in response.text and "message" in response.text:
480msgDict = self._ParseJSON(rawData=response.text)
481uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
482
483counter = retry + 1 # do not retry for 4xx errors
484
485if 500 <= response.status_code < 600:
486errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
487uLogger.debug(" - not oK, {}".format(errMsg))
488
489if "code" in response.text and "message" in response.text:
490errMsgDict = self._ParseJSON(rawData=response.text)
491uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
492
493counter += 1
494
495if counter <= retry:
496uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
497sleep(pause)
498
499responseJSON = self._ParseJSON(rawData=response.text)
500
501if errMsg:
502uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
503uLogger.error(" - not oK, {}".format(errMsg))
504
505return responseJSON
506
507def _IUpdater(self, iType: str) -> tuple:
508"""
509Request instrument by type from server. See available API methods for instruments:
510Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
511Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
512Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
513Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
514Futures: 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"""
519result = []
520
521if iType in TKS_INSTRUMENTS:
522uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
523
524# all instruments have the same body in API v2 requests:
525self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
526instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
527result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
528
529return iType, result
530
531def _IWrapper(self, kwargs):
532"""
533Wrapper runs instrument's update method `_IUpdater()`.
534It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
535"""
536return self._IUpdater(**kwargs)
537
538def Listing(self) -> dict:
539"""
540Gets 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"""
544uLogger.debug("Requesting all available instruments for current account. Wait, please...")
545uLogger.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.
549iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
550
551poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode
552listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations
553poolUpdater.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
557iList = {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:
560for iType in iList.keys():
561for ticker in iList[iType]:
562iList[iType][ticker]["type"] = iType
563
564if "minPriceIncrement" in iList[iType][ticker].keys():
565iList[iType][ticker]["step"] = NanoToFloat(
566iList[iType][ticker]["minPriceIncrement"]["units"],
567iList[iType][ticker]["minPriceIncrement"]["nano"],
568)
569
570else:
571iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures
572
573return iList
574
575def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
576"""
577Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
578
579See also: `DumpInstruments()`, `Listing()`.
580
581:param forceUpdate: if `True` then at first updates data with `Listing()` method,
582otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
583"""
584if self.iListDumpFile is None or not self.iListDumpFile:
585uLogger.error("Output name of dump file must be defined!")
586raise Exception("Filename required")
587
588if not self.iList or forceUpdate:
589self.iList = self.Listing()
590
591xlsxDumpFile = 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:
594with pd.ExcelWriter(
595path=xlsxDumpFile,
596date_format=TKS_DATE_FORMAT,
597datetime_format=TKS_DATE_TIME_FORMAT,
598mode="w",
599) as writer:
600for iType in TKS_INSTRUMENTS:
601df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary
602df = df[sorted(df)] # sorted by column names
603df = df.applymap(
604lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
605na_action="ignore",
606) # converting numbers from nano-type to float in every cell
607df.to_excel(
608writer,
609sheet_name=iType,
610encoding="UTF-8",
611freeze_panes=(1, 1),
612) # saving as XLSX-file with freeze first row and column as headers
613
614uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
615
616def DumpInstruments(self, forceUpdate: bool = True) -> str:
617"""
618Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
619using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
620
621See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
622
623:param forceUpdate: if `True` then at first updates data with `Listing()` method,
624otherwise 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"""
627if self.iListDumpFile is None or not self.iListDumpFile:
628uLogger.error("Output name of dump file must be defined!")
629raise Exception("Filename required")
630
631if not self.iList or forceUpdate:
632self.iList = self.Listing()
633
634jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string
635with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
636fH.write(jsonDump)
637
638uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
639
640return jsonDump
641
642def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
643"""
644Show information about one instrument defined by json data and prints it in Markdown format.
645
646See 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"""
652splitLine = "| | |\n"
653infoText = ""
654
655if iJSON is not None and iJSON and isinstance(iJSON, dict):
656info = [
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
665if "sector" in iJSON.keys() and iJSON["sector"]:
666info.append("| Sector: | {:<54} |\n".format(iJSON["sector"]))
667
668if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
669info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
670
671info.extend([
672splitLine,
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
677if "isin" in iJSON.keys() and iJSON["isin"]:
678info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"]))
679
680if "classCode" in iJSON.keys():
681info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"]))
682
683info.extend([
684splitLine,
685"| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
686splitLine,
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
692if iJSON["figi"]:
693self._figi = iJSON["figi"]
694iJSON = iJSON | self.RequestTradingStatus()
695
696info.extend([
697splitLine,
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
703info.append(splitLine)
704
705if "type" in iJSON.keys() and iJSON["type"]:
706info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"]))
707
708if "shareType" in iJSON.keys() and iJSON["shareType"]:
709info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
710
711if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
712info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"]))
713
714if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
715info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
716
717if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
718info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
719
720if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
721info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"]))
722
723if "focusType" in iJSON.keys() and iJSON["focusType"]:
724info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"]))
725
726if "assetType" in iJSON.keys() and iJSON["assetType"]:
727info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"]))
728
729if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
730info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"]))
731
732if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
733info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
734
735if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
736info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"]))
737
738if "currency" in iJSON.keys():
739info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"]))
740
741if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
742info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"]))
743
744if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
745info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
746
747if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
748info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
749
750if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
751info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
752
753if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
754info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
755
756if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
757info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
758
759if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
760info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
761
762if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
763info.append("| Perpetual bond: | Yes |\n")
764
765if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
766info.append("| Over-the-counter (OTC) securities: | Yes |\n")
767
768iExt = None
769if iJSON["type"] == "Bonds":
770info.extend([
771splitLine,
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("."),
775iJSON["nominal"]["currency"],
776)),
777])
778
779if "floatingCouponFlag" in iJSON.keys():
780info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
781
782if "amortizationFlag" in iJSON.keys():
783info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
784
785info.append(splitLine)
786
787if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
788info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
789
790if iJSON["figi"]:
791iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data
792
793info.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
799if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
800info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format(
801NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
802iJSON["aciValue"]["currency"]
803)))
804
805if "currentPrice" in iJSON.keys():
806info.append(splitLine)
807
808currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument
809aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency
810
811bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond
812bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond
813bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond
814bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond
815bondChangesDelta = 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
817curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
818curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
819
820info.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(
831iJSON["currentPrice"]["changes"],
832" ({}{:.2f} {})".format(
833"+" if bondChangesDelta > 0 else "",
834bondChangesDelta,
835aciCurrency
836) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
837"+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
838iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
839currency
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
859if "lot" in iJSON.keys():
860info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"]))
861
862if "step" in iJSON.keys() and iJSON["step"] != 0:
863info.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:
866if iJSON["type"] == "Bonds":
867strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar
868info.extend(["\n#", strCalendar])
869
870infoText += "".join(info)
871
872if show:
873uLogger.info("{}".format(infoText))
874
875else:
876uLogger.debug("{}".format(infoText))
877
878if self.infoFile is not None:
879with open(self.infoFile, "w", encoding="UTF-8") as fH:
880fH.write(infoText)
881
882uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
883
884if self.useHTMLReports:
885htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
886with open(htmlFilePath, "w", encoding="UTF-8") as fH:
887fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
888
889uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
890
891return infoText
892
893def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
894"""
895Search 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"""
901tickerJSON = {}
902if self.moreDebug:
903uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
904
905if not self._ticker:
906uLogger.warning("self._ticker variable is not be empty!")
907
908else:
909if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
910uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
911raise Exception("Instrument not allowed")
912
913if not self.iList:
914self.iList = self.Listing()
915
916if self._ticker in self.iList["Shares"].keys():
917tickerJSON = self.iList["Shares"][self._ticker]
918if self.moreDebug:
919uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
920
921elif self._ticker in self.iList["Currencies"].keys():
922tickerJSON = self.iList["Currencies"][self._ticker]
923if self.moreDebug:
924uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
925
926elif self._ticker in self.iList["Bonds"].keys():
927tickerJSON = self.iList["Bonds"][self._ticker]
928if self.moreDebug:
929uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
930
931elif self._ticker in self.iList["Etfs"].keys():
932tickerJSON = self.iList["Etfs"][self._ticker]
933if self.moreDebug:
934uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
935
936elif self._ticker in self.iList["Futures"].keys():
937tickerJSON = self.iList["Futures"][self._ticker]
938if self.moreDebug:
939uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
940
941if tickerJSON:
942self._figi = tickerJSON["figi"]
943
944if requestPrice:
945tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
946
947if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
948tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
949
950else:
951tickerJSON["currentPrice"]["changes"] = 0
952
953if show:
954self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text
955
956else:
957if show:
958uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
959
960return tickerJSON
961
962def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
963"""
964Search 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"""
970figiJSON = {}
971if self.moreDebug:
972uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
973
974if not self._figi:
975uLogger.warning("self._figi variable is not be empty!")
976
977else:
978if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
979uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
980raise Exception("Instrument not allowed")
981
982if not self.iList:
983self.iList = self.Listing()
984
985for item in self.iList["Shares"].keys():
986if self._figi == self.iList["Shares"][item]["figi"]:
987figiJSON = self.iList["Shares"][item]
988
989if self.moreDebug:
990uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
991
992break
993
994if not figiJSON:
995for item in self.iList["Currencies"].keys():
996if self._figi == self.iList["Currencies"][item]["figi"]:
997figiJSON = self.iList["Currencies"][item]
998
999if self.moreDebug:
1000uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1001
1002break
1003
1004if not figiJSON:
1005for item in self.iList["Bonds"].keys():
1006if self._figi == self.iList["Bonds"][item]["figi"]:
1007figiJSON = self.iList["Bonds"][item]
1008
1009if self.moreDebug:
1010uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1011
1012break
1013
1014if not figiJSON:
1015for item in self.iList["Etfs"].keys():
1016if self._figi == self.iList["Etfs"][item]["figi"]:
1017figiJSON = self.iList["Etfs"][item]
1018
1019if self.moreDebug:
1020uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1021
1022break
1023
1024if not figiJSON:
1025for item in self.iList["Futures"].keys():
1026if self._figi == self.iList["Futures"][item]["figi"]:
1027figiJSON = self.iList["Futures"][item]
1028
1029if self.moreDebug:
1030uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1031
1032break
1033
1034if figiJSON:
1035self._figi = figiJSON["figi"]
1036self._ticker = figiJSON["ticker"]
1037
1038if requestPrice:
1039figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1040
1041if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1042figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1043
1044else:
1045figiJSON["currentPrice"]["changes"] = 0
1046
1047if show:
1048self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text
1049
1050else:
1051if show:
1052uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1053
1054return figiJSON
1055
1056def GetCurrentPrices(self, show: bool = True) -> dict:
1057"""
1058Get 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
1079See also: `SearchByTicker()` and `SearchByFIGI()`.
1080REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1081Response 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": [....]}`.
1085If an error occurred then returns an empty record:
1086`{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1087"""
1088prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1089
1090if self.depth < 1:
1091uLogger.error("Depth of Market (DOM) must be >=1!")
1092raise Exception("Incorrect value")
1093
1094if not (self._ticker or self._figi):
1095uLogger.error("self._ticker or self._figi variables must be defined!")
1096raise Exception("Ticker or FIGI required")
1097
1098if self._ticker and not self._figi:
1099instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion!
1100self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1101
1102if not self._ticker and self._figi:
1103instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion!
1104self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1105
1106if not self._figi:
1107uLogger.error("FIGI is not defined!")
1108raise Exception("Ticker or FIGI required")
1109
1110else:
1111uLogger.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
1114priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1115self.body = str({"figi": self._figi, "depth": self.depth})
1116pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1117
1118if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1119# list of dicts with sellers orders:
1120prices["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:
1123prices["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:
1126prices["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:
1129prices["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:
1132prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1133
1134# last close price of instrument:
1135prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1136
1137else:
1138uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1139uLogger.debug("Server response: {}".format(pricesResponse))
1140
1141if show:
1142if prices["buy"] or prices["sell"]:
1143info = [
1144"Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1145datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1146self._ticker,
1147self._figi,
1148self.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
1157if not prices["buy"]:
1158info.append(" | No orders!\n")
1159sumBuy = 0
1160
1161else:
1162sumBuy = sum([x["quantity"] for x in prices["buy"]])
1163maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1164for item in maxMinSorted:
1165info.append(" | {} ({})\n".format(item["price"], item["quantity"]))
1166
1167if not prices["sell"]:
1168info.append("No orders! |\n")
1169sumSell = 0
1170
1171else:
1172sumSell = sum([x["quantity"] for x in prices["sell"]])
1173for item in prices["sell"]:
1174info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1175
1176info.extend([
1177"-" * 60, "\n",
1178"{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1179"-" * 60, "\n",
1180])
1181
1182infoText = "".join(info)
1183
1184uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1185
1186else:
1187uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1188
1189return prices
1190
1191def ShowInstrumentsInfo(self, show: bool = True) -> str:
1192"""
1193This method get and show information about all available broker instruments for current user account.
1194If `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"""
1199if not self.iList:
1200self.iList = self.Listing()
1201
1202info = [
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:
1208for iType in self.iList.keys():
1209info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1210
1211headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n"
1212splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1213
1214# generating info tables with all instruments by type:
1215for iType in self.iList.keys():
1216info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1217
1218for instrument in self.iList[iType].keys():
1219iName = self.iList[iType][instrument]["name"] # instrument's name
1220if len(iName) > 57:
1221iName = "{}...".format(iName[:54]) # right trim for a long string
1222
1223info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1224self.iList[iType][instrument]["ticker"],
1225iName,
1226self.iList[iType][instrument]["figi"],
1227self.iList[iType][instrument]["currency"],
1228self.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
1232infoText = "".join(info)
1233
1234if show:
1235uLogger.info(infoText)
1236
1237if self.instrumentsFile:
1238with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1239fH.write(infoText)
1240
1241uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1242
1243if self.useHTMLReports:
1244htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1245with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1246fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1247
1248uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1249
1250return infoText
1251
1252def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1253"""
1254This method search and show information about instruments by part of its ticker, FIGI or name.
1255If `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"""
1261if not self.iList:
1262self.iList = self.Listing()
1263
1264searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments
1265compiledPattern = re.compile(pattern, re.IGNORECASE)
1266
1267for iType in self.iList:
1268for instrument in self.iList[iType].values():
1269searchResult = compiledPattern.search(" ".join(
1270[instrument["ticker"], instrument["figi"], instrument["name"]]
1271))
1272
1273if searchResult:
1274searchResults[iType][instrument["ticker"]] = instrument
1275
1276resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1277info = [
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]
1284infoShort = info[:]
1285
1286headerLine = "| Type | Ticker | Full name | FIGI |\n"
1287splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1288skippedLine = "| ... | ... | ... | ... |\n"
1289
1290if resultsLen == 0:
1291info.append("\nNo results\n")
1292infoShort.append("\nNo results\n")
1293uLogger.warning("No results. Try changing your search pattern.")
1294
1295else:
1296for iType in searchResults:
1297iTypeValuesCount = len(searchResults[iType].values())
1298if iTypeValuesCount > 0:
1299info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1300infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1301
1302for instrument in searchResults[iType].values():
1303info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1304instrument["type"],
1305instrument["ticker"],
1306"{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string
1307instrument["figi"],
1308))
1309
1310if iTypeValuesCount <= 5:
1311infoShort.extend(info[-iTypeValuesCount:])
1312
1313else:
1314infoShort.extend(info[-5:])
1315infoShort.append(skippedLine)
1316
1317infoText = "".join(info)
1318infoTextShort = "".join(infoShort)
1319
1320if show:
1321uLogger.info(infoTextShort)
1322uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1323
1324if self.searchResultsFile:
1325with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1326fH.write(infoText)
1327
1328uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1329
1330if self.useHTMLReports:
1331htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1332with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1333fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1334
1335uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1336
1337return searchResults
1338
1339def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1340"""
1341Creating 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"""
1346requestedInstruments = []
1347for iName in instruments:
1348if iName not in self.aliases.keys():
1349if iName not in requestedInstruments:
1350requestedInstruments.append(iName)
1351
1352else:
1353if iName not in requestedInstruments:
1354if self.aliases[iName] not in requestedInstruments:
1355requestedInstruments.append(self.aliases[iName])
1356
1357uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1358
1359onlyUniqueFIGIs = []
1360for iName in requestedInstruments:
1361if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1362continue
1363
1364self._ticker = iName
1365iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker
1366
1367if not iData:
1368self._ticker = ""
1369self._figi = iName
1370
1371iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI
1372
1373if not iData:
1374self._figi = ""
1375uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1376
1377if iData and iData["figi"] not in onlyUniqueFIGIs:
1378onlyUniqueFIGIs.append(iData["figi"])
1379
1380uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1381
1382return onlyUniqueFIGIs
1383
1384def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1385"""
1386This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1387
1388See limits: https://tinkoff.github.io/investAPI/limits/
1389
1390If `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}}, {...}, ...]`.
1395One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1396"""
1397if instruments is None or not instruments:
1398uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1399raise Exception("Ticker or FIGI required")
1400
1401onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1402
1403uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1404
1405iList = [] # trying to get info and current prices about all unique instruments:
1406for self._figi in onlyUniqueFIGIs:
1407iData = self.SearchByFIGI(requestPrice=True)
1408iList.append(iData)
1409
1410self.ShowListOfPrices(iList, show)
1411
1412return iList
1413
1414def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1415"""
1416Show table contains current prices of given instruments.
1417
1418:param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1419One 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"""
1423infoText = ""
1424
1425if show or self.pricesFile:
1426info = [
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
1432for item in iList:
1433info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1434item["ticker"],
1435item["figi"],
1436item["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(
1441item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1442item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1443),
1444"{} / {}".format(
1445item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1446item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1447),
1448item["currency"],
1449))
1450
1451infoText = "".join(info)
1452
1453if show:
1454uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1455
1456if self.pricesFile:
1457with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1458fH.write(infoText)
1459
1460uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1461
1462if self.useHTMLReports:
1463htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1464with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1465fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1466
1467uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1468
1469return infoText
1470
1471def RequestTradingStatus(self) -> dict:
1472"""
1473Requesting trading status for the instrument defined by `figi` variable.
1474
1475REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1476
1477Documentation: 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"""
1483if self._figi is None or not self._figi:
1484uLogger.error("Variable `figi` must be defined for using this method!")
1485raise Exception("FIGI required")
1486
1487uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1488
1489self.body = str({"figi": self._figi, "instrumentId": self._figi})
1490tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1491tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1492
1493if self.moreDebug:
1494uLogger.debug("Records about current trading status successfully received")
1495
1496return tradingStatus
1497
1498def RequestPortfolio(self) -> dict:
1499"""
1500Requesting actual user's portfolio for current `accountId`.
1501
1502REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1503
1504Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1505
1506:return: dictionary with user's portfolio.
1507"""
1508if self.accountId is None or not self.accountId:
1509uLogger.error("Variable `accountId` must be defined for using this method!")
1510raise Exception("Account ID required")
1511
1512uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1513
1514self.body = str({"accountId": self.accountId})
1515portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1516rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1517
1518if self.moreDebug:
1519uLogger.debug("Records about user's portfolio successfully received")
1520
1521return rawPortfolio
1522
1523def RequestPositions(self) -> dict:
1524"""
1525Requesting open positions by currencies and instruments for current `accountId`.
1526
1527REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1528
1529Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1530
1531:return: dictionary with open positions by instruments.
1532"""
1533if self.accountId is None or not self.accountId:
1534uLogger.error("Variable `accountId` must be defined for using this method!")
1535raise Exception("Account ID required")
1536
1537uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1538
1539self.body = str({"accountId": self.accountId})
1540positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1541rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1542
1543if self.moreDebug:
1544uLogger.debug("Records about current open positions successfully received")
1545
1546return rawPositions
1547
1548def RequestPendingOrders(self) -> list:
1549"""
1550Requesting current actual pending limit orders for current `accountId`.
1551
1552REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1553
1554Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1555
1556:return: list of dictionaries with pending limit orders.
1557"""
1558if self.accountId is None or not self.accountId:
1559uLogger.error("Variable `accountId` must be defined for using this method!")
1560raise Exception("Account ID required")
1561
1562uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1563
1564self.body = str({"accountId": self.accountId})
1565ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1566rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1567
1568uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1569
1570return rawOrders
1571
1572def RequestStopOrders(self) -> list:
1573"""
1574Requesting current actual stop orders for current `accountId`.
1575
1576REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1577
1578Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1579
1580:return: list of dictionaries with stop orders.
1581"""
1582if self.accountId is None or not self.accountId:
1583uLogger.error("Variable `accountId` must be defined for using this method!")
1584raise Exception("Account ID required")
1585
1586uLogger.debug("Requesting current actual stop orders. Wait, please...")
1587
1588self.body = str({"accountId": self.accountId})
1589ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1590rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1591
1592uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1593
1594return rawStopOrders
1595
1596def Overview(self, show: bool = False, details: str = "full") -> dict:
1597"""
1598Get portfolio: all open positions, orders and some statistics for current `accountId`.
1599If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1600and `overviewBondsCalendarFile` are defined then also save information to file.
1601
1602WARNING! It is not recommended to run this method too many times in a loop! The server receives
1603many requests about the state of the portfolio, and then, based on the received data, a large number
1604of 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"""
1616if self.accountId is None or not self.accountId:
1617uLogger.error("Variable `accountId` must be defined for using this method!")
1618raise Exception("Account ID required")
1619
1620view = {
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
1665details = details.lower()
1666availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1667if details not in availableDetails:
1668details = "full"
1669uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1670
1671uLogger.debug("Requesting portfolio of a client. Wait, please...")
1672
1673portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict)
1674view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict)
1675view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list)
1676view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list)
1677
1678# save response headers without "positions" section:
1679for key in portfolioResponse.keys():
1680if key != "positions":
1681view["raw"]["headers"][key] = portfolioResponse[key]
1682
1683else:
1684continue
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
1688for item in portfolioResponse["positions"]:
1689if item["instrumentType"] == "currency":
1690self._figi = item["figi"]
1691curr = self.SearchByFIGI(requestPrice=False)
1692
1693# current price of currency in RUB:
1694view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1695"name": curr["name"],
1696"currentPrice": NanoToFloat(
1697item["currentPrice"]["units"],
1698item["currentPrice"]["nano"]
1699),
1700}
1701
1702view["raw"]["Currencies"].append(item)
1703
1704elif item["instrumentType"] == "share":
1705view["raw"]["Shares"].append(item)
1706
1707elif item["instrumentType"] == "bond":
1708view["raw"]["Bonds"].append(item)
1709
1710elif item["instrumentType"] == "etf":
1711view["raw"]["Etfs"].append(item)
1712
1713elif item["instrumentType"] == "futures":
1714view["raw"]["Futures"].append(item)
1715
1716else:
1717continue
1718
1719# how many volume of currencies (by ISO currency name) are blocked:
1720for item in view["raw"]["positions"]["blocked"]:
1721blocked = NanoToFloat(item["units"], item["nano"])
1722if blocked > 0:
1723view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1724
1725# how many volume of instruments (by FIGI) are blocked:
1726for item in view["raw"]["positions"]["securities"]:
1727blocked = int(item["blocked"])
1728if blocked > 0:
1729view["stat"]["blockedInstruments"][item["figi"]] = blocked
1730
1731allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1732
1733if "rub" in allBlocked.keys():
1734view["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:
1737view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1738view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1739view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1740view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1741view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1742view["stat"]["portfolioCostRUB"] = sum([
1743view["stat"]["allCurrenciesCostRUB"],
1744view["stat"]["sharesCostRUB"],
1745view["stat"]["bondsCostRUB"],
1746view["stat"]["etfsCostRUB"],
1747view["stat"]["futuresCostRUB"],
1748])
1749
1750# --- calculating some portfolio statistics:
1751byComp = {} # distribution by companies
1752bySect = {} # distribution by sectors
1753byCurr = {} # distribution by currencies (include RUB)
1754unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1755byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries)
1756
1757for item in portfolioResponse["positions"]:
1758self._figi = item["figi"]
1759instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI
1760
1761if instrument:
1762if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1763blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency
1764
1765elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1766blocked = allBlocked[item["figi"]] # blocked volume of other instruments
1767
1768else:
1769blocked = 0
1770
1771volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument
1772lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument
1773direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long
1774curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price
1775average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price
1776profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment
1777currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc.
1778cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset
1779baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub)
1780countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1781costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles
1782percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost
1783
1784statData = {
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:
1808if statData["country"] not in byCountry.keys():
1809byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1810
1811else:
1812byCountry[statData["country"]]["cost"] += costRUB
1813byCountry[statData["country"]]["percent"] += percentCostRUB
1814
1815if item["instrumentType"] != "currency":
1816# adding distribution by unique companies:
1817if statData["name"]:
1818if statData["name"] not in byComp.keys():
1819byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1820
1821else:
1822byComp[statData["name"]]["cost"] += costRUB
1823byComp[statData["name"]]["percent"] += percentCostRUB
1824
1825# adding distribution by unique sectors:
1826if statData["sector"] not in bySect.keys():
1827bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1828
1829else:
1830bySect[statData["sector"]]["cost"] += costRUB
1831bySect[statData["sector"]]["percent"] += percentCostRUB
1832
1833# adding distribution by unique currencies:
1834if currency not in byCurr.keys():
1835byCurr[currency] = {
1836"name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1837"cost": costRUB,
1838"percent": percentCostRUB
1839}
1840
1841else:
1842byCurr[currency]["cost"] += costRUB
1843byCurr[currency]["percent"] += percentCostRUB
1844
1845# saving statistics for every instrument:
1846if item["instrumentType"] == "currency":
1847view["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}}
1851view["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
1858elif item["instrumentType"] == "share":
1859view["stat"]["Shares"].append(statData)
1860
1861elif item["instrumentType"] == "bond":
1862view["stat"]["Bonds"].append(statData)
1863
1864elif item["instrumentType"] == "etf":
1865view["stat"]["Etfs"].append(statData)
1866
1867elif item["instrumentType"] == "Futures":
1868view["stat"]["Futures"].append(statData)
1869
1870else:
1871continue
1872
1873# total changes in Russian Ruble:
1874view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies
1875view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1876startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1877view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1878view["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:
1886uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests
1887uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys
1888
1889for item in view["raw"]["orders"]:
1890self._figi = item["figi"]
1891
1892if item["figi"] not in uniquePendingOrdersFIGIs:
1893instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time
1894
1895uniquePendingOrdersFIGIs.append(item["figi"])
1896uniquePendingOrders[item["figi"]] = instrument
1897
1898else:
1899instrument = uniquePendingOrders[item["figi"]]
1900
1901if instrument:
1902action = TKS_ORDER_DIRECTIONS[item["direction"]]
1903orderType = TKS_ORDER_TYPES[item["orderType"]]
1904orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1905orderDate = 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):
1908if item["direction"] == "ORDER_DIRECTION_BUY":
1909lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1910
1911else:
1912lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1913
1914# requested price for order execution:
1915target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1916
1917# necessary changes in percent to reach target from current price:
1918changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1919
1920view["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:
1938uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests
1939uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys
1940
1941for item in view["raw"]["stopOrders"]:
1942self._figi = item["figi"]
1943
1944if item["figi"] not in uniqueStopOrdersFIGIs:
1945instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time
1946
1947uniqueStopOrdersFIGIs.append(item["figi"])
1948uniqueStopOrders[item["figi"]] = instrument
1949
1950else:
1951instrument = uniqueStopOrders[item["figi"]]
1952
1953if instrument:
1954action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1955orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1956createDate = 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
1959if "expirationTime" in item.keys():
1960expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1961expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1962
1963else:
1964expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1965expDate = 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):
1968if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1969lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1970
1971else:
1972lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1973
1974# requested price when stop-order executed:
1975target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1976
1977# price for limit-order, set up when stop-order executed:
1978limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1979
1980# necessary changes in percent to reach target from current price:
1981changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1982
1983view["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:
2003view["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:
2037view["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}
2042view["analytics"]["distrByCompanies"].update(byComp)
2043
2044# portfolio distribution by sectors:
2045view["analytics"]["distrBySectors"]["All money cash"] = {
2046"cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2047"percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2048}
2049view["analytics"]["distrBySectors"].update(bySect)
2050
2051# portfolio distribution by currencies:
2052if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2053view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2054
2055if self.moreDebug:
2056uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2057
2058view["analytics"]["distrByCurrencies"].update(byCurr)
2059view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2060view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2061
2062# portfolio distribution by countries:
2063if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2064view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2065
2066if self.moreDebug:
2067uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2068
2069view["analytics"]["distrByCountries"].update(byCountry)
2070view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2071view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2072
2073# --- Prepare text statistics overview in human-readable:
2074if show:
2075actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2076
2077# Whatever the value `details`, header not changes:
2078info = [
2079"# Client's portfolio\n\n",
2080"* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2081"* **Account ID:** [{}]\n".format(self.accountId),
2082]
2083
2084if details in ["full", "positions", "digest"]:
2085info.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 "",
2089view["stat"]["totalChangesRUB"],
2090"+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2091view["stat"]["totalChangesPercentRUB"],
2092),
2093])
2094
2095if details in ["full", "positions"]:
2096info.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(
2102view["stat"]["availableRUB"],
2103view["stat"]["blockedRUB"],
2104)
2105)
2106])
2107
2108def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2109return [
2110"| | | | | | | |\n",
2111"| {:<27} | | | | | {:>19} | |\n".format(
2112noTradeStr if noTradeStr else typeStr,
2113"" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2114),
2115]
2116
2117def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2118return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2119"{} [{}]".format(data["ticker"], data["figi"]),
2120"{:.2f} ({:.2f}) {}".format(
2121data["volume"],
2122data["blocked"],
2123data["currency"],
2124) if showCurrencyName else "{:.0f} ({:.0f})".format(
2125data["volume"],
2126data["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 "",
2134data["profit"], data["baseCurrencyName"],
2135"+" if data["percentProfit"] > 0 else "",
2136data["percentProfit"],
2137),
2138)
2139
2140# --- Show currencies section:
2141if view["stat"]["Currencies"]:
2142info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2143for item in view["stat"]["Currencies"]:
2144info.append(_InfoStr(item, showCurrencyName=True))
2145
2146else:
2147info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2148
2149# --- Show shares section:
2150if view["stat"]["Shares"]:
2151info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2152
2153for item in view["stat"]["Shares"]:
2154info.append(_InfoStr(item))
2155
2156else:
2157info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2158
2159# --- Show bonds section:
2160if view["stat"]["Bonds"]:
2161info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2162
2163for item in view["stat"]["Bonds"]:
2164info.append(_InfoStr(item))
2165
2166else:
2167info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2168
2169# --- Show etfs section:
2170if view["stat"]["Etfs"]:
2171info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2172
2173for item in view["stat"]["Etfs"]:
2174info.append(_InfoStr(item))
2175
2176else:
2177info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2178
2179# --- Show futures section:
2180if view["stat"]["Futures"]:
2181info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2182
2183for item in view["stat"]["Futures"]:
2184info.append(_InfoStr(item))
2185
2186else:
2187info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2188
2189if details in ["full", "orders"]:
2190# --- Show pending limit orders section:
2191if view["stat"]["orders"]:
2192info.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
2198for item in view["stat"]["orders"]:
2199info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2200"{} [{}]".format(item["ticker"], item["figi"]),
2201item["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"])),
2205item["baseCurrencyName"],
2206"+" if item["percentChanges"] > 0 else "",
2207float(item["percentChanges"]),
2208),
2209"{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2210item["action"],
2211item["type"],
2212item["date"],
2213))
2214
2215else:
2216info.append("\n## Total pending limit-orders: [0]\n")
2217
2218# --- Show stop orders section:
2219if view["stat"]["stopOrders"]:
2220info.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
2226for item in view["stat"]["stopOrders"]:
2227info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2228"{} [{}]".format(item["ticker"], item["figi"]),
2229item["orderID"],
2230item["lotsRequested"],
2231"{} {} ({}{:.2f}%)".format(
2232"{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2233item["baseCurrencyName"],
2234"+" if item["percentChanges"] > 0 else "",
2235float(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"],
2239item["action"],
2240item["type"],
2241item["expType"],
2242item["createDate"],
2243item["expDate"],
2244))
2245
2246else:
2247info.append("\n## Total stop-orders: [0]\n")
2248
2249if details in ["full", "analytics"]:
2250# -- Show analytics section:
2251if view["stat"]["portfolioCostRUB"] > 0:
2252info.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 "",
2258view["stat"]["totalChangesRUB"],
2259"+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2260view["stat"]["totalChangesPercentRUB"],
2261),
2262"\n## Portfolio distribution by assets\n"
2263"\n| Type | Uniques | Percent | Current cost |\n",
2264"|------------------------------------|---------|---------|--------------------|\n",
2265])
2266
2267for key in view["analytics"]["distrByAssets"].keys():
2268if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2269info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2270key,
2271view["analytics"]["distrByAssets"][key]["uniques"],
2272"{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2273"{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2274))
2275
2276aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2277
2278info.extend([
2279"\n## Portfolio distribution by companies\n"
2280"\n| Company | Percent | Current cost |\n",
2281aSepLine,
2282])
2283
2284for company in view["analytics"]["distrByCompanies"].keys():
2285if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2286info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2287"{}{}".format(
2288"[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2289company,
2290),
2291"{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2292"{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2293))
2294
2295info.extend([
2296"\n## Portfolio distribution by sectors\n"
2297"\n| Sector | Percent | Current cost |\n",
2298aSepLine,
2299])
2300
2301for sector in view["analytics"]["distrBySectors"].keys():
2302if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2303info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2304sector,
2305"{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2306"{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2307))
2308
2309info.extend([
2310"\n## Portfolio distribution by currencies\n"
2311"\n| Instruments currencies | Percent | Current cost |\n",
2312aSepLine,
2313])
2314
2315for curr in view["analytics"]["distrByCurrencies"].keys():
2316if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2317info.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
2323info.extend([
2324"\n## Portfolio distribution by countries\n"
2325"\n| Assets by country | Percent | Current cost |\n",
2326aSepLine,
2327])
2328
2329for country in view["analytics"]["distrByCountries"].keys():
2330if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2331info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2332country,
2333"{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2334"{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2335))
2336
2337if details in ["full", "calendar"]:
2338# -- Show bonds payment calendar section:
2339if view["stat"]["Bonds"]:
2340bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2341view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2342info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2343
2344else:
2345info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2346
2347infoText = "".join(info)
2348
2349uLogger.info(infoText)
2350
2351if details == "full" and self.overviewFile:
2352filename = self.overviewFile
2353
2354elif details == "digest" and self.overviewDigestFile:
2355filename = self.overviewDigestFile
2356
2357elif details == "positions" and self.overviewPositionsFile:
2358filename = self.overviewPositionsFile
2359
2360elif details == "orders" and self.overviewOrdersFile:
2361filename = self.overviewOrdersFile
2362
2363elif details == "analytics" and self.overviewAnalyticsFile:
2364filename = self.overviewAnalyticsFile
2365
2366elif details == "calendar" and self.overviewBondsCalendarFile:
2367filename = self.overviewBondsCalendarFile
2368
2369else:
2370filename = ""
2371
2372if filename:
2373with open(filename, "w", encoding="UTF-8") as fH:
2374fH.write(infoText)
2375
2376uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2377
2378if self.useHTMLReports:
2379htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2380with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2381fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="", commonCSS=COMMON_CSS, markdown=infoText))
2382
2383uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2384
2385return view
2386
2387def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2388"""
2389Returns history operations between two given dates for current `accountId`.
2390If `reportFile` string is not empty then also save human-readable report.
2391Shows 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):
2398https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2399and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2400"""
2401if self.accountId is None or not self.accountId:
2402uLogger.error("Variable `accountId` must be defined for using this method!")
2403raise Exception("Account ID required")
2404
2405startDate, 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
2407uLogger.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
2410dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2411self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2412ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker
2413customStat = {} # custom statistics in additional to responseJSON
2414
2415# --- output report in human-readable format:
2416if show or self.reportFile:
2417splitLine1 = "| | | | | |\n" # Summary section
2418splitLine2 = "| | | | | | | | |\n" # Operations section
2419nextDay = ""
2420
2421info = ["# 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
2423if len(ops) > 0:
2424customStat = {
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:
2441for item in ops:
2442if item["state"] == "OPERATION_STATE_EXECUTED":
2443payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2444
2445# count buy operations:
2446if "_BUY" in item["operationType"]:
2447customStat["buyCount"] += 1
2448
2449if item["payment"]["currency"] in customStat["buyTotal"].keys():
2450customStat["buyTotal"][item["payment"]["currency"]] += payment
2451
2452else:
2453customStat["buyTotal"][item["payment"]["currency"]] = payment
2454
2455# count sell operations:
2456elif "_SELL" in item["operationType"]:
2457customStat["sellCount"] += 1
2458
2459if item["payment"]["currency"] in customStat["sellTotal"].keys():
2460customStat["sellTotal"][item["payment"]["currency"]] += payment
2461
2462else:
2463customStat["sellTotal"][item["payment"]["currency"]] = payment
2464
2465# count incoming operations:
2466elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2467if item["payment"]["currency"] in customStat["payIn"].keys():
2468customStat["payIn"][item["payment"]["currency"]] += payment
2469
2470else:
2471customStat["payIn"][item["payment"]["currency"]] = payment
2472
2473# count withdrawals operations:
2474elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2475if item["payment"]["currency"] in customStat["payOut"].keys():
2476customStat["payOut"][item["payment"]["currency"]] += payment
2477
2478else:
2479customStat["payOut"][item["payment"]["currency"]] = payment
2480
2481# count dividends income:
2482elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2483if item["payment"]["currency"] in customStat["divs"].keys():
2484customStat["divs"][item["payment"]["currency"]] += payment
2485
2486else:
2487customStat["divs"][item["payment"]["currency"]] = payment
2488
2489# count coupon's income:
2490elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2491if item["payment"]["currency"] in customStat["coupons"].keys():
2492customStat["coupons"][item["payment"]["currency"]] += payment
2493
2494else:
2495customStat["coupons"][item["payment"]["currency"]] = payment
2496
2497# count broker commissions:
2498elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2499if item["payment"]["currency"] in customStat["brokerCom"].keys():
2500customStat["brokerCom"][item["payment"]["currency"]] += payment
2501
2502else:
2503customStat["brokerCom"][item["payment"]["currency"]] = payment
2504
2505# count service commissions:
2506elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2507if item["payment"]["currency"] in customStat["serviceCom"].keys():
2508customStat["serviceCom"][item["payment"]["currency"]] += payment
2509
2510else:
2511customStat["serviceCom"][item["payment"]["currency"]] = payment
2512
2513# count margin commissions:
2514elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2515if item["payment"]["currency"] in customStat["marginCom"].keys():
2516customStat["marginCom"][item["payment"]["currency"]] += payment
2517
2518else:
2519customStat["marginCom"][item["payment"]["currency"]] = payment
2520
2521# count withholding taxes:
2522elif "_TAX" in item["operationType"]:
2523if item["payment"]["currency"] in customStat["allTaxes"].keys():
2524customStat["allTaxes"][item["payment"]["currency"]] += payment
2525
2526else:
2527customStat["allTaxes"][item["payment"]["currency"]] = payment
2528
2529else:
2530continue
2531
2532customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2533
2534# --- view "Actions" lines:
2535info.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
2549opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2550for key in opsKeys:
2551if key == "rub":
2552continue
2553
2554info.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
2563info.append(splitLine1)
2564
2565def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2566return "| | {:<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:
2574info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n")
2575paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2576
2577for key in paymentsKeys:
2578info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2579
2580info.append(splitLine1)
2581
2582# --- view "Commissions and taxes" lines:
2583info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n")
2584comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2585
2586for key in comKeys:
2587info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2588
2589info.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
2595else:
2596info.append("Broker returned no operations during this period\n")
2597
2598# --- view "Operations" section:
2599for item in ops:
2600if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2601continue
2602
2603else:
2604self._figi = item["figi"] if item["figi"] else ""
2605payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2606instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2607
2608# group of deals during one day:
2609if nextDay and item["date"].split("T")[0] != nextDay:
2610info.append(splitLine2)
2611nextDay = ""
2612
2613else:
2614nextDay = item["date"].split("T")[0] # saving current day for splitting
2615
2616info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2617item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2618self._figi if self._figi else "—",
2619instrument["ticker"] if instrument else "—",
2620instrument["type"] if instrument else "—",
2621item["quantity"] if int(item["quantity"]) > 0 else "—",
2622"{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2623TKS_OPERATION_STATES[item["state"]],
2624TKS_OPERATION_TYPES[item["operationType"]],
2625))
2626
2627infoText = "".join(info)
2628
2629if show:
2630if self.moreDebug:
2631uLogger.debug("Records about history of a client's operations successfully received")
2632
2633uLogger.info(infoText)
2634
2635if self.reportFile:
2636with open(self.reportFile, "w", encoding="UTF-8") as fH:
2637fH.write(infoText)
2638
2639uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2640
2641if self.useHTMLReports:
2642htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2643with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2644fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2645
2646uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2647
2648return ops, customStat
2649
2650def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2651"""
2652This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2653
2654History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2655Warning! Broker server used ISO UTC time by default.
2656
2657If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2658Also, `historyFile` used to update history with `onlyMissing` parameter.
2659
2660See 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`.
2667False by default. Warning! History appends only from last candle to current time
2668with 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"""
2674strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2675headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers
2676history = None # empty pandas object for history
2677
2678if interval not in TKS_CANDLE_INTERVALS.keys():
2679uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2680raise Exception("Incorrect value")
2681
2682if not (self._ticker or self._figi):
2683uLogger.error("Ticker or FIGI must be defined!")
2684raise Exception("Ticker or FIGI required")
2685
2686if self._ticker and not self._figi:
2687instrumentByTicker = self.SearchByTicker(requestPrice=False)
2688self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2689
2690if self._figi and not self._ticker:
2691instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2692self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2693
2694dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string
2695dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string
2696if interval.lower() != "day":
2697dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2698
2699delta = dtEnd - dtStart # current UTC time minus last time in file
2700deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates
2701
2702# calculate history length in candles:
2703length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2704if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2705length += 1 # to avoid fraction time
2706
2707# calculate data blocks count:
2708blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2709
2710uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2711uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2712uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2713uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2714uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2715
2716tempOld = None # pandas object for old history, if --only-missing key present
2717lastTime = None # datetime object of last old candle in file
2718
2719if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2720uLogger.debug("--only-missing key present, add only last missing candles...")
2721uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2722
2723tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2724
2725tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is"
2726tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string
2727tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is"
2728tempOld["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:
2731if len(tempOld) > 0:
2732lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2733
2734else:
2735lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day
2736
2737tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time
2738
2739responseJSONs = [] # raw history blocks of data
2740
2741blockEnd = dtEnd
2742for item in range(blocks):
2743tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2744blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2745
2746uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2747item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2748))
2749
2750if blockStart == blockEnd:
2751uLogger.debug("Skipped this zero-length block...")
2752
2753else:
2754# REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2755historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2756self.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})
2762responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2763
2764if "code" in responseJSON.keys():
2765uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2766
2767else:
2768if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2769responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request
2770
2771responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates
2772
2773blockEnd = blockStart
2774
2775printCount = len(responseJSONs) # candles to show in console
2776if responseJSONs:
2777tempHistory = pd.DataFrame(
2778data={
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},
2787index=range(len(responseJSONs)),
2788columns=["date", "time", "open", "high", "low", "close", "volume"],
2789)
2790tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2791tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2792
2793# append only newest candles to old history if --only-missing key present:
2794if onlyMissing and tempOld is not None and lastTime is not None:
2795index = 0 # find start index in tempHistory data:
2796
2797for i, item in tempHistory.iterrows():
2798curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2799
2800if curTime == lastTime:
2801uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2802index = i
2803printCount = index + 1
2804break
2805
2806history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2807
2808else:
2809history = tempHistory # if no `--only-missing` key then load full data from server
2810
2811uLogger.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
2813if history is not None and not history.empty:
2814if show:
2815uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2816strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2817pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2818))
2819
2820else:
2821uLogger.warning("Received an empty candles history!")
2822
2823if self.historyFile is not None:
2824if history is not None and not history.empty:
2825history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2826uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2827
2828else:
2829uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2830
2831else:
2832uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2833
2834return history
2835
2836def LoadHistory(self, filePath: str) -> pd.DataFrame:
2837"""
2838Load candles history from csv-file and return Pandas DataFrame object.
2839
2840See also: `History()` and `ShowHistoryChart()` methods.
2841
2842:param filePath: path to csv-file to open.
2843"""
2844loadedHistory = None # init candles data object
2845
2846uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2847
2848if os.path.exists(filePath):
2849loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame
2850
2851tfStr = self.priceModel.FormattedDelta(
2852self.priceModel.timeframe,
2853"{days} days {hours}h {minutes}m {seconds}s",
2854) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2855self.priceModel.timeframe,
2856"{hours}h {minutes}m {seconds}s",
2857)
2858
2859if loadedHistory is not None and not loadedHistory.empty:
2860uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2861len(loadedHistory),
2862tfStr,
2863pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2864)
2865
2866else:
2867uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2868
2869else:
2870uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2871
2872return loadedHistory
2873
2874def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2875"""
2876Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2877
2878Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2879Default: `index.html` (both for interact and non-interact candlesticks chart).
2880
2881See 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.
2885See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2886If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2887See 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
2889html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2890"""
2891if isinstance(candles, str):
2892self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file
2893self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator
2894
2895elif isinstance(candles, pd.DataFrame):
2896self.priceModel.prices = candles # set candles chain from variable
2897self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2898
2899if "datetime" not in candles.columns:
2900self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time
2901
2902else:
2903uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2904raise Exception("Incorrect value")
2905
2906self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator
2907
2908if interact:
2909uLogger.debug("Rendering interactive candles chart. Wait, please...")
2910
2911self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2912
2913else:
2914uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2915
2916self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2917
2918uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2919
2920def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2921"""
2922Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2923If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2924
2925See 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,
2932it is a string with format `%Y-%m-%d %H:%M:%S`.
2933:return: JSON with response from broker server.
2934"""
2935if self.accountId is None or not self.accountId:
2936uLogger.error("Variable `accountId` must be defined for using this method!")
2937raise Exception("Account ID required")
2938
2939if operation is None or not operation or operation not in ("Buy", "Sell"):
2940uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2941raise Exception("Incorrect value")
2942
2943if lots is None or lots < 1:
2944uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2945lots = 1
2946
2947if tp is None or tp < 0:
2948tp = 0
2949
2950if sl is None or sl < 0:
2951sl = 0
2952
2953if expDate is None or not expDate:
2954expDate = "Undefined"
2955
2956if not (self._ticker or self._figi):
2957uLogger.error("Ticker or FIGI must be defined!")
2958raise Exception("Ticker or FIGI required")
2959
2960instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2961self._ticker = instrument["ticker"]
2962self._figi = instrument["figi"]
2963
2964uLogger.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
2966openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2967self.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})
2974response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2975
2976if "orderId" in response.keys():
2977uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2978operation, response["orderId"],
2979self._ticker, self._figi, lots,
2980NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2981NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2982NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2983))
2984
2985if tp > 0:
2986self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2987
2988if sl > 0:
2989self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2990
2991else:
2992uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
2993
2994return response
2995
2996def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2997"""
2998More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2999If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3000
3001See 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.
3007String has a format like this: `%Y-%m-%d %H:%M:%S`.
3008:return: JSON with response from broker server.
3009"""
3010return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3011
3012def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3013"""
3014More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3015If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3016
3017See 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.
3023String has a format like this: `%Y-%m-%d %H:%M:%S`.
3024:return: JSON with response from broker server.
3025"""
3026return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3027
3028def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3029"""
3030Close 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.
3034This avoids unnecessary downloading data from the server.
3035"""
3036if instruments is None or not instruments:
3037uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3038raise Exception("Ticker or FIGI required")
3039
3040if isinstance(instruments, str):
3041instruments = [instruments]
3042
3043uniqueInstruments = self.GetUniqueFIGIs(instruments)
3044if uniqueInstruments:
3045if portfolio is None or not portfolio:
3046portfolio = self.Overview(show=False)
3047
3048allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3049uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3050
3051for self._figi in uniqueInstruments:
3052if self._figi not in allOpened:
3053uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3054continue
3055
3056# search open trade info about instrument by ticker:
3057instrument = {}
3058for iType in TKS_INSTRUMENTS:
3059if instrument:
3060break
3061
3062for item in portfolio["stat"][iType]:
3063if item["figi"] == self._figi:
3064instrument = item
3065break
3066
3067if instrument:
3068self._ticker = instrument["ticker"]
3069self._figi = instrument["figi"]
3070
3071uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3072self._ticker,
3073self._figi,
3074int(instrument["volume"]),
3075", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3076))
3077
3078tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation
3079
3080if tradeLots > 0:
3081if instrument["blocked"] > 0:
3082uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3083instrument["blocked"],
3084self._ticker,
3085tradeLots,
3086))
3087
3088# if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3089self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3090
3091else:
3092uLogger.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
3094def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3095"""
3096Close 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.
3100This avoids unnecessary downloading data from the server.
3101"""
3102if iType not in TKS_INSTRUMENTS:
3103uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3104
3105else:
3106if portfolio is None or not portfolio:
3107portfolio = self.Overview(show=False)
3108
3109tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3110uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3111
3112if tickers and portfolio:
3113self.CloseTrades(tickers, portfolio)
3114
3115else:
3116uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3117
3118def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3119"""
3120Universal method to create market or limit orders with all available parameters for current `accountId`.
3121See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3122
3123If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3124current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3125
3126Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3127then broker immediately open market order as you can do simple --buy or --sell operations!
3128
3129If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3130When current price will go up or down to target price value then broker opens a limit order.
3131Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3132
3133Only 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.
3140Broker 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.
3143Stop loss order always executed by market price.
3144:param expDate: string "Undefined" by default or local date in future.
3145String has a format like this: `%Y-%m-%d %H:%M:%S`.
3146This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3147A limit order has no expiration date, it lasts until the end of the trading day.
3148:return: JSON with response from broker server.
3149"""
3150if self.accountId is None or not self.accountId:
3151uLogger.error("Variable `accountId` must be defined for using this method!")
3152raise Exception("Account ID required")
3153
3154if operation is None or not operation or operation not in ("Buy", "Sell"):
3155uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3156raise Exception("Incorrect value")
3157
3158if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3159uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3160raise Exception("Incorrect value")
3161
3162if lots is None or lots < 1:
3163uLogger.error("You must define trade volume > 0: integer count of lots!")
3164raise Exception("Incorrect value")
3165
3166if targetPrice is None or targetPrice <= 0:
3167uLogger.error("Target price for limit-order must be greater than 0!")
3168raise Exception("Incorrect value")
3169
3170if limitPrice is None or limitPrice <= 0:
3171limitPrice = targetPrice
3172
3173if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3174stopType = "Limit"
3175
3176if expDate is None or not expDate:
3177expDate = "Undefined"
3178
3179if not (self._ticker or self._figi):
3180uLogger.error("Tocker or FIGI must be defined!")
3181raise Exception("Ticker or FIGI required")
3182
3183response = {}
3184instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3185self._ticker = instrument["ticker"]
3186self._figi = instrument["figi"]
3187
3188if orderType == "Limit":
3189uLogger.debug(
3190"Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3191self._ticker, self._figi,
3192operation, lots, targetPrice, instrument["currency"],
3193))
3194
3195openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3196self.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})
3204response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3205
3206if "orderId" in response.keys():
3207uLogger.info(
3208"Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3209response["orderId"],
3210self._ticker, self._figi,
3211operation, lots, targetPrice, instrument["currency"],
3212))
3213
3214if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3215if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3216uLogger.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(
3217targetPrice, instrument["currency"],
3218instrument["currentPrice"]["lastPrice"], instrument["currency"],
3219))
3220
3221if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3222uLogger.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(
3223targetPrice, instrument["currency"],
3224instrument["currentPrice"]["lastPrice"], instrument["currency"],
3225))
3226
3227else:
3228uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3229
3230if orderType == "Stop":
3231uLogger.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(
3233self._ticker, self._figi,
3234operation, lots,
3235targetPrice, instrument["currency"],
3236limitPrice, instrument["currency"],
3237stopType, expDate,
3238))
3239
3240openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3241expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3242stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3243
3244body = {
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
3255if expDateUTC:
3256body["expireDate"] = expDateUTC
3257
3258self.body = str(body)
3259response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3260
3261if "stopOrderId" in response.keys():
3262uLogger.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(
3264response["stopOrderId"],
3265self._ticker, self._figi,
3266operation, lots,
3267targetPrice, instrument["currency"],
3268limitPrice, instrument["currency"],
3269TKS_STOP_ORDER_TYPES[stopOrderType],
3270datetime.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
3273if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3274if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3275uLogger.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(
3276targetPrice, instrument["currency"],
3277instrument["currentPrice"]["lastPrice"], instrument["currency"],
3278))
3279
3280if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3281uLogger.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(
3282targetPrice, instrument["currency"],
3283instrument["currentPrice"]["lastPrice"], instrument["currency"],
3284))
3285
3286else:
3287uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3288
3289return response
3290
3291def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3292"""
3293Create 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
3295broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3296See 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"""
3302return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3303
3304def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3305"""
3306Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3307In 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
3309target 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
3314with 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"
3316for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3317:param expDate: string "Undefined" by default or local date in future.
3318String has a format like this: `%Y-%m-%d %H:%M:%S`.
3319This date is converting to UTC format for server.
3320:return: JSON with response from broker server.
3321"""
3322return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3323
3324def SellLimit(self, lots: int, targetPrice: float) -> dict:
3325"""
3326Create 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
3328broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3329See 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"""
3335return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3336
3337def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3338"""
3339Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3340In 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
3342target 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
3347with 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"
3349for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3350:param expDate: string "Undefined" by default or local date in future.
3351String has a format like this: `%Y-%m-%d %H:%M:%S`.
3352This date is converting to UTC format for server.
3353:return: JSON with response from broker server.
3354"""
3355return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3356
3357def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3358"""
3359Cancel 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.
3363This avoids unnecessary downloading data from the server.
3364:param allStopOrdersIDs: pre-received lists of all active stop orders.
3365"""
3366if self.accountId is None or not self.accountId:
3367uLogger.error("Variable `accountId` must be defined for using this method!")
3368raise Exception("Account ID required")
3369
3370if orderIDs:
3371if allOrdersIDs is None:
3372rawOrders = self.RequestPendingOrders()
3373allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID
3374
3375if allStopOrdersIDs is None:
3376rawStopOrders = self.RequestStopOrders()
3377allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID
3378
3379for orderID in orderIDs:
3380idInPendingOrders = orderID in allOrdersIDs
3381idInStopOrders = orderID in allStopOrdersIDs
3382
3383if not (idInPendingOrders or idInStopOrders):
3384uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3385continue
3386
3387else:
3388if idInPendingOrders:
3389uLogger.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
3392self.body = str({"accountId": self.accountId, "orderId": orderID})
3393closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3394responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3395
3396if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3397if self.moreDebug:
3398uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3399
3400uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3401
3402else:
3403uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3404
3405elif idInStopOrders:
3406uLogger.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
3409self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3410closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3411responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3412
3413if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3414if self.moreDebug:
3415uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3416
3417uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3418
3419else:
3420uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3421
3422else:
3423continue
3424
3425def CloseAllOrders(self) -> None:
3426"""
3427Gets a list of open pending and stop orders and cancel it all.
3428"""
3429rawOrders = self.RequestPendingOrders()
3430allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID
3431lenOrders = len(allOrdersIDs)
3432
3433rawStopOrders = self.RequestStopOrders()
3434allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID
3435lenSOrders = len(allStopOrdersIDs)
3436
3437if lenOrders > 0 or lenSOrders > 0:
3438uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3439
3440self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3441
3442else:
3443uLogger.info("Orders not found, nothing to cancel.")
3444
3445def CloseAll(self, *args) -> None:
3446"""
3447Close all available (not blocked) opened trades and orders.
3448
3449Also, 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
3452Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3453"""
3454overview = self.Overview(show=False) # get all open trades info
3455
3456if len(args) == 0:
3457uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3458self.CloseAllOrders() # close all pending and stop orders
3459
3460for iType in TKS_INSTRUMENTS:
3461if iType != "Currencies":
3462self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
3463
3464else:
3465uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3466lowerArgs = [x.lower() for x in args]
3467
3468if "orders" in lowerArgs:
3469self.CloseAllOrders() # close all pending and stop orders
3470
3471for iType in TKS_INSTRUMENTS:
3472if iType.lower() in lowerArgs and iType != "Currencies":
3473self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
3474
3475def CloseAllByTicker(self, instrument: str) -> None:
3476"""
3477Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3478
3479This 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
3482See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3483
3484:param instrument: string with ticker.
3485"""
3486if instrument is None or not instrument:
3487uLogger.error("Ticker name must be defined for using this method!")
3488raise Exception("Ticker required")
3489
3490overview = self.Overview(show=False) # get user portfolio with all open trades info
3491
3492self._ticker = instrument # try to set instrument as ticker
3493self._figi = ""
3494
3495if self.IsInPortfolio(portfolio=overview):
3496uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3497self.CloseTrades(instruments=[instrument], portfolio=overview)
3498
3499limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs
3500stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs
3501
3502if limitAll and self.IsInLimitOrders(portfolio=overview):
3503uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3504self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3505
3506if stopAll and self.IsInStopOrders(portfolio=overview):
3507uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3508self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3509
3510def CloseAllByFIGI(self, instrument: str) -> None:
3511"""
3512Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3513
3514This 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
3517See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3518
3519:param instrument: string with FIGI id.
3520"""
3521if instrument is None or not instrument:
3522uLogger.error("FIGI id must be defined for using this method!")
3523raise Exception("FIGI required")
3524
3525overview = self.Overview(show=False) # get user portfolio with all open trades info
3526
3527self._ticker = ""
3528self._figi = instrument # try to set instrument as FIGI id
3529
3530if self.IsInPortfolio(portfolio=overview):
3531uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3532self.CloseTrades(instruments=[instrument], portfolio=overview)
3533
3534limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs
3535stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs
3536
3537if limitAll and self.IsInLimitOrders(portfolio=overview):
3538uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3539self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3540
3541if stopAll and self.IsInStopOrders(portfolio=overview):
3542uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3543self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3544
3545@staticmethod
3546def ParseOrderParameters(operation, **inputParameters):
3547"""
3548Parse 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
3555Counts 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
3559pass
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
3593def IsInPortfolio(self, portfolio: dict = None) -> bool:
3594"""
3595Checks 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"""
3600result = False
3601msg = "Instrument not defined!"
3602
3603if portfolio is None or not portfolio:
3604portfolio = self.Overview(show=False)
3605
3606if self._ticker:
3607uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3608msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3609
3610for iType in TKS_INSTRUMENTS:
3611for instrument in portfolio["stat"][iType]:
3612if instrument["ticker"] == self._ticker:
3613result = True
3614msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3615break
3616
3617elif self._figi:
3618uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3619msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3620
3621for iType in TKS_INSTRUMENTS:
3622for instrument in portfolio["stat"][iType]:
3623if instrument["figi"] == self._figi:
3624result = True
3625msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3626break
3627
3628else:
3629uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3630
3631uLogger.debug(msg)
3632
3633return result
3634
3635def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3636"""
3637Returns instrument from the user's portfolio if it presents there.
3638Instrument 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"""
3643result = None
3644msg = "Instrument not defined!"
3645
3646if portfolio is None or not portfolio:
3647portfolio = self.Overview(show=False)
3648
3649if self._ticker:
3650uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3651msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3652
3653for iType in TKS_INSTRUMENTS:
3654for instrument in portfolio["stat"][iType]:
3655if instrument["ticker"] == self._ticker:
3656result = instrument
3657msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3658break
3659
3660elif self._figi:
3661uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3662msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3663
3664for iType in TKS_INSTRUMENTS:
3665for instrument in portfolio["stat"][iType]:
3666if instrument["figi"] == self._figi:
3667result = instrument
3668msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3669break
3670
3671else:
3672uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3673
3674uLogger.debug(msg)
3675
3676return result
3677
3678def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3679"""
3680Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3681
3682See 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"""
3687result = False
3688msg = "Instrument not defined!"
3689
3690if portfolio is None or not portfolio:
3691portfolio = self.Overview(show=False)
3692
3693if self._ticker:
3694uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3695msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3696
3697for instrument in portfolio["stat"]["orders"]:
3698if instrument["ticker"] == self._ticker:
3699result = True
3700msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3701break
3702
3703elif self._figi:
3704uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3705msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3706
3707for instrument in portfolio["stat"]["orders"]:
3708if instrument["figi"] == self._figi:
3709result = True
3710msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3711break
3712
3713else:
3714uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3715
3716uLogger.debug(msg)
3717
3718return result
3719
3720def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3721"""
3722Returns list with all `orderID`s of opened pending limit orders for the instrument.
3723Instrument must be defined by `ticker` (highly priority) or `figi`.
3724
3725See 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"""
3730result = []
3731msg = "Instrument not defined!"
3732
3733if portfolio is None or not portfolio:
3734portfolio = self.Overview(show=False)
3735
3736if self._ticker:
3737uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3738msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3739
3740for instrument in portfolio["stat"]["orders"]:
3741if instrument["ticker"] == self._ticker:
3742result.append(instrument["orderID"])
3743
3744if result:
3745msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3746
3747elif self._figi:
3748uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3749msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3750
3751for instrument in portfolio["stat"]["orders"]:
3752if instrument["figi"] == self._figi:
3753result.append(instrument["orderID"])
3754
3755if result:
3756msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3757
3758else:
3759uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3760
3761uLogger.debug(msg)
3762
3763return result
3764
3765def IsInStopOrders(self, portfolio: dict = None) -> bool:
3766"""
3767Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3768
3769See 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"""
3774result = False
3775msg = "Instrument not defined!"
3776
3777if portfolio is None or not portfolio:
3778portfolio = self.Overview(show=False)
3779
3780if self._ticker:
3781uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3782msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3783
3784for instrument in portfolio["stat"]["stopOrders"]:
3785if instrument["ticker"] == self._ticker:
3786result = True
3787msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3788break
3789
3790elif self._figi:
3791uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3792msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3793
3794for instrument in portfolio["stat"]["stopOrders"]:
3795if instrument["figi"] == self._figi:
3796result = True
3797msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3798break
3799
3800else:
3801uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3802
3803uLogger.debug(msg)
3804
3805return result
3806
3807def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3808"""
3809Returns list with all `orderID`s of opened stop orders for the instrument.
3810Instrument must be defined by `ticker` (highly priority) or `figi`.
3811
3812See 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"""
3817result = []
3818msg = "Instrument not defined!"
3819
3820if portfolio is None or not portfolio:
3821portfolio = self.Overview(show=False)
3822
3823if self._ticker:
3824uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3825msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3826
3827for instrument in portfolio["stat"]["stopOrders"]:
3828if instrument["ticker"] == self._ticker:
3829result.append(instrument["orderID"])
3830
3831if result:
3832msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3833
3834elif self._figi:
3835uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3836msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3837
3838for instrument in portfolio["stat"]["stopOrders"]:
3839if instrument["figi"] == self._figi:
3840result.append(instrument["orderID"])
3841
3842if result:
3843msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3844
3845else:
3846uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3847
3848uLogger.debug(msg)
3849
3850return result
3851
3852def RequestLimits(self) -> dict:
3853"""
3854Method for obtaining the available funds for withdrawal for current `accountId`.
3855
3856See 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": [...]}`.
3862Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3863positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3864"""
3865if self.accountId is None or not self.accountId:
3866uLogger.error("Variable `accountId` must be defined for using this method!")
3867raise Exception("Account ID required")
3868
3869uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3870
3871self.body = str({"accountId": self.accountId})
3872portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3873rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3874
3875if self.moreDebug:
3876uLogger.debug("Records about available funds for withdrawal successfully received")
3877
3878return rawLimits
3879
3880def OverviewLimits(self, show: bool = False) -> dict:
3881"""
3882Method for parsing and show table with available funds for withdrawal for current `accountId`.
3883
3884See 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"""
3889if self.accountId is None or not self.accountId:
3890uLogger.error("Variable `accountId` must be defined for using this method!")
3891raise Exception("Account ID required")
3892
3893rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal
3894
3895view = {
3896"rawLimits": rawLimits,
3897"limits": { # parsed data for every currency:
3898"money": { # this is an array of portfolio currency positions
3899item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3900},
3901"blocked": { # this is an array of blocked currency
3902item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3903},
3904"blockedGuarantee": { # this is locked money under collateral for futures
3905item["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:
3911if show:
3912info = [
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
3918if view["limits"]["money"]:
3919info.extend([
3920"\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3921"|------------|---------------|--------------------------|-------------------|-------------------|\n",
3922])
3923
3924else:
3925info.append("\nNo withdrawal limits\n")
3926
3927for curr in view["limits"]["money"].keys():
3928blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3929blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3930availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3931
3932infoStr = "| {:<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
3940if curr == "rub":
3941info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers
3942
3943else:
3944info.append(infoStr)
3945
3946infoText = "".join(info)
3947
3948uLogger.info(infoText)
3949
3950if self.withdrawalLimitsFile:
3951with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3952fH.write(infoText)
3953
3954uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3955
3956if self.useHTMLReports:
3957htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3958with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3959fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3960
3961uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
3962
3963return view
3964
3965def RequestAccounts(self) -> dict:
3966"""
3967Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3968
3969See 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"}, ...]}`.
3978If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3979"""
3980uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3981
3982self.body = str({})
3983portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3984rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3985
3986if self.moreDebug:
3987uLogger.debug("Records about available accounts successfully received")
3988
3989return rawAccounts
3990
3991def RequestUserInfo(self) -> dict:
3992"""
3993Method for requesting common user's information.
3994
3995See 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"""
4005uLogger.debug("Requesting common user's information. Wait, please...")
4006
4007self.body = str({})
4008portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4009rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4010
4011if self.moreDebug:
4012uLogger.debug("Records about current user successfully received")
4013
4014return rawUserInfo
4015
4016def RequestMarginStatus(self, accountId: str = None) -> dict:
4017"""
4018Method for requesting margin calculation for defined account ID.
4019
4020See 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.
4027Example of responses:
4028status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4029status 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"""
4035if accountId is None or not accountId:
4036if self.accountId is None or not self.accountId:
4037uLogger.error("Variable `accountId` must be defined for using this method!")
4038raise Exception("Account ID required")
4039
4040else:
4041accountId = self.accountId # use `self.accountId` (main ID) by default
4042
4043uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4044
4045self.body = str({"accountId": accountId})
4046portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4047rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4048
4049if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4050uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4051rawMargin = {}
4052
4053else:
4054if self.moreDebug:
4055uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4056
4057return rawMargin
4058
4059def RequestTariffLimits(self) -> dict:
4060"""
4061Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4062
4063See 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"""
4074uLogger.debug("Requesting limits of current tariff. Wait, please...")
4075
4076self.body = str({})
4077portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4078rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4079
4080if self.moreDebug:
4081uLogger.debug("Records with limits of current tariff successfully received")
4082
4083return rawTariffLimits
4084
4085def RequestBondCoupons(self, iJSON: dict) -> dict:
4086"""
4087Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4088then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4089All dates are in UTC timezone.
4090
4091REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4092Documentation:
4093- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4094- response: https://tinkoff.github.io/investAPI/instruments/#coupon
4095
4096See also: `ExtendBondsData()`.
4097
4098:param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4099If 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"""
4107if iJSON["figi"] is None or not iJSON["figi"]:
4108uLogger.error("FIGI must be defined for using this method!")
4109raise Exception("FIGI required")
4110
4111startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4112endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4113
4114uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4115"ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4116self._figi,
4117startDate,
4118endDate,
4119))
4120
4121self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4122calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4123calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4124
4125if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4126uLogger.warning("Instrument type is not bond!")
4127
4128else:
4129if self.moreDebug:
4130uLogger.debug("Records about bond payment calendar successfully received")
4131
4132return calendar
4133
4134def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4135"""
4136Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4137Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4138coupon yields, current yields and some statistics etc.
4139
4140WARNING! This is too long operation if a lot of bonds requested from broker server.
4141
4142See 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`,
4146for 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.
4148In 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"""
4152if instruments is None or not instruments:
4153uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4154raise Exception("Ticker or FIGI required")
4155
4156if isinstance(instruments, str):
4157instruments = [instruments]
4158
4159uniqueInstruments = self.GetUniqueFIGIs(instruments)
4160
4161uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4162
4163iCount = len(uniqueInstruments)
4164tooLong = iCount >= 20
4165if tooLong:
4166uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4167
4168bonds = None
4169for i, self._figi in enumerate(uniqueInstruments):
4170instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server
4171
4172if "type" in instrument.keys() and instrument["type"] == "Bonds":
4173# raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4174rawBond = self.SearchByFIGI(requestPrice=True)
4175
4176# Widen raw data with UTC current time (iData["actualDateTime"]):
4177actualDate = datetime.now(tzutc())
4178iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4179
4180# Widen raw data with bond payment calendar (iData["rawCalendar"]):
4181iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4182
4183# Replace some values with human-readable:
4184iData["nominalCurrency"] = iData["nominal"]["currency"]
4185iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4186iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4187iData["aciCurrency"] = iData["aciValue"]["currency"]
4188iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4189iData["issueSize"] = int(iData["issueSize"])
4190iData["issueSizePlan"] = int(iData["issueSizePlan"])
4191iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4192iData["step"] = iData["step"] if "step" in iData.keys() else 0
4193iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4194iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4195iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4196iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4197iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4198iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4199iData["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):
4202iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal
4203iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal
4204iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal
4205iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal
4206iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice`
4207iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal`
4208iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal`
4209iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal`
4210iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal`
4211iData["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:
4214calendarData = []
4215if "events" in iData["rawCalendar"].keys():
4216for item in iData["rawCalendar"]["events"]:
4217calendarData.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:
4230if "maturityDate" not in iData.keys():
4231iData["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%:
4235iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4236iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4237iData["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%:
4241maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4242iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4243iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4244iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value
4245iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4246
4247iData["calendar"] = calendarData # adds calendar at the end
4248
4249# Remove not used data:
4250iData.pop("uid")
4251iData.pop("positionUid")
4252iData.pop("currentPrice")
4253iData.pop("rawCalendar")
4254
4255colNames = list(iData.keys())
4256if bonds is None:
4257bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4258
4259else:
4260bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4261
4262else:
4263uLogger.warning("Instrument is not a bond!")
4264
4265processed = round(100 * (i + 1) / iCount, 1)
4266if tooLong and processed % 5 == 0:
4267uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4268
4269else:
4270uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4271
4272bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names
4273
4274# Saving bonds from Pandas DataFrame to XLSX sheet:
4275if xlsx and self.bondsXLSXFile:
4276with pd.ExcelWriter(
4277path=self.bondsXLSXFile,
4278date_format=TKS_DATE_FORMAT,
4279datetime_format=TKS_DATE_TIME_FORMAT,
4280mode="w",
4281) as writer:
4282bonds.to_excel(
4283writer,
4284sheet_name="Extended bonds data",
4285index=True,
4286encoding="UTF-8",
4287freeze_panes=(1, 1),
4288) # saving as XLSX-file with freeze first row and column as headers
4289
4290uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4291
4292return bonds
4293
4294def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4295"""
4296Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4297
4298WARNING! This is too long operation if a lot of bonds requested from broker server.
4299
4300See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4301
4302:param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4303extended information about bonds: main info, current prices, bond payment calendar,
4304coupon yields, current yields and some statistics etc.
4305If 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,
4307for 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"""
4310if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4311extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4312
4313uLogger.debug("Generating bond payments calendar data. Wait, please...")
4314
4315colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4316colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4317calendar = None
4318for bond in extBonds.iterrows():
4319for item in bond[1]["calendar"]:
4320cData = {
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
4336if calendar is None:
4337calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4338
4339else:
4340calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4341
4342if calendar is not None:
4343calendar = 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:
4346if xlsx:
4347xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4348
4349with pd.ExcelWriter(
4350path=xlsxCalendarFile,
4351date_format=TKS_DATE_FORMAT,
4352datetime_format=TKS_DATE_TIME_FORMAT,
4353mode="w",
4354) as writer:
4355humanReadable = calendar.copy(deep=True)
4356humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4357humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4358humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4359humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4360humanReadable.columns = colNames # human-readable column names
4361
4362humanReadable.to_excel(
4363writer,
4364sheet_name="Bond payments calendar",
4365index=False,
4366encoding="UTF-8",
4367freeze_panes=(1, 2),
4368) # saving as XLSX-file with freeze first row and column as headers
4369
4370del humanReadable # release df in memory
4371
4372uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4373
4374return calendar
4375
4376def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4377"""
4378Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4379Also, creates Markdown file with calendar data, `calendar.md` by default.
4380
4381See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4382
4383:param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4384extended information about bonds: main info, current prices, bond payment calendar,
4385coupon yields, current yields and some statistics etc.
4386If 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,
4388otherwise save to file `calendarFile` only. `False` by default.
4389:return: multilines text in Markdown format with bonds payment calendar as a table.
4390"""
4391if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4392extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4393
4394infoText = "# Bond payments calendar\n\n"
4395
4396calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data
4397
4398if not (calendar is None or calendar.empty):
4399splitLine = "| | | | | | | | | |\n"
4400
4401info = [
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
4407newMonth = False
4408notOneBond = calendar["figi"].nunique() > 1
4409for i, bond in enumerate(calendar.iterrows()):
4410if newMonth and notOneBond:
4411info.append(splitLine)
4412
4413info.append(
4414"| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4415" √" if bond[1]["paid"] else " —",
4416bond[1]["couponDate"].split("T")[0],
4417bond[1]["figi"],
4418bond[1]["ticker"],
4419bond[1]["couponNumber"],
4420"{} {}".format(
4421"{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4422bond[1]["payCurrency"],
4423),
4424bond[1]["couponType"],
4425bond[1]["couponPeriod"],
4426bond[1]["fixDate"].split("T")[0],
4427)
4428)
4429
4430if i < len(calendar.values) - 1:
4431curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4432nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4433newMonth = False if curDate.month == nextDate.month else True
4434
4435else:
4436newMonth = False
4437
4438infoText += "".join(info)
4439
4440if show:
4441uLogger.info("{}".format(infoText))
4442
4443if self.calendarFile is not None:
4444with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4445fH.write(infoText)
4446
4447uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4448
4449if self.useHTMLReports:
4450htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4451with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4452fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4453
4454uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4455
4456else:
4457infoText += "No data\n"
4458
4459return infoText
4460
4461def OverviewAccounts(self, show: bool = False) -> dict:
4462"""
4463Method for parsing and show simple table with all available user accounts.
4464
4465See 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"""
4474rawAccounts = self.RequestAccounts() # Raw responses with accounts
4475
4476# This is an array of dict with user accounts, its `accountId`s and some parsed data:
4477accounts = {
4478item["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:
4489view = {
4490"rawAccounts": rawAccounts,
4491"stat": accounts,
4492}
4493
4494# --- Prepare simple text table with only accounts data in human-readable format:
4495if show:
4496info = [
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
4503for account in view["stat"].keys():
4504info.extend([
4505"| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4506account,
4507view["stat"][account]["type"],
4508view["stat"][account]["status"],
4509view["stat"][account]["name"],
4510)
4511])
4512
4513infoText = "".join(info)
4514
4515uLogger.info(infoText)
4516
4517if self.userAccountsFile:
4518with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4519fH.write(infoText)
4520
4521uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4522
4523if self.useHTMLReports:
4524htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4525with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4526fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4527
4528uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4529
4530return view
4531
4532def OverviewUserInfo(self, show: bool = False) -> dict:
4533"""
4534Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4535
4536See 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"""
4541rawUserInfo = self.RequestUserInfo() # Raw response with common user info
4542overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data
4543rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data
4544accounts = overviewAccount["stat"] # Dict with only statistics about user accounts
4545rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID
4546rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff
4547
4548# This is dict with parsed common user data:
4549userInfo = {
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:
4557margins = {}
4558for accountId in accounts.keys():
4559if rawMargins[accountId]:
4560margins[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
4569else:
4570margins[accountId] = {} # Server response: margin status is disabled for current accountId
4571
4572unary = {} # unary-connection limits
4573for item in rawTariffLimits["unaryLimits"]:
4574if item["limitPerMinute"] in unary.keys():
4575unary[item["limitPerMinute"]].extend(item["methods"])
4576
4577else:
4578unary[item["limitPerMinute"]] = item["methods"]
4579
4580stream = {} # stream-connection limits
4581for item in rawTariffLimits["streamLimits"]:
4582if item["limit"] in stream.keys():
4583stream[item["limit"]].extend(item["streams"])
4584
4585else:
4586stream[item["limit"]] = item["streams"]
4587
4588# This is dict with parsed limits of current tariff (connections, API methods etc.):
4589limits = {
4590"unary": unary,
4591"stream": stream,
4592}
4593
4594# Raw and parsed data as an output result:
4595view = {
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:
4609if show:
4610info = [
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
4621for account in view["stat"]["accounts"].keys():
4622info.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
4634if margins[account]:
4635info.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
4644else:
4645info.append("| Margin status: | Disabled |\n\n")
4646
4647info.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
4657if unary:
4658for key, values in sorted(unary.items()):
4659info.append("\n* Max requests per minute: {}\n".format(key))
4660
4661for value in values:
4662info.append(" - {}\n".format(value))
4663
4664else:
4665info.append("\nNot available\n")
4666
4667info.append("\n### Stream limits\n")
4668
4669if stream:
4670for key, values in sorted(stream.items()):
4671info.append("\n* Max stream connections: {}\n".format(key))
4672
4673for value in values:
4674info.append(" - {}\n".format(value))
4675
4676else:
4677info.append("\nNot available\n")
4678
4679infoText = "".join(info)
4680
4681uLogger.info(infoText)
4682
4683if self.userInfoFile:
4684with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4685fH.write(infoText)
4686
4687uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4688
4689if self.useHTMLReports:
4690htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4691with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4692fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4693
4694uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4695
4696return view
4697
4698
4699class Args:
4700"""
4701If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4702"""
4703def __init__(self, **kwargs):
4704self.__dict__.update(kwargs)
4705
4706def __getattr__(self, item):
4707return None
4708
4709
4710def ParseArgs():
4711"""This function get and parse command line keys."""
4712parser = ArgumentParser() # command-line string parser
4713
4714parser.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"
4715parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4716
4717# --- options:
4718
4719parser.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.")
4720parser.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/")
4721parser.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
4723parser.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`.")
4724parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4725
4726parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4727parser.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
4729parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4730parser.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
4732parser.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.")
4733parser.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.")
4734parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4735
4736parser.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.")
4737parser.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
4741parser.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
4743parser.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`.")
4744parser.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`.")
4745parser.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.")
4746parser.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`.")
4747parser.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!")
4748parser.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.")
4749parser.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!")
4750parser.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
4752parser.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`.")
4753parser.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`.")
4754parser.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`.")
4755parser.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`.")
4756parser.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`.")
4757parser.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
4759parser.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`.")
4760parser.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.")
4761parser.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.")
4762parser.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
4764parser.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.")
4765parser.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`].")
4766parser.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
4768parser.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.")
4769parser.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!")
4770parser.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!")
4771parser.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.")
4772parser.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
4776parser.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`.")
4777parser.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`.")
4778parser.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.")
4779parser.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.")
4780parser.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
4782parser.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`.")
4783parser.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`.")
4784parser.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
4786cmdArgs = parser.parse_args()
4787return cmdArgs
4788
4789
4790def Main(**kwargs):
4791"""
4792Main function for work with TKSBrokerAPI in the console.
4793
4794See 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"""
4798args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters
4799
4800if args.debug_level:
4801uLogger.level = 10 # always debug level by default
4802uLogger.handlers[0].level = args.debug_level # level for STDOUT
4803
4804exitCode = 0
4805start = datetime.now(tzutc())
4806uLogger.debug("=-" * 50)
4807uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4808start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4809start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4810))
4811
4812# trying to calculate full current version:
4813buildVersion = __version__
4814try:
4815v = version("tksbrokerapi")
4816buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script
4817
4818except Exception:
4819buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0
4820
4821uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4822uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4823
4824try:
4825if args.version:
4826print("TKSBrokerAPI {}".format(buildVersion))
4827uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4828
4829else:
4830# Init class for trading with Tinkoff Broker:
4831trader = TinkoffBrokerServer(
4832token=args.token,
4833accountId=args.account_id,
4834useCache=not args.no_cache,
4835)
4836
4837# --- set some options:
4838
4839if args.more:
4840trader.moreDebug = True
4841uLogger.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
4843if args.html:
4844trader.useHTMLReports = True
4845
4846if args.ticker:
4847ticker = str(args.ticker).upper() # Tickers may be upper case only
4848
4849if ticker in trader.aliasesKeys:
4850trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases
4851
4852else:
4853trader.ticker = ticker
4854
4855if args.figi:
4856trader.figi = str(args.figi).upper() # FIGIs may be upper case only
4857
4858if args.depth is not None:
4859trader.depth = args.depth
4860
4861# --- do one command:
4862
4863if args.list:
4864if args.output is not None:
4865trader.instrumentsFile = args.output
4866
4867trader.ShowInstrumentsInfo(show=True)
4868
4869elif args.list_xlsx:
4870trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4871
4872elif args.bonds_xlsx is not None:
4873if args.output is not None:
4874trader.bondsXLSXFile = args.output
4875
4876if len(args.bonds_xlsx) == 0:
4877trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers
4878
4879else:
4880trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds
4881
4882elif args.search:
4883if args.output is not None:
4884trader.searchResultsFile = args.output
4885
4886trader.SearchInstruments(pattern=args.search[0], show=True)
4887
4888elif args.info:
4889if not (args.ticker or args.figi):
4890uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4891raise Exception("Ticker or FIGI required")
4892
4893if args.output is not None:
4894trader.infoFile = args.output
4895
4896if args.ticker:
4897trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name
4898
4899else:
4900trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id
4901
4902elif args.calendar is not None:
4903if args.output is not None:
4904trader.calendarFile = args.output
4905
4906if len(args.calendar) == 0:
4907bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers
4908
4909else:
4910bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds
4911
4912trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only
4913
4914elif args.price:
4915if not (args.ticker or args.figi):
4916uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4917raise Exception("Ticker or FIGI required")
4918
4919trader.GetCurrentPrices(show=True)
4920
4921elif args.prices is not None:
4922if args.output is not None:
4923trader.pricesFile = args.output
4924
4925trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices
4926
4927elif args.overview:
4928if args.output is not None:
4929trader.overviewFile = args.output
4930
4931trader.Overview(show=True, details="full")
4932
4933elif args.overview_digest:
4934if args.output is not None:
4935trader.overviewDigestFile = args.output
4936
4937trader.Overview(show=True, details="digest")
4938
4939elif args.overview_positions:
4940if args.output is not None:
4941trader.overviewPositionsFile = args.output
4942
4943trader.Overview(show=True, details="positions")
4944
4945elif args.overview_orders:
4946if args.output is not None:
4947trader.overviewOrdersFile = args.output
4948
4949trader.Overview(show=True, details="orders")
4950
4951elif args.overview_analytics:
4952if args.output is not None:
4953trader.overviewAnalyticsFile = args.output
4954
4955trader.Overview(show=True, details="analytics")
4956
4957elif args.overview_calendar:
4958if args.output is not None:
4959trader.overviewAnalyticsFile = args.output
4960
4961trader.Overview(show=True, details="calendar")
4962
4963elif args.deals is not None:
4964if args.output is not None:
4965trader.reportFile = args.output
4966
4967if 0 <= len(args.deals) < 3:
4968trader.Deals(
4969start=args.deals[0] if len(args.deals) >= 1 else None,
4970end=args.deals[1] if len(args.deals) == 2 else None,
4971show=True, # Always show deals report in console
4972showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4973)
4974
4975else:
4976uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4977raise Exception("Incorrect value")
4978
4979elif args.history is not None:
4980if args.output is not None:
4981trader.historyFile = args.output
4982
4983if 0 <= len(args.history) < 3:
4984dataReceived = trader.History(
4985start=args.history[0] if len(args.history) >= 1 else None,
4986end=args.history[1] if len(args.history) == 2 else None,
4987interval="hour" if args.interval is None or not args.interval else args.interval,
4988onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4989csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4990show=True, # shows all downloaded candles in console
4991)
4992
4993if args.render_chart is not None and dataReceived is not None:
4994iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4995
4996trader.ShowHistoryChart(
4997candles=dataReceived,
4998interact=iChart,
4999openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file.
5000)
5001
5002else:
5003uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5004raise Exception("Incorrect value")
5005
5006elif args.load_history is not None:
5007histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console
5008
5009if args.render_chart is not None and histData is not None:
5010iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5011trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart
5012
5013trader.ShowHistoryChart(
5014candles=histData,
5015interact=iChart,
5016openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file.
5017)
5018
5019elif args.trade is not None:
5020if 1 <= len(args.trade) <= 5:
5021trader.Trade(
5022operation=args.trade[0],
5023lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5024tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5025sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5026expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5027)
5028
5029else:
5030uLogger.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
5032elif args.buy is not None:
5033if 0 <= len(args.buy) <= 4:
5034trader.Buy(
5035lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5036tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5037sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5038expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5039)
5040
5041else:
5042uLogger.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
5044elif args.sell is not None:
5045if 0 <= len(args.sell) <= 4:
5046trader.Sell(
5047lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5048tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5049sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5050expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5051)
5052
5053else:
5054uLogger.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
5056elif args.order:
5057if 4 <= len(args.order) <= 7:
5058trader.Order(
5059operation=args.order[0],
5060orderType=args.order[1],
5061lots=int(args.order[2]),
5062targetPrice=float(args.order[3]),
5063limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5064stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5065expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5066)
5067
5068else:
5069uLogger.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
5071elif args.buy_limit:
5072trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5073
5074elif args.sell_limit:
5075trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5076
5077elif args.buy_stop:
5078if 2 <= len(args.buy_stop) <= 7:
5079trader.BuyStop(
5080lots=int(args.buy_stop[0]),
5081targetPrice=float(args.buy_stop[1]),
5082limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5083stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5084expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5085)
5086
5087else:
5088uLogger.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
5090elif args.sell_stop:
5091if 2 <= len(args.sell_stop) <= 7:
5092trader.SellStop(
5093lots=int(args.sell_stop[0]),
5094targetPrice=float(args.sell_stop[1]),
5095limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5096stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5097expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5098)
5099
5100else:
5101uLogger.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
5125elif args.close_order is not None:
5126trader.CloseOrders(args.close_order) # close only one order
5127
5128elif args.close_orders is not None:
5129trader.CloseOrders(args.close_orders) # close list of orders
5130
5131elif args.close_trade:
5132if not (args.ticker or args.figi):
5133uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5134raise Exception("Ticker or FIGI required")
5135
5136if args.ticker:
5137trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority)
5138
5139else:
5140trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI
5141
5142elif args.close_trades is not None:
5143trader.CloseTrades(args.close_trades) # close trades for list of tickers
5144
5145elif args.close_all is not None:
5146if args.ticker:
5147trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5148
5149elif args.figi:
5150trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5151
5152else:
5153trader.CloseAll(*args.close_all)
5154
5155elif args.limits:
5156if args.output is not None:
5157trader.withdrawalLimitsFile = args.output
5158
5159trader.OverviewLimits(show=True)
5160
5161elif args.user_info:
5162if args.output is not None:
5163trader.userInfoFile = args.output
5164
5165trader.OverviewUserInfo(show=True)
5166
5167elif args.account:
5168if args.output is not None:
5169trader.userAccountsFile = args.output
5170
5171trader.OverviewAccounts(show=True)
5172
5173else:
5174uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5175raise Exception("There is no command to execute")
5176
5177except Exception:
5178trace = tb.format_exc()
5179for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5180if e in trace:
5181uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5182break
5183
5184uLogger.debug(trace)
5185uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5186exitCode = 255 # an error occurred, must be open a ticket for this issue
5187
5188finally:
5189finish = datetime.now(tzutc())
5190
5191if exitCode == 0:
5192if args.more:
5193uLogger.debug("All operations were finished success (summary code is 0).")
5194
5195else:
5196uLogger.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(
5197os.path.abspath(uLog.defaultLogFile), exitCode,
5198))
5199
5200uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5201uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5202finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5203finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5204))
5205uLogger.debug("=-" * 50)
5206
5207if not kwargs:
5208sys.exit(exitCode)
5209
5210else:
5211return exitCode
5212
5213
5214if __name__ == "__main__":
5215Main()
5216