├── history.json ├── .eslintrc.json ├── .vscode └── launch.json ├── .gitignore ├── sources ├── Hero.js ├── Hertz.js ├── premiums │ ├── Zb.js │ ├── Bitshares.js │ └── PremiumSource.js ├── Coinmarketcap.js ├── Binance.js ├── Lbank.js ├── BitStamp.js ├── Coinegg.js ├── Fixer.js ├── Coindesk.js ├── Quandl.js ├── AlphaVantage.js ├── Aex.js ├── BitcoinAverage.js ├── Graphene.js ├── Okcoin.js ├── Zb.js ├── OpenExchangeRates.js ├── Bittrex.js ├── Huobi.js ├── CurrencyLayer.js ├── BigOne.js ├── Cointiger.js ├── FeedSource.js └── SettleGraphene.js ├── LICENSE ├── package.json ├── README.md ├── lib ├── Logger.js └── Price.js ├── index.js └── pricefeed.js /history.json: -------------------------------------------------------------------------------- 1 | {"marketmean":0.7768895670000409,"marketmedian":0.7806479981558727,"marketweighted":0.7617198412795562,"marketweightedprem":0.8926249969690055} -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "sourceType": "module", 9 | "ecmaVersion": 8 10 | }, 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 4 15 | ], 16 | "linebreak-style": [ 17 | "error", 18 | "windows" 19 | ], 20 | "quotes": [ 21 | "error", 22 | "single" 23 | ], 24 | "semi": [ 25 | "error", 26 | "always" 27 | ] 28 | } 29 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "type": "node", 10 | "request": "attach", 11 | "name": "Attach", 12 | "port": 9229 13 | }, 14 | { 15 | "type": "node", 16 | "request": "launch", 17 | "name": "Launch Program", 18 | "program": "${workspaceFolder}\\index.js" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore docs files 2 | _gh_pages 3 | _site 4 | .ruby-version 5 | 6 | # Numerous always-ignore extensions 7 | *.diff 8 | *.err 9 | *.orig 10 | *.log 11 | *.rej 12 | *.swo 13 | *.swp 14 | *.zip 15 | *.vi 16 | *~ 17 | 18 | # OS or Editor folders 19 | .DS_Store 20 | ._* 21 | Thumbs.db 22 | .cache 23 | .project 24 | .settings 25 | .tmproj 26 | *.esproj 27 | nbproject 28 | *.sublime-project 29 | *.sublime-workspace 30 | .idea 31 | 32 | # Komodo 33 | *.komodoproject 34 | .komodotools 35 | 36 | # grunt-html-validation 37 | validation-status.json 38 | validation-report.json 39 | 40 | # config file to ignore 41 | fox.yaml 42 | config.yml 43 | 44 | # Folders to ignore 45 | node_modules 46 | -------------------------------------------------------------------------------- /sources/Hero.js: -------------------------------------------------------------------------------- 1 | const FeedSource =require('./FeedSource.js'); 2 | 3 | class Hero extends FeedSource { 4 | constructor(config) { 5 | super(config); 6 | this.init(); 7 | } 8 | init() { 9 | 10 | } 11 | async _fetch() { 12 | var feed={}; 13 | 14 | let base = 'HERO'; 15 | let quote = 'USD'; 16 | feed[base] = {}; 17 | let now = new Date(); 18 | let bofr = new Date('1913-12-23'); 19 | let dd = Math.abs(now.getTime() - bofr.getTime()) / (1000 * 3600 * 24); 20 | let price = Math.pow(1.05, (dd / 365.425)); 21 | feed[base][quote] = { 22 | 'price': 1/price, 23 | 'volume': 1.0 24 | }; 25 | 26 | return feed; 27 | } 28 | } 29 | module.exports=Hero; -------------------------------------------------------------------------------- /sources/Hertz.js: -------------------------------------------------------------------------------- 1 | const FeedSource =require('./FeedSource.js'); 2 | 3 | class Hertz extends FeedSource { 4 | constructor(config) { 5 | super(config); 6 | this.init(); 7 | } 8 | init() { 9 | 10 | } 11 | async _fetch() { 12 | var feed={}; 13 | 14 | let base = 'HERTZ'; 15 | let quote = 'USD'; 16 | feed[base] = {}; 17 | let start =Math.round((new Date(Date.UTC(2015,9,14,12,0,0,38))).valueOf()/1000); 18 | let now = Math.round((new Date()).valueOf()/1000); 19 | 20 | let period = 28*24*60*60; 21 | 22 | var phase=((now-start) % period)/period; 23 | 24 | var price = 1 + 0.14*Math.sin(phase*2*Math.PI); 25 | 26 | feed[base][quote] = { 27 | 'price': 1/price, 28 | 'volume': 1.0 29 | }; 30 | 31 | return feed; 32 | } 33 | } 34 | module.exports=Hertz; -------------------------------------------------------------------------------- /sources/premiums/Zb.js: -------------------------------------------------------------------------------- 1 | const PremiumSource =require('./PremiumSource.js'); 2 | const request = require('request-promise-native'); 3 | 4 | class Zb extends PremiumSource { 5 | constructor(config) { 6 | super(config); 7 | this.init(); 8 | } 9 | init() { 10 | 11 | } 12 | async _fetchPremium() { 13 | var premium=1; 14 | 15 | var url = { 16 | uri:'http://api.zb.com/data/v1/ticker?market=BITCNY_QC', 17 | headers: { 18 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36' 19 | } 20 | }; 21 | 22 | var result= await request(url); 23 | result=JSON.parse(result); 24 | if (result['ticker']==undefined) { 25 | return { 'premium': premium}; 26 | } 27 | return { 'premium': result['ticker']['last'] }; 28 | } 29 | } 30 | module.exports=Zb; -------------------------------------------------------------------------------- /sources/premiums/Bitshares.js: -------------------------------------------------------------------------------- 1 | const PremiumSource =require('./PremiumSource.js'); 2 | const {Apis} = require('bitsharesjs-ws'); 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | const Logger= require('../lib/Logger.js'); 5 | let logger= new Logger(argv['d']); 6 | 7 | 8 | class Bitshares extends PremiumSource { 9 | constructor(config) { 10 | super(config); 11 | this.init(); 12 | 13 | } 14 | init() { 15 | 16 | } 17 | async _fetchPremium() { 18 | var premium=1; 19 | var self=this; 20 | 21 | var result = await Apis.instance(self.options.url, true).init_promise.then(() => { 22 | logger.log('Connected to DEX: '+self.options.url); 23 | return Apis.instance().db_api().exec( 'get_ticker', [base,quote] ); 24 | }); 25 | 26 | if ((result['latest']>0) && (result['quote_volume']>0)) { 27 | return { 'premium': result['latest'] }; 28 | }else{ 29 | return { 'premium': premium }; 30 | } 31 | } 32 | } 33 | module.exports=Bitshares; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alex Megalokonomos 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 | -------------------------------------------------------------------------------- /sources/Coinmarketcap.js: -------------------------------------------------------------------------------- 1 | const FeedSource =require('./FeedSource.js'); 2 | const request = require('request-promise-native'); 3 | 4 | class Coinmarketcap extends FeedSource { 5 | constructor(config) { 6 | super(config); 7 | this.init(); 8 | } 9 | init() { 10 | 11 | } 12 | async _fetch() { 13 | var feed={}; 14 | var self=this; 15 | 16 | var url = 'https://api.coinmarketcap.com/v1/ticker/'; 17 | var result= await request(url); 18 | result=JSON.parse(result); 19 | for(var aidx in result) { 20 | var asset=result[aidx]; 21 | for (var qindex=0;qindex { 33 | logger.log('Connected to DEX: '+self.options.url); 34 | return Apis.instance().db_api().exec( 'get_ticker', [base,quote] ); 35 | }); 36 | if((self.options.quoteNames!=undefined) && (self.options.quoteNames[quote]!=undefined)) { 37 | quote=self.options.quoteNames[quote]; 38 | } 39 | if ((result['latest']>0) && (result['quote_volume']>0)) { 40 | feed[base][quote]= { 41 | 'price': result['latest'], 42 | 'volume': result['quote_volume']*self.options.scaleVolumeBy 43 | }; 44 | } 45 | } 46 | } 47 | 48 | return feed; 49 | } 50 | } 51 | module.exports=Graphene; -------------------------------------------------------------------------------- /sources/Okcoin.js: -------------------------------------------------------------------------------- 1 | const FeedSource =require('./FeedSource.js'); 2 | const request = require('request-promise-native'); 3 | 4 | class Okcoin extends FeedSource { 5 | constructor(config) { 6 | super(config); 7 | this.init(); 8 | } 9 | init() { 10 | 11 | } 12 | async _fetch() { 13 | var feed={}; 14 | var self=this; 15 | 16 | for (var bindex=0;bindex -k -d -s --gcd --broadcast --skip_critical 24 | ``` 25 | 26 | Switches: 27 | 28 | ``-c /path/to/config.yml`` 29 | 30 | The yaml config file is the exact same format as the one used by xeroc's bitshares-pricefeed (https://github.com/xeroc/bitshares-pricefeed/). 31 | 32 | ``-k 5Kb8kLf9zgWQnogidDA76Mz_SAMPLE_PRIVATE_KEY_DO_NOT_IMPORT_PL6TsZZY36hWXMssSzNydYXYB9KF`` 33 | 34 | Self-explanatory 35 | 36 | ``-d 3`` 37 | 38 | Debug level can be 0-3 39 | 0: Minimum - Explicit logging & Errors 40 | 1: Info - 0 + Basic logging 41 | 2: Verbose - 1 + Verbose logging 42 | 3: Transient - 2 + Transient messages 43 | 44 | Recommend 3 at first to be able to see what's going on 45 | 46 | ``-s wss://bts-seoul.clockwork.gr`` 47 | 48 | Use whatever API node you prefer 49 | 50 | ``--gcd `` 51 | 52 | An optimisation to @xeroc's original script. Uses a GCD method to optimise the final pricefeed. Default (without --gcd flag) uses xeroc's logic. 53 | 54 | ``--broadcast`` 55 | 56 | Set this flag in order to publish the actual pricefeed. 57 | 58 | ``--skip_critical`` 59 | 60 | Set this flag in order to skip publishing feeds that are over skip_change percentage change (in config) without prompt. 61 | 62 | 63 | ------------------------------------------------------------------------------------------------------------------- 64 | **DONATIONS WELCOME** 65 | @ 66 | BTS: clockwork 67 | EOS: clockworkbts 68 | -------------------------------------------------------------------------------- /sources/BigOne.js: -------------------------------------------------------------------------------- 1 | const FeedSource = require('./FeedSource.js'); 2 | const request = require('request-promise-native'); 3 | 4 | class BigOne extends FeedSource { 5 | constructor(config) { 6 | super(config); 7 | this.init(); 8 | } 9 | init() { 10 | 11 | } 12 | find(tickers,base,quote) { 13 | for(let i=0;i0) { 37 | offset ='+'+date.getTimezoneOffset()/60; 38 | }else{ 39 | offset = ''; 40 | } 41 | } 42 | return [year, month, day].join('-')+' '+lpad(date.getHours(),'0',2)+ ':' + lpad(date.getMinutes(),'0',2) + ':' + lpad(date.getSeconds(),'0',2)+' GMT'+ offset; 43 | } 44 | log(msg) { 45 | readline.clearLine(process.stdout,0); 46 | readline.cursorTo(process.stdout,0,null); 47 | process.stdout.write(chalk.white(this.timestamp())+' - '+chalk.magenta('[LOG]')+' '+msg+'\n'); 48 | } 49 | info(msg) { 50 | if (this.log_level>0) { 51 | readline.clearLine(process.stdout,0); 52 | readline.cursorTo(process.stdout,0,null); 53 | process.stdout.write(chalk.white(this.timestamp())+' - '+chalk.cyan('[INFO]')+' '+msg+'\n'); 54 | } 55 | } 56 | warning(msg) { 57 | readline.clearLine(process.stdout,0); 58 | readline.cursorTo(process.stdout,0,null); 59 | process.stdout.write(chalk.white(this.timestamp())+' - '+chalk.yellow('[WARNING]')+' '+msg+'\n'); 60 | } 61 | error(msg) { 62 | readline.clearLine(process.stdout,0); 63 | readline.cursorTo(process.stdout,0,null); 64 | process.stdout.write(chalk.white(this.timestamp())+' - '+chalk.red('[ERROR]')+' '+msg+'\n'); 65 | } 66 | verbose(msg) { 67 | if (this.log_level>1) { 68 | readline.clearLine(process.stdout,0); 69 | readline.cursorTo(process.stdout,0,null); 70 | process.stdout.write(chalk.white(this.timestamp())+' - '+chalk.blue('[VERBOSE]')+' '+msg+'\n'); 71 | } 72 | } 73 | transient(msg) { 74 | if (this.log_level>2) { 75 | readline.clearLine(process.stdout,0); 76 | readline.cursorTo(process.stdout,0,null); 77 | process.stdout.write(chalk.white(msg)); 78 | } 79 | } 80 | 81 | } 82 | module.exports=Logger; -------------------------------------------------------------------------------- /sources/FeedSource.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const os = require('os'); 4 | const chalk = require('chalk'); 5 | const argv = require('minimist')(process.argv.slice(2)); 6 | const Logger = require('../lib/Logger.js'); 7 | let logger = new Logger(argv['d']); 8 | 9 | class FeedSource { 10 | constructor(config) { 11 | this.options = { 12 | scaleVolumeBy: 1.0, 13 | enable: true, 14 | allowFailure: true, 15 | timeout: 5, 16 | quotes: [], 17 | bases: [] 18 | }; 19 | logger.transient(chalk.white.bold('Source: ' + this.__proto__.constructor.name) + ' - Initialising...'); 20 | this.options = Object.assign(this.options, config); 21 | 22 | } 23 | 24 | async fetch() { 25 | 26 | try { 27 | logger.transient(chalk.white.bold('Source: ' + this.__proto__.constructor.name) + ' - Getting data from source...'); 28 | var feed = await this._fetch(); 29 | logger.transient(chalk.white.bold('Source: ' + this.__proto__.constructor.name) + ' - Updating cache...'); 30 | this.updateCache(feed); 31 | logger.verbose(chalk.white.bold('Source: ' + this.__proto__.constructor.name) + ' - Source loaded.'); 32 | return feed; 33 | } catch (e) { 34 | logger.warning(chalk.white.bold('Source: ' + this.__proto__.constructor.name) + ' - Could not load live data. Trying to recover from cache.'); 35 | if (this.options.allowFailure != true) { 36 | logger.warning(chalk.white.bold('Source: ' + this.__proto__.constructor.name) + ' - Exiting due to source importance (allowFailure is false).'); 37 | process.exit(1); 38 | } 39 | } 40 | try { 41 | let cached = this.recoverFromCache(); 42 | logger.verbose(chalk.white.bold('Source: ' + this.__proto__.constructor.name) + ' - Recovered from cache.'); 43 | return cached; 44 | } catch (e) { 45 | logger.warning(chalk.white.bold('Source: ' + this.__proto__.constructor.name) + ' - Unable to fetch live or cached data. Skipping.'); 46 | } 47 | 48 | } 49 | today() { 50 | return (new Date()).toISOString().substr(0, 10); 51 | } 52 | recoverFromCache() { 53 | var cacheFile = this.getCacheFilename(); 54 | //console.log('this: '+cacheFile); 55 | try { 56 | return JSON.parse(fs.readFileSync(cacheFile, 'utf8')); 57 | } catch (e) { 58 | logger.warning(chalk.white.bold('Source: ' + this.__proto__.constructor.name) + ' - Could not open cache file.'); 59 | throw('Could not open cache file'); 60 | } 61 | } 62 | getCacheFilename() { 63 | 64 | var cachePath = path.join(os.homedir(), 'bitshares-pricefeed', 'cache', this.__proto__.constructor.name); 65 | fs.ensureDirSync(cachePath); 66 | return path.join(cachePath, this.today() + '.json'); 67 | } 68 | updateCache(feed) { 69 | var cacheFile = this.getCacheFilename(); 70 | //console.log('here'+ cacheFile); 71 | try { 72 | fs.writeFileSync(cacheFile, JSON.stringify(feed)); 73 | logger.verbose(chalk.white.bold('Source: ' + this.__proto__.constructor.name) + ' - Cache file updated.'); 74 | return true; 75 | } catch (e) { 76 | logger.warning(chalk.white.bold('Source: ' + this.__proto__.constructor.name) + ' - Could not update cache file.'); 77 | return false; 78 | } 79 | } 80 | } 81 | module.exports = FeedSource; -------------------------------------------------------------------------------- /sources/premiums/PremiumSource.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const os = require('os'); 4 | const chalk = require('chalk'); 5 | const argv = require('minimist')(process.argv.slice(2)); 6 | const Logger = require('../lib/Logger.js'); 7 | let logger = new Logger(argv['d']); 8 | 9 | class PremiumSource { 10 | constructor(config) { 11 | this.options = { 12 | enable: true, 13 | allowFailure: true, 14 | timeout: 5 15 | }; 16 | logger.transient(chalk.white.bold('Premium Source: ' + this.__proto__.constructor.name) + ' - Initialising...'); 17 | this.options = Object.assign(this.options, config); 18 | 19 | } 20 | 21 | async fetchPremium() { 22 | 23 | try { 24 | logger.transient(chalk.white.bold('Premium Source: ' + this.__proto__.constructor.name) + ' - Getting data from source...'); 25 | var premium = await this._fetchPremium(); 26 | logger.transient(chalk.white.bold('Premium Source: ' + this.__proto__.constructor.name) + ' - Updating cache...'); 27 | this.updateCache(premium); 28 | logger.verbose(chalk.white.bold('Premium Source: ' + this.__proto__.constructor.name) + ' - Source loaded.'); 29 | return premium; 30 | } catch (e) { 31 | logger.warning(chalk.white.bold('Premium Source: ' + this.__proto__.constructor.name) + ' - Could not load live data. Trying to recover from cache.'); 32 | if (this.options.allowFailure != true) { 33 | logger.warning(chalk.white.bold('Premium Source: ' + this.__proto__.constructor.name) + ' - Exiting due to source importance (allowFailure is false).'); 34 | process.exit(1); 35 | } 36 | } 37 | try { 38 | let cached = this.recoverFromCache(); 39 | logger.verbose(chalk.white.bold('Premium Source: ' + this.__proto__.constructor.name) + ' - Recovered from cache.'); 40 | return cached; 41 | } catch (e) { 42 | logger.warning(chalk.white.bold('Premium Source: ' + this.__proto__.constructor.name) + ' - Unable to fetch live or cached data. Skipping.'); 43 | } 44 | 45 | } 46 | today() { 47 | return (new Date()).toISOString().substr(0, 10); 48 | } 49 | recoverFromCache() { 50 | var cacheFile = this.getCacheFilename(); 51 | //console.log('this: '+cacheFile); 52 | try { 53 | return JSON.parse(fs.readFileSync(cacheFile, 'utf8')); 54 | } catch (e) { 55 | logger.warning(chalk.white.bold('Premium Source: ' + this.__proto__.constructor.name) + ' - Could not open cache file.'); 56 | throw('Could not open cache file'); 57 | } 58 | } 59 | getCacheFilename() { 60 | 61 | var cachePath = path.join(os.homedir(), 'bitshares-pricefeed', 'cache','premium', this.__proto__.constructor.name); 62 | fs.ensureDirSync(cachePath); 63 | return path.join(cachePath, this.today() + '.json'); 64 | } 65 | updateCache(premium) { 66 | var cacheFile = this.getCacheFilename(); 67 | try { 68 | fs.writeFileSync(cacheFile, JSON.stringify(premium)); 69 | logger.verbose(chalk.white.bold('Premium Source: ' + this.__proto__.constructor.name) + ' - Cache file updated.'); 70 | return true; 71 | } catch (e) { 72 | logger.warning(chalk.white.bold('Premium Source: ' + this.__proto__.constructor.name) + ' - Could not update cache file.'); 73 | return false; 74 | } 75 | } 76 | } 77 | module.exports = PremiumSource; -------------------------------------------------------------------------------- /sources/SettleGraphene.js: -------------------------------------------------------------------------------- 1 | const FeedSource = require('./FeedSource.js'); 2 | const { 3 | Apis 4 | } = require('bitsharesjs-ws'); 5 | const argv = require('minimist')(process.argv.slice(2)); 6 | const Logger = require('../lib/Logger.js'); 7 | let logger = new Logger(argv['d']); 8 | const Price=require('../lib/Price.js'); 9 | 10 | class SettleGraphene extends FeedSource { 11 | constructor(config) { 12 | super(config); 13 | this.init(); 14 | 15 | } 16 | init() { 17 | 18 | } 19 | async _fetch() { 20 | var feed = {}; 21 | var self = this; 22 | 23 | for (var bindex = 0; bindex < self.options.bases.length; bindex++) { 24 | let base = self.options.bases[bindex]; 25 | 26 | feed[base] = {}; 27 | 28 | for (var qindex = 0; qindex < self.options.quotes.length; qindex++) { 29 | let quote = self.options.quotes[qindex]; 30 | 31 | if (quote == base) { 32 | continue; 33 | } 34 | this.Api = await Apis.instance(self.options.url, true).init_promise.then(() => { 35 | logger.log('Connected to DEX: ' + self.options.url); 36 | return Apis.instance(); 37 | }); 38 | var asset = await this.Api.db_api().exec('lookup_asset_symbols', [ 39 | [base] 40 | ]).then((res) => { 41 | logger.transient('Got asset data...'); 42 | let asset = res[0]; 43 | return this.Api.db_api().exec('get_objects', [ 44 | [asset.id] 45 | ]); 46 | }).then((assetstats) => { 47 | logger.transient('Got asset statistics...'); 48 | return assetstats[0]; 49 | }); 50 | if (asset['is_bitasset'] == false) { 51 | return; 52 | } 53 | asset['bitasset_data'] = await this.Api.db_api().exec('get_objects', [ 54 | [asset['bitasset_data_id']] 55 | ]).then((res) => { 56 | logger.transient('Got bitasset data...'); 57 | return res[0]; 58 | }); 59 | 60 | var short_backing_asset = await this.Api.db_api().exec('lookup_asset_symbols', [ 61 | [asset['bitasset_data']['options']['short_backing_asset']] 62 | ]).then((res) => { 63 | logger.transient('Got backing asset data...'); 64 | let asset = res[0]; 65 | return this.Api.db_api().exec('get_objects', [ 66 | [asset.id] 67 | ]); 68 | }).then((assetstats) => { 69 | logger.transient('Got backing asset statistics...'); 70 | return assetstats[0]; 71 | }); 72 | var backing_symbol = short_backing_asset['symbol']; 73 | if (quote!=backing_symbol) { 74 | logger.log('Supplied quote symbol does not match backing asset!'); 75 | continue; 76 | } 77 | 78 | asset['short_backing_asset'] = short_backing_asset; 79 | var settlement_price=asset['bitasset_data']['current_feed']['settlement_price']; 80 | settlement_price.quote.precision=asset['short_backing_asset']['precision']; 81 | settlement_price.base.precision=asset['precision']; 82 | var quotesettlement_price=new Price(settlement_price).Float(); 83 | 84 | feed[base][quote] = { 85 | 'price': quotesettlement_price, 86 | 'volume': 1.0 87 | }; 88 | 89 | } 90 | } 91 | 92 | return feed; 93 | } 94 | } 95 | module.exports = SettleGraphene; -------------------------------------------------------------------------------- /lib/Price.js: -------------------------------------------------------------------------------- 1 | function gcd_iter(a, b) { 2 | while (a!=b) { 3 | if (a > b) { 4 | a -= b; 5 | } else if (b > a) { 6 | b -= a; 7 | } else { 8 | break; 9 | } 10 | } 11 | return a; 12 | } 13 | function precision(a) { 14 | if (!isFinite(a)) return 0; 15 | var e = 1, p = 0; 16 | while (Math.round(a * e) / e !== a) { e *= 10; p++; } 17 | return p; 18 | } 19 | class Price { 20 | 21 | constructor(config) { 22 | this.base=config.base; 23 | this.quote=config.quote; 24 | this._update(); 25 | } 26 | static fromFloat(price,base,quote) { 27 | var num,den; 28 | if (base.precision>quote.precision) { 29 | num = Math.round(price * Math.pow(10,precision(price)+base.precision-quote.precision)); 30 | den = Math.pow(10,precision(price)); 31 | }else{ 32 | num = Math.round(price * Math.pow(10,precision(price))); 33 | den = Math.pow(10,precision(price)+quote.precision-base.precision); 34 | } 35 | var gcd=gcd_iter(num,den); 36 | num=num/gcd; 37 | den=den/gcd; 38 | var newprice={}; 39 | newprice['base']={ amount: num, asset_id: base.asset_id, precision: base.precision}; 40 | newprice['quote']={ amount: den, asset_id: quote.asset_id, precision: quote.precision}; 41 | return new Price(newprice); 42 | } 43 | static fromFloatOld(price,base,quote) { 44 | var num,den; 45 | if (base.precision>quote.precision) { 46 | num = Math.round(price * Math.pow(10,precision(price)+base.precision-quote.precision)); 47 | den = Math.pow(10,precision(price)); 48 | }else{ 49 | num = Math.round(price * Math.pow(10,precision(price))); 50 | den = Math.pow(10,precision(price)+quote.precision-base.precision); 51 | } 52 | var newprice={}; 53 | newprice['base']={ amount: num, asset_id: base.asset_id, precision: base.precision}; 54 | newprice['quote']={ amount: den, asset_id: quote.asset_id, precision: quote.precision}; 55 | return new Price(newprice); 56 | } 57 | _update() { 58 | if (this.quote.amount>0) { 59 | this.price=(this.base.amount*1.0 *Math.pow(10,this.quote.precision))/(this.quote.amount*Math.pow(10,this.base.precision)); 60 | }else{ 61 | this.price=Infinity; 62 | } 63 | } 64 | copy() { 65 | let clone = Object.assign(Object.create(Object.getPrototypeOf(this)), JSON.parse(JSON.stringify(this))); 66 | return clone; 67 | } 68 | symbols() { 69 | return [this.base.asset_id,this.quote.asset_id]; 70 | } 71 | as_base(base_asset_id) { 72 | if (base_asset_id==this.base.asset_id) { 73 | return this.copy(); 74 | }else if (base_asset_id==this.quote.asset_id){ 75 | return this.copy().invert(); 76 | } 77 | } 78 | as_quote(quote_asset_id) { 79 | if (quote_asset_id==this.quote.asset_id) { 80 | return this.copy(); 81 | }else if (quote_asset_id==this.base.asset_id){ 82 | return this.copy().invert(); 83 | } 84 | } 85 | invert() { 86 | var tmp =this.quote; 87 | this.quote=this.base; 88 | this.base=tmp; 89 | this._update(); 90 | 91 | } 92 | Float() { 93 | return this.price; 94 | } 95 | Multiply(other) { 96 | var a=this.copy(); 97 | if (other instanceof Price) { 98 | if ((other.symbols().indexOf(this.base.asset_id)==-1) && (other.symbols().indexOf(this.quote.asset_id)==-1)) { 99 | throw('Invalid Asset'); 100 | } 101 | if (this.quote.asset_id==other.base.asset_id) { 102 | a.base={amount: 1.0*this.base.amount*other.base.amount, asset_id: this.base.asset_id, precision: this.base.precision}; 103 | a.quote={amount: 1.0*this.quote.amount*other.quote.amount, asset_id: other.quote.asset_id, precision: other.quote.precision}; 104 | 105 | }else if (this.base.asset_id==other.quote.asset_id) { 106 | a.base={amount: 1.0*this.base.amount*other.base.amount, asset_id: other.base.asset_id, precision: other.base.precision}; 107 | a.quote={amount: 1.0*this.quote.amount*other.quote.amount, asset_id: this.quote.asset_id, precision: this.quote.precision}; 108 | }else{ 109 | throw('Wrong Rotation'); 110 | } 111 | }else if (other instanceof Object) { 112 | if (other.asset_id==this.quote.asset_id) { 113 | a = Object.assign(Object.create(Object.getPrototypeOf(other)), other); 114 | a.amount=a.amount*this.Float(); 115 | a.asset_id=this.base.asset_id; 116 | } 117 | }else{ 118 | a.base.amount = a.base.amount * Math.round(other * Math.pow(10, precision(other))); 119 | a.quote.amount=a.quote.amount*Math.pow(10,precision(other)); 120 | } 121 | a._update(); 122 | return a; 123 | } 124 | Divide(other) { 125 | var a=this.copy(); 126 | if (other instanceof Price) { 127 | if (other.symbols().sort()==this.symbols.sort()) { 128 | return this.as_base(this.base.asset_id).Float()/other.as_base(this.base.asset_id).Float(); 129 | }else if(other.symbols().indexOf(this.quote.asset_id)!=-1) { 130 | other=other.as_base(this.quote.asset_id); 131 | }else if(other.symbols().indexOf(this.base.asset_id)!=-1) { 132 | other=other.as_base(this.base.asset_id); 133 | }else{ 134 | throw('Invalid asset'); 135 | } 136 | a.base={amount: 1.0*this.quote.amount/other.quote.amount, asset_id: other.quote.asset_id}; 137 | a.quote={amount: 1.0*this.base.amount/other.base.amount, asset_id: this.quote.asset_id}; 138 | }else if (other instanceof Object) { 139 | if (other.asset_id==this.quote.asset_id) { 140 | a = Object.assign(Object.create(Object.getPrototypeOf(other)), other); 141 | a.amount=1.0*a.amount/this.Float(); 142 | a.asset_id=this.base.asset_id; 143 | } 144 | }else{ 145 | a.base.amount=1.0*a.base.amount/other; 146 | } 147 | a._update(); 148 | return a; 149 | } 150 | 151 | } 152 | module.exports=Price; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const yaml = require('js-yaml'); 2 | const fs = require('fs'); 3 | const Feed = require('./pricefeed.js'); 4 | const table = require('table').table; 5 | const chalk = require('chalk'); 6 | const moment = require('moment-timezone'); 7 | const Price = require('./lib/Price.js'); 8 | const prompt = require('prompt-sync')({sigint: true}); 9 | const argv = require('minimist')(process.argv.slice(2)); 10 | const Logger= require('./lib/Logger.js'); 11 | const {Apis} = require('bitsharesjs-ws'); 12 | const {PrivateKey, TransactionBuilder} =require('bitsharesjs'); 13 | 14 | 15 | var privKey = argv['k']; 16 | let pKey = PrivateKey.fromWif(privKey); 17 | let logger= new Logger(argv['d']); 18 | let confYaml = argv['c']; 19 | let apiNode = argv['s']; 20 | let publishFeed = argv['broadcast']; 21 | let usePremium = argv['usepremium']; 22 | let skip_critical = argv['skip_critical']; 23 | 24 | try { 25 | (async ()=> { 26 | //console.log(chalk.white('Loading config file...')); 27 | logger.log('Starting pricefeed run'); 28 | logger.transient('Loading config file...'); 29 | var config = yaml.safeLoad(fs.readFileSync(confYaml, 'utf8')); 30 | logger.info('Config file loaded.'); 31 | var feed = new Feed(config); 32 | logger.transient('Initialising pricefeed...'); 33 | await feed.init(); 34 | logger.info('Pricefeed class initialised.'); 35 | logger.info('Setting up pricefeed sources...'); 36 | await feed.fetch(); 37 | logger.info('Pricefeed sources setup complete.'); 38 | if (usePremium) { 39 | feed.calc_premium(); 40 | } 41 | logger.info('Calculating prices...'); 42 | await feed.derive([]); 43 | logger.info('Prices calculation complete.'); 44 | var prices = await feed.get_prices(); 45 | printLog(prices); 46 | printPrices(prices); 47 | Apis.instance(apiNode, true).init_promise.then(() => { 48 | logger.log('Connected to API node: '+ apiNode); 49 | 50 | let tr = new TransactionBuilder(); 51 | 52 | for (var symbol in prices) { 53 | let price=prices[symbol]; 54 | if(price==undefined) { 55 | logger.warning('No price for '+chalk.white(symbol)+'. '+chalk.red('SKIPPING.')); 56 | continue; 57 | } 58 | let flags = price['flags']; 59 | if ((flags.indexOf('min_change')==-1) && (flags.indexOf('over_max_age')==-1)) { 60 | logger.info(chalk.white(symbol)+' below min change and not old enough. '+chalk.green('SKIPPING.')); 61 | continue; 62 | } 63 | if (flags.indexOf('over_warn_change')>=0) { 64 | if (flags.indexOf('skip_change')==-1) { 65 | let includeprice= prompt(chalk.yellow('WARNING:')+' Price change for '+symbol+' ('+price['priceChange']+') above \'warn_change\'. Include in feed (Y/n)?'); 66 | if ((includeprice=='n') || (includeprice=='N')) { 67 | logger.log('Skipping...'); 68 | continue; 69 | } 70 | }else{ 71 | if (skip_critical) { 72 | logger.log('Skipping...'); 73 | continue; 74 | }else{ 75 | let includeprice= prompt(chalk.red('CRITICAL:')+' Price change for '+symbol+' ('+price['priceChange']+') above \'skip_change\'. Include in feed (y/N)?'); 76 | if ((includeprice!='y') && (includeprice!='Y')) { 77 | logger.log('Skipping...'); 78 | continue; 79 | } 80 | } 81 | } 82 | } 83 | 84 | 85 | 86 | let newprice=price.new_feed; 87 | let newcer = newprice.Multiply(price.cef); 88 | let op= { 89 | publisher: feed.producer.id, 90 | asset_id: newprice.base.asset_id, 91 | feed: { 92 | settlement_price: { 93 | base: { 94 | amount: newprice.base.amount, 95 | asset_id: newprice.base.asset_id }, 96 | quote: { 97 | amount: newprice.quote.amount, 98 | asset_id: newprice.quote.asset_id } 99 | }, 100 | maintenance_collateral_ratio: price.mcr*10, 101 | maximum_short_squeeze_ratio: price.mssr*10, 102 | core_exchange_rate: { 103 | base: { 104 | amount: newcer.base.amount, 105 | asset_id: newcer.base.asset_id }, 106 | quote: { 107 | amount: newcer.quote.amount, 108 | asset_id: newcer.quote.asset_id } 109 | } 110 | } 111 | } ; 112 | tr.add_type_operation( 'asset_publish_feed', op); 113 | } 114 | tr.set_required_fees().then(() => { 115 | tr.add_signer(pKey, pKey.toPublicKey().toPublicKeyString()); 116 | if (publishFeed) { 117 | logger.log('Publishing feed to blockchain.'); 118 | tr.broadcast().then(() => { process.exit();}); 119 | }else{ 120 | logger.log('\'--broadcast\' flag not set. Not publishing feed.'); 121 | process.exit(); 122 | } 123 | }); 124 | }); 125 | })(); 126 | } catch (e) { 127 | logger.log(e); 128 | } 129 | 130 | function formatPrice(price) { 131 | return chalk.yellow(Number(price).toFixed(9)); 132 | } 133 | function highlightlargeDeviation(d, p) { 134 | var perc = ((d - p) / p) * 100; 135 | if (perc < 0) { 136 | return chalk.red(perc.toFixed(2) + '%'); 137 | } else { 138 | return chalk.green('+' + perc.toFixed(2) + '%'); 139 | } 140 | } 141 | function printLog(prices) { 142 | var tabledata = [['base', 'quote', 'price', 'diff', 'volume', 'source']]; 143 | for (var symbol in prices) { 144 | var backing_symbol = prices[symbol]['short_backing_symbol']; 145 | var data = prices[symbol]['log']; 146 | var price = data[symbol]; 147 | if (price == undefined) { 148 | continue; 149 | } 150 | for (var didx in price[backing_symbol]) { 151 | var d = price[backing_symbol][didx]; 152 | tabledata.push([symbol, backing_symbol, formatPrice(d['price']), highlightlargeDeviation(d['price'], prices[symbol]['price']), d['volume'], JSON.stringify(d['sources'])]); 153 | } 154 | 155 | } 156 | 157 | let options = { 158 | drawHorizontalLine: (index, size) => { 159 | return index === 0 || index === 1 || index === size; 160 | }, 161 | columns: { 162 | 0: { 163 | alignment: 'right', 164 | width: 7, 165 | paddingLeft: 1, 166 | paddingRight: 1 167 | }, 168 | 1: { 169 | alignment: 'right', 170 | width: 7, 171 | paddingLeft: 1, 172 | paddingRight: 1 173 | }, 174 | 2: { 175 | alignment: 'right', 176 | width: 13, 177 | paddingLeft: 1, 178 | paddingRight: 1 179 | }, 180 | 3: { 181 | alignment: 'right', 182 | width: 6, 183 | paddingLeft: 1, 184 | paddingRight: 1 185 | }, 186 | 4: { 187 | alignment: 'right', 188 | width: 24, 189 | paddingLeft: 1, 190 | paddingRight: 1 191 | }, 192 | 5: { 193 | alignment: 'left', 194 | width: 40, 195 | paddingLeft: 1, 196 | paddingRight: 1 197 | } 198 | } 199 | }; 200 | 201 | let output = table(tabledata, options); 202 | logger.log('Source details:\r\n'+output); 203 | } 204 | function printPrices(prices) { 205 | var tabledata = [['symbol', 'backing', 'new price', 'cer', 'mean', 'median', 'wgt. avg', 'wgt. std(#)', 'blockchain', 'mssr', 'mcr', 'my last price', 'last update']]; 206 | for (var symbol in prices) { 207 | var feed = prices[symbol]; 208 | if (feed == undefined) { 209 | continue; 210 | } 211 | var last, age; 212 | let myprice = feed['price']; 213 | let blockchain = new Price(feed['global_feed']['settlement_price']).Float(); 214 | if (feed['current_feed'] != undefined) { 215 | last = new Price(feed['current_feed']['settlement_price']).Float(); 216 | age = moment.tz(feed['current_feed']['date'], 'UTC').fromNow(); 217 | } else { 218 | last = -1; 219 | age = 'Unknown'; 220 | } 221 | tabledata.push([ 222 | symbol, 223 | feed['short_backing_symbol'], 224 | formatPrice(feed['price']), 225 | formatPrice(feed['cer']), 226 | formatPrice(feed['mean']) + ' (' + priceChange(myprice, feed['mean']) + ')', 227 | formatPrice(feed['median']) + ' (' + priceChange(myprice, feed['median']) + ')', 228 | formatPrice(feed['weighted']) + ' (' + priceChange(myprice, feed['weighted']) + ')', 229 | formatStd(feed['std']) + ' (' + feed['number'] + ')', 230 | formatPrice(blockchain) + ' (' + priceChange(myprice, blockchain) + ')', 231 | feed['mssr'], 232 | feed['mcr'], 233 | formatPrice(last) + ' (' + priceChange(myprice, last) + ')', 234 | age 235 | ]); 236 | } 237 | 238 | let options = { 239 | drawHorizontalLine: (index, size) => { 240 | return index === 0 || index === 1 || index === size; 241 | }, 242 | columns: { 243 | 0: { 244 | alignment: 'right', 245 | width: 7, 246 | paddingLeft: 1, 247 | paddingRight: 1 248 | }, 249 | 1: { 250 | alignment: 'right', 251 | width: 7, 252 | paddingLeft: 1, 253 | paddingRight: 1 254 | }, 255 | 2: { 256 | alignment: 'right', 257 | width: 13, 258 | paddingLeft: 1, 259 | paddingRight: 1 260 | }, 261 | 3: { 262 | alignment: 'right', 263 | width: 13, 264 | paddingLeft: 1, 265 | paddingRight: 1 266 | }, 267 | 4: { 268 | alignment: 'right', 269 | width: 22, 270 | paddingLeft: 1, 271 | paddingRight: 1 272 | }, 273 | 5: { 274 | alignment: 'right', 275 | width: 22, 276 | paddingLeft: 1, 277 | paddingRight: 1 278 | }, 279 | 6: { 280 | alignment: 'right', 281 | width: 22, 282 | paddingLeft: 1, 283 | paddingRight: 1 284 | }, 285 | 7: { 286 | alignment: 'right', 287 | width: 12, 288 | paddingLeft: 1, 289 | paddingRight: 1 290 | }, 291 | 8: { 292 | alignment: 'right', 293 | width: 22, 294 | paddingLeft: 1, 295 | paddingRight: 1 296 | }, 297 | 9: { 298 | alignment: 'right', 299 | width: 5, 300 | paddingLeft: 1, 301 | paddingRight: 1 302 | }, 303 | 10: { 304 | alignment: 'right', 305 | width: 5, 306 | paddingLeft: 1, 307 | paddingRight: 1 308 | }, 309 | 11: { 310 | alignment: 'right', 311 | width: 22, 312 | paddingLeft: 1, 313 | paddingRight: 1 314 | }, 315 | 12: { 316 | alignment: 'right', 317 | width: 18, 318 | paddingLeft: 1, 319 | paddingRight: 1 320 | } 321 | } 322 | }; 323 | 324 | let output = table(tabledata, options); 325 | logger.log('Pricefeed details:\r\n'+output); 326 | } 327 | function priceChange(newp, old) { 328 | if (old == 0) { 329 | return -1; 330 | } else { 331 | let perc = ((newp - old) / old) * 100; 332 | if (perc < 0) { 333 | return chalk.red(perc.toFixed(2) + '%'); 334 | } else { 335 | return chalk.green('+' + perc.toFixed(2) + '%'); 336 | } 337 | } 338 | } 339 | function formatStd(std) { 340 | return chalk.bold(Number(std).toFixed(2)); 341 | } -------------------------------------------------------------------------------- /pricefeed.js: -------------------------------------------------------------------------------- 1 | const {Apis} = require('bitsharesjs-ws'); 2 | const math = require('mathjs'); 3 | const Price = require('./lib/Price.js'); 4 | const argv = require('minimist')(process.argv.slice(2)); 5 | const Logger= require('./lib/Logger.js'); 6 | const fs = require('fs'); 7 | let logger= new Logger(argv['d']); 8 | let optimised = argv['gcd']; 9 | let apiNode = argv['s']; 10 | 11 | function weightedAvg(arrValues, arrWeights) { 12 | 13 | var result = arrValues.map(function (value, i) { 14 | 15 | var weight = arrWeights[i]; 16 | var sum = value * weight; 17 | 18 | return [sum, weight]; 19 | }).reduce(function (p, c) { 20 | 21 | return [p[0] + c[0], p[1] + c[1]]; 22 | }, [0, 0]); 23 | 24 | return result[0] / result[1]; 25 | } 26 | function weightedStd(arrValues, arrWeights) { 27 | 28 | var average= weightedAvg(arrValues, arrWeights); 29 | var variance=weightedAvg(arrValues.map(x=>{ return Math.pow((x-average),2);}),arrWeights); 30 | return Math.sqrt(variance); 31 | } 32 | class Feed { 33 | constructor(config) { 34 | this.config=config; 35 | this.Api={}; 36 | this.feed={}; 37 | this.price={}; 38 | this.volume={}; 39 | this.price_result={}; 40 | this.reset(); 41 | this.premium=1; 42 | this.history={}; 43 | try { 44 | this.history = fs.readFileSync('history.json'); 45 | } catch (err) { 46 | this.history={}; 47 | } 48 | } 49 | async init() { 50 | return this.getProducer(); 51 | } 52 | reset() { 53 | this.data = {}; 54 | for (var base in this.config.assets) { 55 | this.data[base]={}; 56 | for(var quote in this.config.assets) { 57 | this.data[base][quote]=[]; 58 | } 59 | } 60 | } 61 | async getProducer() { 62 | this.producer=await Apis.instance(apiNode, true).init_promise.then(() => { 63 | this.Api=Apis.instance(); 64 | logger.transient('Connected to API node: '+apiNode); 65 | return this.Api.db_api().exec( 'get_account_by_name', [this.config.producer] ); 66 | }); 67 | return this.producer; 68 | } 69 | get_my_current_feed(asset) { 70 | var feeds=asset.bitasset_data.feeds; 71 | 72 | for (var feed in feeds) { 73 | 74 | if(feeds[feed][0]==this.producer['id']) { 75 | let myfeed=feeds[feed][1][1]; 76 | myfeed['date']=feeds[feed][1][0]; 77 | return myfeed; 78 | } 79 | } 80 | let myfeed=asset.bitasset_data.current_feed; 81 | var date = new Date(); 82 | var yesterday = date - 1000 * 60 * 60 * 24 * 2; 83 | myfeed['date']=( new Date(yesterday)).toISOString(); 84 | return myfeed; 85 | } 86 | async obtain_price_change(symbol) { 87 | var asset=await this.Api.db_api().exec( 'lookup_asset_symbols', [[symbol]] ).then((res)=>{ 88 | let asset=res[0]; 89 | return this.Api.db_api().exec( 'get_objects', [[asset.id]] ); 90 | }).then((assetstats)=> { 91 | return assetstats[0]; 92 | }); 93 | if (asset['is_bitasset']==false) { 94 | return; 95 | } 96 | asset['bitasset_data']=await this.Api.db_api().exec( 'get_objects', [[asset['bitasset_data_id']]] ).then((res) => { return res[0]; }); 97 | 98 | var short_backing_asset = await this.Api.db_api().exec( 'lookup_asset_symbols', [[asset['bitasset_data']['options']['short_backing_asset']]] ).then((res)=>{ 99 | logger.transient('Got backing asset data...'); 100 | let asset=res[0]; 101 | return this.Api.db_api().exec( 'get_objects', [[asset.id]] ); 102 | }).then((assetstats)=> { 103 | logger.transient('Got backing asset statistics...'); 104 | return assetstats[0]; 105 | }); 106 | 107 | asset['short_backing_asset'] = short_backing_asset; 108 | var price=this.price_result[symbol]; 109 | var newPrice=price['price']; 110 | var current_feed=this.get_my_current_feed(asset); 111 | current_feed['settlement_price'].base['precision']=asset.precision; 112 | current_feed['settlement_price'].quote['precision']=asset['short_backing_asset']['precision']; 113 | var oldPrice; 114 | 115 | if ((current_feed!==undefined) && (current_feed['settlement_price']!==undefined) && (current_feed['settlement_price'].base.asset_id!=current_feed['settlement_price'].quote.asset_id)) { 116 | oldPrice=new Price(current_feed['settlement_price']).Float(); 117 | current_feed['settlement_price'].base['precision']=asset.precision; 118 | current_feed['settlement_price'].quote['precision']=asset['short_backing_asset']['precision']; 119 | }else{ 120 | oldPrice=Infinity; 121 | current_feed={}; 122 | current_feed['settlement_price']={ 'base' : { 'amount': 0, 'asset_id': asset['id'], 'precision' : asset['precision'] }, 'quote':{ 'amount': 0, 'asset_id': '1.3.0', 'precision' : asset['short_backing_asset']['precision'] }}; 123 | } 124 | if (optimised) { 125 | this.price_result[symbol]['new_feed'] = Price.fromFloat(+parseFloat(newPrice).toFixed(current_feed['settlement_price'].base['precision']), current_feed['settlement_price'].base, current_feed['settlement_price'].quote); 126 | } else { 127 | this.price_result[symbol]['new_feed'] = Price.fromFloatOld(+parseFloat(newPrice).toFixed(current_feed['settlement_price'].base['precision']), current_feed['settlement_price'].base, current_feed['settlement_price'].quote); 128 | } 129 | this.price_result[symbol]['priceChange'] = (oldPrice - newPrice) / newPrice * 100.0; 130 | this.price_result[symbol]['current_feed'] = current_feed; 131 | this.price_result[symbol]['global_feed'] = asset['bitasset_data']['current_feed']; 132 | this.price_result[symbol]['global_feed']['settlement_price'].base['precision']=asset.precision; 133 | this.price_result[symbol]['global_feed']['settlement_price'].quote['precision']=asset['short_backing_asset']['precision']; 134 | return; 135 | } 136 | obtain_flags(symbol) { 137 | this.price_result[symbol].flags=[]; 138 | 139 | if (Math.abs(this.price_result[symbol]['priceChange']) > Math.abs(this.assetconf(symbol, 'min_change'))) { 140 | this.price_result[symbol]['flags'].push('min_change'); 141 | } 142 | if (Math.abs(this.price_result[symbol]['priceChange']) > Math.abs(this.assetconf(symbol, 'warn_change'))) { 143 | this.price_result[symbol]['flags'].push('over_warn_change'); 144 | } 145 | if (Math.abs(this.price_result[symbol]['priceChange']) > Math.abs(this.assetconf(symbol, 'skip_change'))) { 146 | this.price_result[symbol]['flags'].push('skip_change'); 147 | } 148 | var feed_age; 149 | if (this.price_result[symbol]['current_feed']!==undefined){ 150 | feed_age = Date.parse(this.price_result[symbol]['current_feed']['date']); 151 | } else { 152 | feed_age=0; 153 | } 154 | if (((new Date()).getTime() - feed_age) > this.assetconf(symbol, 'maxage')) { 155 | this.price_result[symbol]['flags'].push('over_max_age'); 156 | } 157 | } 158 | get_cer(symbol,price) { 159 | 160 | //if ((this.config['assets'][symbol]!=undefined) && (this.config['assets'][symbol]['core_exchange_factor']!=undefined)) { 161 | if ((this.config['assets'][symbol]!==undefined) ) { 162 | return price * this.assetconf(symbol,'core_exchange_factor'); 163 | } 164 | if ((this.config['assets'][symbol]!==undefined) && (this.config['assets'][symbol]['core_exchange_rate']!==undefined)) { 165 | let cer = this.config['assets'][symbol]['core_exchange_rate']; 166 | if ((cer['orientation']===undefined) || (cer['factor']===undefined) || (cer['ref_ticker']===undefined)) { 167 | throw('Missing one of required settings for cer: {}'); 168 | } 169 | 170 | // TODO: Deal with CER cases that require deeper understanding of PyBitshares class instantiation and operator overloading 171 | process.exit(1); 172 | //var tick=cer['ref_ticker'].split('/').split(':'); 173 | //ticker = await this.Api.db_api().exec( 'get_ticker', [tick[0],tick[1]] ); 174 | //price = ticker[cer['ref_ticker_attribute']]; 175 | //price. *= cer['factor'] 176 | //orientation = Market(cer['orientation']) 177 | //return price.as_quote(orientation['quote']['symbol']) 178 | } 179 | } 180 | async fetch() { 181 | if ((this.config['exchanges']===undefined) || (this.config['exchanges'].length==0)) { 182 | return; 183 | } 184 | 185 | for (var exchange in this.config.exchanges) { 186 | if (this.config.exchanges[exchange].enable) { 187 | var instance=new (require('./sources/'+this.config.exchanges[exchange].klass))(this.config.exchanges[exchange]); 188 | 189 | var afeed=await instance.fetch(); 190 | 191 | this.feed[exchange]=afeed; 192 | } 193 | } 194 | } 195 | assethasconf(symbol,parameter) { 196 | if ((this.config.assets[symbol]!==undefined) && (this.config.assets[symbol][parameter]!==undefined)) { 197 | return true; 198 | }else{ 199 | return false; 200 | } 201 | } 202 | assetconf(symbol,parameter,no_fail) { 203 | if ((this.config.assets[symbol]!==undefined) && (this.config.assets[symbol]!==null) && (this.config.assets[symbol][parameter]!==undefined)) { 204 | return this.config.assets[symbol][parameter]; 205 | }else{ 206 | if ((this.config['default']!==undefined) && (this.config['default'][parameter]!==undefined)) { 207 | return this.config['default'][parameter]; 208 | }else{ 209 | if (no_fail) { 210 | return; 211 | }else{ 212 | throw(parameter+' for '+symbol+' not defined!'); 213 | } 214 | } 215 | } 216 | } 217 | addPrice(base,quote,price,volume,sources) { 218 | if (this.data[base]===undefined) { 219 | this.data[base]=[]; 220 | } 221 | if (this.data[base][quote]===undefined) { 222 | this.data[base][quote]=[]; 223 | } 224 | var flat_list=[]; 225 | for (var i=0;i0) && (this.feed[datasource][base][quote]['volume'] > 0)) { 275 | 276 | this.addPrice(quote,base,1.0/this.feed[datasource][base][quote]['price'],this.feed[datasource][base][quote]['volume']*this.feed[datasource][base][quote]['price'],sources); 277 | } 278 | } 279 | } 280 | } 281 | } 282 | derive2Markets(asset,target_symbol) { 283 | var symbol=asset['symbol']; 284 | var premium=this.premium; 285 | if (!this.assetconf(symbol, 'use_premium')) { 286 | premium=1; 287 | } 288 | for (var iaidx in this.config['intermediate_assets']) { 289 | var interasset=this.config['intermediate_assets'][iaidx]; 290 | if (interasset==symbol) { 291 | continue; 292 | } 293 | 294 | for (var ridx in (this.data[symbol][interasset])) { 295 | var ratio= this.data[symbol][interasset][ridx]; 296 | 297 | if ((this.data[interasset]!==undefined) && (this.data[interasset][target_symbol]!==undefined)) { 298 | 299 | for (var idx=0; idx=this.feed['settlecny']['CNY']['BTS'].price) { 308 | this.addPrice(symbol,target_symbol,this.data[interasset][target_symbol][idx]['price']*ratio['price']*premium,this.data[interasset][target_symbol][idx]['volume']*ratio['volume'],sources); 309 | }else{ 310 | let marketdiff=(this.feed['settlecny']['CNY']['BTS'].price-this.data[interasset][target_symbol][idx]['price']*ratio['price'])/(this.data[interasset][target_symbol][idx]['price']*ratio['price']); 311 | let dampen = Math.pow(1-marketdiff,2); 312 | let new_premium=1+(premium -1)*dampen; 313 | this.addPrice(symbol,target_symbol,this.feed['settlecny']['CNY']['BTS'].price*new_premium,this.data[interasset][target_symbol][idx]['volume']*ratio['volume'],sources); 314 | }*/ 315 | //}else{ 316 | this.addPrice(symbol,target_symbol,this.data[interasset][target_symbol][idx]['price']*ratio['price']*premium,this.data[interasset][target_symbol][idx]['volume']*ratio['volume'],sources); 317 | //} 318 | } 319 | } 320 | } 321 | } 322 | } 323 | get_prices() { 324 | return this.price_result; 325 | } 326 | calc_premium() { 327 | let premium=parseFloat(this.feed['zb']['QC']['BITCNY']['price']); 328 | if (premium<1) { 329 | premium=1; 330 | } 331 | if (premium>1.05) { 332 | premium=1.05; 333 | } 334 | premium=(premium+0.002); 335 | let scale=((premium-1)*10) +1.1; 336 | delete(this.feed['zb']['QC']); 337 | this.premium=Math.pow(premium,scale); 338 | 339 | } 340 | derive3Markets(asset,target_symbol) { 341 | 342 | var symbol = asset['symbol']; 343 | if ((this.config['intermediate_assets'] === undefined) || (this.config['intermediate_assets'] === null)) { 344 | return; 345 | } 346 | if (this.assetconf(symbol, 'derive_across_3markets')) { 347 | for (var iaaidx in this.config['intermediate_assets']) { 348 | var interassetA=this.config['intermediate_assets'][iaaidx]; 349 | for (var iabidx in this.config['intermediate_assets']) { 350 | var interassetB=this.config['intermediate_assets'][iabidx]; 351 | if (interassetA==symbol) { 352 | continue; 353 | } 354 | if (interassetB==symbol) { 355 | continue; 356 | } 357 | if (interassetA==interassetB) { 358 | continue; 359 | } 360 | 361 | for (var raidx in (this.data[interassetB][interassetA])) { 362 | var ratioA = this.data[interassetB][interassetA][raidx]; 363 | for (var rbidx in (this.data[symbol][interassetB])) { 364 | var ratioB = this.data[symbol][interassetB][rbidx]; 365 | if ((this.data[interassetA] !== undefined) && (this.data[interassetA][target_symbol] !== undefined)) { 366 | 367 | for (var idx = 0; idx < this.data[interassetA][target_symbol].length; idx++) { 368 | 369 | if (this.data[interassetA][target_symbol][idx]['volume']==0) { 370 | continue; 371 | } 372 | var sources=this.data[interassetA][target_symbol][idx]['sources'].concat(ratioA['sources']).concat(ratioB['sources']); 373 | this.addPrice(symbol,target_symbol,this.data[interassetA][target_symbol][idx]['price']*ratioA['price']*ratioB['price'],this.data[interassetA][target_symbol][idx]['volume']*ratioA['volume']*ratioB['volume'],sources); 374 | } 375 | } 376 | } 377 | } 378 | } 379 | } 380 | } 381 | } 382 | async derive(assets_derive) { 383 | 384 | 385 | if (assets_derive.length==0) { 386 | assets_derive = this.config['assets']; 387 | } 388 | 389 | this.price_result = {}; 390 | var symbol; 391 | for (symbol in assets_derive) { 392 | this.price_result[symbol] = {}; 393 | } 394 | 395 | 396 | for (symbol in assets_derive) { 397 | logger.verbose('Calculating price for: '+symbol+'...'); 398 | await this.type_extern(symbol); 399 | logger.verbose('Price for: '+symbol+' calculated.'); 400 | } 401 | 402 | for (symbol in assets_derive) { 403 | if (this.price_result[symbol]===undefined) { 404 | continue; 405 | } 406 | logger.verbose('Calculating price change for: '+symbol+'...'); 407 | await this.obtain_price_change(symbol); 408 | await this.obtain_flags(symbol); 409 | logger.verbose('Price change for: '+symbol+' calculated.'); 410 | } 411 | return this.price_result; 412 | 413 | } 414 | async type_extern(symbol) { 415 | 416 | logger.info('Deriving '+symbol+' price feed.'); 417 | logger.transient('Querying blockchain...'); 418 | var asset=await this.Api.db_api().exec( 'lookup_asset_symbols', [[symbol]] ).then((res)=>{ 419 | logger.transient('Got asset data...'); 420 | let asset=res[0]; 421 | return this.Api.db_api().exec( 'get_objects', [[asset.id]] ); 422 | }).then((assetstats)=> { 423 | logger.transient('Got asset statistics...'); 424 | return assetstats[0]; 425 | }); 426 | if (asset['is_bitasset']==false) { 427 | return; 428 | } 429 | asset['bitasset_data']=await this.Api.db_api().exec( 'get_objects', [[asset['bitasset_data_id']]] ).then((res) => { logger.transient('Got bitasset data...'); return res[0]; }); 430 | 431 | var short_backing_asset = await this.Api.db_api().exec( 'lookup_asset_symbols', [[asset['bitasset_data']['options']['short_backing_asset']]] ).then((res)=>{ 432 | logger.transient('Got backing asset data...'); 433 | let asset=res[0]; 434 | return this.Api.db_api().exec( 'get_objects', [[asset.id]] ); 435 | }).then((assetstats)=> { 436 | logger.transient('Got backing asset statistics...'); 437 | return assetstats[0]; 438 | }); 439 | var backing_symbol = short_backing_asset['symbol']; 440 | asset['short_backing_asset'] = short_backing_asset; 441 | 442 | if ((this.assetconf(symbol, 'type')!='extern') && (this.assetconf(symbol, 'type')!='alias')) { 443 | 444 | return; 445 | } 446 | var alias; 447 | if (this.assetconf(symbol, 'type') == 'alias') { 448 | alias = this.assetconf(symbol, 'alias'); 449 | asset = await this.Api.db_api().exec( 'lookup_asset_symbols', [[alias]] ).then((res)=>{ 450 | logger.transient('Got aliased asset data...'); 451 | let asset=res[0]; 452 | return this.Api.db_api().exec( 'get_objects', [[asset.id]] ); 453 | }).then((assetstats)=> { 454 | logger.transient('Got aliased asset statistics...'); 455 | return assetstats[0]; 456 | }); 457 | }else{ 458 | alias = symbol; 459 | } 460 | 461 | this.reset(); 462 | 463 | this.appendOriginalPrices(symbol); 464 | this.derive2Markets(asset, backing_symbol); 465 | //TODO 3 Markets not implemented yet 466 | //this.derive3Markets(asset, backing_symbol) 467 | 468 | if (this.data[alias]===undefined) { 469 | logger.warning('\''+alias+'\' not in this.data'); 470 | return; 471 | } 472 | if (this.data[alias][backing_symbol]===undefined) { 473 | logger.warning('backing symbol \''+backing_symbol+'\' not in this.data[\''+alias+'\']'); 474 | return; 475 | } 476 | var assetvolume=[]; 477 | var assetprice=[]; 478 | for (var v in this.data[alias][backing_symbol]) { 479 | assetvolume.push(this.data[alias][backing_symbol][v]['volume']); 480 | assetprice.push(this.data[alias][backing_symbol][v]['price']); 481 | } 482 | var price_median,price_mean,price_weighted,price_std; 483 | if (assetvolume.length > 1) { 484 | price_median = math.median(assetprice); 485 | price_mean = math.mean(assetprice); 486 | price_weighted = weightedAvg(assetprice, assetvolume); 487 | price_std = weightedStd(assetprice, assetvolume); 488 | }else if (assetvolume.length == 1) { 489 | price_median = assetprice[0]; 490 | price_mean = assetprice[0]; 491 | price_weighted = assetprice[0]; 492 | price_std = 0; 493 | }else{ 494 | logger.warning('No market route found for '+symbol+'. Skipping price.'); 495 | return; 496 | } 497 | 498 | var metric = this.assetconf(symbol, 'metric'); 499 | var p; 500 | if (metric == 'median') { 501 | p = price_median; 502 | }else if (metric == 'mean') { 503 | p = price_mean; 504 | }else if(metric == 'weighted') { 505 | p = price_weighted; 506 | }else { 507 | throw('Asset '+symbol+' has an unknown metric \''+metric+'\''); 508 | } 509 | 510 | var cer = this.get_cer(symbol, p); 511 | 512 | logger.verbose('Adding pricefeed data for '+symbol+'.'); 513 | if ((this.config['assets'][symbol]!==undefined) ) { 514 | var cef = this.assetconf(symbol,'core_exchange_factor'); 515 | } 516 | if (symbol=='CNY') { 517 | let new_history={}; 518 | new_history.marketmean=price_mean; 519 | new_history.marketmedian=price_median; 520 | new_history.marketweighted=price_weighted; 521 | let premium=this.premium; 522 | if (this.feed['settlecny']['CNY']['BTS'].price>price_weighted) { 523 | let marketdiff=(this.feed['settlecny']['CNY']['BTS'].price-price_weighted)/(price_weighted); 524 | let dampen = Math.pow(1-marketdiff,2); 525 | let new_premium=1+(premium -1)*dampen; 526 | new_history.marketweightedprem=this.feed['settlecny']['CNY']['BTS'].price*new_premium; 527 | cer=cer*new_premium; 528 | }else{ 529 | let new_premium=premium; 530 | new_history.marketweightedprem=price_weighted*new_premium; 531 | cer=cer*new_premium; 532 | } 533 | let ch1=(new_history.marketweightedprem-this.history.marketweightedprem)/this.history.marketweightedprem; 534 | let ch2=(new_history.marketweighted-this.history.marketweighted)/this.history.marketweighted; 535 | if (ch1<0) { 536 | if (ch1