├── .gitignore ├── Code.gs ├── LICENSE ├── README.md └── images ├── get-price-by-ticker.gif ├── get-trades-in-action.gif └── main-image.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .clasp.json 3 | .claspignore 4 | appsscript.json 5 | .vscode/ -------------------------------------------------------------------------------- /Code.gs: -------------------------------------------------------------------------------- 1 | /** @OnlyCurrentDoc */ 2 | 3 | const scriptProperties = PropertiesService.getScriptProperties() 4 | const OPENAPI_TOKEN = scriptProperties.getProperty('OPENAPI_TOKEN') 5 | 6 | const CACHE = CacheService.getScriptCache() 7 | const CACHE_MAX_AGE = 21600 // 6 Hours 8 | 9 | const TRADING_START_AT = new Date('Apr 01, 2020 10:00:00') 10 | const MILLIS_PER_DAY = 1000 * 60 * 60 * 24 11 | 12 | /** 13 | * Добавляет меню с командой вызова функции обновления значений служебной ячейки (для обновления вычислнений функций, ссылающихся на эту ячейку) 14 | * 15 | **/ 16 | function onOpen() { 17 | var sheet = SpreadsheetApp.getActiveSpreadsheet() 18 | var entries = [{ 19 | name : "Обновить", 20 | functionName : "refresh" 21 | }] 22 | sheet.addMenu("TI", entries) 23 | }; 24 | 25 | function _convertRangeToOneCell(range){ 26 | if (range == null) return; 27 | if (range.getA1Notation == undefined) return; // range should be the instance of Range 28 | return range.getCell(1, 1); 29 | } 30 | 31 | function refresh() { 32 | const updateDateRange = _convertRangeToOneCell(SpreadsheetApp.getActiveSpreadsheet().getRangeByName('UPDATE_DATE')); 33 | if (updateDateRange != null) { 34 | updateDateRange.setValue(new Date()); 35 | } else { 36 | SpreadsheetApp.getUi().ui.alert('You should specify the named range "UPDATE_DATE" for using this function.'); 37 | } 38 | } 39 | 40 | function isoToDate(dateStr){ 41 | // How to format date string so that google scripts recognizes it? 42 | // https://stackoverflow.com/a/17253060 43 | const str = dateStr.replace(/-/,'/').replace(/-/,'/').replace(/T/,' ').replace(/\+/,' \+').replace(/Z/,' +00') 44 | return new Date(str) 45 | } 46 | 47 | class TinkoffClient { 48 | // Doc: https://tinkoffcreditsystems.github.io/invest-openapi/swagger-ui/ 49 | // How to create a token: https://tinkoffcreditsystems.github.io/invest-openapi/auth/ 50 | constructor(token) { 51 | this.token = token 52 | this.baseUrl = 'https://api-invest.tinkoff.ru/openapi/' 53 | } 54 | 55 | _makeApiCall(methodUrl) { 56 | const url = this.baseUrl + methodUrl 57 | Logger.log(`[API Call] ${url}`) 58 | const params = {'escaping': false, 'headers': {'accept': 'application/json', "Authorization": `Bearer ${this.token}`}} 59 | const response = UrlFetchApp.fetch(url, params) 60 | if (response.getResponseCode() == 200) 61 | return JSON.parse(response.getContentText()) 62 | } 63 | 64 | getInstrumentByTicker(ticker) { 65 | const url = `market/search/by-ticker?ticker=${ticker}` 66 | const data = this._makeApiCall(url) 67 | return data.payload 68 | } 69 | 70 | getOrderbookByFigi(figi, depth) { 71 | const url = `market/orderbook?depth=${depth}&figi=${figi}` 72 | const data = this._makeApiCall(url) 73 | return data.payload 74 | } 75 | 76 | getOperations(from, to, figi) { 77 | // Arguments `from` && `to` should be in ISO 8601 format 78 | const url = `operations?from=${from}&to=${to}&figi=${figi}` 79 | const data = this._makeApiCall(url) 80 | return data.payload.operations 81 | } 82 | 83 | getPortfolio(){ 84 | const url = `portfolio` 85 | const data = this._makeApiCall(url) 86 | return data.payload.positions 87 | } 88 | } 89 | 90 | const tinkoffClient = new TinkoffClient(OPENAPI_TOKEN) 91 | 92 | function _getFigiByTicker(ticker) { 93 | const CACHE_KEY_PREFIX = 'figi_' 94 | const ticker_cache_key = CACHE_KEY_PREFIX + ticker 95 | 96 | const cached = CACHE.get(ticker) 97 | if (cached != null) 98 | return cached 99 | const {instruments,total} = tinkoffClient.getInstrumentByTicker(ticker) 100 | if (total > 0) { 101 | const figi = instruments[0].figi 102 | CACHE.put(ticker_cache_key, figi, CACHE_MAX_AGE) 103 | return figi 104 | } else { 105 | return null 106 | } 107 | } 108 | 109 | /** 110 | * Получение последней цены инструмента по тикеру 111 | * @param {"GAZP"} ticker Тикер инструмента 112 | * @return {} Last price 113 | * @customfunction 114 | */ 115 | function getPriceByTicker(ticker, dummy) { 116 | // dummy attribute uses for auto-refreshing the value each time the sheet is updating. 117 | // see https://stackoverflow.com/a/27656313 118 | const figi = _getFigiByTicker(ticker) 119 | const {lastPrice} = tinkoffClient.getOrderbookByFigi(figi, 1) 120 | return lastPrice 121 | } 122 | 123 | /** 124 | * Получение Bid/Ask спреда инструмента по тикеру 125 | * @param {"GAZP"} ticker Тикер инструмента 126 | * @return {0.03} Спред в % 127 | * @customfunction 128 | */ 129 | function getBidAskSpreadByTicker(ticker) { // dummy parameter is optional 130 | const figi = _getFigiByTicker(ticker) 131 | const {tradeStatus,bids,asks} = tinkoffClient.getOrderbookByFigi(figi, 1) 132 | if (tradeStatus != 'NotAvailableForTrading') 133 | return (asks[0].price-bids[0].price) / asks[0].price 134 | else 135 | return null 136 | } 137 | 138 | function getMaxBidByTicker(ticker, dummy) { 139 | // dummy attribute uses for auto-refreshing the value each time the sheet is updating. 140 | // see https://stackoverflow.com/a/27656313 141 | const figi = _getFigiByTicker(ticker) 142 | const {tradeStatus,bids} = tinkoffClient.getOrderbookByFigi(figi, 1) 143 | if (tradeStatus != 'NotAvailableForTrading') 144 | return [ 145 | ["Max bid", "Quantity"], 146 | [bids[0].price, bids[0].quantity] 147 | ] 148 | else 149 | return null 150 | } 151 | 152 | function getMinAskByTicker(ticker, dummy) { 153 | // dummy attribute uses for auto-refreshing the value each time the sheet is updating. 154 | // see https://stackoverflow.com/a/27656313 155 | const figi = _getFigiByTicker(ticker) 156 | const {tradeStatus,asks} = tinkoffClient.getOrderbookByFigi(figi, 1) 157 | if (tradeStatus != 'NotAvailableForTrading') 158 | return [ 159 | ["Min ask", "Quantity"], 160 | [asks[0].price, asks[0].quantity] 161 | ] 162 | else 163 | return null 164 | } 165 | 166 | function _calculateTrades(trades) { 167 | let totalSum = 0 168 | let totalQuantity = 0 169 | for (let j in trades) { 170 | const {quantity, price} = trades[j] 171 | totalQuantity += quantity 172 | totalSum += quantity * price 173 | } 174 | const weigthedPrice = totalSum / totalQuantity 175 | return [totalQuantity, totalSum, weigthedPrice] 176 | } 177 | 178 | /** 179 | * Получение списка операций по тикеру инструмента 180 | * @param {String} ticker Тикер инструмента для фильтрации 181 | * @param {String} from Начальная дата 182 | * @param {String} to Конечная дата 183 | * @return {Array} Массив результата 184 | * @customfunction 185 | */ 186 | function getTrades(ticker, from, to) { 187 | const figi = _getFigiByTicker(ticker) 188 | if (!from) { 189 | from = TRADING_START_AT.toISOString() 190 | } 191 | if (!to) { 192 | const now = new Date() 193 | to = new Date(now + MILLIS_PER_DAY) 194 | to = to.toISOString() 195 | } 196 | const operations = tinkoffClient.getOperations(from, to, figi) 197 | 198 | const values = [ 199 | ["ID", "Date", "Operation", "Ticker", "Quantity", "Price", "Currency", "SUM", "Commission"], 200 | ] 201 | for (let i=operations.length-1; i>=0; i--) { 202 | const {operationType, status, trades, id, date, currency, commission} = operations[i] 203 | if (operationType == "BrokerCommission" || status == "Decline" || operationType == "Dividend") 204 | continue 205 | let [totalQuantity, totalSum, weigthedPrice] = _calculateTrades(trades) // calculate weighted values 206 | if (operationType == "Buy") { // inverse values in a way, that it will be easier to work with 207 | totalQuantity = -totalQuantity 208 | totalSum = -totalSum 209 | } 210 | let com_val = 0 211 | if (commission){ 212 | com_val = commission.value 213 | }else{ 214 | com_val = null 215 | } 216 | values.push([ 217 | id, isoToDate(date), operationType, ticker, totalQuantity, weigthedPrice, currency, totalSum, com_val 218 | ]) 219 | } 220 | return values 221 | } 222 | 223 | /** 224 | * Получение портфеля 225 | * @return {Array} Массив с результатами 226 | * @customfunction 227 | */ 228 | function getPortfolio() { 229 | const portfolio = tinkoffClient.getPortfolio() 230 | const values = [] 231 | values.push(["Тикер","Название","Тип","Кол-во","Ср.цена покупки","Ст-ть покупки","Валюта","Доход","Тек.ст-ть","Валюта","НКД","Валюта"]) 232 | for (let i=0; i= 0) { 429 | const data = tinkoffClientV2._GetAccounts() 430 | 431 | return data.accounts[accountNum].id 432 | } 433 | } 434 | 435 | /** 436 | * Получение Bid/Ask спреда инструмента по тикеру 437 | * @param {"GAZP"} ticker Тикер инструмента 438 | * @return {0.03} Спред в % 439 | * @customfunction 440 | */ 441 | function TI_GetBidAskSpread(ticker) { 442 | const figi = _getFigiByTicker(ticker) 443 | if(figi) { 444 | const {depth,bids,asks} = tinkoffClientV2._GetOrderBookByFigi(figi, 1) 445 | if ((bids.length > 0) && (asks.length > 0)) 446 | return ((Number(asks[0].price.units)+asks[0].price.nano/1000000000) - (Number(bids[0].price.units)+bids[0].price.nano/1000000000)) / (Number(asks[0].price.units) + asks[0].price.nano/1000000000) 447 | else 448 | return null 449 | } 450 | } 451 | 452 | /** 453 | * Получение портфеля 454 | * @param {"12345678"} accountId Номер брокерского счета 455 | * @return {Array} Массив с результатами 456 | * @customfunction 457 | **/ 458 | function TI_GetPortfolio(accountId) { 459 | const portfolio = tinkoffClientV2._GetPortfolio(accountId) 460 | const values = [] 461 | values.push(["Тикер","Название","Тип","Кол-во","Ср.цена покупки","Ст-ть покупки","Валюта","Доход","Тек.ст-ть","Валюта","НКД","Валюта"]) 462 | for (let i=0; i Script properties` со значением токена, полученным выше. 16 | * Сохранить скрипт 💾 17 | * В документе Google Spreadsheets выбрать любую ненужную ячейку и присвоить ей имя `UPDATE_DATE` с помощью меню `Data`->`Named ranges`->`Add named range`. В эту ячейку по команде меню TI->Обновить вставляется текущая дата. Данная ячейка может использоваться в качестве необязательного параметра `UPDATE_DATE` любой функции для [принудительного обновления формул](https://stackoverflow.com/a/27656313). Например, при использовании функции `TI_GetLastPriceByFigi("BBG004730RP0",UPDATE_DATE)` можно реализовать обновление цены акции "Газпром" по команде меню TI->Обновить. 18 | 19 | На этом всё. Теперь при работе с этим документом на всех листах будут доступны функции: 20 | * API v2: `TI_GetAccounts()`, `TI_GetAccountID()`, `TI_GetInstrumentsID()`, `TI_GetLastPriceByFigi()`, `TI_GetPortfolio()`, `TI_GetOperations()`, `TI_GetBidAskSpread()` и `TI_GetLastPrice()` (использует API v1 и может перестать работать) 21 | * API v1: `getPriceByTicker()`, `getTrades()`, `getPortfolio()`, `getMaxBidByTicker()`, `getMinAskByTicker()` и `getBidAskSpread()` 22 | 23 | ## Функции API v2 24 | 25 | * `TI_GetAccounts()` - получение информации по счетам (идентификатор, тип, название, статус, дата открытия, права доступа). 26 | 27 | * `TI_GetAccountID(accountNum)` - возвращает идентификатор счета по его порядковому номеру (начиная с 0). 28 | 29 | * `=TI_GetLastPriceByFigi(FIGI, UPDATE_DATE)` - требует на вход FIGI инструмента (может быть получен через `=VLOOKUP()` по отдельной вкладке с информацией по всем инструментам, полученной вызовом `TI_GetInstrumentsID()`) и опциональный параметр с именем ячейки `UPDATE_DATE`, которая будет обновлятся через меню TI->Обновить). 30 | 31 | * `=TI_GetInstrumentsID()` - выводит информацию по всем инструментам, включая их тикер, FIGI, класс, биржу, валюту и ISIN. 32 | 33 | * `=TI_GetOperations(accountId, from, to)` - выводит операции по заданному идентификатору счета (можно получить через функцию `TI_GetAccounts()`) и, опционально, фильтрацию по времени. Параметры `from` и `to` являются ссылками на ячейки с типом Дата. 34 | 35 | * `=TI_GetPortfolio(accountId)` - выводит портфель по заданному идентификатору счета (можно получить через функцию `TI_GetAccounts()`. 36 | 37 | ## Функции API v1 (перестанут работать в 2023 году) 38 | 39 | * `=getPriceByTicker(ticker, UPDATE_DATE)` - требует на вход [тикер](https://ru.wikipedia.org/wiki/%D0%A2%D0%B8%D0%BA%D0%B5%D1%80), и опциональный параметр с именем ячейки `UPDATE_DATE`, которая будет обновлятся через меню TI->Обновить). 40 | 41 | * `=getTrades(ticker, from, to)` - требует на вход [тикер](https://ru.wikipedia.org/wiki/%D0%A2%D0%B8%D0%BA%D0%B5%D1%80), и опционально фильтрацию по времени. Параметры `from` и `to` являются строками и должны быть в [ISO 8601 формате](https://ru.wikipedia.org/wiki/ISO_8601) 42 | 43 | * `=getPortfolio()` - выводит портфель. 44 | 45 | ## Особенности 46 | 47 | * Среди настроек скрипта есть `TRADING_START_AT` - дефолтная дата, начиная с которой фильтруются операции `getTrades`. По умолчанию это `Apr 01, 2020 10:00:00`, но данную константу можно в любой момент поменять в исходном коде. 48 | 49 | ## Пример использования работы функций API v1 50 | 51 | ``` 52 | =getPriceByTicker("V", UPDATE_DATE) # Возвращает текущую цену акции Visa 53 | =getPriceByTicker("FXMM", UPDATE_DATE) # Возвращает текущую цену фонда казначейских облигаций США 54 | 55 | =getTrades("V") 56 | # Вернёт все операции с акцией Visa, которые произошли начиная с TRADING_START_AT и по текущий момент. 57 | =getTrades("V", "2020-05-01T00:00:00.000Z") 58 | # Вернёт все операции с акцией Visa, которые произошли начиная с 1 мая и по текущий моментs. 59 | =getTrades("V", "2020-05-01T00:00:00.000Z", "2020-05-05T23:59:59.999Z") 60 | # Вернёт все операции с акцией Visa, которые произошли в период с 1 и по 5 мая. 61 | ``` 62 | 63 | ## Пример работы функций API v1 64 | 65 | #### `=getTrades()` 66 | ![getTrades in action](https://github.com/ErhoSen/gas-tinkoff-trades/raw/master/images/get-trades-in-action.gif "getTrades in Action") 67 | 68 | #### `=getPriceByTicker()` 69 | ![Get price by ticker in action](https://github.com/ErhoSen/gas-tinkoff-trades/raw/master/images/get-price-by-ticker.gif) 70 | -------------------------------------------------------------------------------- /images/get-price-by-ticker.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erhosen/gas-tinkoff-trades/a3f1aa5206006b1497aef0dfedbaf4ca90584421/images/get-price-by-ticker.gif -------------------------------------------------------------------------------- /images/get-trades-in-action.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erhosen/gas-tinkoff-trades/a3f1aa5206006b1497aef0dfedbaf4ca90584421/images/get-trades-in-action.gif -------------------------------------------------------------------------------- /images/main-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erhosen/gas-tinkoff-trades/a3f1aa5206006b1497aef0dfedbaf4ca90584421/images/main-image.jpg --------------------------------------------------------------------------------