├── blacklist.txt ├── assets ├── logo.png └── shot1.JPG ├── package.json ├── README.md ├── LICENSE ├── .gitignore ├── trainingData.js ├── humanFactor.js └── bot.js /blacklist.txt: -------------------------------------------------------------------------------- 1 | 1095,11,365,7,30,5,4,2,1,3,6 -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ognjengt/coin-instinct-bot/HEAD/assets/logo.png -------------------------------------------------------------------------------- /assets/shot1.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ognjengt/coin-instinct-bot/HEAD/assets/shot1.JPG -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coin-instinct-bot", 3 | "version": "1.0.0", 4 | "description": "Twitter bot that tweets future bitcoin value predictions.", 5 | "main": "bot.js", 6 | "scripts": { 7 | "start": "node bot.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/ognjengt/coin-instinct-bot.git" 13 | }, 14 | "author": "Ognjen Gatalo", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/ognjengt/coin-instinct-bot/issues" 18 | }, 19 | "homepage": "https://github.com/ognjengt/coin-instinct-bot#readme", 20 | "dependencies": { 21 | "node-fetch": "^1.7.3", 22 | "number-format.js": "^1.1.11", 23 | "p-iteration": "^1.1.5", 24 | "twit": "^2.2.9" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coin-instinct-bot 2 | ![Logo](https://raw.githubusercontent.com/ognjengt/coin-instinct-bot/master/assets/logo.png) 3 | 4 | Twitter bot that tweets future bitcoin value predictions in the next n days. 5 | 6 | The algorithm analyzes historical bitcoin data, and using K nearest neighbours algorithm, makes a prediction. You can tweet to the bot "@coin_instinct Predict for (number) days" and it will collect all of the tweets in the past 2 hours, and make a prediction based on the most requested number. 7 | 8 | Visit https://twitter.com/coin_instinct 9 | 10 | ![Screenshot](https://raw.githubusercontent.com/ognjengt/coin-instinct-bot/master/assets/shot1.JPG) 11 | 12 | 13 | Bitcoin value API powered by Blockchain.info https://blockchain.info/
14 | Bitcoin historical API powered by CoinDesk https://www.coindesk.com 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ognjen Gatalo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Custom 2 | /config.js 3 | /package-lock.json 4 | dumps/ 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (http://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Typescript v1 declaration files 45 | typings/ 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Output of 'npm pack' 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | # dotenv environment variables file 63 | .env 64 | 65 | -------------------------------------------------------------------------------- /trainingData.js: -------------------------------------------------------------------------------- 1 | const validDropData = [ 2 | 'will drop tomorrow', 'is likely to drop tomorrow', 'will drop', 'is likely to drop', 'says bitcoin will drop', 'are saying bitcoin will drop', 'is going to drop', 'bitcoin price will go down', 'bitcoin will drop', 'bitcoin is likely to drop soon', 'will drop soon', "bitcoin's value will drop soon", 'drop in the next 2 days', 'drop in the next 3 days', 'drop in the next 4 days', 'drop in the next 5 days', 'will drop in the next', 'felt bitcoin will drop',' feel bitcoin will drop', 'will drop a bit', 'I think bitcoin will drop', 'price will fall', 'bitcoin will fall', 'bitcoin will drop $',' will drop to $', '#bitcoin will drop', 'feel #bitcoin will drop', 'says #bitcoin will drop','are saying #bitcoin will drop', '#bitcoin value will drop soon', 'sell now', 'you should sell', 'you should start selling', 'sell bitcoin fast', 'sell #bitcoin fast', 'going to drop', 'going to drop a lot', 'bitcoin is going to drop a lot over next', 'bitcoin is going to drop over next', 'bitcoin is going to drop over the next', 'bitcoin is going to drop a lot over the next','' 3 | ]; 4 | const invalidDropData = [ 5 | 'will drop once', 'will not drop', 'if bitcoin will drop', 'will drop if', 'bitcoin will drop if', 'will bitcoin drop', 'when bitcoin will drop', 'when #bitcoin will drop', "don't sell", 'do not sell', 'you should not sell', "don't sell bitcoin", 'you should not sell bitcoin', "you shouldn't sell bitcoin","don't sell #bitcoin", 'you should not sell #bitcoin', "you shouldn't sell #bitcoin" 6 | ]; 7 | 8 | module.exports = { 9 | validDropData, 10 | invalidDropData 11 | } -------------------------------------------------------------------------------- /humanFactor.js: -------------------------------------------------------------------------------- 1 | var humanFactor = {}; 2 | var trainingData = require('./trainingData'); 3 | 4 | /** 5 | * Checks if the sentence contains some of the training data 6 | * Returns an object containing number of matches from validDropData and invalidDropData for that sentence 7 | * @param {*string} sentence 8 | */ 9 | async function runStringThroughTrainingData(sentence) { 10 | var result = { 11 | sentence: sentence, 12 | validDropData: [], 13 | invalidDropData: [] 14 | } 15 | 16 | trainingData.invalidDropData.forEach( (trainData) => { 17 | if(sentence.includes(trainData)) { 18 | result.invalidDropData.push(trainData); 19 | } 20 | }); 21 | 22 | trainingData.validDropData.forEach( (trainData) => { 23 | if(sentence.includes(trainData)) { 24 | result.validDropData.push(trainData); 25 | } 26 | }); 27 | 28 | return result; 29 | } 30 | /** 31 | * Gets the sentences that contain valid or invalid data, as long with the trainingData that matches those sentences 32 | * @param {*Array} sentences 33 | */ 34 | async function getValidSentences(sentences) { 35 | var validSentences = []; 36 | var matchesPromise = null; 37 | var matches = null; 38 | sentences.forEach( (sentence) => { 39 | runStringThroughTrainingData(sentence).then( (result) => { 40 | if(result.validDropData[0] || result.invalidDropData[0]) { // i ako vec ne containuje tu recenicu 41 | validSentences.push(result); 42 | } 43 | }) 44 | 45 | }) 46 | return validSentences; 47 | } 48 | 49 | /** 50 | * Returns the human factor 51 | * @param {*Array} sentences 52 | */ 53 | humanFactor.calculateHumanFactor = async (sentences) => { 54 | var validSentences = await getValidSentences(sentences); 55 | // Go through all of the sentences and calculate, if it has lets say 3 validDropData, then the higher the disasterFactor is. 56 | 57 | 58 | return 0.5; 59 | } 60 | 61 | module.exports = humanFactor; -------------------------------------------------------------------------------- /bot.js: -------------------------------------------------------------------------------- 1 | /* 2 | ------------ TODOS ----------- 3 | - Human factor 4 | */ 5 | const { forEach } = require('p-iteration'); 6 | const format = require('number-format.js'); 7 | const fs = require('fs'); 8 | 9 | const config = require('./config'); 10 | var Twit = require('twit'); 11 | var fetch = require('node-fetch'); 12 | var humanFactorFunctions = require('./humanFactor'); 13 | 14 | var bitcoinData = { 15 | results: '' 16 | }; 17 | const K = 10; 18 | const QUERY_RANGE = 60; // How much days to go back in history to search for results (QUERY_RANGE + requestedDays) 19 | const WORK_TIMEOUT = 1000*60*120; // Wake the bot every 2 hours (tweet every 2 hours) 20 | const COIN_FETCH_TIMEOUT = 1000*60*118; // Fetch latest bitcoin prices 2 minutes before the bot awakens 21 | const MAX_GENERATED_DAY_VALUE = 20; 22 | const MIN_GENERATED_DAY_VALUE = 1; 23 | const BLACKLIST_TIME_TO_CLEAR = 5; 24 | 25 | var prediction = {}; 26 | var lastRequestedDaySpan = 0; 27 | var lastNumberOfPeopleThatRequested = 0; 28 | var tickerApiUrl = "https://blockchain.info/ticker"; 29 | var chartsApiUrl = "https://api.coindesk.com/v1/bpi/historical/close.json"; 30 | var coinDeskApiResults = {}; 31 | 32 | var blackListArray = []; 33 | var BLACKLIST_FILL_COUNTER = 0; 34 | 35 | var todayDate = new Date(); 36 | todayDate = todayDate.toISOString().split('T')[0]; 37 | 38 | var demandSearchParams = { 39 | q: '@coin_instinct Predict for since:'+todayDate, 40 | count: 100 41 | }; 42 | var bitcoinDropRatesSearchParams = { 43 | q: 'bitcoin will drop', 44 | count: 100 45 | }; 46 | 47 | var twitClient = new Twit({ 48 | consumer_key: config.consumer_key, 49 | consumer_secret: config.consumer_secret, 50 | access_token: config.access_token, 51 | access_token_secret: config.access_token_secret, 52 | timeout_ms: 60*1000, 53 | }); 54 | 55 | refreshBitcoinPrices(tickerApiUrl); 56 | 57 | work(); 58 | 59 | function work() { 60 | run(); 61 | setInterval(function(){ 62 | 63 | run(); 64 | 65 | },WORK_TIMEOUT); 66 | } 67 | 68 | function run() { 69 | fetchBlacklist() 70 | .then( (blackList) => { 71 | // Collects tweets that people tweeted to @coin_instinct 72 | blackListArray = blackList; 73 | return collectTweets(demandSearchParams); 74 | }) 75 | .then( (response) => { 76 | return response.data.statuses.map(status => status.text); 77 | }) 78 | // When all the tweets are here, go through them, extract numbers and find most frequent day 79 | .then( (tweets) => { 80 | var requestedDays = []; 81 | tweets = tweets.filter(tweet => tweet.includes('@coin_instinct Predict for') || tweet.includes('@coin_instinct predict for')); 82 | console.log(tweets); 83 | 84 | tweets.forEach((tweet) => { 85 | var day = tweet.match(/\d+/g); 86 | if(day != null && day > 0 && day < 1000) requestedDays.push(day[0]); 87 | }); 88 | this.lastNumberOfPeopleThatRequested = requestedDays.length; 89 | return getMostFrequentDay(requestedDays); 90 | }) 91 | // When most frequent day is found, run the algorithm and get final results 92 | .then( (mostFrequentDay) => { 93 | this.lastRequestedDaySpan = mostFrequentDay; 94 | return queryChartHistory(chartsApiUrl,mostFrequentDay); 95 | }) 96 | // When the kNearest Neighbours algorithm is finished, get the start and end bitcoin value for the timespan in history, and calculate ratio between every one of them 97 | .then( (finalResults) => { 98 | return calculatePrediction(finalResults,bitcoinData.results.USD.last); 99 | }) 100 | // When the prediction is calculated, get all tweets containing strings such as 'bitcoin drops' etc. to calculate humanFactor 101 | .then( (prediction) => { 102 | this.prediction = prediction; 103 | 104 | console.log('Time of prediction: '+new Date().getHours()+':'+new Date().getMinutes()); 105 | console.log('Prediction for the next '+this.lastRequestedDaySpan+' days is: '+this.prediction.finalValue); 106 | if(this.prediction.positive == 'true') { 107 | console.log('Gain: '+this.prediction.raw); 108 | console.log('Gain prctg: '+this.prediction.percentage); 109 | } 110 | else { 111 | console.log('Loss: '+this.prediction.raw); 112 | console.log('Loss prctg: '+this.prediction.percentage); 113 | } 114 | 115 | return collectTweets(bitcoinDropRatesSearchParams).then( (response) => { 116 | return response.data.statuses.map(status => status.text); 117 | }) 118 | }) 119 | .then( (negativeTweets) => { 120 | return humanFactorFunctions.calculateHumanFactor(negativeTweets); 121 | }) 122 | .then( (humanFactor) => { 123 | // TODO multiply the prediction by human factor 124 | return tweetPrediction(this.prediction, this.lastRequestedDaySpan, lastNumberOfPeopleThatRequested); 125 | }) 126 | .then( (tweetPostData) => { 127 | return writeToDump(this.prediction); 128 | }) 129 | .then( () => { 130 | console.log('Tweeted!'); 131 | }) 132 | } 133 | 134 | /** 135 | * Collects tweets, based on parameters 136 | * @param {*Object} searchParams query 137 | */ 138 | async function collectTweets(searchParams) { 139 | return await twitClient.get('search/tweets', searchParams); 140 | } 141 | 142 | /** 143 | * Tweets the prediction! 144 | * @param {*Object} prediction Object containing prediction data 145 | * @param {*Number} lastRequestedDaySpan number of days in the future to predict 146 | * @param {*Number} peopleRequested number of people that requested this prediction 147 | */ 148 | async function tweetPrediction(prediction, lastRequestedDaySpan, peopleRequested) { 149 | var gainLoss = ''; 150 | var percentageEmoji = ''; 151 | var peopleData = ''; 152 | if(prediction.positive == 'true') { 153 | gainLoss = '📈 Gain'; 154 | percentageEmoji = '⬆️'; 155 | } 156 | else { 157 | gainLoss = '📉 Loss'; 158 | percentageEmoji = '⬇️'; 159 | } 160 | 161 | if(peopleRequested == 0) { 162 | peopleData = 'No one requested a prediction in this cycle.' 163 | } else peopleData = peopleRequested+' 🤵 people requested this prediction.'; 164 | 165 | var tweetText = `#Bitcoin value in the next ${lastRequestedDaySpan} days should be somewhere about 💰 $${format("#,##0.##",prediction.finalValue)}. 166 | ${gainLoss}: $${format("#,##0.##",prediction.raw)} 167 | ${gainLoss} percentage: ${prediction.percentage.toFixed(2)}% ${percentageEmoji} 168 | ${peopleData} 169 | 💎 Current BTC value: $${format("#,##0.##",bitcoinData.results.USD.last)} 170 | 171 | Request a prediction by tweeting "@coin_instinct Predict for number days". 172 | `; 173 | //console.log(tweetText); 174 | //console.log('Note to self: Uncomment post line to tweet'); 175 | return await twitClient.post('statuses/update', { status: tweetText }); 176 | } 177 | 178 | /** 179 | * Finds the most frequent day in array of days, and returns it 180 | * @param {*Array} requestedDays Array of day values 181 | */ 182 | async function getMostFrequentDay(requestedDays) { 183 | // If no one requested this day, generate random day to predict for 184 | var convertedArray = []; 185 | if(requestedDays[0] != null) { 186 | convertedArray = requestedDays.map(Number); 187 | return await findMostFrequent(convertedArray, blackListArray); 188 | } else { 189 | return await generateRandom(blackListArray); 190 | } 191 | } 192 | 193 | /** 194 | * Refreshes bitcoin prices minute before the main thread starts the work 195 | * @param {*String} tickerApiUrl API endpoint for getting the latest bitcoin prices 196 | */ 197 | function refreshBitcoinPrices(tickerApiUrl) { 198 | const request = async (tickerApiUrl) => { 199 | var results = await fetch(tickerApiUrl); 200 | results = await results.json(); 201 | 202 | bitcoinData.results = results; 203 | if(bitcoinData.results) {console.log('Blockchain API works!');} 204 | else console.log('Blockchain API is down at the moment.'); 205 | 206 | // Get todays date 207 | this.todayDate = new Date(); 208 | this.todayDate = this.todayDate.toISOString().split('T')[0]; 209 | console.log(this.todayDate); 210 | 211 | console.log('New prices fetched.'); 212 | console.log('Most recent bitcoin price: '+bitcoinData.results.USD.last); 213 | console.log('Time: '+new Date().getHours()+':'+new Date().getMinutes()); 214 | } 215 | 216 | request(tickerApiUrl); 217 | 218 | // Enable in prod 219 | setInterval(() => { 220 | request(tickerApiUrl); 221 | },COIN_FETCH_TIMEOUT); 222 | } 223 | 224 | /** 225 | * Queries the CoinDesk api for specific date span 226 | * Returns array of bitcoin values based on k nearest neighbours, start - value on the start day, end - value after n days 227 | * @param {*String} chartsApiUrl API endpoint for getting bitcoin history values 228 | * @param {*String} nDays how many days to go backwards 229 | */ 230 | async function queryChartHistory(chartsApiUrl, nDays) { 231 | 232 | var nDaysBack = new Date(); 233 | nDaysBack.setDate(nDaysBack.getDate() - (nDays+1)); 234 | nDaysBack = nDaysBack.toISOString().split('T')[0]; 235 | 236 | var nMonthsBack = new Date(); 237 | nMonthsBack.setDate(nMonthsBack.getDate() - (nDays+1) - QUERY_RANGE); 238 | nMonthsBack = nMonthsBack.toISOString().split('T')[0]; 239 | 240 | var today = new Date(); 241 | today.setDate(today.getDate() - 1); 242 | today = today.toISOString().split('T')[0]; 243 | 244 | const results = await fetch(chartsApiUrl+'?start='+nMonthsBack+'&end='+nDaysBack); 245 | const resultsJson = await results.json(); 246 | 247 | const fullResults = await fetch(chartsApiUrl+'?start='+nMonthsBack+'&end='+today); 248 | this.coinDeskApiResults = await fullResults.json(); 249 | 250 | //console.log(resultsJson); 251 | 252 | const similarities = await calculateSimilarity(resultsJson, bitcoinData.results.USD.last); 253 | const kNearest = await getNearestNeighbours(similarities); 254 | const finalResults = await getFinalResults(kNearest,nDays); 255 | console.log(kNearest); 256 | console.log(finalResults); 257 | 258 | return finalResults; 259 | } 260 | 261 | /** 262 | * Calculates the similarity score (distance between current bitcoin value and all of the other bitcoin values in the past) 263 | * Returns JSON object with similarity scores ID: date, value: the similarity between current btc value and the value on that day (the lower the number, they are more similar) 264 | * @param {*Object} data Data from QUERY_RANGE days back 265 | * @param {*Int} currentBTCValue Current bitcoin value 266 | */ 267 | async function calculateSimilarity(data, currentBTCValue) { 268 | // Go through all and calculate currentBtc - data[key] 269 | var similarities = []; 270 | /* 271 | Similarity object looks something like this: 272 | { 273 | date: '2017-10-08', 274 | similarityScore: 1140 (difference between current BTC and btc on that day) 275 | } 276 | */ 277 | Object.keys(data.bpi).forEach( (key,index) => { 278 | var similarity = { 279 | date: key, 280 | similarityScore: currentBTCValue - data.bpi[key] 281 | } 282 | similarities.push(similarity); 283 | }); 284 | return similarities; 285 | } 286 | 287 | /** 288 | * Returns k nearest neighbours (dates) based on similarityScores 289 | * @param {*Array} similarities data with all of the similarity scores compared to current bitcoin value, all the way up to QUERY_RANGE days back 290 | */ 291 | async function getNearestNeighbours(similarities) { 292 | // Run through, and find k(10) that are closest to 0 293 | var absSimilarities = []; 294 | similarities.forEach( (similarity) => { 295 | absSimilarities.push({ 296 | date: similarity.date, 297 | similarityScore: Math.abs(similarity.similarityScore) 298 | }) 299 | }) 300 | absSimilarities = absSimilarities.sort(function(a,b) { 301 | return (a.similarityScore > b.similarityScore) ? 1 : ((b.similarityScore > a.similarityScore) ? -1 : 0); 302 | }); 303 | var kNearest = []; 304 | for(var i = 0; i < K; i++) { 305 | kNearest.push(absSimilarities[i].date); 306 | } 307 | return kNearest; 308 | } 309 | 310 | /** 311 | * Returns array of objects containing start and end values 312 | * start - value of bitcoin on the start day 313 | * end - value of bitcoin after n days 314 | * @param {*Array} kNearest Array of dates for which the bitcoin value was the most similar to current btc value 315 | * @param {*Int} nDays Days to go in the future, and get the value of btc on that date, to compare 316 | */ 317 | async function getFinalResults(kNearest,nDays) { 318 | var finalResults = []; 319 | var finalResult = {}; 320 | 321 | await forEach(kNearest, async(date) => { 322 | var dateTime = new Date(date); 323 | var pastDate = dateTime.toISOString().split('T')[0]; 324 | 325 | var futureDate = new Date(date); 326 | futureDate.setDate(futureDate.getDate() + nDays); 327 | futureDate = futureDate.toISOString().split('T')[0]; 328 | 329 | var valueForThatDay = this.coinDeskApiResults.bpi[pastDate]; 330 | var valueForFutureDay = this.coinDeskApiResults.bpi[futureDate]; 331 | 332 | finalResult = { 333 | start: valueForThatDay, 334 | end: valueForFutureDay 335 | } 336 | 337 | finalResults.push(finalResult); 338 | }) 339 | return finalResults; 340 | 341 | } 342 | 343 | /** 344 | * Calculates the prediction 345 | * Returns object containing valuable data for prediction 346 | * @param {*Array} data Array of objects, containing start and end bitcoin values 347 | * @param {*Float} currentBitcoinValue Current btc value 348 | */ 349 | async function calculatePrediction(data,currentBitcoinValue) { 350 | 351 | var finalPredictionData = { 352 | raw: 0, 353 | percentage: 0, 354 | positive: '', 355 | finalValue: 0 356 | } 357 | var sum = 0; 358 | await forEach(data, async (value) => { 359 | sum += value.end - value.start; 360 | }) 361 | 362 | sum = sum / K; 363 | finalPredictionData.raw = sum; 364 | finalPredictionData.finalValue = currentBitcoinValue + sum; 365 | finalPredictionData.positive = sum > 0 ? 'true' : 'false'; 366 | finalPredictionData.percentage = ((finalPredictionData.finalValue - currentBitcoinValue) / currentBitcoinValue) * 100; 367 | return finalPredictionData; 368 | } 369 | 370 | /** 371 | * Generates random number, to predict for 372 | * @param {*Array} blackListArr Array of days that have already been predicted and tweeted for 373 | */ 374 | async function generateRandom(blackListArr) { 375 | var randomDay = 0; 376 | while(true) { 377 | randomDay = Math.floor(Math.random() * MAX_GENERATED_DAY_VALUE) + MIN_GENERATED_DAY_VALUE; 378 | if(blackListArr.includes(randomDay)) continue; 379 | else break; 380 | } 381 | lastNumberOfPeopleThatRequested = 0; 382 | addToBlackList(randomDay); 383 | return randomDay; 384 | } 385 | 386 | /** 387 | * Algorithm behind finding the most frequent number in array 388 | * @param {*Array} array array of days 389 | * @param {*Array} blackListArr blackList array 390 | */ 391 | async function findMostFrequent(array, blackListArr) 392 | { 393 | if(array.length == 0) 394 | return null; 395 | var modeMap = {}; 396 | var maxEl = array[0], maxCount = 1; 397 | 398 | // First go through blackList array and set maxEl to the first element that isn't in the blacklist, if every of the numbers in the array is in the blacklist, then just return generateRandom 399 | 400 | var containsValidNumbers = false; 401 | for(var i = 0; i < array.length; i++) { 402 | if(!blackListArr.includes(array[i])) { 403 | maxEl = array[i]; 404 | containsValidNumbers = true; 405 | break; 406 | } 407 | } 408 | if(!containsValidNumbers) return await generateRandom(blackListArr); 409 | 410 | for(var i = 0; i < array.length; i++) 411 | { 412 | var el = array[i]; 413 | if(blackListArr.includes(el)) continue; 414 | if(modeMap[el] == null) 415 | modeMap[el] = 1; 416 | else 417 | modeMap[el]++; 418 | if(modeMap[el] > maxCount) 419 | { 420 | maxEl = el; 421 | maxCount = modeMap[el]; 422 | } 423 | } 424 | lastNumberOfPeopleThatRequested = maxCount; 425 | await addToBlackList(maxEl); 426 | return maxEl; 427 | 428 | } 429 | 430 | /** 431 | * Adds newest day to blackList, so it wont tweet predictions for that day in the next 6 hours 432 | * @param {*Number} day day (number) to add to blackListArray 433 | */ 434 | async function addToBlackList(day) { 435 | if(BLACKLIST_FILL_COUNTER == BLACKLIST_TIME_TO_CLEAR) await clearBlackList(); 436 | if(blackListArray.includes(day)) return; 437 | blackListArray.push(day); 438 | BLACKLIST_FILL_COUNTER++; 439 | // Write to blacklist.txt 440 | fs.writeFileSync('blacklist.txt',blackListArray.toString()); 441 | 442 | console.log('---Current blacklist---'); 443 | console.log(blackListArray); 444 | console.log('---BLACKLIST_FILL_COUNTER: '+BLACKLIST_FILL_COUNTER+'---'); 445 | } 446 | 447 | /** 448 | * Clears the blackListArray 449 | */ 450 | async function clearBlackList() { 451 | blackListArray = []; 452 | BLACKLIST_FILL_COUNTER = 0; 453 | 454 | console.log('---- BLACKLIST CLEARED ----'); 455 | } 456 | 457 | /** 458 | * Creates a file and writes all of the todays predictions in it 459 | * @param {*Object} prediction 460 | * @param {*Date} todaysDate 461 | */ 462 | async function writeToDump(prediction) { 463 | // TODO write to file 464 | var today = new Date(); 465 | today = today.toISOString().split('T')[0]; 466 | var pathToFile = './dumps/'+today+'_dump.txt'; 467 | 468 | var futureDate = new Date(); 469 | futureDate.setDate(futureDate.getDate() + this.lastRequestedDaySpan); 470 | futureDate = futureDate.toISOString().split('T')[0]; 471 | var lineToWrite = futureDate+':'+prediction.finalValue+'\n'; 472 | 473 | if (!fs.existsSync(pathToFile)) { 474 | fs.writeFileSync(pathToFile,''); 475 | } 476 | fs.appendFileSync(pathToFile,lineToWrite); 477 | } 478 | 479 | /** 480 | * Fetches the blacklist from blacklist.txt 481 | * @returns array of numbers in blacklist 482 | */ 483 | async function fetchBlacklist() { 484 | var blackListString = fs.readFileSync('blacklist.txt').toString(); 485 | var fetched = blackListString.split(",").map(Number); 486 | return fetched; 487 | } --------------------------------------------------------------------------------