├── .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 | 
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 |
--------------------------------------------------------------------------------