├── .gitignore ├── Dockerfile ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.md ├── bitshares_pricefeed ├── __init__.py ├── cli.py ├── history.py ├── pricefeed.py ├── sources │ ├── __init__.py │ ├── aex.py │ ├── alphavantage.py │ ├── bigone.py │ ├── biki.py │ ├── binance.py │ ├── bitcoinaverage.py │ ├── bitcoinvenezuela.py │ ├── bitshares_orderbook.py │ ├── bitsharesfeed.py │ ├── bitstamp.py │ ├── bittrex.py │ ├── coinbase.py │ ├── coincap.py │ ├── coindesk.py │ ├── coinegg.py │ ├── coingecko.py │ ├── coinmarketcap.py │ ├── cointiger.py │ ├── composite.py │ ├── currencylayer.py │ ├── fixer.py │ ├── gateio.py │ ├── graphene.py │ ├── hero.py │ ├── hertz.py │ ├── hitbtc.py │ ├── huobi.py │ ├── huobi_otc.py │ ├── iex.py │ ├── indodax.py │ ├── kraken.py │ ├── lbank.py │ ├── magicwallet.py │ ├── main.py │ ├── manual.py │ ├── norm.py │ ├── okcoin.py │ ├── okex_otc.py │ ├── openexchangerate.py │ ├── poloniex.py │ ├── quandl.py │ ├── robinhood.py │ ├── worldcoinindex.py │ └── zb.py └── ui.py ├── cli.py ├── docs ├── Makefile ├── _build │ ├── doctrees │ │ ├── environment.pickle │ │ └── index.doctree │ └── html │ │ ├── .buildinfo │ │ ├── _sources │ │ └── index.rst.txt │ │ ├── _static │ │ ├── ajax-loader.gif │ │ ├── basic.css │ │ ├── comment-bright.png │ │ ├── comment-close.png │ │ ├── comment.png │ │ ├── dialog-note.png │ │ ├── dialog-seealso.png │ │ ├── dialog-todo.png │ │ ├── dialog-topic.png │ │ ├── dialog-warning.png │ │ ├── doctools.js │ │ ├── down-pressed.png │ │ ├── down.png │ │ ├── epub.css │ │ ├── file.png │ │ ├── footerbg.png │ │ ├── headerbg.png │ │ ├── ie6.css │ │ ├── jquery-3.1.0.js │ │ ├── jquery.js │ │ ├── middlebg.png │ │ ├── minus.png │ │ ├── plus.png │ │ ├── pygments.css │ │ ├── pyramid.css │ │ ├── searchtools.js │ │ ├── transparent.gif │ │ ├── underscore-1.3.1.js │ │ ├── underscore.js │ │ ├── up-pressed.png │ │ ├── up.png │ │ └── websupport.js │ │ ├── genindex.html │ │ ├── index.html │ │ ├── objects.inv │ │ ├── search.html │ │ └── searchindex.js ├── conf.py ├── html │ ├── .buildinfo │ ├── .doctrees │ │ ├── environment.pickle │ │ └── index.doctree │ ├── _sources │ │ └── index.rst.txt │ ├── _static │ │ ├── ajax-loader.gif │ │ ├── basic.css │ │ ├── comment-bright.png │ │ ├── comment-close.png │ │ ├── comment.png │ │ ├── dialog-note.png │ │ ├── dialog-seealso.png │ │ ├── dialog-todo.png │ │ ├── dialog-topic.png │ │ ├── dialog-warning.png │ │ ├── doctools.js │ │ ├── down-pressed.png │ │ ├── down.png │ │ ├── epub.css │ │ ├── file.png │ │ ├── footerbg.png │ │ ├── headerbg.png │ │ ├── ie6.css │ │ ├── jquery-3.1.0.js │ │ ├── jquery.js │ │ ├── middlebg.png │ │ ├── minus.png │ │ ├── plus.png │ │ ├── pygments.css │ │ ├── pyramid.css │ │ ├── searchtools.js │ │ ├── transparent.gif │ │ ├── underscore-1.3.1.js │ │ ├── underscore.js │ │ ├── up-pressed.png │ │ ├── up.png │ │ └── websupport.js │ ├── genindex.html │ ├── index.html │ ├── objects.inv │ ├── search.html │ └── searchindex.js ├── index.rst └── requirements.txt ├── examples ├── XCD.yaml ├── baip2.yaml ├── bitshares_orderbook.yaml ├── bsip42.yaml ├── bsip76.yaml ├── composite.yaml ├── default.yaml ├── hero.yaml ├── hertz.yaml ├── norm.yaml └── stocks.yaml ├── requirements-test.txt ├── setup.cfg ├── setup.py ├── tests ├── formula │ ├── conftest.py │ ├── test_hero.py │ ├── test_hertz.py │ ├── test_norm.py │ └── test_xcd.py ├── sources │ ├── conftest.py │ ├── test_aex.py │ ├── test_alphavantage.py │ ├── test_bigone.py │ ├── test_biki.py │ ├── test_binance.py │ ├── test_bitcoinaverage.py │ ├── test_bitcoinvenezuela.py │ ├── test_bitshares_orderbook.py │ ├── test_bitsharesfeed.py │ ├── test_bitstamp.py │ ├── test_bittrex.py │ ├── test_coinbase.py │ ├── test_coincap.py │ ├── test_coindesk.py │ ├── test_coinegg.py │ ├── test_coingecko.py │ ├── test_coinmarketcap.py │ ├── test_cointiger.py │ ├── test_composite.py │ ├── test_currencylayer.py │ ├── test_fixer.py │ ├── test_gateio.py │ ├── test_graphene.py │ ├── test_hitbtc.py │ ├── test_huobi.py │ ├── test_huobi_otc.py │ ├── test_iex.py │ ├── test_indodax.py │ ├── test_kraken.py │ ├── test_lbank.py │ ├── test_magicwallet.py │ ├── test_manual.py │ ├── test_okcoin.py │ ├── test_okex_otc.py │ ├── test_openexchangerate.py │ ├── test_poloniex.py │ ├── test_quandl.py │ ├── test_robinhood.py │ ├── test_worldcoinindex.py │ └── test_zb.py └── test_history.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | *.eggs 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | *.pot 48 | 49 | # Django stuff: 50 | *.log 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | docs/html 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # Vim temp files 60 | *.swp 61 | 62 | config.yaml 63 | config.yml 64 | tmp.py 65 | .ropeproject 66 | bitshares 67 | 68 | /.vscode 69 | /.pytest_cache 70 | tests/api_keys.sh 71 | wrappers_env* 72 | config 73 | pid_historic_values.json 74 | prices_db -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY . . 6 | 7 | RUN python3 setup.py install 8 | RUN pip3 install .[history_db_postgresql] 9 | 10 | VOLUME ["/conf", "/root/.local/share/bitshares"] 11 | 12 | CMD [ "/usr/local/bin/bitshares-pricefeed", "--configfile", "/config/config.yaml", "update" ] 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 ChainSquad GmbH 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | include examples/*.yaml 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc clean-build docs 2 | 3 | clean: clean-build clean-pyc 4 | 5 | clean-build: 6 | rm -fr build/ dist/ *.egg-info .eggs/ .tox/ __pycache__/ .cache/ .coverage htmlcov src 7 | 8 | clean-pyc: 9 | find . -name '*.pyc' -exec rm -f {} + 10 | find . -name '*.pyo' -exec rm -f {} + 11 | find . -name '*~' -exec rm -f {} + 12 | 13 | lint: 14 | flake8 bitshares_pricefeed 15 | 16 | build: 17 | python3 setup.py build 18 | 19 | install: build 20 | python3 setup.py install 21 | 22 | install-user: build 23 | python3 setup.py install --user 24 | 25 | git: 26 | git push --all 27 | git push --tags 28 | 29 | check: 30 | python3 setup.py check 31 | 32 | dist: 33 | python3 setup.py sdist upload -r pypi 34 | python3 setup.py bdist_wheel upload 35 | 36 | release: clean check dist git 37 | -------------------------------------------------------------------------------- /bitshares_pricefeed/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "cli", 3 | "pricefeed", 4 | "sources", 5 | "ui", 6 | ] 7 | -------------------------------------------------------------------------------- /bitshares_pricefeed/history.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime, timedelta 3 | import dateutil.parser 4 | import csv 5 | 6 | class FileHistory: 7 | def __init__(self, dirname, **kvargs): 8 | self.dirname = dirname 9 | os.makedirs(dirname, exist_ok=True) 10 | 11 | def _get_filename(self, asset_symbol): 12 | return os.path.join(self.dirname, asset_symbol + '.csv') 13 | 14 | def save(self, asset_symbol, price, at=datetime.utcnow()): 15 | with open(self._get_filename(asset_symbol), 'a+') as f: 16 | writer = csv.writer(f) 17 | writer.writerow((at.isoformat(), price)) 18 | 19 | def load(self, asset_symbol, n_days, with_dates=False): 20 | try: 21 | with open(self._get_filename(asset_symbol), 'r') as f: 22 | reader = csv.reader(f) 23 | oldest_valid_date = datetime.utcnow() - timedelta(days=n_days) 24 | prices = [] 25 | for row in reader: 26 | timestamp = dateutil.parser.isoparse(row[0]) 27 | price = float(row[1]) 28 | if timestamp >= oldest_valid_date: 29 | if with_dates: 30 | prices.append([timestamp, price]) 31 | else: 32 | prices.append(price) 33 | return prices 34 | except IOError: 35 | return [] 36 | 37 | 38 | from sqlalchemy import create_engine 39 | from sqlalchemy import Table, Column, DateTime, Float, String, MetaData 40 | from sqlalchemy.sql import select, and_ 41 | from sqlalchemy.schema import CreateSchema 42 | 43 | class SqlHistory: 44 | def __init__(self, url, schema_name='price_feed', table_name='raw_prices', **kvargs): 45 | extra = {} 46 | if 'extras' in kvargs: 47 | extra = kvargs['extras'] 48 | self.db = create_engine(url, **extra) 49 | 50 | if not self.db.dialect.has_schema(self.db, schema_name): 51 | self.db.execute(CreateSchema(schema_name)) 52 | 53 | metadata = MetaData() 54 | 55 | self.prices = Table(table_name, metadata, 56 | Column('timestamp', DateTime, nullable=False), 57 | Column('asset', String, nullable=False), 58 | Column('price', Float, nullable=False), 59 | schema=schema_name 60 | ) 61 | 62 | metadata.create_all(self.db) 63 | 64 | 65 | def save(self, asset_symbol, price, at=datetime.utcnow()): 66 | self.db.execute(self.prices.insert(), { 67 | 'timestamp': at, 68 | 'asset': asset_symbol, 69 | 'price': price 70 | }) 71 | pass 72 | 73 | def load(self, asset_symbol, n_days, with_dates=False): 74 | fields = [self.prices.c.price] 75 | if with_dates: 76 | fields = [self.prices.c.timestamp, self.prices.c.price] 77 | 78 | oldest_valid_date = datetime.utcnow() - timedelta(days=n_days) 79 | query = select(fields).where( 80 | and_( 81 | self.prices.c.asset == asset_symbol, 82 | self.prices.c.timestamp >= oldest_valid_date 83 | )).order_by(self.prices.c.timestamp) 84 | rows = self.db.execute(query).fetchall() 85 | if with_dates: 86 | return [[timestamp, price] for (timestamp, price) in rows ] 87 | else: 88 | return [ price for (price, ) in rows ] 89 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import FeedSource, _request_headers, fetch_all 2 | 3 | # Exchanges 4 | from .bitcoinaverage import BitcoinAverage 5 | from .bitcoinvenezuela import BitcoinVenezuela 6 | from .bittrex import Bittrex 7 | from .coincap import Coincap 8 | from .coinmarketcap import Coinmarketcap, CoinmarketcapPro 9 | from .currencylayer import CurrencyLayer 10 | from .fixer import Fixer 11 | from .graphene import Graphene 12 | from .bitshares_orderbook import BitsharesOrderbook 13 | from .huobi import Huobi 14 | from .indodax import IndoDax 15 | from .okcoin import Okcoin 16 | from .openexchangerate import OpenExchangeRates 17 | from .poloniex import Poloniex, PoloniexVWAP 18 | from .quandl import Quandl 19 | from .bitstamp import Bitstamp 20 | from .aex import Aex 21 | from .zb import Zb 22 | from .lbank import Lbank 23 | from .alphavantage import AlphaVantage 24 | from .binance import Binance 25 | from .iex import Iex 26 | from .worldcoinindex import WorldCoinIndex 27 | from .coindesk import Coindesk 28 | from .bitsharesfeed import BitsharesFeed 29 | from .manual import Manual 30 | from .coinegg import CoinEgg 31 | from .composite import Composite 32 | from .cointiger import CoinTiger 33 | from .magicwallet import MagicWallet 34 | from .hertz import Hertz 35 | from .norm import Norm 36 | from .hero import Hero 37 | from .kraken import Kraken 38 | from .coinbase import Coinbase 39 | from .coingecko import CoinGecko 40 | from .biki import Biki 41 | from .huobi_otc import HuobiOtc 42 | from .gateio import GateIO 43 | from .hitbtc import HitBTC 44 | from .okex_otc import OkExOtc 45 | 46 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/aex.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | 5 | class Aex(FeedSource): 6 | def _fetch(self): 7 | feed = {} 8 | url = "https://api.aex.com/ticker.php" 9 | for base in self.bases: 10 | for quote in self.quotes: 11 | if base == quote: 12 | continue 13 | params = {'c': quote.lower(), 'mk_type': base.lower()} 14 | response = requests.get(url=url, params=params, headers=_request_headers, timeout=self.timeout, verify=False) 15 | result = response.json() 16 | if result != None and \ 17 | "ticker" in result and \ 18 | "last" in result["ticker"] and \ 19 | "vol" in result["ticker"]: 20 | self.add_rate(feed, base, quote, float(result["ticker"]["last"]), float(result["ticker"]["vol"])) 21 | else: 22 | print("\nFetched data from {0} is empty!".format(type(self).__name__)) 23 | continue 24 | return feed 25 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/alphavantage.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | # pylint: disable=no-member 5 | class AlphaVantage(FeedSource): # Alpha Vantage 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | self.timeout = getattr(self, 'timeout', 15) 9 | if not hasattr(self, "api_key"): 10 | raise Exception("AlphaVantage FeedSource requires an 'api_key'.") 11 | 12 | def _fetchForex(self, feed): 13 | for base in self.bases: 14 | for quote in self.quotes: 15 | if quote == base: 16 | continue 17 | url = ( 18 | 'https://www.alphavantage.co/query' 19 | '?function=CURRENCY_EXCHANGE_RATE&from_currency={quote}&to_currency={base}&apikey={apikey}' 20 | ).format(base=base, quote=quote, apikey=self.api_key) 21 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout) 22 | result = response.json() 23 | price = float(result['Realtime Currency Exchange Rate']['5. Exchange Rate']) 24 | self.add_rate(feed, base, quote, price, 1.0) 25 | return feed 26 | 27 | 28 | def _fetchEquities(self, feed): 29 | if not hasattr(self, "equities") or len(self.equities) == 0: 30 | return feed 31 | 32 | symbols = ",".join([ equity.split(':')[0] for equity in self.equities ]) 33 | url = ( 34 | 'https://www.alphavantage.co/query' 35 | '?function=BATCH_STOCK_QUOTES&symbols={symbols}&apikey={apikey}' 36 | ).format(symbols=symbols, apikey=self.api_key) 37 | 38 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout) 39 | result = response.json() 40 | 41 | for equity in self.equities: 42 | (name, base) = equity.split(':') 43 | for ticker in result['Stock Quotes']: 44 | if ticker['1. symbol'] == name: 45 | price = float(ticker['2. price']) 46 | volume = float(ticker['3. volume']) if ticker['3. volume'] != '--' else 1.0 47 | self.add_rate(feed, base, name, price, volume) 48 | return feed 49 | 50 | 51 | def _fetch(self): 52 | feed = {} 53 | try: 54 | feed = self._fetchForex(feed) 55 | feed = self._fetchEquities(feed) 56 | except Exception as e: 57 | raise Exception("\nError fetching results from {1}! ({0})".format(str(e), type(self).__name__)) 58 | return feed 59 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/bigone.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | 5 | class BigONE(FeedSource): 6 | def _fetch(self): 7 | feed = {} 8 | url = "https://big.one/api/v2/tickers" 9 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout) 10 | result = response.json() 11 | for ticker in result["data"]: 12 | for base in self.bases: 13 | for quote in self.quotes: 14 | if base == quote: 15 | continue 16 | if ticker['market_id'] == "{}-{}".format(quote.upper(), base.upper()): 17 | price = (float(ticker['bid']['price']) + float(ticker['ask']['price'])) / 2 18 | volume = float(ticker['volume']) if ticker['volume'] else 0 19 | self.add_rate(feed, base, quote, price, volume) 20 | return feed 21 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/biki.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | 5 | class Biki(FeedSource): 6 | def _fetch(self): 7 | feed = {} 8 | url = "https://openapi.biki.cc/open/api/get_ticker?symbol={quote}{base}" 9 | for base in self.bases: 10 | for quote in self.quotes: 11 | if quote == base: 12 | continue 13 | response = requests.get(url=url.format( 14 | quote=quote, 15 | base=base 16 | ), headers=_request_headers, timeout=self.timeout) 17 | result = response.json() 18 | if 'msg' in result and result['msg'] != 'suc': 19 | continue 20 | self.add_rate(feed, base, quote, float(result['data']["last"]), float(result['data']["vol"])) 21 | return feed -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/binance.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | 5 | class Binance(FeedSource): 6 | def _fetch(self): 7 | feed = {} 8 | url = "https://www.binance.com/api/v1/ticker/24hr?symbol={quote}{base}" 9 | for base in self.bases: 10 | for quote in self.quotes: 11 | if quote == base: 12 | continue 13 | response = requests.get(url=url.format( 14 | quote=quote, 15 | base=base 16 | ), headers=_request_headers, timeout=self.timeout) 17 | result = response.json() 18 | if 'msg' in result and result['msg'] == 'Invalid symbol.': 19 | continue 20 | self.add_rate(feed, base, quote, float(result["lastPrice"]), float(result["volume"])) 21 | return feed -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/bitcoinaverage.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | class BitcoinAverage(FeedSource): 5 | def __init__(self, *args, **kwargs): 6 | super().__init__(*args, **kwargs) 7 | self.symbol_set = getattr(self, "symbol_set", 'local') 8 | valid_symbol_sets = ['global', 'local', 'crypto', 'tokens'] 9 | assert self.symbol_set in valid_symbol_sets, "BitcoinAverage needs 'symbol_set' to be one of {}".format(valid_symbol_sets) 10 | 11 | def _fetch(self): 12 | feed = {} 13 | url = "https://apiv2.bitcoinaverage.com/indices/{}/ticker/{}{}" 14 | for base in self.bases: 15 | for quote in self.quotes: 16 | if quote == base: 17 | continue 18 | endpoint = url.format(self.symbol_set, quote, base) 19 | response = requests.get(url=endpoint, headers=_request_headers, timeout=self.timeout) 20 | if response.status_code == 200: 21 | result = response.json() 22 | self.add_rate(feed, base, quote, result["last"], result["volume"] * result['last']) 23 | return feed 24 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/bitcoinvenezuela.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | class BitcoinVenezuela(FeedSource): 5 | def _fetch(self): 6 | feed = {} 7 | # FIXME: SSL check deactivated, issue with Comodo SSL certificate. 8 | url = "https://api.bitcoinvenezuela.com" 9 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout, verify=False) 10 | result = response.json() 11 | for base in self.bases: 12 | for quote in self.quotes: 13 | if quote in result and base in result[quote]: 14 | self.add_rate(feed, base, quote, result[quote][base], 1.0) 15 | else: 16 | exchange_rate = base + '_' + quote 17 | if exchange_rate in result["exchange_rates"]: 18 | self.add_rate(feed, base, quote, result["exchange_rates"][exchange_rate], 1.0) 19 | return feed 20 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/bitsharesfeed.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | 5 | # pylint: disable=no-member 6 | class BitsharesFeed(FeedSource): 7 | def _fetch(self): 8 | from bitshares.asset import Asset 9 | feed = {} 10 | for assetName in self.assets: 11 | asset = Asset(assetName, full=True) 12 | currentPrice = asset.feed['settlement_price'] 13 | (base, quote) = currentPrice.symbols() 14 | self.add_rate(feed, base, quote, currentPrice['price'], 1.0) 15 | return feed 16 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/bitstamp.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | 5 | class Bitstamp(FeedSource): 6 | def _fetch(self): 7 | feed = {} 8 | url = "https://www.bitstamp.net/api/v2/ticker/{quote}{base}" 9 | for base in self.bases: 10 | for quote in self.quotes: 11 | if quote == base: 12 | continue 13 | # btcusd, btceur 14 | response = requests.get(url=url.format( 15 | quote=quote.lower(), 16 | base=base.lower() 17 | ), headers=_request_headers, timeout=self.timeout) 18 | result = response.json() 19 | self.add_rate(feed, base, quote, float(result["last"]), float(result["volume"])) 20 | return feed 21 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/bittrex.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | 5 | class Bittrex(FeedSource): 6 | def _fetch(self): 7 | feed = {} 8 | url = "https://bittrex.com/api/v1.1/public/getmarketsummaries" 9 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout) 10 | result = response.json()["result"] 11 | feed["response"] = response.json() 12 | for thisMarket in result: 13 | for base in self.bases: 14 | for quote in self.quotes: 15 | if quote == base: 16 | continue 17 | if thisMarket["MarketName"] == base + "-" + quote: 18 | self.add_rate(feed, base, quote, float(thisMarket["Last"]), float(thisMarket["Volume"])) 19 | return feed 20 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/coinbase.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | class Coinbase(FeedSource): 5 | def _fetch(self): 6 | feed = {} 7 | for base in self.bases: 8 | for quote in self.quotes: 9 | if quote == base: 10 | continue 11 | 12 | pair = '{}-{}'.format(quote.upper(), base.upper()) 13 | url = "https://api.pro.coinbase.com/products/{}/ticker".format(pair) 14 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout) 15 | if response.status_code != requests.codes.ok: # pylint: disable=no-member 16 | print('No result on Coinbase for {}'.format(pair)) 17 | continue 18 | result = response.json() 19 | self.add_rate(feed, base, quote, float(result['price']), float(result['volume'])) 20 | return feed -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/coincap.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | 5 | class Coincap(FeedSource): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | self.nb_coins_included_in_altcap_x = getattr(self, 'nb_coins_included_in_altcap_x', 10) 9 | 10 | def _fetch(self): 11 | feed = {} 12 | base = self.bases[0] 13 | if base == 'BTC': 14 | coincap_front = requests.get('http://www.coincap.io/front').json() 15 | coincap_global = requests.get('http://www.coincap.io/global').json() 16 | alt_cap = float(coincap_global["altCap"]) 17 | alt_caps_x = [float(coin['mktcap']) 18 | for coin in coincap_front[0:self.nb_coins_included_in_altcap_x+1] 19 | if coin['short'] != "BTC"][0:self.nb_coins_included_in_altcap_x] 20 | alt_cap_x = sum(alt_caps_x) 21 | btc_cap = float(coincap_global["btcCap"]) 22 | 23 | btc_altcap_price = alt_cap / btc_cap 24 | btc_altcapx_price = alt_cap_x / btc_cap 25 | 26 | if 'ALTCAP' in self.quotes: 27 | self.add_rate(feed, base, 'ALTCAP', btc_altcap_price, 1.0) 28 | if 'ALTCAP.X' in self.quotes: 29 | self.add_rate(feed, base, 'ALTCAP.X', btc_altcapx_price, 1.0) 30 | return feed 31 | 32 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/coindesk.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | 5 | class Coindesk(FeedSource): 6 | def _fetch(self): 7 | feed = {} 8 | url = "https://api.coindesk.com/v1/bpi/currentprice/{base}.json" 9 | for base in self.bases: 10 | for quote in self.quotes: 11 | if quote != 'BTC': 12 | raise Exception('Coindesk FeedSource only handle BTC quotes.') 13 | response = requests.get(url=url.format( 14 | base=base 15 | ), headers=_request_headers, timeout=self.timeout) 16 | result = response.json() 17 | self.add_rate(feed, base, quote, float(result['bpi'][base]['rate_float']), 1.0) 18 | return feed -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/coinegg.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | class CoinEgg(FeedSource): 5 | def _fetch(self): 6 | feed = {} 7 | for base in self.bases: 8 | for quote in self.quotes: 9 | if quote == base: 10 | continue 11 | 12 | url = "https://api.coinegg.im/api/v1/ticker/region/{}?coin={}".format(base.lower(), quote.lower()) 13 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout) 14 | result = response.json() 15 | 16 | if 'result' in result and result['result'] == False: 17 | continue 18 | 19 | self.add_rate(feed, base, quote, float(result['last']), float(result["vol"])) 20 | return feed 21 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/coingecko.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | class CoinGecko(FeedSource): 5 | def _fetch(self): 6 | feed = {} 7 | formated_bases = ','.join([ base.lower() for base in self.bases ]) 8 | formated_quotes = ','.join([ quote.lower() for quote in self.quotes ]) 9 | url = "https://api.coingecko.com/api/v3/simple/price?ids={}&vs_currencies={}&include_24hr_vol=true".format(formated_quotes, formated_bases) 10 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout) 11 | result = response.json() 12 | for quote in self.quotes: 13 | for base in self.bases: 14 | low_quote = quote.lower() 15 | low_base = base.lower() 16 | if low_quote in result and low_base in result[low_quote]: 17 | ticker = float(result[low_quote][low_base]) 18 | volume = float(result[low_quote][low_base + '_24h_vol']) / ticker 19 | self.add_rate(feed, base, quote, ticker, volume) 20 | return feed 21 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/coinmarketcap.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | 5 | class Coinmarketcap(FeedSource): 6 | def _fetch(self): 7 | feed = {} 8 | url = 'https://api.coinmarketcap.com/v1/ticker/' 9 | response = requests.get( 10 | url=url, headers=_request_headers, timeout=self.timeout) 11 | result = response.json() 12 | for asset in result: 13 | for quote in self.quotes: 14 | if asset["symbol"] == quote: 15 | 16 | self.add_rate(feed, 'BTC', quote, 17 | float(asset["price_btc"]), 18 | float(asset["24h_volume_usd"]) / float(asset["price_btc"])) 19 | 20 | self.add_rate(feed, 'USD', quote, 21 | float(asset["price_usd"]), 22 | float(asset["24h_volume_usd"]) / float(asset["price_usd"])) 23 | self._fetch_altcap(feed) 24 | 25 | return feed 26 | 27 | def _fetch_altcap(self, feed): 28 | if 'BTC' in self.bases and ('ALTCAP' in self.quotes or 'ALTCAP.X' in self.quotes): 29 | ticker = requests.get( 30 | 'https://api.coinmarketcap.com/v1/ticker/').json() 31 | global_data = requests.get( 32 | 'https://api.coinmarketcap.com/v1/global/').json() 33 | bitcoin_data = requests.get( 34 | 'https://api.coinmarketcap.com/v1/ticker/bitcoin/' 35 | ).json()[0] 36 | alt_caps_x = [float(coin['market_cap_usd']) 37 | for coin in ticker if 38 | float(coin['rank']) <= 11 and 39 | coin['symbol'] != "BTC" 40 | ] 41 | alt_cap = ( 42 | float(global_data['total_market_cap_usd']) - 43 | float(bitcoin_data['market_cap_usd'])) 44 | alt_cap_x = sum(alt_caps_x) 45 | btc_cap = next(( 46 | coin['market_cap_usd'] 47 | for coin in ticker if coin["symbol"] == "BTC")) 48 | 49 | btc_altcap_price = float(alt_cap) / float(btc_cap) 50 | btc_altcapx_price = float(alt_cap_x) / float(btc_cap) 51 | 52 | if 'ALTCAP' in self.quotes: 53 | self.add_rate(feed, 'BTC', 'ALTCAP', btc_altcap_price, 1.0) 54 | if 'ALTCAP.X' in self.quotes: 55 | self.add_rate(feed, 'BTC', 'ALTCAP.X', btc_altcapx_price, 1.0) 56 | return feed 57 | 58 | class CoinmarketcapPro(FeedSource): 59 | def __init__(self, *args, **kwargs): 60 | super().__init__(*args, **kwargs) 61 | if not hasattr(self, 'api_key'): 62 | raise Exception("CoinmarketcapPro FeedSource requires 'api_key'.") 63 | 64 | def _fetch(self): 65 | feed = {} 66 | url = 'https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest?symbol={}&convert={}' 67 | headers = { **_request_headers, 'X-CMC_PRO_API_KEY': self.api_key } # pylint: disable=no-member 68 | all_quotes = ','.join(self.quotes) 69 | for base in self.bases: 70 | response = requests.get(url=url.format(all_quotes, base), headers=headers, timeout=self.timeout) 71 | result = response.json() 72 | for quote, ticker in result['data'].items(): 73 | price = ticker['quote'][base]['price'] 74 | volume = ticker['quote'][base]['volume_24h'] / price 75 | self.add_rate(feed, base, quote, price, volume) 76 | 77 | return feed -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/cointiger.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | class CoinTiger(FeedSource): 5 | def _fetch(self): 6 | feed = {} 7 | url = "https://www.cointiger.com/exchange/api/public/market/detail" 8 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout) 9 | result = response.json() 10 | 11 | for base in self.bases: 12 | for quote in self.quotes: 13 | if quote == base: 14 | continue 15 | pair = '{}{}'.format(quote, base) 16 | if pair in result: 17 | # USe baseVolume for volume as it seems that cointiger invert quote and base. 18 | self.add_rate(feed, base, quote, float(result[pair]['last']), float(result[pair]["baseVolume"])) 19 | else: 20 | pair = '{}{}'.format(base, quote) 21 | if pair in result: 22 | self.add_rate(feed, base, quote, 1/float(result[pair]['last']), float(result[pair]["quoteVolume"])) 23 | return feed 24 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/composite.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=no-member 2 | from . import FeedSource, fetch_all 3 | from operator import itemgetter 4 | import itertools 5 | import statistics 6 | import numpy as num 7 | import logging 8 | log = logging.getLogger(__name__) 9 | 10 | class Composite(FeedSource): 11 | def __init__(self, *args, **kwargs): 12 | super().__init__(*args, **kwargs) 13 | assert hasattr(self, "aggregation_type"), "Composite needs 'aggregation_type'" 14 | valid_aggregation_types = ['min', 'max', 'mean', 'median', 'weighted_mean', 'first_valid'] 15 | assert getattr(self, "aggregation_type") in valid_aggregation_types, "Composite needs 'aggregation_type' to be one of {}".format(valid_aggregation_types) 16 | if self.aggregation_type == 'first': 17 | assert hasattr(self, "order"), "Composite needs an 'order' of source when 'first_valid' aggregation type is selected" 18 | assert hasattr(self, "exchanges"), "Composite needs 'exchanges'" 19 | 20 | def _extract_assets(self, feed): 21 | quotes = [ source.keys() for source in feed.values() ] 22 | quotes = set(itertools.chain.from_iterable(quotes)) # Flatten + uniq 23 | # Remove special 'response' key. 24 | if 'response' in quotes: 25 | quotes.remove('response') 26 | 27 | bases = [] 28 | for source in feed.values(): 29 | for quote in quotes: 30 | if quote in source: 31 | bases.append(source[quote].keys()) 32 | bases = set(itertools.chain.from_iterable(bases)) # Flatten + uniq 33 | 34 | return (bases, quotes) 35 | 36 | def _extract_feeds(self, base, quote, feeds): 37 | extracted_feeds = [] 38 | for source, data in feeds.items(): 39 | if quote in data and base in data[quote]: 40 | data[quote][base]['source'] = source 41 | extracted_feeds.append(data[quote][base]) 42 | return extracted_feeds 43 | 44 | def _select_feed(self, feeds): 45 | # pylint: disable=no-member 46 | if self.aggregation_type == 'min': 47 | return min(feeds, key=itemgetter('price')) 48 | elif self.aggregation_type == 'max': 49 | return max(feeds, key=itemgetter('price')) 50 | elif self.aggregation_type == 'median': 51 | return { 52 | 'price': statistics.median(x['price'] for x in feeds), 53 | 'volume': sum( x['volume'] for x in feeds ), 54 | 'source': 'median({})'.format(', '.join([x['source'] for x in feeds])) 55 | } 56 | elif self.aggregation_type == 'mean': 57 | return { 58 | 'price': statistics.mean(x['price'] for x in feeds), 59 | 'volume': sum(x['volume'] for x in feeds), 60 | 'source': 'mean({})'.format(', '.join([x['source'] for x in feeds])) 61 | } 62 | elif self.aggregation_type == 'weighted_mean': 63 | return { 64 | 'price': num.average([x['price'] for x in feeds], weights = [x['volume'] for x in feeds]), 65 | 'volume': sum(x['volume'] for x in feeds), 66 | 'source': 'weighted_mean({})'.format(', '.join([x['source'] for x in feeds])) 67 | } 68 | elif self.aggregation_type == 'first_valid': 69 | for source in self.order: 70 | found = next((feed for feed in feeds if feed['source'] == source), None) 71 | if found: 72 | return found 73 | 74 | def _filter(self, feed): 75 | bases, quotes = self._extract_assets(feed) 76 | filtered_feed = {} 77 | for quote in quotes: 78 | filtered_feed[quote] = {} 79 | for base in bases: 80 | extracted_feeds = self._extract_feeds(base, quote, feed) 81 | filtered_feed[quote][base] = self._select_feed(extracted_feeds) 82 | return filtered_feed 83 | 84 | def _fetch(self): 85 | feed = fetch_all(self.exchanges) 86 | result = self._filter(feed) 87 | return result 88 | 89 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/currencylayer.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | # pylint: disable=no-member 5 | class CurrencyLayer(FeedSource): # Hourly updated data over http with free subscription 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | if not hasattr(self, "api_key") or not hasattr(self, "free_subscription"): 9 | raise Exception("CurrencyLayer FeedSource requires 'api_key' and 'free_subscription'") 10 | 11 | def _fetch(self): 12 | feed = {} 13 | for base in self.bases: 14 | url = "http://apilayer.net/api/live?access_key=%s¤cies=%s&source=%s&format=1" % (self.api_key, ",".join(self.quotes), base) 15 | if self.free_subscription and base != 'USD': 16 | continue 17 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout) 18 | result = response.json() 19 | if result.get("source") == base: 20 | for quote in self.quotes: 21 | if quote == base: 22 | continue 23 | self.add_rate(feed, base, quote, 1 / result["quotes"][base + quote], 1.0) 24 | else: 25 | raise Exception(result.get("description")) 26 | return feed 27 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/fixer.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | # pylint: disable=no-member 5 | class Fixer(FeedSource): # fixer.io daily updated data from European Central Bank. 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | if not hasattr(self, "api_key") or not hasattr(self, "free_subscription"): 9 | raise Exception("Fixer FeedSource requires 'api_key' and 'free_subscription'") 10 | 11 | def _fetch(self): 12 | feed = {} 13 | for base in self.bases: 14 | if self.free_subscription and base != 'EUR': 15 | continue 16 | url = "http://data.fixer.io/api/latest?access_key=%s&base=%s" % (self.api_key, base) 17 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout) 18 | result = response.json() 19 | for quote in self.quotes: 20 | if quote == base: 21 | continue 22 | self.add_rate(feed, base, quote, 1.0 / float(result["rates"][quote]), 1.0) 23 | return feed 24 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/gateio.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | 5 | class GateIO(FeedSource): 6 | def _fetch(self): 7 | feed = {} 8 | url = "https://api.gateio.ws/api/v4/spot/tickers?currency_pair={quote}_{base}" 9 | for base in self.bases: 10 | for quote in self.quotes: 11 | if quote == base: 12 | continue 13 | response = requests.get( 14 | url=url.format( 15 | base=base.upper(), 16 | quote=quote.upper() 17 | ), 18 | headers=_request_headers, timeout=self.timeout) 19 | result = response.json() 20 | if response.status_code != 200 or 'message' in result: 21 | print("Unable to fetch data from GateIO: {}".format(result['message'])) 22 | continue 23 | ticker = result[0] 24 | self.add_rate(feed, base, quote, float(ticker["last"]), float(ticker["quote_volume"])) 25 | return feed 26 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/graphene.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | 5 | class Graphene(FeedSource): 6 | def _fetch(self): 7 | from bitshares.market import Market 8 | feed = {} 9 | for base in self.bases: 10 | for quote in self.quotes: 11 | if quote == base: 12 | continue 13 | ticker = Market("%s:%s" % (quote, base)).ticker() 14 | if (float(ticker["latest"])) > 0 and float(ticker["quoteVolume"]) > 0: 15 | self.add_rate(feed, base, quote, float(ticker["latest"]), float(ticker["quoteVolume"])) 16 | return feed 17 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/hero.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from . import FeedSource 3 | 4 | class Hero(FeedSource): 5 | def _fetch(self): 6 | feed = {} 7 | 8 | hero_reference_timestamp = date(1913, 12, 23) 9 | current_timestamp = date.today() 10 | hero_days_in_year = 365.2425 11 | hero_inflation_rate = 1.05 12 | hero_value = hero_inflation_rate ** ((current_timestamp - hero_reference_timestamp).days / hero_days_in_year) 13 | 14 | self.add_rate(feed, 'USD', 'HERO', hero_value, 1.0) 15 | return feed 16 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/hertz.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import math 3 | from . import FeedSource 4 | 5 | SECONDS_PER_DAY = 60 * 60 * 24 6 | 7 | class Hertz(FeedSource): 8 | 9 | def _compute_hertz(self, reference_timestamp, current_timestamp, period_days, phase_days, reference_asset_value, amplitude): 10 | hz_reference_timestamp = datetime.strptime(reference_timestamp, "%Y-%m-%dT%H:%M:%S").timestamp() 11 | hz_period = SECONDS_PER_DAY * period_days 12 | hz_phase = SECONDS_PER_DAY * phase_days 13 | hz_waveform = math.sin(((((current_timestamp - (hz_reference_timestamp + hz_phase))/hz_period) % 1) * hz_period) * ((2*math.pi)/hz_period)) # Only change for an alternative HERTZ ABA. 14 | hz_value = reference_asset_value + ((amplitude * reference_asset_value) * hz_waveform) 15 | return hz_value 16 | 17 | def _fetch(self): 18 | feed = {} 19 | hertz_reference_timestamp = "2015-10-13T14:12:24" # Bitshares 2.0 genesis block timestamp 20 | hertz_current_timestamp = datetime.now().timestamp() # Current timestamp for reference within the hertz script 21 | hertz_amplitude = 0.14 # 14% fluctuating the price feed $+-0.14 (2% per day) 22 | hertz_period_days = 28 # Aka wavelength, time for one full SIN wave cycle. 23 | hertz_phase_days = 0.908056 # Time offset from genesis till the first wednesday, to set wednesday as the primary Hz day. 24 | hertz_reference_asset_value = 1.00 # $1.00 USD, not much point changing as the ratio will be the same. 25 | 26 | # Calculate the current value of Hertz in USD 27 | hertz_value = self._compute_hertz(hertz_reference_timestamp, hertz_current_timestamp, hertz_period_days, hertz_phase_days, hertz_reference_asset_value, hertz_amplitude) 28 | 29 | self.add_rate(feed, 'USD', 'HERTZ', hertz_value, 1.0) 30 | return feed 31 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/hitbtc.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | 5 | class HitBTC(FeedSource): 6 | def _fetch(self): 7 | feed = {} 8 | url = "https://api.hitbtc.com/api/3/public/ticker/{quote}{base}" 9 | for base in self.bases: 10 | for quote in self.quotes: 11 | if quote == base: 12 | continue 13 | response = requests.get( 14 | url=url.format( 15 | base=base.upper(), 16 | quote=quote.upper() 17 | ), 18 | headers=_request_headers, timeout=self.timeout) 19 | result = response.json() 20 | if response.status_code != 200 or 'error' in result: 21 | print("Unable to fetch data from HitBTC: {}".format(result['error']['message'])) 22 | continue 23 | self.add_rate(feed, base, quote, float(result["last"]), float(result["volume"])) 24 | return feed 25 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/huobi.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | 5 | class Huobi(FeedSource): 6 | def _fetch(self): 7 | feed = {} 8 | url = "https://api.huobi.pro/market/tickers" 9 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout) 10 | result = response.json() 11 | if result['status'] == 'error': 12 | raise Exception(result['err-msg']) 13 | for ticker in result['data']: 14 | for base in self.bases: 15 | for quote in self.quotes: 16 | if quote == base: 17 | continue 18 | if ticker['symbol'] == '{}{}'.format(quote.lower(), base.lower()): 19 | self.add_rate(feed, base, quote, ticker['close'], ticker["vol"] /ticker['close']) 20 | return feed 21 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/huobi_otc.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | import json 4 | 5 | 6 | class HuobiOtc(FeedSource): 7 | def _request(self, side): 8 | extracted_tickers = {} 9 | url = "https://otc-api.hbg.com/v1/trade/fast/config/list?side={}&tradeMode=fast".format(side) 10 | headers = { **_request_headers, 'portal': 'web' } 11 | response = requests.get(url=url, headers=headers, timeout=self.timeout) 12 | result = response.json() 13 | if not result['success'] : 14 | raise Exception(result['message']) 15 | for ticker in result['data']: 16 | for base in self.bases: 17 | if ticker['cryptoAsset']['name'] == base: 18 | extracted_tickers[base] = {} 19 | for ticker_quote in ticker['quoteAsset']: 20 | if ticker_quote['name'] in self.quotes: 21 | extracted_tickers[base][ticker_quote['name']] = 1.0 / float(ticker_quote['price']) 22 | return extracted_tickers 23 | 24 | def _fetch(self): 25 | feed = {} 26 | 27 | tickers_buy = self._request('buy') 28 | tickers_sell = self._request('sell') 29 | 30 | for base in self.bases: 31 | for quote in self.quotes: 32 | if quote == base: 33 | continue 34 | mid_price = (tickers_buy[base][quote] + tickers_sell[base][quote]) / 2 35 | self.add_rate(feed, base, quote, mid_price, 1.0) 36 | 37 | return feed 38 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/iex.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | # pylint: disable=no-member 5 | class Iex(FeedSource): # Stocks prices from iextrading.com 6 | def _extract_symbols(self): 7 | symbols_by_base = {} 8 | for equity in self.equities: 9 | (symbol, base) = equity.split(':') 10 | if base not in symbols_by_base: 11 | symbols_by_base[base] = [] 12 | symbols_by_base[base].append(symbol) 13 | return symbols_by_base 14 | 15 | def _fetch(self): 16 | symbols_by_base = self._extract_symbols() 17 | feed = {} 18 | url = "https://api.iextrading.com/1.0/stock/market/batch?symbols={symbols}&types=quote" 19 | for base in symbols_by_base.keys(): 20 | response = requests.get(url=url.format( 21 | symbols=','.join(symbols_by_base[base]) 22 | ), headers=_request_headers, timeout=self.timeout) 23 | result = response.json() 24 | for symbol in result.keys(): 25 | ticker = result[symbol]['quote'] 26 | self.add_rate(feed, base, symbol, float(ticker["latestPrice"]), float(ticker["latestVolume"])) 27 | return feed -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/indodax.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | 5 | class IndoDax(FeedSource): 6 | def _fetch(self): 7 | feed = {} 8 | for base in self.bases: 9 | for quote in self.quotes: 10 | if quote == base: 11 | continue 12 | url = "https://indodax.com/api/%s_%s/ticker" % (quote.lower(), base.lower()) 13 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout) 14 | result = response.json() 15 | if response.status_code != 200 or 'error' in result: 16 | print("\nFetched data from {0} has error for pair {2}/{3}: {1}!".format(type(self).__name__, result['error_description'], quote, base)) 17 | continue 18 | 19 | ticker = result["ticker"] 20 | self.add_rate(feed, base, quote, float(ticker["last"]), float(ticker["vol_" + quote.lower()])) 21 | feed[self.alias(base)]["response"] = result 22 | return feed 23 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/kraken.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | 5 | class Kraken(FeedSource): 6 | def _fetch(self): 7 | feed = {} 8 | for base in self.bases: 9 | for quote in self.quotes: 10 | if quote == base: 11 | continue 12 | 13 | pair = '{}{}'.format(quote.upper(), base.upper()) 14 | url = "https://api.kraken.com/0/public/Ticker?pair={}".format(pair) 15 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout) 16 | if response.status_code != requests.codes.ok: # pylint: disable=no-member 17 | print('No result on Kraken for {}'.format(pair)) 18 | continue 19 | result = response.json()['result'][pair] 20 | self.add_rate(feed, base, quote, float(result['c'][0]), float(result['v'][1])) 21 | 22 | return feed 23 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/lbank.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | 5 | class Lbank(FeedSource): 6 | def _fetch(self): 7 | feed = {} 8 | url = "https://api.lbank.info/v1/ticker.do?symbol={quote}_{base}" 9 | for base in self.bases: 10 | for quote in self.quotes: 11 | if quote == base: 12 | continue 13 | response = requests.get( 14 | url=url.format( 15 | base=base.lower(), 16 | quote=quote.lower() 17 | ), 18 | headers=_request_headers, timeout=self.timeout) 19 | result = response.json() 20 | if 'result' in result and result['result'] == 'false': 21 | raise Exception('Error %s from LBank (see https://www.lbank.info/documents.html#/rest/api-reference).' 22 | % result['error_code']) 23 | ticker = result['ticker'] 24 | self.add_rate(feed, base, quote, float(ticker["latest"]), float(ticker["vol"])) 25 | return feed 26 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/magicwallet.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | # pylint: disable=no-member 5 | class MagicWallet(FeedSource): 6 | 7 | def __init__(self, *args, **kwargs): 8 | super().__init__(*args, **kwargs) 9 | assert hasattr(self, 'api_key'), "MagicWallet feedSource requires an 'api_key'" 10 | self.period = getattr(self, 'period', '1h') 11 | self.nb_operation_threshold = getattr(self, 'nb_operation_threshold', 10) 12 | self.valid_periods = ['1m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '12h', '24h'] 13 | assert getattr(self, "period") in self.valid_periods, "MagicWallet needs 'period' to be one of {}".format(self.valid_periods) 14 | 15 | def _compute_rate_and_volume(self, data, period): 16 | for stat in data: 17 | if stat['datatype'] == period: 18 | dbitcny = float(stat['depositBitCNY']) 19 | wbitcny = float(stat['withdrawBitCNY']) 20 | dfiatcny = float(stat['depositFiatCNY']) 21 | wfiatcny = float(stat['withdrawFiatCNY']) 22 | dcount = int(stat['depositCount']) 23 | wcount = int(stat['withdrawCount']) 24 | if (dcount + wcount) == 0: 25 | return (1, 0) 26 | return (round((dfiatcny + wfiatcny) / (dbitcny + wbitcny), 4), dcount + wcount) 27 | raise Exception("Invalid period {}, should be one of: {}".format(period, self.valid_periods)) 28 | 29 | def _fetch(self): 30 | feed = {} 31 | if self.bases and ( len(self.bases) != 1 or (self.bases[0] != 'CNY' and self.bases[0] != 'BITCNY')): 32 | raise Exception("MagicWallet only supports BITCNY/CNY pair.") 33 | if self.quotes and ( len(self.quotes) != 1 or (self.quotes[0] != 'CNY' and self.quotes[0] != 'BITCNY')): 34 | raise Exception("MagicWallet only supports BITCNY/CNY pair.") 35 | 36 | url = 'https://redemption.icowallet.net/api_v2/RechargeAndWithdrawTables/GetListForRechargeAndWithdrawtable' 37 | response = requests.post(url=url, headers={ **_request_headers, 'apikey': self.api_key } , timeout=self.timeout) 38 | result = response.json() 39 | if response.status_code != 200: 40 | raise Exception('Error from MagicWallet API: {}'.format(result)) 41 | 42 | rate, volume = self._compute_rate_and_volume(result, self.period) 43 | if volume < self.nb_operation_threshold: 44 | rate, volume = self._compute_rate_and_volume(result, '24h') 45 | 46 | self.add_rate(feed, 'CNY', 'BITCNY', rate, volume) 47 | return feed 48 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/main.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import os 4 | import sys 5 | import traceback 6 | from concurrent import futures 7 | from .. import sources 8 | 9 | import requests 10 | 11 | from appdirs import user_data_dir 12 | 13 | import logging 14 | log = logging.getLogger(__name__) 15 | 16 | _request_headers = {'content-type': 'application/json', 17 | 'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0'} 18 | 19 | def fetch_all(exchanges): 20 | feed = {} 21 | pool = futures.ThreadPoolExecutor(max_workers=8) 22 | 23 | threads = {} 24 | 25 | for name, exchange in exchanges.items(): 26 | if "enable" in exchange and not exchange["enable"]: 27 | continue 28 | if not hasattr(sources, exchange["klass"]): 29 | raise ValueError("Klass %s not known!" % exchange["klass"]) 30 | klass = getattr(sources, exchange["klass"]) 31 | instance = klass(**exchange) 32 | threads[name] = pool.submit(instance.fetch) 33 | 34 | for name in threads: 35 | log.info("Checking %s ...", name) 36 | feed[name] = threads[name].result() 37 | return feed 38 | 39 | class FeedSource(): 40 | def __init__(self, scaleVolumeBy=1.0, 41 | enable=True, 42 | allowFailure=True, 43 | allowCache=False, 44 | timeout=5, 45 | quotes=[], 46 | bases=[], 47 | aliases={}, 48 | **kwargs): 49 | self.scaleVolumeBy = scaleVolumeBy 50 | self.enabled = enable 51 | self.allowFailure = allowFailure 52 | self.allowCache = allowCache 53 | self.timeout = timeout 54 | self.bases = bases 55 | self.aliases = aliases 56 | self.quotes = quotes 57 | 58 | [setattr(self, key, kwargs[key]) for key in kwargs] 59 | # Why fail if the scaleVolumeBy is 0 60 | if self.scaleVolumeBy == 0.0: 61 | self.allowFailure = True 62 | 63 | def fetch(self): 64 | try: 65 | feed = self._fetch() # pylint: disable=no-member 66 | if self.allowCache: 67 | self.updateCache(feed) 68 | return feed 69 | except Exception: 70 | traceback.print_exc() 71 | if not self.allowCache: 72 | print("\n{0} encountered an error while loading live data.".format(type(self).__name__)) 73 | return {} 74 | 75 | print("\n{0} encountered an error while loading live data. Trying to recover from cache!".format(type(self).__name__)) 76 | 77 | # Terminate if not allow Failure 78 | if not self.allowFailure: 79 | sys.exit("\nExiting due to exchange importance on %s!" % type(self).__name__) 80 | 81 | try: 82 | return self.recoverFromCache() 83 | except: 84 | print("We were unable to fetch live or cached data from %s. Skipping", type(self).__name__) 85 | 86 | def today(self): 87 | return datetime.datetime.strftime(datetime.datetime.now(), "%Y-%m-%d") 88 | 89 | def recoverFromCache(self): 90 | cacheFile = self.getCacheFileName() 91 | if os.path.isfile(cacheFile): 92 | with open(self.getCacheFileName(), 'r') as fp: 93 | return json.load(fp) 94 | return {} 95 | 96 | def getCacheFileName(self): 97 | cacheDir = os.path.join( 98 | user_data_dir("bitshares_pricefeed", "ChainSquad GmbH"), 99 | "cache", 100 | type(self).__name__ 101 | ) 102 | if not os.path.exists(cacheDir): 103 | os.makedirs(cacheDir) 104 | return os.path.join(cacheDir, self.today() + ".json") 105 | 106 | def updateCache(self, feed): 107 | with open(self.getCacheFileName(), 'w') as fp: 108 | json.dump(feed, fp) 109 | 110 | def alias(self, symbol): 111 | if symbol in self.aliases: 112 | return self.aliases[symbol] 113 | return symbol 114 | 115 | def add_rate(self, feed, base, quote, price, volume): 116 | resolved_base = self.alias(base) 117 | if resolved_base not in feed: 118 | feed[resolved_base] = {} 119 | feed[resolved_base][self.alias(quote)] = { "price": price, "volume": volume * self.scaleVolumeBy } 120 | return feed 121 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/manual.py: -------------------------------------------------------------------------------- 1 | from . import FeedSource 2 | 3 | # pylint: disable=no-member 4 | class Manual(FeedSource): 5 | def _fetch(self): 6 | return self.feed 7 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/norm.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import math 3 | from . import FeedSource 4 | 5 | SECONDS_PER_DAY = 60 * 60 * 24 6 | 7 | class Norm(FeedSource): 8 | 9 | def _norn_feed(self, amplitude, reference_timestamp, current_timestamp, period, phase_offset): 10 | """ 11 | Given the reference timestamp, the current timestamp, the period (in days), the phase (in days), the reference asset value (ie 1.00) and the amplitude (> 0 && < 1), output the current value. 12 | """ 13 | waveform = math.sin(((((current_timestamp - (reference_timestamp + phase_offset))/period) % 1) * period) * ((2*math.pi)/period)) # Only change for an alternative HERTZ ABA. 14 | return 1 + (amplitude * waveform) 15 | 16 | def _fetch(self): 17 | feed = {} 18 | 19 | reference_timestamp = datetime.strptime("2015-10-13T14:12:24", "%Y-%m-%dT%H:%M:%S").timestamp() # Bitshares 2.0 genesis block timestamp 20 | current_timestamp = datetime.now().timestamp() # Current timestamp for reference within the script 21 | amplitude = 0.05303030303 22 | period = SECONDS_PER_DAY * 28 23 | 24 | urthr_value = self._norn_feed( 25 | amplitude, 26 | reference_timestamp, 27 | current_timestamp, 28 | period, 29 | SECONDS_PER_DAY * 0 # phase offset 30 | ) 31 | self.add_rate(feed, 'BTS', 'URTHR', urthr_value, 1.0) 32 | 33 | verthandi_value = self._norn_feed( 34 | amplitude, 35 | reference_timestamp, 36 | current_timestamp, 37 | period, 38 | SECONDS_PER_DAY * 9.33 # phase offset 39 | ) 40 | self.add_rate(feed, 'BTS', 'VERTHANDI', verthandi_value, 1.0) 41 | 42 | skuld_value = self._norn_feed( 43 | amplitude, 44 | reference_timestamp, 45 | current_timestamp, 46 | period, 47 | SECONDS_PER_DAY * 18.66 # phase offset 48 | ) 49 | self.add_rate(feed, 'BTS', 'SKULD', skuld_value, 1.0) 50 | 51 | return feed 52 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/okcoin.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import requests 3 | from . import FeedSource, _request_headers 4 | 5 | 6 | class Okcoin(FeedSource): 7 | def _fetch(self): 8 | feed = {} 9 | for base in self.bases: 10 | for quote in self.quotes: 11 | if quote == base: 12 | continue 13 | url = "https://www.okcoin.com/api/spot/v3/instruments/{}-{}/ticker".format(quote.upper(), base.upper()) 14 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout) 15 | result = response.json() 16 | self.add_rate(feed, base, quote, float(result["last"]), float(result["base_volume_24h"])) 17 | feed[self.alias(base)]["response"] = result 18 | return feed 19 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/okex_otc.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import requests 3 | import time 4 | from . import FeedSource, _request_headers 5 | 6 | 7 | class OkExOtc(FeedSource): 8 | def _fetch(self): 9 | feed = {} 10 | t = int(time.time()) 11 | for base in self.bases: 12 | for quote in self.quotes: 13 | if quote == base: 14 | continue 15 | url = "https://www.okex.com/v3/c2c/otc-ticker/quotedPrice?t={}&baseCurrency={}"eCurrency={}&side=buy&amount=&standard=1&paymentMethod=aliPay".format(t, base.upper(), quote.upper()) 16 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout) 17 | result = response.json() 18 | 19 | self.add_rate(feed, base, quote, 1. / float(result["data"][0]["price"]), 1) 20 | feed[self.alias(base)]["response"] = result 21 | return feed 22 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/openexchangerate.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | # pylint: disable=no-member 5 | class OpenExchangeRates(FeedSource): # Hourly updated data with free subscription 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | if not hasattr(self, "api_key") or not hasattr(self, "free_subscription"): 9 | raise Exception("OpenExchangeRates FeedSource requires 'api_key' and 'free_subscription'") 10 | 11 | def _fetch(self): 12 | feed = {} 13 | for base in self.bases: 14 | url = "https://openexchangerates.org/api/latest.json?app_id=%s&base=%s" % (self.api_key, base) 15 | if self.free_subscription and base != 'USD': 16 | continue 17 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout) 18 | result = response.json() 19 | if result.get("base") != base: 20 | raise Exception("Error fetching from url. Returned: {}".format(result)) 21 | for quote in self.quotes: 22 | if quote == base: 23 | continue 24 | self.add_rate(feed, base, quote, 1 / result["rates"][quote], 1.0) 25 | return feed 26 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/poloniex.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from datetime import timedelta, datetime 3 | from . import FeedSource, _request_headers 4 | 5 | 6 | class Poloniex(FeedSource): 7 | def _fetch(self): 8 | feed = {} 9 | url = "https://poloniex.com/public?command=returnTicker" 10 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout) 11 | result = response.json() 12 | feed["response"] = result 13 | for base in self.bases: 14 | for quote in self.quotes: 15 | if quote == base: 16 | continue 17 | marketName = base + "_" + quote 18 | if marketName in result: 19 | self.add_rate(feed, base, quote, float(result[marketName]["last"]), float(result[marketName]["quoteVolume"])) 20 | return feed 21 | 22 | class PoloniexVWAP(FeedSource): 23 | def __init__(self, *args, **kwargs): 24 | super().__init__(*args, **kwargs) 25 | self.period = getattr(self, "period", 900) 26 | 27 | def _fetch(self): 28 | feed = {} 29 | start_date = datetime.utcnow() - timedelta(hours=1) 30 | for base in self.bases: 31 | for quote in self.quotes: 32 | if quote == base: 33 | continue 34 | url = "https://poloniex.com/public?command=returnChartData¤cyPair={}_{}&start={}&end=9999999999&period={}".format(base, quote, start_date.timestamp(), self.period) 35 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout) 36 | result = response.json() 37 | vwap = result[-1] 38 | self.add_rate(feed, base, quote, vwap["weightedAverage"], vwap["quoteVolume"]) 39 | return feed 40 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/quandl.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import requests 3 | from . import FeedSource, _request_headers 4 | import quandl 5 | 6 | # pylint: disable=no-member 7 | class Quandl(FeedSource): # Quandl using Python API client 8 | def __init__(self, *args, **kwargs): 9 | super().__init__(*args, **kwargs) 10 | self.maxAge = getattr(self, "maxAge", 5) 11 | 12 | quandl.ApiConfig.api_key = self.api_key 13 | quandl.ApiConfig.api_version = '2015-04-09' 14 | 15 | def _fetch(self): 16 | feed = {} 17 | for market in self.datasets: 18 | quote, base = market.split(":") 19 | for dataset in self.datasets[market]: 20 | data = quandl.get(dataset, rows=1, returns='numpy') 21 | self.add_rate(feed, base, quote, data[0][1], 1.0) 22 | return feed 23 | 24 | 25 | class QuandlPlain(FeedSource): # Quandl direct HTTP 26 | def __init__(self, *args, **kwargs): 27 | super().__init__(*args, **kwargs) 28 | self.maxAge = getattr(self, "maxAge", 5) 29 | 30 | def _fetch(self): 31 | feed = {} 32 | 33 | for market in self.datasets: 34 | quote, base = market.split(":") 35 | prices = [] 36 | for dataset in self.datasets[market]: 37 | url = "https://www.quandl.com/api/v3/datasets/{dataset}.json?start_date={date}".format( 38 | dataset=dataset, 39 | date=datetime.datetime.strftime(datetime.datetime.now() - 40 | datetime.timedelta(days=self.maxAge), 41 | "%Y-%m-%d") 42 | ) 43 | if hasattr(self, "api_key"): 44 | url += "&api_key=%s" % self.api_key 45 | response = requests.get(url=url, headers=_request_headers, timeout=self.timeout) 46 | data = response.json() 47 | if "quandl_error" in data: 48 | raise Exception(data["quandl_error"]["message"]) 49 | if "dataset" not in data: 50 | raise Exception("Feed has not returned a dataset for url: %s" % url) 51 | d = data["dataset"] 52 | if len(d["data"]): 53 | prices.append(d["data"][0][1]) 54 | self.add_rate(feed, base, quote, sum(prices) / len(prices), 1.0) 55 | return feed 56 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/robinhood.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | # pylint: disable=no-member 5 | class RobinHood(FeedSource): # Stocks prices from RobinHood: https://github.com/sanko/Robinhood 6 | def _extract_symbols(self): 7 | symbols_by_base = {} 8 | for equity in self.equities: 9 | (symbol, base) = equity.split(':') 10 | if base not in symbols_by_base: 11 | symbols_by_base[base] = [] 12 | symbols_by_base[base].append(symbol) 13 | return symbols_by_base 14 | 15 | def _fetch(self): 16 | symbols_by_base = self._extract_symbols() 17 | feed = {} 18 | url = "https://api.robinhood.com/quotes/?symbols={symbols}" 19 | for base in symbols_by_base.keys(): 20 | response = requests.get(url=url.format( 21 | symbols=','.join(symbols_by_base[base]) 22 | ), headers=_request_headers, timeout=self.timeout) 23 | result = response.json()['results'] 24 | for ticker in result: 25 | self.add_rate(feed, base, ticker['symbol'], float(ticker["last_trade_price"]), 1.0) 26 | return feed -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/worldcoinindex.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | # pylint: disable=no-member 5 | class WorldCoinIndex(FeedSource): # Weighted average from WorldCoinIndex 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | self.timeout = getattr(self, 'timeout', 15) 9 | if not hasattr(self, 'api_key'): 10 | raise Exception("WorldCoinIndex FeedSource requires 'api_key'.") 11 | 12 | def _fetch(self): 13 | feed = {} 14 | for base in self.bases: 15 | url = "https://www.worldcoinindex.com/apiservice/v2getmarkets?key={apikey}&fiat={base}" 16 | response = requests.get(url=url.format(apikey=self.api_key, base=base), 17 | headers=_request_headers, timeout=self.timeout) 18 | result = response.json()['Markets'] 19 | for market in result: 20 | for ticker in market: 21 | (quote, returnedBase) = ticker['Label'].split('/') 22 | if base == returnedBase and quote in self.quotes: 23 | self.add_rate(feed, base, quote, ticker['Price'], ticker['Volume_24h'] / ticker['Price']) 24 | return feed 25 | -------------------------------------------------------------------------------- /bitshares_pricefeed/sources/zb.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import FeedSource, _request_headers 3 | 4 | 5 | class Zb(FeedSource): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | self.base_url = getattr(self, 'url', 'http://api.zb.com') 9 | 10 | def _fetch(self): 11 | feed = {} 12 | url = "{base_url}/data/v1/ticker?market={quote}_{base}" 13 | for base in self.bases: 14 | for quote in self.quotes: 15 | if base == quote: 16 | continue 17 | response = requests.get( 18 | url=url.format(base_url=self.base_url, base=base, quote=quote), 19 | headers=_request_headers, 20 | timeout=self.timeout) 21 | result = response.json() 22 | if "ticker" in result and \ 23 | "last" in result["ticker"] and \ 24 | "vol" in result["ticker"]: 25 | self.add_rate(feed, base, quote, float(result["ticker"]["last"]), float(result["ticker"]["vol"])) 26 | else: 27 | print("\nFetched data from {0} is empty!".format(type(self).__name__)) 28 | continue 29 | return feed 30 | -------------------------------------------------------------------------------- /bitshares_pricefeed/ui.py: -------------------------------------------------------------------------------- 1 | import os 2 | import click 3 | import logging 4 | import yaml 5 | from math import fabs 6 | from datetime import datetime, timezone 7 | from bitshares.price import Price 8 | from bitshares.asset import Asset 9 | from prettytable import PrettyTable 10 | from functools import update_wrapper 11 | from bitshares import BitShares 12 | from bitshares.instance import set_shared_bitshares_instance 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | def configfile(f): 17 | @click.pass_context 18 | def new_func(ctx, *args, **kwargs): 19 | ctx.config = yaml.safe_load(open(ctx.obj["configfile"])) 20 | return ctx.invoke(f, *args, **kwargs) 21 | return update_wrapper(new_func, f) 22 | 23 | 24 | def priceChange(new, old): 25 | if float(old) == 0.0: 26 | return -1 27 | else: 28 | percent = ((float(new) - float(old))) / float(old) * 100 29 | if percent >= 0: 30 | return click.style("%.2f" % percent, fg="green") 31 | else: 32 | return click.style("%.2f" % percent, fg="red") 33 | 34 | 35 | def highlightLargeDeviation(value, ref, thres=5): 36 | percent = ((float(value) - float(ref))) / float(ref) * 100 37 | if fabs(percent) >= thres: 38 | return click.style("%+5.2f" % percent, fg="red") 39 | else: 40 | return click.style("%+5.2f" % percent, fg="green") 41 | 42 | 43 | def formatPrice(f): 44 | return click.style("%.6f" % f, fg="yellow") 45 | 46 | 47 | def formatStd(f): 48 | return click.style("%.2f" % f, bold=True) 49 | 50 | 51 | def print_log(feeds): 52 | t = PrettyTable([ 53 | "base", 54 | "quote", 55 | "price", 56 | "diff", 57 | "quote volume", 58 | "source" 59 | ]) 60 | t.align = 'l' 61 | for symbol, feed in feeds.items(): 62 | asset = Asset(symbol, full=True) 63 | asset.ensure_full() 64 | short_backing_asset = Asset( 65 | asset["bitasset_data"]["options"]["short_backing_asset"]) 66 | backing_symbol = short_backing_asset["symbol"] 67 | data = feed.get("log", {}) 68 | price = data.get(symbol) 69 | if not price: 70 | continue 71 | for d in price.get(backing_symbol, []): 72 | t.add_row([ 73 | symbol, 74 | backing_symbol, 75 | formatPrice(d.get("price")), 76 | highlightLargeDeviation(d.get("price"), feed["price"]), 77 | d.get("volume"), 78 | str(d.get("sources")), 79 | ]) 80 | print(t.get_string()) 81 | 82 | def print_premium_details(feeds): 83 | for symbol, feed in feeds.items(): 84 | if not feed: 85 | continue 86 | print('Premium details for {}:'.format(symbol)) 87 | print(' BIT{}/BTS (on DEX): {} ({})'. 88 | format(symbol, formatPrice(feed['premium_details']['dex_price']), \ 89 | priceChange(feed['premium_details']['dex_price'], feed["unadjusted_price"]))) 90 | if 'alternative' in feed['premium_details']: 91 | print(' BIT{}/{} (alternative premiums):'.format(symbol, symbol)) 92 | for alt in feed['premium_details']['alternative']: 93 | print(' - {} : {} ({})'.format(alt['sources'], formatPrice(alt['price']), priceChange(alt['price'], 1))) 94 | 95 | def print_prices(feeds): 96 | t = PrettyTable([ 97 | "symbol", "collateral", 98 | "new price", "cer", "premium", "unadjusted price", 99 | "mean", "median", "wgt. avg.", 100 | "wgt. std (#)", "blockchain", 101 | "mssr", "mcr", 102 | "my last price", "last update", 103 | ]) 104 | t.align = 'c' 105 | t.border = True 106 | 107 | for symbol, feed in feeds.items(): 108 | if not feed: 109 | continue 110 | collateral = feed["short_backing_symbol"] 111 | myprice = feed["price"] 112 | blockchain = feed["global_feed"]["settlement_price"].as_quote(collateral)['price'] 113 | if "current_feed" in feed and feed["current_feed"]: 114 | last = feed["current_feed"]["settlement_price"].as_quote(collateral)['price'] 115 | age = (str(datetime.now(timezone.utc) - feed["current_feed"]["date"])) 116 | else: 117 | last = -1.0 118 | age = "unknown" 119 | # Get Final Price according to price metric 120 | t.add_row([ 121 | symbol, 122 | ("%s" % collateral), 123 | ("%s" % formatPrice(feed["price"])), 124 | ("%s" % formatPrice(feed["cer"])), 125 | ("%.1f%%" % feed["premium"]), 126 | ("%s (%s)" % (formatPrice(feed["unadjusted_price"]), priceChange(myprice, feed.get("unadjusted_price")))), 127 | ("%s (%s)" % (formatPrice(feed["mean"]), priceChange(myprice, feed.get("mean")))), 128 | ("%s (%s)" % (formatPrice(feed["median"]), priceChange(myprice, feed.get("median")))), 129 | ("%s (%s)" % (formatPrice(feed["weighted"]), priceChange(myprice, feed.get("weighted")))), 130 | ("%s (%2d)" % (formatStd(feed["std"]), feed.get("number"))), 131 | ("%s (%s)" % (formatPrice(blockchain), priceChange(myprice, blockchain))), 132 | ("%.1f%%" % feed["mssr"]), 133 | ("%.1f%%" % feed["mcr"]), 134 | ("%s (%s)" % (formatPrice(last), priceChange(myprice, last))), 135 | age + " ago" 136 | ]) 137 | print(t.get_string()) 138 | 139 | 140 | def warning(msg): 141 | click.echo( 142 | "[" + 143 | click.style("Warning", fg="yellow") + 144 | "] " + msg, 145 | err=True # this will cause click to post to stderr 146 | ) 147 | 148 | 149 | def confirmwarning(msg): 150 | return click.confirm( 151 | "[" + 152 | click.style("Warning", fg="yellow") + 153 | "] " + msg 154 | ) 155 | 156 | 157 | def alert(msg): 158 | click.echo( 159 | "[" + 160 | click.style("alert", fg="yellow") + 161 | "] " + msg, 162 | err=True # this will cause click to post to stderr 163 | ) 164 | 165 | 166 | def confirmalert(msg): 167 | return click.confirm( 168 | "[" + 169 | click.style("Alert", fg="red") + 170 | "] " + msg 171 | ) 172 | -------------------------------------------------------------------------------- /cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import warnings 3 | warnings.filterwarnings("ignore", message="numpy.dtype size changed") 4 | 5 | from bitshares_pricefeed import cli 6 | 7 | cli.main() 8 | -------------------------------------------------------------------------------- /docs/_build/doctrees/environment.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/doctrees/environment.pickle -------------------------------------------------------------------------------- /docs/_build/doctrees/index.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/doctrees/index.doctree -------------------------------------------------------------------------------- /docs/_build/html/.buildinfo: -------------------------------------------------------------------------------- 1 | # Sphinx build info version 1 2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. 3 | config: b31e9f8870699feecb3144cc5804e2a4 4 | tags: 645f666f9bcd5a90fca523b33c5a78b7 5 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/index.rst.txt: -------------------------------------------------------------------------------- 1 | Welcome to bitshares-pricefeed documentation! 2 | =============================================== 3 | 4 | Documentation is still needed ;-) 5 | -------------------------------------------------------------------------------- /docs/_build/html/_static/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/ajax-loader.gif -------------------------------------------------------------------------------- /docs/_build/html/_static/comment-bright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/comment-bright.png -------------------------------------------------------------------------------- /docs/_build/html/_static/comment-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/comment-close.png -------------------------------------------------------------------------------- /docs/_build/html/_static/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/comment.png -------------------------------------------------------------------------------- /docs/_build/html/_static/dialog-note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/dialog-note.png -------------------------------------------------------------------------------- /docs/_build/html/_static/dialog-seealso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/dialog-seealso.png -------------------------------------------------------------------------------- /docs/_build/html/_static/dialog-todo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/dialog-todo.png -------------------------------------------------------------------------------- /docs/_build/html/_static/dialog-topic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/dialog-topic.png -------------------------------------------------------------------------------- /docs/_build/html/_static/dialog-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/dialog-warning.png -------------------------------------------------------------------------------- /docs/_build/html/_static/down-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/down-pressed.png -------------------------------------------------------------------------------- /docs/_build/html/_static/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/down.png -------------------------------------------------------------------------------- /docs/_build/html/_static/epub.css: -------------------------------------------------------------------------------- 1 | /* 2 | * default.css_t 3 | * ~~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- default theme. 6 | * 7 | * :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: {{ theme_bodyfont }}; 18 | font-size: 100%; 19 | background-color: {{ theme_footerbgcolor }}; 20 | color: #000; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.document { 26 | background-color: {{ theme_sidebarbgcolor }}; 27 | } 28 | 29 | div.documentwrapper { 30 | float: left; 31 | width: 100%; 32 | } 33 | 34 | div.bodywrapper { 35 | margin: 0 0 0 230px; 36 | } 37 | 38 | div.body { 39 | background-color: {{ theme_bgcolor }}; 40 | color: {{ theme_textcolor }}; 41 | padding: 0 20px 30px 20px; 42 | } 43 | 44 | {%- if theme_rightsidebar|tobool %} 45 | div.bodywrapper { 46 | margin: 0 230px 0 0; 47 | } 48 | {%- endif %} 49 | 50 | div.footer { 51 | color: {{ theme_footertextcolor }}; 52 | width: 100%; 53 | padding: 9px 0 9px 0; 54 | text-align: center; 55 | font-size: 75%; 56 | } 57 | 58 | div.footer a { 59 | color: {{ theme_footertextcolor }}; 60 | text-decoration: underline; 61 | } 62 | 63 | div.related { 64 | background-color: {{ theme_relbarbgcolor }}; 65 | line-height: 30px; 66 | color: {{ theme_relbartextcolor }}; 67 | } 68 | 69 | div.related a { 70 | color: {{ theme_relbarlinkcolor }}; 71 | } 72 | 73 | div.sphinxsidebar { 74 | {%- if theme_stickysidebar|tobool %} 75 | top: 30px; 76 | bottom: 0; 77 | margin: 0; 78 | position: fixed; 79 | overflow: auto; 80 | height: auto; 81 | {%- endif %} 82 | {%- if theme_rightsidebar|tobool %} 83 | float: right; 84 | {%- if theme_stickysidebar|tobool %} 85 | right: 0; 86 | {%- endif %} 87 | {%- endif %} 88 | } 89 | 90 | {%- if theme_stickysidebar|tobool %} 91 | /* this is nice, but it it leads to hidden headings when jumping 92 | to an anchor */ 93 | /* 94 | div.related { 95 | position: fixed; 96 | } 97 | 98 | div.documentwrapper { 99 | margin-top: 30px; 100 | } 101 | */ 102 | {%- endif %} 103 | 104 | div.sphinxsidebar h3 { 105 | font-family: {{ theme_headfont }}; 106 | color: {{ theme_sidebartextcolor }}; 107 | font-size: 1.4em; 108 | font-weight: normal; 109 | margin: 0; 110 | padding: 0; 111 | } 112 | 113 | div.sphinxsidebar h3 a { 114 | color: {{ theme_sidebartextcolor }}; 115 | } 116 | 117 | div.sphinxsidebar h4 { 118 | font-family: {{ theme_headfont }}; 119 | color: {{ theme_sidebartextcolor }}; 120 | font-size: 1.3em; 121 | font-weight: normal; 122 | margin: 5px 0 0 0; 123 | padding: 0; 124 | } 125 | 126 | div.sphinxsidebar p { 127 | color: {{ theme_sidebartextcolor }}; 128 | } 129 | 130 | div.sphinxsidebar p.topless { 131 | margin: 5px 10px 10px 10px; 132 | } 133 | 134 | div.sphinxsidebar ul { 135 | margin: 10px; 136 | padding: 0; 137 | color: {{ theme_sidebartextcolor }}; 138 | } 139 | 140 | div.sphinxsidebar a { 141 | color: {{ theme_sidebarlinkcolor }}; 142 | } 143 | 144 | div.sphinxsidebar input { 145 | border: 1px solid {{ theme_sidebarlinkcolor }}; 146 | font-family: sans-serif; 147 | font-size: 1em; 148 | } 149 | 150 | {% if theme_collapsiblesidebar|tobool %} 151 | /* for collapsible sidebar */ 152 | div#sidebarbutton { 153 | background-color: {{ theme_sidebarbtncolor }}; 154 | } 155 | {% endif %} 156 | 157 | /* -- hyperlink styles ------------------------------------------------------ */ 158 | 159 | a { 160 | color: {{ theme_linkcolor }}; 161 | text-decoration: none; 162 | } 163 | 164 | a:visited { 165 | color: {{ theme_visitedlinkcolor }}; 166 | text-decoration: none; 167 | } 168 | 169 | a:hover { 170 | text-decoration: underline; 171 | } 172 | 173 | {% if theme_externalrefs|tobool %} 174 | a.external { 175 | text-decoration: none; 176 | border-bottom: 1px dashed {{ theme_linkcolor }}; 177 | } 178 | 179 | a.external:hover { 180 | text-decoration: none; 181 | border-bottom: none; 182 | } 183 | 184 | a.external:visited { 185 | text-decoration: none; 186 | border-bottom: 1px dashed {{ theme_visitedlinkcolor }}; 187 | } 188 | {% endif %} 189 | 190 | /* -- body styles ----------------------------------------------------------- */ 191 | 192 | div.body h1, 193 | div.body h2, 194 | div.body h3, 195 | div.body h4, 196 | div.body h5, 197 | div.body h6 { 198 | font-family: {{ theme_headfont }}; 199 | background-color: {{ theme_headbgcolor }}; 200 | font-weight: normal; 201 | color: {{ theme_headtextcolor }}; 202 | border-bottom: 1px solid #ccc; 203 | margin: 20px -20px 10px -20px; 204 | padding: 3px 0 3px 10px; 205 | } 206 | 207 | div.body h1 { margin-top: 0; font-size: 200%; } 208 | div.body h2 { font-size: 160%; } 209 | div.body h3 { font-size: 140%; } 210 | div.body h4 { font-size: 120%; } 211 | div.body h5 { font-size: 110%; } 212 | div.body h6 { font-size: 100%; } 213 | 214 | a.headerlink { 215 | color: {{ theme_headlinkcolor }}; 216 | font-size: 0.8em; 217 | padding: 0 4px 0 4px; 218 | text-decoration: none; 219 | } 220 | 221 | a.headerlink:hover { 222 | background-color: {{ theme_headlinkcolor }}; 223 | color: white; 224 | } 225 | 226 | div.body p, div.body dd, div.body li { 227 | text-align: justify; 228 | line-height: 130%; 229 | } 230 | 231 | div.admonition p.admonition-title + p { 232 | display: inline; 233 | } 234 | 235 | div.admonition p { 236 | margin-bottom: 5px; 237 | } 238 | 239 | div.admonition pre { 240 | margin-bottom: 5px; 241 | } 242 | 243 | div.admonition ul, div.admonition ol { 244 | margin-bottom: 5px; 245 | } 246 | 247 | div.note { 248 | background-color: #eee; 249 | border: 1px solid #ccc; 250 | } 251 | 252 | div.seealso { 253 | background-color: #ffc; 254 | border: 1px solid #ff6; 255 | } 256 | 257 | div.topic { 258 | background-color: #eee; 259 | } 260 | 261 | div.warning { 262 | background-color: #ffe4e4; 263 | border: 1px solid #f66; 264 | } 265 | 266 | p.admonition-title { 267 | display: inline; 268 | } 269 | 270 | p.admonition-title:after { 271 | content: ":"; 272 | } 273 | 274 | pre { 275 | padding: 5px; 276 | background-color: {{ theme_codebgcolor }}; 277 | color: {{ theme_codetextcolor }}; 278 | line-height: 120%; 279 | border: 1px solid #ac9; 280 | border-left: none; 281 | border-right: none; 282 | } 283 | 284 | code { 285 | background-color: #ecf0f3; 286 | padding: 0 1px 0 1px; 287 | font-size: 0.95em; 288 | } 289 | 290 | th { 291 | background-color: #ede; 292 | } 293 | 294 | .warning code { 295 | background: #efc2c2; 296 | } 297 | 298 | .note code { 299 | background: #d6d6d6; 300 | } 301 | 302 | .viewcode-back { 303 | font-family: {{ theme_bodyfont }}; 304 | } 305 | 306 | div.viewcode-block:target { 307 | background-color: #f4debf; 308 | border-top: 1px solid #ac9; 309 | border-bottom: 1px solid #ac9; 310 | } 311 | -------------------------------------------------------------------------------- /docs/_build/html/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/file.png -------------------------------------------------------------------------------- /docs/_build/html/_static/footerbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/footerbg.png -------------------------------------------------------------------------------- /docs/_build/html/_static/headerbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/headerbg.png -------------------------------------------------------------------------------- /docs/_build/html/_static/ie6.css: -------------------------------------------------------------------------------- 1 | * html img, 2 | * html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", 3 | this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", 4 | this.src = "_static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), 5 | this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", 6 | this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) 7 | );} 8 | -------------------------------------------------------------------------------- /docs/_build/html/_static/middlebg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/middlebg.png -------------------------------------------------------------------------------- /docs/_build/html/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/minus.png -------------------------------------------------------------------------------- /docs/_build/html/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/plus.png -------------------------------------------------------------------------------- /docs/_build/html/_static/pygments.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight { background: #eeffcc; } 3 | .highlight .c { color: #408090; font-style: italic } /* Comment */ 4 | .highlight .err { border: 1px solid #FF0000 } /* Error */ 5 | .highlight .k { color: #007020; font-weight: bold } /* Keyword */ 6 | .highlight .o { color: #666666 } /* Operator */ 7 | .highlight .ch { color: #408090; font-style: italic } /* Comment.Hashbang */ 8 | .highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ 9 | .highlight .cp { color: #007020 } /* Comment.Preproc */ 10 | .highlight .cpf { color: #408090; font-style: italic } /* Comment.PreprocFile */ 11 | .highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ 12 | .highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ 13 | .highlight .gd { color: #A00000 } /* Generic.Deleted */ 14 | .highlight .ge { font-style: italic } /* Generic.Emph */ 15 | .highlight .gr { color: #FF0000 } /* Generic.Error */ 16 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 17 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 18 | .highlight .go { color: #333333 } /* Generic.Output */ 19 | .highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 20 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 21 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 22 | .highlight .gt { color: #0044DD } /* Generic.Traceback */ 23 | .highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ 24 | .highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ 25 | .highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ 26 | .highlight .kp { color: #007020 } /* Keyword.Pseudo */ 27 | .highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ 28 | .highlight .kt { color: #902000 } /* Keyword.Type */ 29 | .highlight .m { color: #208050 } /* Literal.Number */ 30 | .highlight .s { color: #4070a0 } /* Literal.String */ 31 | .highlight .na { color: #4070a0 } /* Name.Attribute */ 32 | .highlight .nb { color: #007020 } /* Name.Builtin */ 33 | .highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ 34 | .highlight .no { color: #60add5 } /* Name.Constant */ 35 | .highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ 36 | .highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ 37 | .highlight .ne { color: #007020 } /* Name.Exception */ 38 | .highlight .nf { color: #06287e } /* Name.Function */ 39 | .highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ 40 | .highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 41 | .highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ 42 | .highlight .nv { color: #bb60d5 } /* Name.Variable */ 43 | .highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ 44 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 45 | .highlight .mb { color: #208050 } /* Literal.Number.Bin */ 46 | .highlight .mf { color: #208050 } /* Literal.Number.Float */ 47 | .highlight .mh { color: #208050 } /* Literal.Number.Hex */ 48 | .highlight .mi { color: #208050 } /* Literal.Number.Integer */ 49 | .highlight .mo { color: #208050 } /* Literal.Number.Oct */ 50 | .highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ 51 | .highlight .sc { color: #4070a0 } /* Literal.String.Char */ 52 | .highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ 53 | .highlight .s2 { color: #4070a0 } /* Literal.String.Double */ 54 | .highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ 55 | .highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ 56 | .highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ 57 | .highlight .sx { color: #c65d09 } /* Literal.String.Other */ 58 | .highlight .sr { color: #235388 } /* Literal.String.Regex */ 59 | .highlight .s1 { color: #4070a0 } /* Literal.String.Single */ 60 | .highlight .ss { color: #517918 } /* Literal.String.Symbol */ 61 | .highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ 62 | .highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ 63 | .highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ 64 | .highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ 65 | .highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /docs/_build/html/_static/pyramid.css: -------------------------------------------------------------------------------- 1 | /* 2 | * pyramid.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- pylons theme. 6 | * 7 | * :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: "Nobile", sans-serif; 18 | font-size: 100%; 19 | background-color: #393939; 20 | color: #ffffff; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.documentwrapper { 26 | float: left; 27 | width: 100%; 28 | } 29 | 30 | div.bodywrapper { 31 | margin: 0 0 0 230px; 32 | } 33 | 34 | hr { 35 | border: 1px solid #B1B4B6; 36 | } 37 | 38 | div.document { 39 | background-color: #eee; 40 | } 41 | 42 | div.header { 43 | width:100%; 44 | background: #f4ad32 url(headerbg.png) repeat-x 0 top; 45 | border-bottom: 2px solid #ffffff; 46 | } 47 | 48 | div.logo { 49 | text-align: center; 50 | padding-top: 10px; 51 | } 52 | 53 | div.body { 54 | background-color: #ffffff; 55 | color: #3E4349; 56 | padding: 0 30px 30px 30px; 57 | font-size: 1em; 58 | border: 2px solid #ddd; 59 | border-right-style: none; 60 | overflow: auto; 61 | } 62 | 63 | div.footer { 64 | color: #ffffff; 65 | width: 100%; 66 | padding: 13px 0; 67 | text-align: center; 68 | font-size: 75%; 69 | background: transparent; 70 | clear:both; 71 | } 72 | 73 | div.footer a { 74 | color: #ffffff; 75 | text-decoration: none; 76 | } 77 | 78 | div.footer a:hover { 79 | color: #e88f00; 80 | text-decoration: underline; 81 | } 82 | 83 | div.related { 84 | line-height: 30px; 85 | color: #373839; 86 | font-size: 0.8em; 87 | background-color: #eee; 88 | } 89 | 90 | div.related a { 91 | color: #1b61d6; 92 | } 93 | 94 | div.related ul { 95 | padding-left: 240px; 96 | } 97 | 98 | div.sphinxsidebar { 99 | font-size: 0.75em; 100 | line-height: 1.5em; 101 | } 102 | 103 | div.sphinxsidebarwrapper{ 104 | padding: 10px 0; 105 | } 106 | 107 | div.sphinxsidebar h3, 108 | div.sphinxsidebar h4 { 109 | font-family: "Neuton", sans-serif; 110 | color: #373839; 111 | font-size: 1.4em; 112 | font-weight: normal; 113 | margin: 0; 114 | padding: 5px 10px; 115 | border-bottom: 2px solid #ddd; 116 | } 117 | 118 | div.sphinxsidebar h4{ 119 | font-size: 1.3em; 120 | } 121 | 122 | div.sphinxsidebar h3 a { 123 | color: #000000; 124 | } 125 | 126 | 127 | div.sphinxsidebar p { 128 | color: #888; 129 | padding: 5px 20px; 130 | } 131 | 132 | div.sphinxsidebar p.topless { 133 | } 134 | 135 | div.sphinxsidebar ul { 136 | margin: 10px 20px; 137 | padding: 0; 138 | color: #373839; 139 | } 140 | 141 | div.sphinxsidebar a { 142 | color: #444; 143 | } 144 | 145 | div.sphinxsidebar input { 146 | border: 1px solid #ccc; 147 | font-family: sans-serif; 148 | font-size: 1em; 149 | } 150 | 151 | div.sphinxsidebar input[type=text]{ 152 | margin-left: 20px; 153 | } 154 | 155 | div.sphinxsidebar input[type=submit]{ 156 | margin-left: 20px; 157 | } 158 | 159 | /* -- sidebars -------------------------------------------------------------- */ 160 | 161 | div.sidebar { 162 | margin: 0 0 0.5em 1em; 163 | border: 2px solid #c6d880; 164 | background-color: #e6efc2; 165 | width: 40%; 166 | float: right; 167 | border-right-style: none; 168 | border-left-style: none; 169 | padding: 10px 20px; 170 | } 171 | 172 | p.sidebar-title { 173 | font-weight: bold; 174 | } 175 | 176 | /* -- body styles ----------------------------------------------------------- */ 177 | 178 | a, a .pre { 179 | color: #1b61d6; 180 | text-decoration: none; 181 | } 182 | 183 | a:hover, a:hover .pre { 184 | text-decoration: underline; 185 | } 186 | 187 | div.body h1, 188 | div.body h2, 189 | div.body h3, 190 | div.body h4, 191 | div.body h5, 192 | div.body h6 { 193 | font-family: "Neuton", sans-serif; 194 | background-color: #ffffff; 195 | font-weight: normal; 196 | color: #373839; 197 | margin: 30px 0px 10px 0px; 198 | padding: 5px 0; 199 | } 200 | 201 | div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; } 202 | div.body h2 { font-size: 150%; background-color: #ffffff; } 203 | div.body h3 { font-size: 120%; background-color: #ffffff; } 204 | div.body h4 { font-size: 110%; background-color: #ffffff; } 205 | div.body h5 { font-size: 100%; background-color: #ffffff; } 206 | div.body h6 { font-size: 100%; background-color: #ffffff; } 207 | 208 | a.headerlink { 209 | color: #1b61d6; 210 | font-size: 0.8em; 211 | padding: 0 4px 0 4px; 212 | text-decoration: none; 213 | } 214 | 215 | a.headerlink:hover { 216 | text-decoration: underline; 217 | } 218 | 219 | div.body p, div.body dd, div.body li { 220 | line-height: 1.5em; 221 | } 222 | 223 | div.admonition p.admonition-title + p { 224 | display: inline; 225 | } 226 | 227 | div.admonition { 228 | background: #eeeeec; 229 | border: 2px solid #babdb6; 230 | border-right-style: none; 231 | border-left-style: none; 232 | padding: 10px 20px 10px 60px; 233 | } 234 | 235 | div.highlight{ 236 | background-color: white; 237 | } 238 | 239 | div.note { 240 | border: 2px solid #7a9eec; 241 | border-right-style: none; 242 | border-left-style: none; 243 | padding: 10px 20px 10px 60px; 244 | background: #e1ecfe url(dialog-note.png) no-repeat 10px 8px; 245 | } 246 | 247 | div.seealso { 248 | background: #fff6bf url(dialog-seealso.png) no-repeat 10px 8px; 249 | border: 2px solid #ffd324; 250 | border-left-style: none; 251 | border-right-style: none; 252 | padding: 10px 20px 10px 60px; 253 | } 254 | 255 | div.topic { 256 | background: #eeeeee; 257 | border: 2px solid #C6C9CB; 258 | padding: 10px 20px; 259 | border-right-style: none; 260 | border-left-style: none; 261 | } 262 | 263 | div.warning { 264 | background: #fbe3e4 url(dialog-warning.png) no-repeat 10px 8px; 265 | border: 2px solid #fbc2c4; 266 | border-right-style: none; 267 | border-left-style: none; 268 | padding: 10px 20px 10px 60px; 269 | } 270 | 271 | div.admonition-todo { 272 | background: #f2d9b4 url(dialog-todo.png) no-repeat 10px 8px; 273 | border: 2px solid #e9b96e; 274 | border-right-style: none; 275 | border-left-style: none; 276 | padding: 10px 20px 10px 60px; 277 | } 278 | 279 | div.note p.admonition-title, 280 | div.warning p.admonition-title, 281 | div.seealso p.admonition-title, 282 | div.admonition-todo p.admonition-title { 283 | display: none; 284 | } 285 | 286 | p.admonition-title:after { 287 | content: ":"; 288 | } 289 | 290 | pre { 291 | padding: 10px; 292 | background-color: #fafafa; 293 | color: #222; 294 | line-height: 1.2em; 295 | border: 2px solid #C6C9CB; 296 | font-size: 1.1em; 297 | margin: 1.5em 0 1.5em 0; 298 | border-right-style: none; 299 | border-left-style: none; 300 | } 301 | 302 | code { 303 | background-color: transparent; 304 | color: #222; 305 | font-size: 1.1em; 306 | font-family: monospace; 307 | } 308 | 309 | .viewcode-back { 310 | font-family: "Nobile", sans-serif; 311 | } 312 | 313 | div.viewcode-block:target { 314 | background-color: #fff6bf; 315 | border: 2px solid #ffd324; 316 | border-left-style: none; 317 | border-right-style: none; 318 | padding: 10px 20px; 319 | } 320 | 321 | table.highlighttable { 322 | width: 100%; 323 | } 324 | 325 | table.highlighttable td { 326 | padding: 0; 327 | } 328 | 329 | a em.std-term { 330 | color: #007f00; 331 | } 332 | 333 | a:hover em.std-term { 334 | text-decoration: underline; 335 | } 336 | 337 | .download { 338 | font-family: "Nobile", sans-serif; 339 | font-weight: normal; 340 | font-style: normal; 341 | } 342 | 343 | code.xref { 344 | font-weight: normal; 345 | font-style: normal; 346 | } 347 | 348 | div.code-block-caption { 349 | background-color: #ddd; 350 | color: #222; 351 | } -------------------------------------------------------------------------------- /docs/_build/html/_static/transparent.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/transparent.gif -------------------------------------------------------------------------------- /docs/_build/html/_static/up-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/up-pressed.png -------------------------------------------------------------------------------- /docs/_build/html/_static/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zapata/bitshares-pricefeed/5ce483c407f351ce0eb821fd67ffb90a97b866d7/docs/_build/html/_static/up.png -------------------------------------------------------------------------------- /docs/_build/html/genindex.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 |
8 | 9 | 10 |65 | Please activate JavaScript to enable the search 66 | functionality. 67 |
68 |70 | From here you can search these documents. Enter your search 71 | words into the box below and click "search". Note that the search 72 | function will automatically search for all of the words. Pages 73 | containing fewer words won't appear in the result list. 74 |
75 | 80 | 81 |65 | Please activate JavaScript to enable the search 66 | functionality. 67 |
68 |70 | From here you can search these documents. Enter your search 71 | words into the box below and click "search". Note that the search 72 | function will automatically search for all of the words. Pages 73 | containing fewer words won't appear in the result list. 74 |
75 | 80 | 81 |