├── .gitignore ├── README.md ├── crypto-tracker-screenshot.png └── source ├── CoinMarketApis.gs ├── Helper.gs ├── Sheet.gs └── init.gs /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crypto portfolio tracker for Google Sheets 2 | This repository is taking care of the Google Apps Script files for the free portfolio tracker using Google Sheets. 3 | 4 | Please see [Free Crypto Portfolio Tracker based on Google Sheets on Medium](https://medium.com/@techupbusiness/free-crypto-portfolio-tracker-based-on-google-sheets-ef76070ec325). 5 | 6 | ![Crypto Tracker Screenshot](crypto-tracker-screenshot.png "Crypto Tracker Screenshot") 7 | 8 | ## NOTE 9 | You can't run this code directly and independent of [its Google Sheet](https://medium.com/@techupbusiness/free-crypto-portfolio-tracker-based-on-google-sheets-ef76070ec325). -------------------------------------------------------------------------------- /crypto-tracker-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechupBusiness/coin-tracker-google-sheets/f543a7db1f113ec5ccfe0525f21cbf607e0f98f1/crypto-tracker-screenshot.png -------------------------------------------------------------------------------- /source/CoinMarketApis.gs: -------------------------------------------------------------------------------- 1 | /** 2 | * @OnlyCurrentDoc 3 | */ 4 | /* 5 | Copyright (C) 2018 TechupBusiness (info@techupbusiness.com) 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | /******************************** 22 | * 23 | * CLASSES 24 | * 25 | ********************************/ 26 | 27 | /* 28 | "id": "bitcoin", 29 | "name": "Bitcoin", 30 | "symbol": "BTC", 31 | "rank": "1", 32 | "price_usd": "8883.34", 33 | "price_btc": "1.0", 34 | "24h_volume_usd": "8120150000.0", 35 | "market_cap_usd": "150215280648", 36 | "available_supply": "16909775.0", 37 | "total_supply": "16909775.0", 38 | "max_supply": "21000000.0", 39 | "percent_change_1h": "0.17", 40 | "percent_change_24h": "-5.98", 41 | "percent_change_7d": "-19.33", 42 | "last_updated": "1520626767", 43 | "price_eur": "7207.65780912", 44 | "24h_volume_eur": "6588429865.2", 45 | "market_cap_eur": "121879871829" 46 | */ 47 | 48 | /******************************** 49 | * 50 | * METHODS 51 | * 52 | ********************************/ 53 | 54 | function getCryptoCompareApiKeyUrlSuffix() { 55 | var apiKey = getSetting(6,2) 56 | if(!isEmpty(apiKey)) { 57 | return '&api_key=' + apiKey 58 | } else { 59 | return ''; 60 | } 61 | } 62 | 63 | function getValueFromOhlcCryptoCompare(priceData) { 64 | var candle = priceData['Data'][1] 65 | var average = (candle.open + candle.close) / 2 66 | return average; 67 | } 68 | 69 | function fetchCryptoCompareRates(CryptoCurrencies, FiatCurrency, DateTime) { 70 | 71 | var neededCryptoCurrencies; 72 | var rates = [ ]; 73 | var cacheKey, cachedValue; 74 | 75 | var cacheKeySuffix = ""; // used for DateTime 76 | 77 | if(!FiatCurrency) { 78 | FiatCurrency = getFiatName(); 79 | } 80 | 81 | if(!isEmpty(DateTime) && DateTime instanceof Date) { 82 | // Note: API is only storing one value per day (not accurate unfortunately) 83 | var strCacheDate = "T"+DateTime.getFullYear()+(DateTime.getMonth()+1).padLeft(2)+DateTime.getDate().padLeft(2); 84 | var dCacheDate = new Date(DateTime.getFullYear(), DateTime.getMonth(), DateTime.getDate(), DateTime.getHours()+1, 0, 0); 85 | cacheKeySuffix = strCacheDate; 86 | } 87 | 88 | // Get cached values 89 | if(CryptoCurrencies instanceof Array) { 90 | neededCryptoCurrencies = [ ]; 91 | 92 | for(var currencyIndex in CryptoCurrencies) { 93 | var currency=CryptoCurrencies[currencyIndex]; 94 | cacheKey = "cc"+FiatCurrency+currency+cacheKeySuffix; 95 | cachedValue = getCache(cacheKey); 96 | if(cachedValue>0) { 97 | rates[currency] = cachedValue; 98 | } else { 99 | neededCryptoCurrencies.push(currency); 100 | } 101 | } 102 | } else { 103 | cacheKey = "cc"+FiatCurrency+CryptoCurrencies+cacheKeySuffix; 104 | cachedValue = getCache(cacheKey); 105 | if(isNumeric(cachedValue) && cachedValue>0) { 106 | rates[CryptoCurrencies] = cachedValue; 107 | } else if(cachedValue=="notfound") { 108 | return null; 109 | } else { 110 | neededCryptoCurrencies = CryptoCurrencies; 111 | } 112 | } 113 | 114 | // Start getting data from API 115 | if(neededCryptoCurrencies!=undefined) { 116 | var urls = [ ]; 117 | if(neededCryptoCurrencies instanceof Array) { 118 | if(!isEmpty(DateTime) && DateTime instanceof Date) { 119 | for(var currencyIndex in neededCryptoCurrencies) { 120 | var currency=neededCryptoCurrencies[currencyIndex]; 121 | urls.push("https://min-api.cryptocompare.com/data/histohour?limit=1&fsym=" + currency + "&tsym=" + FiatCurrency + "&toTs=" + (dCacheDate.getTime() / 1000) + getCryptoCompareApiKeyUrlSuffix()); 122 | } 123 | } else { 124 | urls.push("https://min-api.cryptocompare.com/data/pricemulti?fsyms=" + neededCryptoCurrencies.join(',') + "&tsyms=" + FiatCurrency + getCryptoCompareApiKeyUrlSuffix()); 125 | } 126 | } else { 127 | if(!isEmpty(DateTime) && DateTime instanceof Date) { 128 | urls.push("https://min-api.cryptocompare.com/data/histohour?limit=1&fsym=" + neededCryptoCurrencies + "&tsym=" + FiatCurrency + "&toTs=" + (dCacheDate.getTime() / 1000) + getCryptoCompareApiKeyUrlSuffix()); 129 | } else { 130 | urls.push("https://min-api.cryptocompare.com/data/price?fsym=" + neededCryptoCurrencies + "&tsyms=" + FiatCurrency + getCryptoCompareApiKeyUrlSuffix()); 131 | } 132 | } 133 | 134 | for(var url in urls) { 135 | var returnText = fetchUrl(urls[url]); 136 | if(returnText=="") { 137 | //writeLog(new Date(),"fetchCryptoCompareRates","CryptoCompare Server Response is empty for " + neededCryptoCurrencies); 138 | return null; 139 | } 140 | var priceData = JSON.parse(returnText); 141 | if(priceData.Response == "Error") { 142 | //writeLog(new Date(),"fetchCryptoCompareRates","CryptoCompare Server Response error: " + priceData.Message); 143 | 144 | // Save to cache if no data found to avoid further searches 145 | if(priceData.Message.indexOf("no data") !== -1 && isString(neededCryptoCurrencies)) { 146 | cacheKey = "cc"+FiatCurrency+neededCryptoCurrencies+cacheKeySuffix; 147 | setCache(cacheKey,"notfound"); 148 | } 149 | return null; 150 | } else { 151 | if(neededCryptoCurrencies instanceof Array) { 152 | for(var cryptoIndex in neededCryptoCurrencies) { 153 | var CryptoCurrency = neededCryptoCurrencies[cryptoIndex]; 154 | 155 | if(priceData[CryptoCurrency][FiatCurrency]>0) { 156 | rates[CryptoCurrency] = priceData[CryptoCurrency][FiatCurrency]; 157 | 158 | // Save to cache 159 | cacheKey = "cc"+FiatCurrency+CryptoCurrency+cacheKeySuffix; 160 | setCache(cacheKey,rates[CryptoCurrency]); 161 | } 162 | } 163 | } else { 164 | if(!isEmpty(DateTime) && DateTime instanceof Date) { 165 | rates[neededCryptoCurrencies] = getValueFromOhlcCryptoCompare(priceData); 166 | } else if(priceData[FiatCurrency]>0) { 167 | rates[neededCryptoCurrencies] = priceData[FiatCurrency]; 168 | } 169 | 170 | // Save to cache 171 | if(rates[neededCryptoCurrencies]>0) { 172 | cacheKey = "cc"+FiatCurrency+neededCryptoCurrencies+cacheKeySuffix; 173 | setCache(cacheKey,rates[neededCryptoCurrencies]); 174 | } 175 | } 176 | } 177 | } 178 | 179 | } 180 | 181 | return rates; 182 | } 183 | 184 | 185 | function getCryptoFiatRate(currency, DateTime, FiatCurrency) { 186 | 187 | if(!FiatCurrency) { 188 | FiatCurrency = getFiatName(); 189 | } 190 | 191 | // Second Fallback CryptoCompare 192 | var serviceCurrencyName = getFinalCoinName("CryptoCompare",currency); 193 | var cc = fetchCryptoCompareRates(serviceCurrencyName, FiatCurrency, DateTime); 194 | 195 | if(!isEmpty(cc) && isNumeric(cc[currency])) { 196 | return parseFloat(cc[currency]); 197 | } else { 198 | // Third Fallback is Price in Sheet 199 | var localCurrencyPrice = findValue("Coin Settings", "FallbackRateFiat", "Currency", currency, true); 200 | if(isNumeric(localCurrencyPrice)) { 201 | return parseFloat(localCurrencyPrice); 202 | } else { 203 | return null; 204 | } 205 | } 206 | 207 | } 208 | -------------------------------------------------------------------------------- /source/Helper.gs: -------------------------------------------------------------------------------- 1 | /** 2 | * @OnlyCurrentDoc 3 | */ 4 | /* 5 | Copyright (C) 2018 TechupBusiness (info@techupbusiness.com) 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | /******************************** 22 | * 23 | * Read external data handler 24 | * 25 | ********************************/ 26 | 27 | function fetchUrl(url) { 28 | var finalUrl; 29 | 30 | var proxyUrl = getCache("settings-proxy-url"); 31 | var proxyPassword = getCache("settings-proxy-pw"); 32 | if(isEmpty(proxyUrl)) { 33 | var spreadsheet = SpreadsheetApp.getActive(); 34 | var sheet = spreadsheet.getSheetByName("Settings"); 35 | proxyUrl = sheet.getRange(3, 2).getValue(); 36 | proxyPassword = sheet.getRange(4, 2).getValue(); 37 | 38 | if(isEmpty(proxyUrl)) { 39 | setCache("settings-proxy-url","NONE"); 40 | proxyUrl = 'NONE'; 41 | } else { 42 | setCache("settings-proxy-url", proxyUrl); 43 | setCache("settings-proxy-pw", proxyPassword); 44 | } 45 | } 46 | 47 | if(proxyUrl!="NONE") { 48 | finalUrl = proxyUrl + "?url=" + encodeURIComponent(url) + "&token=" + encodeURIComponent(proxyPassword); 49 | } else { 50 | finalUrl = url; 51 | } 52 | 53 | var response = UrlFetchApp.fetch(finalUrl); 54 | var returnText = response.getContentText(); 55 | return returnText; 56 | } 57 | 58 | 59 | /******************************** 60 | * 61 | * Data type helper (string, array, ...) 62 | * 63 | ********************************/ 64 | 65 | if (!String.prototype.splice) { 66 | /** 67 | * {JSDoc} 68 | * 69 | * The splice() method changes the content of a string by removing a range of 70 | * characters and/or adding new characters. 71 | * 72 | * @this {String} 73 | * @param {number} start Index at which to start changing the string. 74 | * @param {number} delCount An integer indicating the number of old chars to remove. 75 | * @param {string} newSubStr The String that is spliced in. 76 | * @return {string} A new string with the spliced substring. 77 | */ 78 | String.prototype.splice = function(start, delCount, newSubStr) { 79 | return this.slice(0, start) + newSubStr + this.slice(start + Math.abs(delCount)); 80 | }; 81 | } 82 | 83 | function isEmpty(variable) { 84 | if(variable==undefined || variable==null) { 85 | return true; 86 | } 87 | 88 | if(typeof variable == "string" && variable=="") { 89 | return true; 90 | } 91 | 92 | 93 | return false; 94 | } 95 | 96 | /** 97 | * Split a string into chunks of the given size 98 | * @param {String} string is the String to split 99 | * @param {Number} size is the size you of the cuts 100 | * @return {Array} an Array with the strings 101 | */ 102 | function splitString (string, size) { 103 | var re = new RegExp('.{1,' + size + '}', 'g'); 104 | return string.match(re); 105 | } 106 | 107 | function padLeft(nr, n, str){ 108 | return Array(n-String(nr).length+1).join(str||'0')+nr; 109 | } 110 | Number.prototype.padLeft = function (n,str){ 111 | return Array(n-String(this).length+1).join(str||'0')+this; 112 | } 113 | 114 | 115 | function inArray(needle, haystack) { 116 | if(needle in haystack) { 117 | return true; 118 | } else { 119 | return false; 120 | } 121 | } 122 | 123 | function isString(str) { 124 | return typeof str === "string"; 125 | } 126 | 127 | function isNumeric(n) { 128 | return !isNaN(parseFloat(n)) && isFinite(n); 129 | } 130 | 131 | function Dictionary (pkey, pvalue) { 132 | this.keys = []; 133 | this.values = []; 134 | 135 | // Constructor 136 | if(pkey && pvalue) { 137 | this.set(pkey, pvalue); 138 | } 139 | 140 | this.get = function (key) { 141 | return this.values[this.keys.indexOf(key)] 142 | }; 143 | 144 | this.ifGet = function (key, alternativeValue) { 145 | if(this.contains(key)) { 146 | return this.values[this.keys.indexOf(key)] 147 | } else { 148 | this.set(alternativeValue); 149 | return alternativeValue; 150 | } 151 | }; 152 | 153 | // Alias for set 154 | this.add = function(key, value) { 155 | this.set(key,value); 156 | } 157 | 158 | this.set = function (key, value) { 159 | var i = this.keys.indexOf(key); 160 | if (i === -1) { 161 | i = this.keys.length; 162 | } 163 | this.keys[i] = key; 164 | this.values[i] = value; 165 | }; 166 | 167 | this.contains = function(key) { 168 | return this.keys.indexOf(key) > -1; 169 | }; 170 | 171 | this.remove = function (key) { 172 | var i = this.keys.indexOf(key); 173 | this.keys.splice(i, 1); 174 | this.values.splice(i, 1); 175 | }; 176 | 177 | this.toAssocArray = function(callback) { 178 | var newArray = [ ]; 179 | return this.toIterator(newArray, callback); 180 | }; 181 | 182 | this.toArray = function(callback) { 183 | if(callback instanceof Function) { 184 | var newArray = [ ]; 185 | for(var i=0; i 0) 268 | { 269 | temp = (column - 1) % 26; 270 | letter = String.fromCharCode(temp + 65) + letter; 271 | column = (column - temp - 1) / 26; 272 | } 273 | return letter; 274 | } 275 | 276 | function letterToColumn(letter) { 277 | var column = 0, length = letter.length; 278 | for (var i = 0; i < length; i++) 279 | { 280 | column += (letter.charCodeAt(i) - 64) * Math.pow(26, length - i - 1); 281 | } 282 | return column; 283 | } 284 | 285 | function findValue(SheetName, ResultHeaderName, RowFilterColumnName, RowFilterValue, DisableCache) { 286 | var values = findValues(SheetName, ResultHeaderName, RowFilterColumnName, RowFilterValue, DisableCache); 287 | if(!isEmpty(values) && !isEmpty(values[ResultHeaderName])) { 288 | return values[ResultHeaderName]; 289 | } else { 290 | return null; 291 | } 292 | } 293 | 294 | function findValues(SheetName, ResultHeaderNames, RowFilterColumnName, RowFilterValue, DisableCache) { 295 | 296 | var foundValues = [ ]; 297 | if(!(ResultHeaderNames instanceof Array)) { 298 | ResultHeaderNames = [ ResultHeaderNames ]; 299 | } 300 | 301 | var cacheKey = "findCache" + SheetName + ResultHeaderNames.join('-') + RowFilterColumnName + RowFilterValue; 302 | if(DisableCache!=true) { 303 | foundValues = getCache( cacheKey ); 304 | if(foundValues!=null) { 305 | if(foundValues.length==0) { 306 | return null; 307 | } else { 308 | return foundValues; 309 | } 310 | } else { 311 | foundValues = [ ]; 312 | } 313 | } 314 | 315 | var spreadsheet = SpreadsheetApp.getActive(); 316 | var sheet = spreadsheet.getSheetByName(SheetName); 317 | var range = sheet.getDataRange(); 318 | var data = range.getValues(); 319 | 320 | var header = data[0]; 321 | var targetColIndexes = new Dictionary(); 322 | var lookupColIndex; 323 | 324 | for (var c = 0; c < header.length; c++) { 325 | if( ResultHeaderNames.indexOf(header[c]) > -1) { 326 | targetColIndexes.set(c,header[c]); 327 | } 328 | 329 | if(header[c] == RowFilterColumnName) { 330 | lookupColIndex = c; 331 | } 332 | } 333 | 334 | if(!isEmpty(lookupColIndex)) { 335 | for (var r = 1; r < data.length; r++) { 336 | if(data[r][lookupColIndex] == RowFilterValue) { 337 | var result = targetColIndexes.toAssocArray(); 338 | for(var resultIndex in result) { 339 | foundValues[result[resultIndex]] = data[r][resultIndex]; 340 | } 341 | } 342 | } 343 | } 344 | 345 | if(DisableCache!=true) { 346 | setCache( cacheKey , foundValues ); 347 | } 348 | 349 | return foundValues; 350 | } 351 | 352 | function getSetting(rowIndex, columnIndex) { 353 | var spreadsheet = SpreadsheetApp.getActive(); 354 | var sheet = spreadsheet.getSheetByName("Settings"); 355 | var content = sheet.getRange(rowIndex, columnIndex).getValue(); 356 | return content 357 | } 358 | 359 | 360 | /******************************** 361 | * 362 | * Cache handling 363 | * https://developers.google.com/apps-script/reference/cache/cache 364 | * 365 | ********************************/ 366 | 367 | var cacheDisabled; 368 | function disableCache() { 369 | cacheDisabled = true; 370 | } 371 | function enableCache() { 372 | cacheDisabled = undefined; 373 | } 374 | 375 | function setCache(key, value, time) { 376 | time = parseInt(time); 377 | if(time==0 || isNaN(time)) { 378 | time = 600; // 10 min; the maximum time the value will remain in the cache, in seconds. The minimum is 1 second and the maximum is 21600 seconds (6 hours). 379 | } 380 | var cacheService = CacheService.getUserCache(); 381 | 382 | value = JSON.stringify(value); 383 | if(value.length > 250) { 384 | return false; 385 | } 386 | 387 | cacheService.put(key, value, time); 388 | return true; 389 | } 390 | 391 | function getCache(key, value) { 392 | if(cacheDisabled==true) { 393 | return null; 394 | } 395 | var cacheService = CacheService.getUserCache(); 396 | 397 | var items = cacheService.get(key); 398 | 399 | if(!items) { 400 | return null 401 | } 402 | 403 | return JSON.parse(items); 404 | } 405 | 406 | function deleteCache(key) { 407 | var cacheService = CacheService.getUserCache(); 408 | cacheService.remove(key); 409 | } 410 | 411 | 412 | -------------------------------------------------------------------------------- /source/Sheet.gs: -------------------------------------------------------------------------------- 1 | /** 2 | * @OnlyCurrentDoc 3 | */ 4 | /* 5 | Copyright (C) 2018 TechupBusiness (info@techupbusiness.com) 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | /******************************** 22 | * 23 | * CLASSES 24 | * 25 | ********************************/ 26 | 27 | var CoinMarketCapCoin = function(coinRow) { 28 | this.Id = coinRow[0]; 29 | this.Name = coinRow[1]; 30 | this.Symbol = coinRow[2]; 31 | this.Rank = coinRow[3]; 32 | this.PriceUsd = coinRow[4]; 33 | this.PriceBtc = coinRow[5]; 34 | this.Volume24hUsd = coinRow[6]; 35 | this.MarketCapUsd = coinRow[7]; 36 | this.AvailableSupply = coinRow[8]; 37 | this.TotalSupply = coinRow[9]; 38 | this.MaxSupply = coinRow[10]; 39 | this.PercentChange1h = coinRow[11]; 40 | this.PercentChange24h = coinRow[12]; 41 | this.PercentChange7d = coinRow[13]; 42 | this.LastUpdated = coinRow[14]; 43 | this.PriceEur = coinRow[15]; 44 | this.Volume24hEur = coinRow[16]; 45 | this.MarketCapEur = coinRow[17]; 46 | 47 | this.toArray = function() { 48 | var newArray = [ 49 | this.Id, 50 | this.Name, 51 | this.Symbol, 52 | this.Rank, 53 | this.PriceUsd, 54 | this.PriceBtc, 55 | this.Volume24hUsd, 56 | this.MarketCapUsd, 57 | this.AvailableSupply, 58 | this.TotalSupply, 59 | this.MaxSupply, 60 | this.PercentChange1h, 61 | this.PercentChange24h, 62 | this.PercentChange7d, 63 | this.LastUpdated, 64 | this.PriceEur, 65 | this.Volume24hEur, 66 | this.MarketCapEur, 67 | ]; 68 | return newArray; 69 | }; 70 | }; 71 | 72 | 73 | 74 | var Trade = function(tradeRow) { 75 | this.Date = tradeRow[0]; 76 | this.Type = tradeRow[1]; 77 | this.BuyValue = tradeRow[2]; 78 | this.BuyCurrency = tradeRow[3]; 79 | this.BuyFiatValue = tradeRow[4]; 80 | this.SellValue = tradeRow[5]; 81 | this.SellCurrency = tradeRow[6]; 82 | this.SellFiatValue = tradeRow[7]; 83 | this.FeeValue = tradeRow[8]; 84 | this.FeeCurrency = tradeRow[9]; 85 | this.FeeFiatValue = tradeRow[10]; 86 | this.Exchange = tradeRow[11]; 87 | this.Wallet = tradeRow[12]; 88 | this.Account = tradeRow[13]; 89 | this.Group = tradeRow[14]; 90 | this.Comment = tradeRow[15]; 91 | 92 | this.toArray = function() { 93 | var newArray = [ 94 | this.Date, 95 | this.Type, 96 | this.BuyValue, 97 | this.BuyCurrency, 98 | this.BuyFiatValue, 99 | this.SellValue, 100 | this.SellCurrency, 101 | this.SellFiatValue, 102 | this.FeeValue, 103 | this.FeeCurrency, 104 | this.FeeFiatValue, 105 | this.Exchange, 106 | this.Wallet, 107 | this.Account, 108 | this.Group, 109 | this.Comment, 110 | ]; 111 | return newArray; 112 | }; 113 | }; 114 | 115 | 116 | var CoinValue = function(item) { 117 | if(item instanceof Array) { 118 | this.CoinCode = item[0]; 119 | this.CoinCount = item[1]; 120 | this.AverageCoinPriceFiat = item[2]; 121 | this.CurrentCoinPriceFiat = item[3]; 122 | this.PayedCoinPriceTotalFiat = item[4]; 123 | this.CurrentCoinPriceTotalFiat = item[5]; 124 | this.ProfitLossFiat = item[6]; 125 | this.ProfitLossPercent = item[7]; 126 | } else { 127 | this.CoinCode = ""; 128 | this.CoinCount = 0.0; 129 | this.AverageCoinPriceFiat = 0.0; 130 | this.CurrentCoinPriceFiat = 0.0; 131 | this.PayedCoinPriceTotalFiat = 0.0; 132 | this.CurrentCoinPriceTotalFiat = 0.0; 133 | this.ProfitLossFiat = 0.0; 134 | this.ProfitLossPercent = 0.0; 135 | } 136 | 137 | this.toArray = function() { 138 | var newArray = [ 139 | this.CoinCode, 140 | this.CoinCount, 141 | this.AverageCoinPriceFiat, 142 | this.CurrentCoinPriceFiat, 143 | this.PayedCoinPriceTotalFiat, 144 | this.CurrentCoinPriceTotalFiat, 145 | this.ProfitLossFiat, 146 | this.ProfitLossPercent, 147 | ]; 148 | return newArray; 149 | }; 150 | }; 151 | 152 | var AccountValue = function(item) { 153 | if(item instanceof Array) { 154 | this.Account = item[0]; 155 | this.Exchange = item[1]; 156 | this.Wallet = item[2]; 157 | this.Currency = item[3]; 158 | this.Value = item[4]; 159 | this.FiatValue = item[5]; 160 | } else { 161 | this.Account = ""; 162 | this.Exchange = ""; 163 | this.Wallet = ""; 164 | this.Currency = ""; 165 | this.Value = 0.0; 166 | this.FiatValue = 0.0; 167 | } 168 | 169 | this.toArray = function() { 170 | var newArray = [ 171 | this.Account, 172 | this.Exchange, 173 | this.Wallet, 174 | this.Currency, 175 | this.Value, 176 | this.FiatValue 177 | ]; 178 | return newArray; 179 | }; 180 | }; 181 | 182 | 183 | /******************************** 184 | * 185 | * HELPER METHODS 186 | * 187 | ********************************/ 188 | 189 | function getFiatName() { 190 | var cacheKey = "settings_fiatname"; 191 | var cache = CacheService.getUserCache(); 192 | var cacheName = cache.get(cacheKey); 193 | 194 | if(!isEmpty(cacheName)) { 195 | return cacheName; 196 | } else { 197 | var spreadsheet = SpreadsheetApp.getActive(); 198 | var sheet = spreadsheet.getSheetByName("Settings"); 199 | var currencyName = sheet.getRange(1, 2).getValue(); 200 | cache.put(cacheKey, currencyName); 201 | return currencyName; 202 | } 203 | } 204 | 205 | /******************************** 206 | * 207 | * MAIN METHODS 208 | * 209 | ********************************/ 210 | 211 | /** 212 | * Returns the data of the custom coin sheet (10 Second cache) 213 | * 214 | * @param {String} CoinCode Currency code of coin 215 | * @param {String} Field Field from 216 | * @return Object 217 | * @customfunction 218 | */ 219 | function CustomCoinData(CoinCode, Field, Sheet) { 220 | if(Sheet == null) { 221 | Sheet = "Coin Settings"; 222 | } 223 | 224 | var cache = CacheService.getUserCache(); 225 | var cacheCoin = cache.get("customcoin"+CoinCode); 226 | 227 | if(cacheCoin != null) { 228 | 229 | var coin = JSON.parse(cacheCoin); 230 | return coin[Field]; 231 | 232 | } else { 233 | 234 | var spreadsheet = SpreadsheetApp.getActive(); 235 | var sheet = spreadsheet.getSheetByName(Sheet); 236 | var data = sheet.getDataRange().getValues(); 237 | for (var i = 0; i < data.length; i++) { 238 | 239 | if(CoinCode == data[i][0]) { 240 | var coin = new CoinMarketCapCoin(); 241 | coin.Symbol = data[i][0]; 242 | coin.Name = coinData[i][1]; 243 | coin.PriceUsd = data[i][2]; 244 | coin.PriceEur = data[i][3]; 245 | 246 | cache.put("custom" + CoinCode, JSON.stringify(coin), 10); 247 | 248 | return coin[Field]; 249 | } 250 | } 251 | } 252 | 253 | return ""; 254 | } 255 | 256 | function writeHistoricalTradeData() { 257 | 258 | var spreadsheet = SpreadsheetApp.getActive(); 259 | var tradeSheet = spreadsheet.getSheetByName("Trades"); 260 | var tradeValues = tradeSheet.getDataRange().getValues(); 261 | var fiatCurrency = getFiatName(); 262 | 263 | for (var i = 1; i < tradeValues.length; i++) { 264 | var trade = new Trade(tradeValues[i]); 265 | var FiatSellValue = 0.0; 266 | var FiatBuyValue = 0.0; 267 | 268 | if(trade.SellValue>0 && trade.SellCurrency!="" && trade.SellCurrency!=fiatCurrency && typeof trade.SellFiatValue!="number") { 269 | if(trade.BuyCurrency==fiatCurrency && trade.BuyValue>0) { 270 | FiatSellValue = trade.BuyValue; 271 | tradeSheet.getRange(i+1, 8).setValue(FiatSellValue); 272 | } else { 273 | var FiatCoinValue = getCryptoFiatRate(trade.SellCurrency, trade.Date, fiatCurrency); 274 | if(FiatCoinValue>0) { 275 | FiatSellValue = FiatCoinValue * trade.SellValue; 276 | tradeSheet.getRange(i+1, 8).setValue(FiatSellValue); 277 | } 278 | } 279 | } 280 | 281 | if(trade.BuyValue>0 && trade.BuyCurrency!="" && trade.BuyCurrency!=fiatCurrency && typeof trade.BuyFiatValue!="number") { 282 | if(trade.SellCurrency==fiatCurrency && trade.SellValue>0) { 283 | FiatBuyValue = trade.SellValue; 284 | tradeSheet.getRange(i+1, 5).setValue(FiatBuyValue); 285 | } else if(FiatSellValue>0) { 286 | tradeSheet.getRange(i+1, 5).setValue(FiatSellValue); 287 | } else { 288 | var FiatCoinValue = getCryptoFiatRate(trade.BuyCurrency, trade.Date, fiatCurrency); 289 | if(FiatCoinValue>0) { 290 | FiatBuyValue = FiatCoinValue * trade.BuyValue; 291 | tradeSheet.getRange(i+1, 5).setValue(FiatBuyValue); 292 | } 293 | } 294 | } 295 | 296 | if(trade.FeeValue>0 && trade.FeeCurrency!="" && trade.FeeCurrency!=fiatCurrency && typeof trade.FeeFiatValue!="number") { 297 | var FiatCoinValue = getCryptoFiatRate(trade.FeeCurrency, trade.Date, fiatCurrency); 298 | if(FiatCoinValue>0) { 299 | var FiatValue = FiatCoinValue * trade.FeeValue; 300 | tradeSheet.getRange(i+1, 11).setValue(FiatValue); 301 | } 302 | } 303 | } 304 | } 305 | 306 | function processCoinValue(typeBuySell, trade, coins, accounts, fiatCurrency) { 307 | var Buy = "Buy"; 308 | var Sell = "Sell"; 309 | var Fee = "Fee"; 310 | var currency = (trade[typeBuySell + "Currency"]!="" ? trade[typeBuySell + "Currency"] : ""); 311 | 312 | if(typeBuySell != Buy && typeBuySell != Sell && typeBuySell != Fee) { 313 | writeLog(new Date(),"processCoinValue", "Problem on currency " + currency + ": Type is not value 'Buy' nor 'Sell' nor 'Fee' (value is '" + typeBuySell + "')"); 314 | return false; 315 | } 316 | 317 | if(currency != "" && currency!=undefined) { 318 | /****************** 319 | * Coin profit 320 | ******************/ 321 | var valueCrypto = isNumeric(trade[typeBuySell + "Value"]) ? parseFloat(trade[typeBuySell + "Value"]) : 0.00; 322 | var valueFiat = isNumeric(trade[typeBuySell + "FiatValue"]) ? parseFloat(trade[typeBuySell + "FiatValue"]) : 0.00; 323 | 324 | var coin; 325 | 326 | if(!coins.contains(currency)) { 327 | var currentPriceFiat = 0.0; 328 | 329 | if(currency==fiatCurrency) { 330 | currentPriceFiat = 1.0; 331 | } else { 332 | // Get rates online (if possible) 333 | currentPriceFiat = getCryptoFiatRate(currency); 334 | } 335 | 336 | if(currentPriceFiat==0.0 || !isNumeric(currentPriceFiat)) { 337 | currentPriceFiat = null; 338 | } 339 | 340 | // Initiate new coin 341 | coin = new CoinValue(); 342 | coin.CoinCode = currency; 343 | coin.CurrentCoinPriceFiat = currentPriceFiat; 344 | coins.set(currency, coin); 345 | } 346 | 347 | coin = coins.get(currency); 348 | 349 | if(typeBuySell == Buy) { 350 | // Add all buys (count and fiat value) 351 | coin.CoinCount += valueCrypto; 352 | if(trade.Type==='Trade' || trade.Type==='Mining' || trade.Type==='Airdrop' || trade.Type==='Dividend' || trade.Type==='Correction' || trade.Type==='Gift') { 353 | coin.PayedCoinPriceTotalFiat += valueFiat; 354 | } 355 | } else if(typeBuySell == Sell) { 356 | // Substract all sells (count and fiat value) 357 | coin.CoinCount -= valueCrypto; 358 | if(trade.Type==='Trade' || trade.Type==='Loss' || trade.Type==='Expense' || trade.Type==='Correction' || trade.Type==='Gift') { 359 | coin.PayedCoinPriceTotalFiat -= valueFiat; 360 | } 361 | } else if(typeBuySell == Fee) { 362 | // Fee substract doesnt reduce the payed coin price (we include the fee in the coin price) 363 | coin.CoinCount -= valueCrypto; 364 | } 365 | 366 | if(coin.CurrentCoinPriceFiat == null || coin.CoinCount === 0) { 367 | coin.CurrentCoinPriceTotalFiat = null; 368 | coin.ProfitLossFiat = null; 369 | coin.ProfitLossPercent = null; 370 | } else { 371 | coin.CurrentCoinPriceTotalFiat = coin.CurrentCoinPriceFiat * coin.CoinCount; 372 | coin.ProfitLossFiat = currency!=fiatCurrency ? coin.CurrentCoinPriceTotalFiat - coin.PayedCoinPriceTotalFiat : null; 373 | coin.ProfitLossPercent = coin.PayedCoinPriceTotalFiat!=0 ? ((coin.ProfitLossFiat * 100) / coin.PayedCoinPriceTotalFiat) / 100 : null; 374 | } 375 | 376 | if(coin.CoinCount == 0) { 377 | coins.remove(currency); 378 | } 379 | else { 380 | coin.AverageCoinPriceFiat = coin.PayedCoinPriceTotalFiat / coin.CoinCount; 381 | } 382 | 383 | /****************** 384 | * Account Holdings 385 | ******************/ 386 | if(valueCrypto > 0) { 387 | var accountID = trade.Account + '-' + currency; 388 | if(!accounts.contains(accountID)) { 389 | accounts.set(accountID, new AccountValue([ 390 | trade.Account, 391 | trade.Exchange, 392 | trade.Wallet, 393 | currency, 394 | 0.0 395 | ])); 396 | } 397 | 398 | var account = accounts.get(accountID); 399 | 400 | if(typeBuySell == Buy) { 401 | account.Value += valueCrypto; 402 | } else if(typeBuySell == Sell || typeBuySell==Fee) { 403 | account.Value -= valueCrypto; 404 | } 405 | 406 | if(account.Value < 0.0001 && account.Value > -0.0001) { // Exclude very small amounts (some exchange are not able to widthdraw them) 407 | accounts.remove(accountID); 408 | } 409 | } 410 | 411 | return true; 412 | } 413 | } 414 | 415 | function writeCalculatedCoinValues() { 416 | var spreadsheet = SpreadsheetApp.getActive(); 417 | var coinValueSheet = spreadsheet.getSheetByName("🔒 Portfolio Values"); 418 | var accountHoldingsSheet = spreadsheet.getSheetByName("🔒 Account Holdings"); 419 | var tradeSheet = spreadsheet.getSheetByName("Trades"); 420 | var investmentSheet = spreadsheet.getSheetByName("🔒 Profit Overview"); 421 | var tradeValues = tradeSheet.getDataRange().getValues(); 422 | 423 | var coins = new Dictionary(); 424 | var accounts = new Dictionary(); 425 | var overviewFiatInvest = 0.0; 426 | var fiatCurrency = getFiatName(); 427 | 428 | for (var i = 1; i < tradeValues.length; i++) { 429 | var trade = new Trade(tradeValues[i]); 430 | 431 | processCoinValue("Buy", trade, coins, accounts, fiatCurrency); 432 | processCoinValue("Sell", trade, coins, accounts, fiatCurrency); 433 | processCoinValue("Fee", trade, coins, accounts, fiatCurrency); 434 | 435 | overviewFiatInvest = calculateFiatInvestOverview(trade, overviewFiatInvest, fiatCurrency); 436 | } // end for 437 | 438 | // Write profit/loss data to sheet 439 | var portfolioRows = coins.toArray(function(value) { return value.toArray(); }); 440 | coinValueSheet.getRange(2, 1, coinValueSheet.getLastRow(), coinValueSheet.getLastColumn()).setValue(null); // Clear all 441 | coinValueSheet.getRange(2, 1, portfolioRows.length, 8).setValues(portfolioRows); // Write values 442 | 443 | // Write investment summary 444 | var investmentValue = 0.0; 445 | var arrCoins = coins.toArray(); 446 | for (var c in arrCoins) { 447 | if(arrCoins[c].CoinCode!=fiatCurrency && isNumeric(arrCoins[c].ProfitLossFiat)) { 448 | investmentValue += parseFloat(arrCoins[c].CurrentCoinPriceTotalFiat); 449 | } 450 | } 451 | investmentSheet.getRange(2, 2).setValue(overviewFiatInvest); 452 | investmentSheet.getRange(3, 2).setValue(investmentValue); 453 | 454 | // Get fiat rates for account values 455 | var arrAccounts = accounts.toArray() 456 | for ( var accIndex in arrAccounts) { 457 | var account = accounts.get(arrAccounts[accIndex].Account + '-' + arrAccounts[accIndex].Currency) 458 | account.FiatValue = account.Value * getCryptoFiatRate(account.Currency); 459 | } 460 | 461 | // Write account data to sheet 462 | var accountRows = accounts.toArray(function(value) { return value.toArray(); }); 463 | accountHoldingsSheet.getRange(2, 1, accountHoldingsSheet.getLastRow(), accountHoldingsSheet.getLastColumn()).setValue(null); // Clear all 464 | accountHoldingsSheet.getRange(2, 1, accountRows.length, 6).setValues(accountRows); // Write values 465 | 466 | } 467 | 468 | function calculateFiatInvestOverview(trade, overviewFiatInvest, fiatCurrency) { 469 | if(trade.BuyCurrency == fiatCurrency && trade.BuyValue > 0 && (trade.Type == "Deposit" /* || trade.Type == "Gift" */)) { 470 | overviewFiatInvest += trade.BuyValue; 471 | } 472 | if(trade.SellCurrency == fiatCurrency && trade.SellValue > 0 && (trade.Type == "Withdraw" || trade.Type == "Gift")) { 473 | overviewFiatInvest -= trade.SellValue; 474 | } 475 | 476 | // Substract some special cases (investment for others) 477 | if(trade.Type == "Gift" && trade.SellCurrency != fiatCurrency && trade.SellFiatValue>0) { 478 | overviewFiatInvest -= trade.SellFiatValue; // ToDo Fees should be included here 479 | if(trade.FeeFiatValue > 0) { 480 | overviewFiatInvest -= trade.FeeFiatValue; 481 | } 482 | } 483 | // Disabled - gifts are not investment 484 | //if(trade.Type == "Gift" && trade.BuyCurrency != fiatCurrency && trade.BuyFiatValue>0) { 485 | // overviewFiatInvest += trade.BuyFiatValue; // ToDo Fees should be included here 486 | //} 487 | return overviewFiatInvest; 488 | } 489 | 490 | function debugSheet() { 491 | 492 | } 493 | -------------------------------------------------------------------------------- /source/init.gs: -------------------------------------------------------------------------------- 1 | /** 2 | * @OnlyCurrentDoc 3 | */ 4 | /* 5 | Copyright (C) 2018 TechupBusiness (info@techupbusiness.com) 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | /******************************** 22 | * 23 | * INIT CLASSES 24 | * 25 | ********************************/ 26 | 27 | function onOpen() { 28 | var ui = SpreadsheetApp.getUi(); 29 | // Or DocumentApp or FormApp. 30 | ui.createMenu('Cryptocurrency') 31 | .addItem('Add fiat rates', 'menuWriteTradeValue') 32 | .addItem('Update portfolio value', 'menuCalculateCoinValues') 33 | //.addSeparator() 34 | //.addSubMenu(ui.createMenu('Sub-menu') 35 | // .addItem('Second item', 'menuItem2')) 36 | .addToUi(); 37 | } 38 | 39 | function menuWriteTradeValue() { 40 | var ui = SpreadsheetApp.getUi(); 41 | 42 | var result = ui.alert( 43 | 'Please confirm get missing fiat exchange rates', 44 | 'Do you want to continue to evaluate/rate your trades in fiat (Trades Sheet)?', 45 | ui.ButtonSet.OK_CANCEL); 46 | 47 | if (result == ui.Button.OK) { 48 | writeHistoricalTradeData(); 49 | ui.alert('Finished exchange rate writing!'); 50 | } 51 | 52 | } 53 | 54 | function menuCalculateCoinValues() { 55 | var ui = SpreadsheetApp.getUi(); 56 | 57 | var result = ui.alert( 58 | 'Please confirm report generation', 59 | 'Do you want to continue creating the report for calculate coin and account values? Please make sure you sort the trades-sheet Date Z-A before you continue now.', 60 | ui.ButtonSet.OK_CANCEL); 61 | 62 | // Process the user's response. 63 | if (result == ui.Button.OK) { 64 | writeCalculatedCoinValues(); 65 | ui.alert('Finished report for coin and account values!'); 66 | } 67 | } 68 | --------------------------------------------------------------------------------